import numpy as np
import matplotlib.pyplot as plt
import cv2
import csv

def read_csv(csv_path):
    """Reads a CSV file and organizes the points into paths."""
    np_path_XYs = np.genfromtxt(csv_path, delimiter=',')
    path_XYs = []
    for i in np.unique(np_path_XYs[:, 0]):
        npXYs = np_path_XYs[np_path_XYs[:, 0] == i][:, 1:]
        XYs = []
        for j in np.unique(npXYs[:, 0]):
            XY = npXYs[npXYs[:, 0] == j][:, 1:]
            XYs.append(XY)
        path_XYs.append(XYs)
    return path_XYs

def detect_shape(XY):
    """Detects the shape type from the provided points."""
    contour = np.array(XY, dtype=np.int32).reshape((-1, 1, 2))
    peri = cv2.arcLength(contour, True)
    approx = cv2.approxPolyDP(contour, 0.04 * peri, True)
    area = cv2.contourArea(contour)
    
    if area < 100:
        return " "

    if is_straight_line(XY):
        return "Straight Line"

    if len(approx) == 4:
        if is_square(contour):
            return "Square"
        else:
            return "Rectangle"

    elif len(approx) > 5:
        if is_star(approx):
            return "Star"
        
        circularity = 4 * np.pi * area / (peri * peri)
        
        if circularity > 0.95:
            return "Circle"
        elif 0.8 <= circularity < 0.95:
            ellipse = cv2.fitEllipse(contour)
            major_axis, minor_axis = max(ellipse[1]), min(ellipse[1])
            if major_axis / minor_axis > 1.05:
                return "Ellipse"
            else:
                return "Circle"
    
    return " "

def is_square(contour, tolerance=0.05):
    """Checks if the contour forms a square."""
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.intp(box)

    width = np.linalg.norm(box[0] - box[1])
    height = np.linalg.norm(box[1] - box[2])
    aspect_ratio = min(width, height) / max(width, height)

    contour_area = cv2.contourArea(contour)
    rect_area = width * height
    area_ratio = contour_area / rect_area

    is_square_shape = abs(1 - aspect_ratio) < tolerance
    is_filled = area_ratio > (1 - tolerance)

    return is_square_shape and is_filled

def is_star(approx):
    """Checks if the contour forms a star shape."""
    num_vertices = len(approx)
    if num_vertices > 8:
        angles = []
        for i in range(num_vertices):
            p1 = approx[i][0]
            p2 = approx[(i + 1) % num_vertices][0]
            p3 = approx[(i + 2) % num_vertices][0]
            angle = calculate_angle(p1, p2, p3)
            angles.append(angle)
        
        sharp_angles = [angle for angle in angles if angle < 60 or angle > 300]
        if len(sharp_angles) >= 5:
            return True

    return False

def calculate_angle(p1, p2, p3):
    """Calculates the angle between three points."""
    a = np.array(p1)
    b = np.array(p2)
    c = np.array(p3)
    ab = b - a
    cb = b - c
    cosine_angle = np.dot(ab, cb) / (np.linalg.norm(ab) * np.linalg.norm(cb))
    angle = np.degrees(np.arccos(cosine_angle))
    return angle

def is_straight_line(XY, tolerance=0.05):
    """Checks if the points form a straight line."""
    if len(XY) < 3:
        return True
    
    total_length = np.sum(np.sqrt(np.sum(np.diff(XY, axis=0)**2, axis=1)))
    start_to_end = np.linalg.norm(XY[-1] - XY[0])
    return (total_length - start_to_end) / total_length < tolerance

def regularize_circle(XY):
    """Regularizes the points to form a perfect circle."""
    xc, yc, r = kasa_circle_fitting(XY)
    t = np.linspace(0, 2 * np.pi, 100)
    x_fit = xc + r * np.cos(t)
    y_fit = yc + r * np.sin(t)
    return np.vstack((x_fit, y_fit)).T

def kasa_circle_fitting(XY):
    """Fits a circle to the points using Kasa's method."""
    x = XY[:, 0]
    y = XY[:, 1]
    A = np.array([x, y, np.ones_like(x)]).T
    b = x**2 + y**2
    c = np.linalg.lstsq(A, b, rcond=None)[0]
    xc = c[0] / 2
    yc = c[1] / 2
    r = np.sqrt(c[2] + xc**2 + yc**2)
    return xc, yc, r

def regularize_ellipse(XY):
    """Regularizes the points to form a perfect ellipse."""
    x_min, y_min = np.min(XY, axis=0)
    x_max, y_max = np.max(XY, axis=0)
    center = [(x_min + x_max) / 2, (y_min + y_max) / 2]
    width = x_max - x_min
    height = y_max - y_min
    t = np.linspace(0, 2 * np.pi, 200)
    x = center[0] + width/2 * np.cos(t)
    y = center[1] + height/2 * np.sin(t)
    
    return np.column_stack((x, y))

