pythongeometryshapesshapely

Star-center of a star convex shape


I'm working with 2D shapes represented by their boundary contours (as ordered x,y coordinates), and I want to check if a shape is star-convex. If it is, I'd like to find a star center — i.e., a point from which the entire shape is visible (meaning: every line segment to every boundary point lies entirely within the shape).?

Is there an elaborate non heuristic way of doing so?

If there is already a python code for this then it would be even better :)

This is what I have tried so far: I implemented a heuristic approach based on a midpoint visibility test. The idea is:

A point is a valid star center if, for every boundary point, the midpoint of the line segment between them lies inside the polygon. I sample candidate points starting from the centroid, perturb them randomly within the bounding box, and check if one satisfies the condition. but this doesn't really guarantee finding the star-center or checking if it star-convex. There's also no guarantee that random sampling will find the right region (kernel) in the first place.

def is_star_convex_and_get_center(x, y, n_samples=500):
    coords = np.column_stack((x, y))
    poly = Polygon(coords)

    if not poly.is_valid or poly.area == 0:
        print("Invalid shape.")
        return None

    # Try centroid first
    candidates = [np.mean(coords, axis=0)]

    # Add some random points around the centroid
    bbox = poly.bounds
    scale = max(bbox[2] - bbox[0], bbox[3] - bbox[1])
    for _ in range(n_samples - 1):
        offset = (np.random.rand(2) - 0.5) * scale
        candidates.append(candidates[0] + offset)

    # Check visibility condition for each candidate
    for pt in candidates:
        visible = True
        for px, py in coords:
            mid = 0.5 * (pt + np.array([px, py]))
            if not poly.contains(Point(mid)):
                visible = False
                break
        if visible:
            return pt  # pt is a valid star center

    # No valid star center found
    return None

Solution

  • I think you can do this by linear-programming.

    enter image description here

    Every CONVEX corner of your figure will give you a triangular wedge in which any solution must lie. In this wedge the X,Y point which is hopefully the centre must differ from the corner node by a combination of positive multiples of the two side vectors. i.e.

    enter image description here

    or

    enter image description here

    You then have to solve a matrix equation for X, Y and all the ak and bk, subject to all a's and b's being positive. This is exactly what scipy.optimize.linprog does: see https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linprog.html

    In the following code IT IS ASSUMED THAT THE FIGURE IS TRACED ANTICLOCKWISE!!!

    import numpy as np
    from scipy.optimize import linprog
    import matplotlib.pyplot as plt
    
    
    def starConvex( x, y ):
        if x[0] != x[-1] or y[0] != y[-1]:     # close the loop if necessary
            x.append( x[0] )
            y.append( y[0] )
        N = len( x ) - 1
        
        # Count convex corners
        Nconvex = 0
        for i in range( N ):
            i0, im, ip = i, i - 1, i + 1
            if im < 0: im = N - 1
            dxlow , dylow  = x[im] - x[i0], y[im] - y[i0]
            dxhigh, dyhigh = x[ip] - x[i0], y[ip] - y[i0]
            if dxhigh * dylow - dxlow * dyhigh >= 0: Nconvex += 1
    
        # Set up matrices for linear programming
        A = np.zeros( ( 2 * Nconvex, 2 * Nconvex + 2 ) )
        b = np.zeros(   2 * Nconvex                    )
        c = np.ones(                 2 * Nconvex + 2   );   c[0] = c[1] = 0
        k = 0
        for i in range( N ):
            i0, im, ip = i, i - 1, i + 1
            if im < 0: im = N - 1
            dxlow , dylow  = x[im] - x[i0], y[im] - y[i0]
            dxhigh, dyhigh = x[ip] - x[i0], y[ip] - y[i0]
            if dxhigh * dylow - dxlow * dyhigh >= 0: 
                A[2*k  ,0    ] = 1
                A[2*k+1,1    ] = 1
                A[2*k  ,2*k+2] = -dxlow
                A[2*k  ,2*k+3] = -dxhigh
                A[2*k+1,2*k+2] = -dylow
                A[2*k+1,2*k+3] = -dyhigh
                b[2*k        ] = x[i0]
                b[2*k+1      ] = y[i0]
                k += 1
    
        bounds=[(None,None),(None,None)]
        for _ in range( 2*Nconvex ): bounds.append( (0,None) )
    
        res = linprog( c, A_eq=A, b_eq=b, bounds=bounds )
        values = res.x
        if res.success:
            return True, values[0], values[1]
        else:
            return False, None, None
    
    
    xpts = [  0.0,  1.0, 3.0, 1.0,  0.0, -1.0, -3.0, -1.0 ]
    ypts = [ -3.0, -1.0, 0.0, 1.0,  3.0,  1.0,  0.0, -1.0 ]         # This succeeds
    #ypts = [ -3.0, -1.0, 0.0, 1.0, -1.0,  1.0,  0.0, -1.0 ]         # This succeeds
    #ypts = [ -3.0, -1.0, 0.0, 1.0, -2.0,  1.0,  0.0, -1.0 ]         # This fails
    
    test, x0, y0 = starConvex( xpts, ypts )
    if test:
        print( "SUCCESS! x0, y0 = ", x0, y0 )
        plt.plot( [x0], [y0], 'ro' )
    else:
        print( "FAILURE" )
    
    plt.plot( xpts, ypts, 'b' )
    plt.show()
    

    Here is the first case, which works OK (but you can't guarantee which feasible point will be chose as "star centre"; one possibility is shown in red.)

    enter image description here

    Below is the last case, where there is no point from which you can see the whole of the star.

    enter image description here