I have a list of points (x,y). A line is drawn somewhere 'inside' these points (within the area they form). I have been trying different algorithms for ordering these points to create a polygon, with these constraints:
alphashape
concave hull algorithm, but I was not able to get it to work (it ignored some points, and I wasn't sure what to tune the alpha
value to)Some clarification for how the points are ordered: they roughly follow the profile of a contoured line, which is unknown and different from the drawn line. The drawn line will be intrinsically quite similar to this unspecified line however.
I have also tried the very simplistic angle sort algorithm, which unsurprisingly does not work. However, running the algorithm at differnt points along the line gave more promising results, but I am not sure how to use the data.
Above is a screenshot of the following test program I cobbled together:
import pygame
import numpy as np
from scipy.interpolate import splprep, splev
import sys
import random
# Pygame setup
pygame.init()
screen = pygame.display.set_mode((1000, 1000))
pygame.display.set_caption("Smoothed Curve with Draggable Points")
clock = pygame.time.Clock()
# Colors
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
GREEN = (0, 200, 0)
BLACK = (0, 0, 0)
drawing = False
points = []
smoothed = []
normals = []
# Dot settings
DOT_RADIUS = 6
DRAG_RADIUS = 10
# Dot class for draggable points
class Dot:
def __init__(self, pos):
self.x, self.y = pos
self.dragging = False
def pos(self):
return (int(self.x), int(self.y))
def is_mouse_over(self, mx, my):
return (self.x - mx)**2 + (self.y - my)**2 <= DRAG_RADIUS**2
def start_drag(self):
self.dragging = True
def stop_drag(self):
self.dragging = False
def update_pos(self, mx, my):
if self.dragging:
self.x, self.y = mx, my
# crude test arrangement of dots.
random_dots = [Dot(tup) for tup in [(159, 459),(133, 193),(286, 481),(241, 345),(411, 404),(280, 349),(352, 471),(395, 361),(85, 390),(203, 321),(41, 281),(58, 348),(175, 275),(75, 185),(385, 443),(44, 219),(148, 229),(215, 477),(338, 339),(122, 430)]]
def downsample(points, step=2):
return points[::step]
def smooth_curve(points, smoothness=150):
if len(points) < 4:
return [], []
points = downsample(points, step=2)
x, y = zip(*points)
try:
tck, u = splprep([x, y], s=100.0, k=3)
u_new = np.linspace(0, 1, smoothness)
# Evaluate curve points
x_vals, y_vals = splev(u_new, tck)
# Derivatives: dx/du, dy/du
dx_du, dy_du = splev(u_new, tck, der=1)
gradients = np.array(dy_du) / np.array(dx_du) # dy/dx
return list(zip(x_vals, y_vals)), list(zip(dx_du, dy_du))
except:
return [], []
def draw_normals(screen, smoothed, derivatives, spacing=10, length=30):
for i in range(0, len(smoothed), spacing):
x, y = smoothed[i]
dx, dy = derivatives[i]
# Normal vector is (-dy, dx)
nx, ny = -dy, dx
norm = np.hypot(nx, ny)
if norm == 0:
continue
nx /= norm
ny /= norm
x1 = x - nx * length / 2
y1 = y - ny * length / 2
x2 = x + nx * length / 2
y2 = y + ny * length / 2
pygame.draw.line(screen, GREEN, (x1, y1), (x2, y2), 2)
def generate_random_dots(n=20, w=800, h=600):
return [Dot((random.randint(0, w), random.randint(0, h))) for _ in range(n)]
# Main loop
dragged_dot = None
running = True
while running:
screen.fill(WHITE)
mx, my = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
drawing = True
points = []
smoothed = []
normals = []
# Check for draggable dot
for dot in random_dots:
if dot.is_mouse_over(mx, my):
dragged_dot = dot
dot.start_drag()
drawing = False
break
elif event.type == pygame.MOUSEBUTTONUP:
drawing = False
if dragged_dot:
dragged_dot.stop_drag()
dragged_dot = None
else:
smoothed, normals = smooth_curve(points)
elif event.type == pygame.MOUSEMOTION:
if dragged_dot:
dragged_dot.update_pos(mx, my)
elif drawing:
points.append((mx, my))
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_p:
random_dots = generate_random_dots()
if event.key == pygame.K_l:
for i in random_dots: print(f"({i.x}, {i.y})")
# Draw random draggable dots
for dot in random_dots:
pygame.draw.circle(screen, BLACK, dot.pos(), DOT_RADIUS)
# Draw raw stroke
if len(points) > 1:
pygame.draw.lines(screen, BLUE, False, points, 1)
# Draw smoothed curve
if len(smoothed) > 1:
pygame.draw.lines(screen, RED, False, smoothed, 3)
# Draw normals
if smoothed and normals:
draw_normals(screen, smoothed, normals)
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()
What algorithm could I use to accomplish this?
Some details about the diagram. The black dots are the input points to form the polygon. The red is the drawn line, which is a smoothed version of the blue one. The green lines show some equidistant test points on the drawn line. The required output order is the order in which the points have to be joined up to form the polygon, i.e. A,B,C,D for a rectangle. The input order of the points is not ordered in any meaningful way.
Here is some demo code in C++ to show how this algorithm works
void cSolver::gen1()
{
// Example from https://stackoverflow.com/q/79696095/16582
// (159, 459),(133, 193),(286, 481),(241, 345),(411, 404),(280, 349),(352, 471),(395, 361),(85, 390),(203, 321),(41, 281),(58, 348),(175, 275),(75, 185),(385, 443),(44, 219),(148, 229),(215, 477),(338, 339),(122, 430)]]
myInputPoints = {{159, 459}, {133, 193}, {286, 481}, {241, 345}, {411, 404}, {280, 349}, {352, 471}, {395, 361}, {85, 390}, {203, 321}, {41, 281}, {58, 348}, {175, 275}, {75, 185}, {385, 443}, {44, 219}, {148, 229}, {215, 477}, {338, 339}, {122, 430}};
}
void cSolver::connect()
{
// working copy of input
auto work = myInputPoints;
mySortedPoints.clear();
// select first point
cxy p = work[0];
// loop unitl all points connected
while (mySortedPoints.size() < work.size())
{
// output selected point
mySortedPoints.push_back(p);
// find nearest unconnected point
double mind = INT_MAX;
int mindex; // index of nearest point
for (int i = 1; i < work.size(); i++)
{
// ignore connected and self points
if (p.isValid() && ( ! ( p == work[i] )))
{
double d = p.dist2(work[i]);
if (d < mind)
{
mind = d;
mindex = i;
}
}
}
// select nearest point
p = work[mindex];
// prevemt it being reconsidered
work[mindex].invalidate();
}
}
Running on your sample data this produces
Complete app code at https://codeberg.org/JamesBremner/Polygonify
As @stef pointed out, if the polygon has a narrow neck like a sand timer then this algorithm may become confused by jumping across the narrow neck. This could be prevented by checking that all connections between dots do not intersect the 'drawn line' in the middle of the polygon.
I took a look at your code, but cannot spot where the drawn line specification are. So I guessed.
void cSolver::gen1()
{
// Verices of polygon in random order
// Example from https://stackoverflow.com/q/79696095/16582
// (159, 459),(133, 193),(286, 481),(241, 345),(411, 404),(280, 349),(352, 471),(395, 361),(85, 390),(203, 321),(41, 281),(58, 348),(175, 275),(75, 185),(385, 443),(44, 219),(148, 229),(215, 477),(338, 339),(122, 430)]]
myInputPoints = {{159, 459}, {133, 193}, {286, 481}, {241, 345}, {411, 404}, {280, 349}, {352, 471}, {395, 361}, {85, 390}, {203, 321}, {41, 281}, {58, 348}, {175, 275}, {75, 185}, {385, 443}, {44, 219}, {148, 229}, {215, 477}, {338, 339}, {122, 430}};
// polygon middle line
// my guess
myMiddleLine = {{100, 300}, {200, 400}, {300, 450}};
}
void cSolver::connect()
{
// working copy of input
auto work = myInputPoints;
mySortedPoints.clear();
// select first point
cxy p = work[0];
// loop unitl all points connected
while (mySortedPoints.size() < work.size())
{
// output selected point
mySortedPoints.push_back(p);
// find nearest unconnected point
double mind = INT_MAX;
int mindex; // index of nearest point
for (int i = 1; i < work.size(); i++)
{
// ignore previously connected points
if (!p.isValid())
continue;
// ignore self points
if (p == work[i])
continue;
// ignore connections that cross the middle line
bool cross = false;
cxy prev;
for (cxy &mp : myMiddleLine)
{
if (!prev.isValid())
{
prev = mp;
continue;
}
cxy intersect;
if (cxy::isIntersection(
intersect,
p, work[i], // test connection
prev, mp)) // middle line segment
{
cross = true;
break;
}
}
if( cross )
continue;
double d = p.dist2(work[i]);
if (d < mind)
{
mind = d;
mindex = i;
}
}
// select nearest point
p = work[mindex];
// prevent it being reconsidered
work[mindex].invalidate();
}
Minor notes:
This will produce clockwise and anticlockwise vertex orders at random - here is a test to find out which https://stackoverflow.com/a/1165943/16582
The code produces open polygon listings. To close simply add the first vertex to the end of the sorted list