def regularize_rectangle(XY):
    """Regularizes the points to form a perfect rectangle."""
    x_min, y_min = np.min(XY, axis=0)
    x_max, y_max = np.max(XY, axis=0)

    regularized = np.array([
        [x_min, y_min],
        [x_max, y_min],
        [x_max, y_max],
        [x_min, y_max],
        [x_min, y_min]  
    ])
    
    return regularized

def regularize_square(XY):
    """Regularizes the points to form a perfect square."""
    x_min, y_min = np.min(XY, axis=0)
    x_max, y_max = np.max(XY, axis=0)

    side_length = min(x_max - x_min, y_max - y_min)

    reg_XY = np.array([
        [x_min, y_min],
        [x_min + side_length, y_min],
        [x_min + side_length, y_min + side_length],
        [x_min, y_min + side_length],
        [x_min, y_min]  
    ])
    
    return reg_XY

def regularize_star(XY, num_points=5):
    """Regularizes the points to form a perfect star."""
    centroid = np.mean(XY, axis=0)

    radii = np.linalg.norm(XY - centroid, axis=1)
    outer_radius = np.max(radii)
    inner_radius = np.min(radii)
    
    angles = np.linspace(0, 2*np.pi, 2*num_points, endpoint=False)
    
    star_points = []
    for i, angle in enumerate(angles):
        r = outer_radius if i % 2 == 0 else inner_radius
        x = centroid[0] + r * np.cos(angle)
        y = centroid[1] + r * np.sin(angle)
        star_points.append([x, y])
    
    star_points.append(star_points[0])  
    
    return np.array(star_points)

def regularize_straight_line(XY):
    """Regularizes the points to form a perfect straight line."""
    return np.array([XY[0], XY[-1]])

def detect_and_regularize_shape(XY):
    """Detects the shape and returns the regularized points."""
    if is_straight_line(XY):
        return "Straight Line", regularize_straight_line(XY)
    
    shape = detect_shape(XY)
    if shape == "Circle":
        regularized_XY = regularize_circle(XY)
    elif shape == "Ellipse":
        regularized_XY = regularize_ellipse(XY)
    elif shape == "Rectangle":
        regularized_XY = regularize_rectangle(XY)
    elif shape == "Square":
        regularized_XY = regularize_square(XY)
    elif shape == "Star":
        regularized_XY = regularize_star(XY)
    else:
        regularized_XY = XY
    return shape, regularized_XY

def calculate_symmetry_lines(XY, shape_type):
    """Calculates symmetry lines for the given shape."""
    if shape_type in ["Circle", "Ellipse", "Square", "Rectangle"]:
        centroid = np.mean(XY, axis=0)
        if shape_type == "Circle" or shape_type == "Square":
            return [
                ([centroid[0], np.min(XY[:, 1])], [centroid[0], np.max(XY[:, 1])]),  # Vertical
                ([np.min(XY[:, 0]), centroid[1]], [np.max(XY[:, 0]), centroid[1]])   # Horizontal
            ]
        elif shape_type == "Ellipse" or shape_type == "Rectangle":
            return [
                ([np.min(XY[:, 0]), centroid[1]], [np.max(XY[:, 0]), centroid[1]])   # Horizontal
            ]
    elif shape_type == "Star":
        centroid = np.mean(XY, axis=0)
        angles = np.linspace(0, 2*np.pi, 5, endpoint=False)
        lines = []
        for angle in angles:
            x = centroid[0] + np.cos(angle) * 1000
            y = centroid[1] + np.sin(angle) * 1000
            lines.append(([centroid[0], centroid[1]], [x, y]))
        return lines
    return []

def plot_shapes(path_XYs, regularized_XYs):
    """Plots original and regularized shapes with symmetry lines, and prints information about detected shapes."""
    fig, ax = plt.subplots(1, 2, tight_layout=True, figsize=(16, 8))
    colors = ['r', 'g', 'b', 'c', 'm', 'y', 'k']
    
    for a in ax:
        a.set_aspect('equal')

    # Print shapes detected and regularized in bold
    print("\033[1mShapes Detected and Regularized:\033[0m")

    for i, (orig_XYs, reg_XYs) in enumerate(zip(path_XYs, regularized_XYs)):
        color = colors[i % len(colors)]
        for XY, reg_XY in zip(orig_XYs, reg_XYs):
            shape_type, _ = detect_and_regularize_shape(XY)
            
            # Print the name of the detected shape
            print(f"- {shape_type}")

            ax[0].plot(XY[:, 0], XY[:, 1], c=color, linewidth=2)
            ax[1].plot(reg_XY[:, 0], reg_XY[:, 1], c=color, linewidth=2)
            
            # Draw symmetry lines and count them
            draw_symmetry_lines(ax[1], shape_type, reg_XY, 'k')

    ax[0].set_title('Original Shapes')
    ax[1].set_title('Regularized Shapes with Symmetry Lines')

    plt.show()

def draw_symmetry_lines(ax, shape, reg_XY, color):
    def plot_line(ax, start, end, color):
        ax.plot([start[0], end[0]], [start[1], end[1]], color=color, linestyle='--', linewidth=1)

    if shape == "Ellipse":
        x_min, y_min = np.min(reg_XY, axis=0)
        x_max, y_max = np.max(reg_XY, axis=0)
        center_x, center_y = (x_min + x_max) / 2, (y_min + y_max) / 2
        plot_line(ax, [x_min, center_y], [x_max, center_y], color)
        plot_line(ax, [center_x, y_min], [center_x, y_max], color)
        
        # Print number of symmetry lines
        print(f"  - {shape} has 2 lines of symmetry.")

    elif shape == "Rectangle":
        x_min, y_min = np.min(reg_XY, axis=0)
        x_max, y_max = np.max(reg_XY, axis=0)
        plot_line(ax, [(x_min + x_max)/2, y_min], [(x_min + x_max)/2, y_max], color)
        plot_line(ax, [x_min, (y_min + y_max)/2], [x_max, (y_min + y_max)/2], color)
        
        # Print number of symmetry lines
        print(f"  - {shape} has 2 lines of symmetry.")

    elif shape == "Star":
        num_points = len(reg_XY) // 2
        for i in range(num_points):
            start_point = reg_XY[i]
            end_point = reg_XY[(i + num_points) % len(reg_XY)]
            plot_line(ax, start_point, end_point, color)
        
        # Print number of symmetry lines
        print(f"  - {shape} has {num_points} lines of symmetry.")

    else:
        print(f"  - No specific symmetry lines defined for {shape}.")

def polylines2csv(paths_XYs, csv_path):
    """Writes the regularized shapes to a CSV file."""
    with open(csv_path, 'w', newline='') as f:
        writer = csv.writer(f)
        for path_XYs in paths_XYs:
            for i, XY in enumerate(path_XYs):
                for point in XY:
                    writer.writerow([i] + point.tolist())

def main():
    input_csv = "problems/isolated.csv"
    output_csv = "regularized_shapes.csv"
    path_XYs = read_csv(input_csv)
    regularized_XYs = []

    for path in path_XYs:
        reg_XYs = []
        for XY in path:
            _, reg_XY = detect_and_regularize_shape(XY)
            reg_XYs.append(reg_XY)
        regularized_XYs.append(reg_XYs)

    plot_shapes(path_XYs, regularized_XYs)
    polylines2csv(regularized_XYs, output_csv)

if __name__ == "__main__":
    main()

this is my old script and this is it's output
(base) kartik@Kartiks-MacBook-Air-2 MINOR % python -u "/Users/kartik/Desktop/CLG/MINOR/regularisation.py"
Shapes Detected and Regularized:
- Ellipse
  - Ellipse has 2 lines of symmetry.
- Star
  - Star has 5 lines of symmetry.
- Rectangle
  - Rectangle has 2 lines of symmetry.
2024-11-01 14:08:10.355 Python[60534:525359] +[IMKClient subclass]: chose IMKClient_Legacy
2024-11-01 14:08:10.355 Python[60534:525359] +[IMKInputSession subclass]: chose IMKInputSession_Legacy


then i tried some alternative algorithms in the following script

import numpy as np
import matplotlib.pyplot as plt
import cv2
import csv
import pandas as pd
from rdp import rdp
from scipy.interpolate import splprep, splev
from scipy.optimize import least_squares
import sys
import os


def read_csv(csv_path):
    """
    Reads a CSV file and organizes the points into paths.

    Parameters:
        csv_path (str): Path to the input CSV file.

    Returns:
        path_XYs (list): List of paths, each containing subpaths with their XY coordinates.
    """
    try:
        df = pd.read_csv(csv_path, header=None)
    except FileNotFoundError:
        print(f"Error: The file {csv_path} was not found.")
        sys.exit(1)
    except pd.errors.EmptyDataError:
        print(f"Error: The file {csv_path} is empty.")
        sys.exit(1)
    except pd.errors.ParserError:
        print(f"Error: The file {csv_path} could not be parsed.")
        sys.exit(1)

    # Assuming columns: PathID, SubpathID, X, Y
    path_XYs = []
    for path_id in df[0].unique():
        path_df = df[df[0] == path_id]
        subpaths = []
        for subpath_id in path_df[1].unique():
            subpath_df = path_df[path_df[1] == subpath_id]
            XY = subpath_df[[2, 3]].values  # X is column 2, Y is column 3
            if XY.shape[0] < 2:
                continue  # Ignore subpaths with insufficient points
            subpaths.append(XY)
        path_XYs.append(subpaths)
    return path_XYs


def polylines2csv(paths_XYs, shapes, csv_path):
    """
    Writes the regularized shapes and their types to a CSV file.

    Parameters:
        paths_XYs (list): List of paths containing regularized subpaths.
        shapes (list): List of shape types corresponding to each subpath.
        csv_path (str): Path to the output CSV file.
    """
    with open(csv_path, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(['PathID', 'SubpathID', 'ShapeType', 'X', 'Y'])
        for path_id, (path_XYs, shape_types) in enumerate(zip(paths_XYs, shapes)):
            for subpath_id, (reg_XY, shape_type) in enumerate(zip(path_XYs, shape_types)):
                for point in reg_XY:
                    writer.writerow([path_id, subpath_id, shape_type] + point.tolist())


def simplify_contour(XY, epsilon=2.0):
    """
    Simplifies the contour using the RDP algorithm.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        epsilon (float): Distance threshold for simplification.

    Returns:
        simplified_XY (ndarray): Simplified contour.
    """
    simplified_XY = rdp(XY, epsilon=epsilon)
    return simplified_XY


def smooth_contour(XY, s=2.0):
    """
    Smooths the contour using B-Splines.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        s (float): Smoothing factor.

    Returns:
        smooth_XY (ndarray): Smoothed contour.
    """
    try:
        tck, _ = splprep([XY[:,0], XY[:,1]], s=s)
        u = np.linspace(0, 1, 200)
        smooth_X, smooth_Y = splev(u, tck)
        smooth_XY = np.vstack((smooth_X, smooth_Y)).T
        return smooth_XY
    except Exception as e:
        print(f"Error in smoothing contour: {e}")
        return XY  # Return original if smoothing fails


def calculate_angle(p1, p2, p3):
    """
    Calculates the angle between three points.

    Parameters:
        p1, p2, p3 (list or ndarray): Three points [x, y].

    Returns:
        angle (float): Angle in degrees at point p2.
    """
    a = np.array(p1)
    b = np.array(p2)
    c = np.array(p3)
    ab = b - a
    cb = b - c
    norm_ab = np.linalg.norm(ab) + 1e-8
    norm_cb = np.linalg.norm(cb) + 1e-8
    cosine_angle = np.dot(ab, cb) / (norm_ab * norm_cb)
    angle = np.degrees(np.arccos(np.clip(cosine_angle, -1.0, 1.0)))
    return angle


def is_straight_line(XY, tolerance=0.02):
    """
    Checks if the points form a straight line.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        tolerance (float): Tolerance level.

    Returns:
        bool: True if straight line, else False.
    """
    if len(XY) < 3:
        return True

    # Fit a line using linear regression
    A = np.vstack([XY[:,0], np.ones(len(XY))]).T
    m, c = np.linalg.lstsq(A, XY[:,1], rcond=None)[0]
    # Calculate residuals
    residuals = XY[:,1] - (m * XY[:,0] + c)
    rms = np.sqrt(np.mean(residuals**2))
    # Normalize RMS by the bounding box height
    bbox_height = np.max(XY[:,1]) - np.min(XY[:,1])
    normalized_rms = rms / (bbox_height + 1e-8)
    return normalized_rms < tolerance


def is_square(contour, tolerance=0.05):
    """
    Checks if the contour forms a square.

    Parameters:
        contour (ndarray): Array of XY coordinates.
        tolerance (float): Aspect ratio tolerance.

    Returns:
        bool: True if square, else False.
    """
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.intp(box)

    width = np.linalg.norm(box[0] - box[1])
    height = np.linalg.norm(box[1] - box[2])
    aspect_ratio = min(width, height) / max(width, height)

    contour_area = cv2.contourArea(contour)
    rect_area = width * height
    area_ratio = contour_area / rect_area

    is_square_shape = abs(1 - aspect_ratio) < tolerance
    is_filled = area_ratio > (1 - tolerance)

    return is_square_shape and is_filled


def is_star(approx, tolerance=0.3):
    """
    Enhanced star detection by checking alternating edge lengths.

    Parameters:
        approx (ndarray): Approximated contour points.
        tolerance (float): Tolerance for edge length differences.

    Returns:
        bool: True if star, else False.
    """
    num_vertices = len(approx)
    if num_vertices != 10:  # Typically, a 5-point star has 10 vertices
        return False

    # Calculate edge lengths
    edge_lengths = [np.linalg.norm(approx[i][0] - approx[(i + 1) % num_vertices][0]) for i in range(num_vertices)]
    short_edges = edge_lengths[::2]
    long_edges = edge_lengths[1::2]

    # Check for significant difference between short and long edges
    for se, le in zip(short_edges, long_edges):
        if le < se * (1 + tolerance):
            return False
    return True


def detect_shape(XY):
    """
    Detects the shape type from the provided points.

    Parameters:
        XY (ndarray): Array of XY coordinates.

    Returns:
        shape (str): Detected shape type.
    """
    contour = np.array(XY, dtype=np.int32).reshape((-1, 1, 2))
    peri = cv2.arcLength(contour, True)
    approx = cv2.approxPolyDP(contour, 0.02 * peri, True)  # Reduced epsilon for better approximation
    area = cv2.contourArea(contour)

    if area < 100:
        return "Unknown"

    if is_straight_line(XY):
        return "Straight Line"

    if len(approx) == 4:
        if is_square(contour):
            return "Square"
        else:
            return "Rectangle"

    elif len(approx) == 10:
        if is_star(approx):
            return "Star"

    # Additional condition for circles and ellipses
    circularity = 4 * np.pi * area / (peri * peri + 1e-8)
    if circularity > 0.90:
        return "Circle"
    elif circularity > 0.75:
        return "Ellipse"

    return "Unknown"


def lm_circle_fitting(XY, initial_guess=(0, 0, 1)):
    """
    Fits a circle to the points using the Levenberg-Marquardt algorithm.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        initial_guess (tuple): Initial guess for (xc, yc, r).

    Returns:
        xc (float): X-coordinate of circle center.
        yc (float): Y-coordinate of circle center.
        r (float): Radius of the circle.
    """
    def residuals(params, x, y):
        xc, yc, r = params
        return np.sqrt((x - xc)**2 + (y - yc)**2) - r

    x = XY[:, 0]
    y = XY[:, 1]
    try:
        result = least_squares(residuals, initial_guess, args=(x, y), method='lm')
        xc, yc, r = result.x
        return xc, yc, r
    except Exception as e:
        print(f"LM Circle Fitting failed: {e}")
        return np.mean(XY[:,0]), np.mean(XY[:,1]), 1  # Fallback values


def regularize_circle(XY):
    """
    Regularizes the points to form a perfect circle using LM fitting.

    Parameters:
        XY (ndarray): Array of XY coordinates.

    Returns:
        regularized_XY (ndarray): Regularized circle coordinates.
    """
    initial_guess = (
        np.mean(XY[:,0]),
        np.mean(XY[:,1]),
        np.mean(np.linalg.norm(XY - np.mean(XY, axis=0), axis=1))
    )
    xc, yc, r = lm_circle_fitting(XY, initial_guess)
    t = np.linspace(0, 2 * np.pi, len(XY))  # Match the number of original points
    x_fit = xc + r * np.cos(t)
    y_fit = yc + r * np.sin(t)
    return np.vstack((x_fit, y_fit)).T


def regularize_ellipse(XY):
    """
    Regularizes the points to form a perfect ellipse based on fitted parameters.

    Parameters:
        XY (ndarray): Array of XY coordinates.

    Returns:
        regularized_XY (ndarray): Regularized ellipse coordinates.
    """
    try:
        ellipse = cv2.fitEllipse(np.array(XY, dtype=np.int32))
        center, axes, angle = ellipse
        a, b = axes  # Major and minor axes lengths
        theta = np.deg2rad(angle)
        t = np.linspace(0, 2 * np.pi, len(XY))  # Match number of points

        # Parametric equation of ellipse
        x_fit = center[0] + (a / 2) * np.cos(t) * np.cos(theta) - (b / 2) * np.sin(t) * np.sin(theta)
        y_fit = center[1] + (a / 2) * np.cos(t) * np.sin(theta) + (b / 2) * np.sin(t) * np.cos(theta)
        return np.vstack((x_fit, y_fit)).T
    except Exception as e:
        print(f"Ellipse fitting failed: {e}")
        return XY  # Return original if fitting fails


def regularize_rectangle(XY):
    """
    Regularizes the points to form a perfect rectangle.

    Parameters:
        XY (ndarray): Array of XY coordinates.

    Returns:
        regularized_XY (ndarray): Regularized rectangle coordinates.
    """
    try:
        rect = cv2.minAreaRect(np.array(XY, dtype=np.int32))
        box = cv2.boxPoints(rect)
        box = np.intp(box)

        # Order the points in a consistent order (clockwise starting from top-left)
        box = order_box_points(box)

        # Close the rectangle
        regularized_XY = np.vstack((box, box[0]))
        return regularized_XY
    except Exception as e:
        print(f"Rectangle fitting failed: {e}")
        return XY  # Return original if fitting fails


def regularize_square(XY):
    """
    Regularizes the points to form a perfect square.

    Parameters:
        XY (ndarray): Array of XY coordinates.

    Returns:
        regularized_XY (ndarray): Regularized square coordinates.
    """
    try:
        rect = cv2.minAreaRect(np.array(XY, dtype=np.int32))
        box = cv2.boxPoints(rect)
        box = np.intp(box)

        # Order the points
        box = order_box_points(box)

        # Calculate side lengths and adjust to make all sides equal (average side length)
        side_lengths = [
            np.linalg.norm(box[0] - box[1]),
            np.linalg.norm(box[1] - box[2]),
            np.linalg.norm(box[2] - box[3]),
            np.linalg.norm(box[3] - box[0])
        ]
        avg_length = np.mean(side_lengths)

        # Calculate direction vectors
        vectors = [
            (box[1] - box[0]) / (side_lengths[0] + 1e-8),
            (box[2] - box[1]) / (side_lengths[1] + 1e-8),
            (box[3] - box[2]) / (side_lengths[2] + 1e-8),
            (box[0] - box[3]) / (side_lengths[3] + 1e-8)
        ]

        # Adjust points based on average side length
        new_box = []
        for i in range(4):
            new_point = box[i] + vectors[i] * (avg_length - side_lengths[i]) / 2
            new_box.append(new_point)

        new_box = np.array(new_box, dtype=np.float32)

        # Re-order and close the square
        new_box = order_box_points(new_box)
        regularized_XY = np.vstack((new_box, new_box[0]))
        return regularized_XY
    except Exception as e:
        print(f"Square fitting failed: {e}")
        return XY  # Return original if fitting fails


def regularize_star(XY, num_points=5):
    """
    Regularizes the points to form a perfect star.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        num_points (int): Number of star points.

    Returns:
        star_points (ndarray): Regularized star coordinates.
    """
    centroid = np.mean(XY, axis=0)

    # Define alternating angles for outer and inner points
    angles = np.linspace(0, 2 * np.pi, num_points * 2, endpoint=False)
    
    # Calculate distances from centroid
    distances = np.linalg.norm(XY - centroid, axis=1)
    outer_radius = np.percentile(distances, 90)  # More robust percentile
    inner_radius = np.percentile(distances, 50)
    
    star_points = []
    for i, angle in enumerate(angles):
        r = outer_radius if i % 2 == 0 else inner_radius
        x = centroid[0] + r * np.cos(angle)
        y = centroid[1] + r * np.sin(angle)
        star_points.append([x, y])

    star_points.append(star_points[0])  # Close the star
    return np.array(star_points)


def regularize_straight_line(XY):
    """
    Regularizes the points to form a perfect straight line.

    Parameters:
        XY (ndarray): Array of XY coordinates.

    Returns:
        line_XY (ndarray): Regularized straight line coordinates.
    """
    return np.array([XY[0], XY[-1]])


def order_box_points(box):
    """
    Orders the box points in clockwise order starting from the top-left.

    Parameters:
        box (ndarray): Array of four points.

    Returns:
        ordered_box (ndarray): Ordered array of four points.
    """
    rect = cv2.minAreaRect(box)
    box = cv2.boxPoints(rect)
    box = np.intp(box)

    # Initialize a list to hold the ordered points
    ordered_box = np.zeros((4, 2), dtype="int")

    # Sum and difference of points for ordering
    s = box.sum(axis=1)
    diff = np.diff(box, axis=1)

    ordered_box[0] = box[np.argmin(s)]      # Top-left has the smallest sum
    ordered_box[2] = box[np.argmax(s)]      # Bottom-right has the largest sum
    ordered_box[1] = box[np.argmin(diff)]   # Top-right has the smallest difference
    ordered_box[3] = box[np.argmax(diff)]   # Bottom-left has the largest difference

    return ordered_box


def detect_symmetry_lines_hough(reg_XY, shape_type, min_line_length_ratio=0.4, max_line_gap=20):
    """
    Detects symmetry lines using the Hough Transform within the bounds of the shape.

    Parameters:
        reg_XY (ndarray): Regularized XY coordinates.
        shape_type (str): Type of the detected shape.
        min_line_length_ratio (float): Minimum line length as a ratio of the shape's maximum dimension.
        max_line_gap (int): Maximum allowed gap between line segments.

    Returns:
        symmetry_lines (list): List of symmetry lines represented as tuples of start and end points.
    """
    # Normalize coordinates to positive range
    x_min, y_min = np.min(reg_XY, axis=0)
    x_max, y_max = np.max(reg_XY, axis=0)
    padding = 10
    x_min_p, y_min_p = int(x_min) - padding, int(y_min) - padding
    x_max_p, y_max_p = int(x_max) + padding, int(y_max) + padding
    width = x_max_p - x_min_p
    height = y_max_p - y_min_p

    # Create a blank image
    img = np.zeros((height, width), dtype=np.uint8)

    # Shift coordinates to positive range
    XY_shifted = reg_XY - np.array([x_min_p, y_min_p])
    XY_int = np.round(XY_shifted).astype(np.int32)

    # Draw the contour on the image
    cv2.polylines(img, [XY_int], isClosed=True, color=255, thickness=2)

    # Apply Gaussian Blur to reduce noise
    img_blur = cv2.GaussianBlur(img, (5, 5), 0)

    # Determine minimum line length based on shape size
    max_dim = max(width, height)
    min_line_length = int(max_dim * min_line_length_ratio)

    # Apply Hough Transform
    lines = cv2.HoughLinesP(img_blur, 1, np.pi/180, threshold=80, minLineLength=min_line_length, maxLineGap=max_line_gap)
    symmetry_lines = []
    if lines is not None:
        for line in lines:
            x1, y1, x2, y2 = line[0]
            # Shift back to original coordinates
            symmetry_lines.append( ([x1 + x_min_p, y1 + y_min_p], [x2 + x_min_p, y2 + y_min_p]) )
    return symmetry_lines


def detect_symmetry_fourier(XY, num_symmetries=5):
    """
    Detects reflective symmetry lines using Fourier Descriptor Analysis.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        num_symmetries (int): Number of expected symmetry lines.

    Returns:
        lines (list): List of symmetry lines represented as tuples of start and end points.
    """
    # Normalize the contour
    XY_centered = XY - np.mean(XY, axis=0)
    norm = np.max(np.linalg.norm(XY_centered, axis=1)) + 1e-8
    XY_normalized = XY_centered / norm

    # Convert to complex numbers
    contour_complex = XY_normalized[:,0] + 1j * XY_normalized[:,1]

    # Perform FFT
    descriptors = np.fft.fft(contour_complex)

    # Placeholder: Assuming num_symmetries lines equally spaced
    centroid = np.mean(XY, axis=0)
    lines = []
    for n in range(num_symmetries):
        angle = 2 * np.pi * n / num_symmetries
        x_dir = np.cos(angle)
        y_dir = np.sin(angle)

        # Determine the extent of the line based on shape size
        radius = np.max(np.linalg.norm(XY - centroid, axis=1))
        length = radius * 1.0  # Adjust as necessary

        x_start = centroid[0] - x_dir * length
        y_start = centroid[1] - y_dir * length
        x_end = centroid[0] + x_dir * length
        y_end = centroid[1] + y_dir * length

        lines.append( ([x_start, y_start], [x_end, y_end]) )
    return lines


def calculate_symmetry_lines(XY, shape_type):
    """
    Calculates symmetry lines for the given shape using Hough Transform and Fourier Descriptors.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        shape_type (str): Type of the detected shape.

    Returns:
        symmetry_lines (list): List of symmetry lines represented as tuples of start and end points.
    """
    symmetry_lines = []
    if shape_type in ["Circle", "Ellipse", "Square", "Rectangle"]:
        centroid = np.mean(XY, axis=0)
        if shape_type in ["Circle", "Square"]:
            # Vertical and Horizontal symmetry lines
            vertical = ([centroid[0], np.min(XY[:,1])], [centroid[0], np.max(XY[:,1])])
            horizontal = ([np.min(XY[:,0]), centroid[1]], [np.max(XY[:,0]), centroid[1]])
            symmetry_lines.extend([vertical, horizontal])
        elif shape_type in ["Ellipse", "Rectangle"]:
            # Only Horizontal symmetry line
            horizontal = ([np.min(XY[:,0]), centroid[1]], [np.max(XY[:,0]), centroid[1]])
            symmetry_lines.append(horizontal)

        # Hough Transform symmetry lines
        hough_lines = detect_symmetry_lines_hough(XY, shape_type)
        symmetry_lines.extend(hough_lines)

        # Fourier Descriptor-based symmetry lines (exclude circles to avoid infinite lines)
        if shape_type != "Circle":
            fourier_lines = detect_symmetry_fourier(XY)
            symmetry_lines.extend(fourier_lines)

    elif shape_type == "Star":
        num_points = 5  # Assuming a 5-point star
        centroid = np.mean(XY, axis=0)
        for i in range(num_points):
            angle = 2 * np.pi * i / num_points
            x_dir = np.cos(angle)
            y_dir = np.sin(angle)
            # Determine the extent of the line based on shape size
            radius = np.max(np.linalg.norm(XY - centroid, axis=1))
            length = radius * 1.0  # Adjust as necessary
            x_start = centroid[0] - x_dir * length
            y_start = centroid[1] - y_dir * length
            x_end = centroid[0] + x_dir * length
            y_end = centroid[1] + y_dir * length
            symmetry_lines.append( ([x_start, y_start], [x_end, y_end]) )

    elif shape_type == "Straight Line":
        # The line itself is the symmetry line
        symmetry_lines.append( ([XY[0][0], XY[0][1]], [XY[-1][0], XY[-1][1]]) )

    return symmetry_lines


def detect_and_regularize_shape(XY, rdp_epsilon=1.5, spline_s=1.0):
    """
    Detects the shape and returns the regularized points after simplification and smoothing.

    Parameters:
        XY (ndarray): Array of XY coordinates.
        rdp_epsilon (float): Epsilon parameter for RDP.
        spline_s (float): Smoothing factor for B-Splines.

    Returns:
        shape (str): Detected shape type.
        regularized_XY (ndarray): Regularized XY coordinates.
    """
    simplified_XY = simplify_contour(XY, epsilon=rdp_epsilon)
    smooth_XY = smooth_contour(simplified_XY, s=spline_s)

    shape = detect_shape(smooth_XY)
    if shape == "Circle":
        regularized_XY = regularize_circle(smooth_XY)
    elif shape == "Ellipse":
        regularized_XY = regularize_ellipse(smooth_XY)
    elif shape == "Rectangle":
        regularized_XY = regularize_rectangle(smooth_XY)
    elif shape == "Square":
        regularized_XY = regularize_square(smooth_XY)
    elif shape == "Star":
        regularized_XY = regularize_star(smooth_XY)
    elif shape == "Straight Line":
        regularized_XY = regularize_straight_line(smooth_XY)
    else:
        regularized_XY = smooth_XY  # If unknown, keep the smoothed contour

    return shape, regularized_XY


def plot_shapes(path_XYs, regularized_XYs, shapes):
    """
    Plots original and regularized shapes with symmetry lines, and prints information about detected shapes.

    Parameters:
        path_XYs (list): List of original paths.
        regularized_XYs (list): List of regularized paths.
        shapes (list): List of detected shape types.
    """
    fig, axes = plt.subplots(1, 2, figsize=(18, 9))
    ax_orig, ax_reg = axes
    ax_orig.set_title('Original Shapes')
    ax_reg.set_title('Regularized Shapes with Symmetry Lines')

    colors = plt.cm.get_cmap('tab20', 20).colors  # Supports up to 20 distinct colors

    print("\033[1mShapes Detected and Regularized:\033[0m")

    for path_id, (orig_subpaths, reg_subpaths, shape_types) in enumerate(zip(path_XYs, regularized_XYs, shapes)):
        color = colors[path_id % len(colors)]
        for subpath_id, (orig_XY, reg_XY, shape_type) in enumerate(zip(orig_subpaths, reg_subpaths, shape_types)):
            print(f"Path {path_id}, Subpath {subpath_id}: {shape_type}")

            # Plot original shape
            ax_orig.plot(orig_XY[:,0], orig_XY[:,1], color=color, linewidth=1.5, label=f'Path {path_id}' if subpath_id == 0 else "")

            # Plot regularized shape
            ax_reg.plot(reg_XY[:,0], reg_XY[:,1], color=color, linewidth=2, label=f'Path {path_id}' if subpath_id == 0 else "")

            # Calculate and plot symmetry lines
            symmetry_lines = calculate_symmetry_lines(reg_XY, shape_type)
            for line in symmetry_lines:
                (x_start, y_start), (x_end, y_end) = line
                ax_reg.plot([x_start, x_end], [y_start, y_end], color='k', linestyle='--', linewidth=1)

    ax_orig.set_aspect('equal', adjustable='datalim')
    ax_reg.set_aspect('equal', adjustable='datalim')

    # Handle legends
    handles_orig, labels_orig = ax_orig.get_legend_handles_labels()
    by_label_orig = dict(zip(labels_orig, handles_orig))
    ax_orig.legend(by_label_orig.values(), by_label_orig.keys())

    handles_reg, labels_reg = ax_reg.get_legend_handles_labels()
    by_label_reg = dict(zip(labels_reg, handles_reg))
    ax_reg.legend(by_label_reg.values(), by_label_reg.keys())

    plt.show()


def main():
    """
    Main function to execute the shape regularization and symmetry detection workflow.
    """
    # Configuration Parameters
    input_csv = "problems/isolated.csv"        # Input CSV path
    output_csv = "regularized_shapes.csv"      # Output CSV path
    rdp_epsilon = 1.5                           # RDP simplification parameter (reduced to prevent oversimplification)
    spline_s = 1.0                              # B-Spline smoothing factor (reduced for better shape fidelity)

    if not os.path.exists(input_csv):
        print(f"Error: Input file {input_csv} does not exist.")
        sys.exit(1)

    # Read input CSV
    path_XYs = read_csv(input_csv)

    regularized_XYs = []
    shapes_detected = []

    # Process each path and subpath
    for path in path_XYs:
        reg_path = []
        shape_types = []
        for XY in path:
            shape, reg_XY = detect_and_regularize_shape(XY, rdp_epsilon=rdp_epsilon, spline_s=spline_s)
            reg_path.append(reg_XY)
            shape_types.append(shape)
        regularized_XYs.append(reg_path)
        shapes_detected.append(shape_types)

    # Plot the results
    plot_shapes(path_XYs, regularized_XYs, shapes_detected)

    # Write regularized shapes to CSV
    polylines2csv(regularized_XYs, shapes_detected, output_csv)
    print(f"\nRegularized shapes have been saved to {output_csv}")


if __name__ == "__main__":
    main()

and the output was 
Shapes Detected and Regularized:
Path 0, Subpath 0: Circle
Path 1, Subpath 0: Unknown
Path 2, Subpath 0: Unknown
2024-11-01 14:08:59.604 Python[60990:527171] +[IMKClient subclass]: chose IMKClient_Legacy
2024-11-01 14:08:59.604 Python[60990:527171] +[IMKInputSession subclass]: chose IMKInputSession_Legacy

as you can see the new script doesn't even identify the shapes 
what is the issue
help me out please please