hey 
so i was given a task of regularising and finding symmetry 
now i made two scripts
the 1st one is very good when it comes to shape detection and symmetry and the second one is good with segements as segments too form a shape sometimes
so i combined them but now the symmetry is not working properly please help
this is my 1st script
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 2nd script
from svgpathtools import svg2paths, Path, Line, CubicBezier, QuadraticBezier, Arc
import numpy as np
import csv
from collections import defaultdict
from typing import List, Tuple
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.distance import cdist
import heapq
from scipy.spatial.distance import cdist
import math
from collections import defaultdict
import heapq
class CSVProcessor:
"""
Processes and loads data points from a CSV file.
"""


def __init__(self, csv_file: str):
    """
    Initializes with the CSV file path.
    """
    self.csv_file = csv_file
    self.all_points = []

def load_points_from_csv(self):
    """
    Loads data points from the CSV file into all_points.
    """
    with open(self.csv_file, newline='') as csvfile:
        reader = csv.reader(csvfile)
        for row in reader:
            curve_index = row[0]
            x = float(row[2])
            y = float(row[3])
            self.all_points.append((curve_index, 0.0000, x, y))
class CurveProcessor:
"""
Processes curves for simplification and refinement based on given parameters.
"""


def __init__(self, curves: dict[int, List[Tuple[float, float]]], epsilon: float = 5.0, threshold: float = 5.0):
    """
    Initializes with curves, epsilon for simplification, and threshold for endpoint adjustment.
    """
    self.curves = curves
    self.epsilon = epsilon
    self.threshold = threshold
    self.simplified_curves = {}
    self.inverse_dict = {}
    self.segment_points_dict = {}
    self.updated_curves = {}

def point_line_distance(self, point, start, end):
    """
    Calculates the perpendicular distance from a point to a line segment.
    """
    point = np.array(point)
    start = np.array(start)
    end = np.array(end)

    if np.array_equal(start, end):
        return np.linalg.norm(point - start)
    else:
        n = abs((end[1] - start[1]) * point[0] - (end[0] - start[0]) * point[1] + end[0] * start[1] - end[1] * start[0])
        d = np.linalg.norm(end - start)
        return n / d

def ramer_douglas_peucker(self, points, epsilon):
    """
    Simplifies a list of points using the Ramer-Douglas-Peucker algorithm.
    """
    dmax = 0.0
    index = 0
    end = len(points)
    for i in range(1, end - 1):
        d = self.point_line_distance(points[i], points[0], points[-1])
        if d > dmax:
            index = i
            dmax = d

    if dmax > epsilon:
        rec_results1, idx1 = self.ramer_douglas_peucker(points[:index+1], epsilon)
        rec_results2, idx2 = self.ramer_douglas_peucker(points[index:], epsilon)
        result = rec_results1[:-1] + rec_results2
        result_indices = idx1[:-1] + [i + index for i in idx2]
    else:
        result = [points[0], points[-1]]
        result_indices = [0, len(points) - 1]

    return result, result_indices

def simplify_curves(self):
    """
    Simplifies all curves and stores segment information.
    """
    simplified_curves = {}
    inverse_dict = {}
    segment_points_dict = {}
    segment_id = 0

    for index, points in self.curves.items():
        simplified_points, indices = self.ramer_douglas_peucker(points, self.epsilon)

        for i in range(len(simplified_points) - 1):
            start_point = tuple(simplified_points[i])
            end_point = tuple(simplified_points[i + 1])
            line_key = (start_point, end_point)

            segment_indices = indices[i:i+2]
            segment_start_idx = segment_indices[0]
            segment_end_idx = segment_indices[1]

            simplified_curves[segment_id] = [start_point, end_point]
            inverse_dict[line_key] = segment_id
            segment_points_dict[segment_id] = points[segment_start_idx:segment_end_idx+1]

            segment_id += 1

    self.simplified_curves = simplified_curves
    self.inverse_dict = inverse_dict
    self.segment_points_dict = segment_points_dict

def distance(self, point1, point2):
    """
    Computes the Euclidean distance between two points.
    """
    return np.linalg.norm(np.array(point1) - np.array(point2))

def midpoint(self, point1, point2):
    """
    Returns the midpoint between two points.
    """
    return ((point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2)

def round_point(self, point):
    """
    Rounds the coordinates of a point to one decimal place.
    """
    return (round(point[0], 1), round(point[1], 1))

def update_endpoints_with_midpoints(self):
    """
    Adjusts segment endpoints based on proximity and rounds the results.
    """
    endpoints = {}
    updated_curves = {k: v[:] for k, v in self.simplified_curves.items()}
    inverse_dict = {}

    for key, points in self.simplified_curves.items():
        endpoints[key] = (points[0], points[-1])

    for key, (start, end) in endpoints.items():
        if self.distance(start, end) < self.threshold:
            mid = self.midpoint(start, end)
            updated_curves[key][0] = mid
            updated_curves[key][-1] = mid

    for key1, (start1, end1) in endpoints.items():
        for key2, (start2, end2) in endpoints.items():
            if key1 < key2:
                if self.distance(start1, start2) < self.threshold:
                    mid = self.midpoint(start1, start2)
                    updated_curves[key1][0] = mid
                    updated_curves[key2][0] = mid
                if self.distance(start1, end2) < self.threshold:
                    mid = self.midpoint(start1, end2)
                    updated_curves[key1][0] = mid
                    updated_curves[key2][-1] = mid
                if self.distance(end1, start2) < self.threshold:
                    mid = self.midpoint(end1, start2)
                    updated_curves[key1][-1] = mid
                    updated_curves[key2][0] = mid
                if self.distance(end1, end2) < self.threshold:
                    mid = self.midpoint(end1, end2)
                    updated_curves[key1][-1] = mid
                    updated_curves[key2][-1] = mid

    for key, points in updated_curves.items():
        updated_curves[key] = [self.round_point(point) for point in points]

    for index, points in updated_curves.items():
        for i in range(len(points) - 1):
            start_point = tuple(points[i])
            end_point = tuple(points[i + 1])
            line_key = (start_point, end_point)
            inverse_dict[line_key] = index

    self.updated_curves = updated_curves
    self.inverse_dict = inverse_dict

def plot_segments(self, curves, inverse_dict, title):
    """
    Plots line segments with labels.
    """
    plt.figure(figsize=(10, 6))
    for (start, end), segment_id in inverse_dict.items():
        x = [start[0], end[0]]
        y = [start[1], end[1]]
        plt.plot(x, y, marker='o', label=f'Segment {segment_id}')
    plt.title(title)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.legend()
    plt.grid(True)
    plt.show()

def plot_points(self, curves, title):
    """
    Plots points for all curves.
    """
    plt.figure(figsize=(10, 6))
    for points in curves.values():
        x, y = zip(*points)
        plt.plot(x, y, marker='o', linestyle='None')
    plt.title(title)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.grid(True)
    plt.show()

def process(self):
    """
    Executes curve simplification and endpoint updates.
    """
    self.simplify_curves()
    self.update_endpoints_with_midpoints()
class CycleDetector:
"""Detects cycles in a set of updated curves."""


def __init__(self, updated_curves: dict[int, List[Tuple[float, float]]]):
    """Initialize with updated curves and construct adjacency list."""
    self.adj_list, self.segments = self.construct_adj_list(updated_curves)

def construct_adj_list(self, updated_curves):
    """Create an adjacency list and segment list from the updated curves."""
    adj_list = defaultdict(list)
    segments = []
    for polyline in updated_curves.values():
        for i in range(len(polyline) - 1):
            start, end = polyline[i], polyline[i + 1]
            adj_list[start].append(end)
            adj_list[end].append(start)
            segments.append((start, end))
    return adj_list, segments

def find_cycles(self, graph):
    """Find all cycles in the graph using depth-first search (DFS)."""
    def dfs(node, start, visited, path):
        visited[node] = True
        path.append(node)

        for neighbor in graph[node]:
            if neighbor == start and len(path) > 2:
                cycle = path[:] + [start]
                cycles.append(cycle)
            elif not visited[neighbor]:
                dfs(neighbor, start, visited, path)

        path.pop()
        visited[node] = False

    cycles = []
    visited = defaultdict(bool)

    for node in graph:
        if not visited[node]:
            dfs(node, node, visited, [])

    unique_cycles = []
    for cycle in cycles:
        cycle_set = set(cycle)
        if all(cycle_set != set(c) for c in unique_cycles):
            unique_cycles.append(cycle)

    return unique_cycles

def separate_non_cycle_lines(self, segments, cycles):
    """Separate non-cycle lines from the segments."""
    cycle_edges = set()
    for cycle in cycles:
        for i in range(len(cycle) - 1):
            edge = tuple(sorted([cycle[i], cycle[i + 1]]))
            cycle_edges.add(edge)

    non_cycle_lines = []
    for start, end in segments:
        edge = tuple(sorted([start, end]))
        if edge not in cycle_edges:
            non_cycle_lines.append((start, end))

    return non_cycle_lines

def process_cycles(self):
    """Detect cycles and separate non-cycle lines."""
    cycles = self.find_cycles(self.adj_list)
    non_cycle_lines = self.separate_non_cycle_lines(self.segments, cycles)
    return cycles, non_cycle_lines
class CircleDetector:
"""Detects circles that best fit polygons and identifies remaining sides."""


def __init__(self, unique_cycles):
    """Initialize with a list of unique cycles.

    Args:
        unique_cycles (list): List of polygons where each polygon is a list of points.
    """
    self.unique_cycles = unique_cycles
    self.remaining_sides = set()
    self.marked_sides = set()

def point_to_segment_dist(self, p, v, w):
    """Compute the squared distance from point p to the segment vw.

    Args:
        p (np.array): Point for which distance is to be computed.
        v (np.array): One endpoint of the segment.
        w (np.array): The other endpoint of the segment.

    Returns:
        float: Squared distance from p to the segment vw.
    """
    l2 = np.sum((v - w) ** 2)
    if l2 == 0:
        return np.sum((p - v) ** 2)
    t = max(0, min(1, np.dot(p - v, w - v) / l2))
    projection = v + t * (w - v)
    return np.sum((p - projection) ** 2)

def mean_square_circle_error(self, polygon, center, radius, num_points=10):
    """Calculate the mean squared error of fitting a circle to a polygon.

    Args:
        polygon (list): List of points representing the polygon.
        center (np.array): Center of the circle.
        radius (float): Radius of the circle.
        num_points (int): Number of points to sample per segment.

    Returns:
        float: Mean squared error of the circle fit.
    """
    total_error = 0
    total_samples = 0

    for i in range(len(polygon)):
        p1 = polygon[i]
        p2 = polygon[(i + 1) % len(polygon)]

        segment_points = np.linspace(p1, p2, num_points)

        for point in segment_points:
            closest_point = self.closest_point_on_circle(point, center, radius)
            dist = np.linalg.norm(point - closest_point)
            error = dist ** 2
            total_error += error

        total_samples += len(segment_points)

    mse = total_error / total_samples
    return mse

def closest_point_on_circle(self, point, center, radius):
    """Find the closest point on the circle to a given point.

    Args:
        point (np.array): Point for which closest circle point is computed.
        center (np.array): Center of the circle.
        radius (float): Radius of the circle.

    Returns:
        np.array: Closest point on the circle to the given point.
    """
    direction = point - center
    direction /= np.linalg.norm(direction)
    return center + direction * radius

def best_fit_circle(self, polygon):
    """Find the best fit circle for a polygon using random sampling.

    Args:
        polygon (list): List of points representing the polygon.

    Returns:
        tuple: Center (np.array), radius (float), and mean squared error (float) of the best fit circle.
    """
    polygon = np.array(polygon)

    best_mse = float('inf')
    best_center = None
    best_radius = None

    for _ in range(50):
        sample_indices = np.random.choice(len(polygon), 3, replace=False)
        sample_points = polygon[sample_indices]

        A = sample_points[0]
        B = sample_points[1]
        C = sample_points[2]

        D = 2 * (A[0] * (B[1] - C[1]) + B[0] * (C[1] - A[1]) + C[0] * (A[1] - B[1]))
        if D == 0:
            continue

        Ux = ((A[0]**2 + A[1]**2) * (B[1] - C[1]) + (B[0]**2 + B[1]**2) * (C[1] - A[1]) + (C[0]**2 + C[1]**2) * (A[1] - B[1])) / D
        Uy = ((A[0]**2 + A[1]**2) * (C[0] - B[0]) + (B[0]**2 + B[1]**2) * (A[0] - C[0]) + (C[0]**2 + C[1]**2) * (B[0] - A[0])) / D
        center = np.array([Ux, Uy])
        radius = np.linalg.norm(center - A)

        mse = self.mean_square_circle_error(polygon, center, radius)
        if mse < best_mse:
            best_mse = mse
            best_center = center
            best_radius = radius

    return best_center, best_radius, best_mse

def plot_polygon_and_circle(self, polygon, center, radius, label):
    """Plot the polygon and the best fit circle.

    Args:
        polygon (list): List of points representing the polygon.
        center (np.array): Center of the circle.
        radius (float): Radius of the circle.
        label (str): Label for the circle in the plot.
    """
    polygon = np.array(polygon)
    theta = np.linspace(0, 2 * np.pi, 100)
    circle_x = center[0] + radius * np.cos(theta)
    circle_y = center[1] + radius * np.sin(theta)

    plt.plot(circle_x, circle_y, 'r--', label=label)
    plt.scatter(*center, color='green', zorder=5, label='Center')

def plot_remaining_sides(self, unique_cycles, marked_sides):
    """Plot remaining sides of polygons that are not marked.

    Args:
        unique_cycles (list): List of polygons where each polygon is a list of points.
        marked_sides (set): Set of marked sides.
    """
    for polygon in unique_cycles:
        polygon = np.array(polygon)
        for i in range(len(polygon)):
            side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
            if side not in marked_sides:
                plt.plot(*np.array([polygon[i], polygon[(i + 1) % len(polygon)]]).T, 'k-', label='Remaining Side' if i == 0 else "")

def detect_circles(self):
    """Detect circles fitting the polygons and plot results.

    Returns:
        tuple: A tuple containing two elements:
            - filtered_unused_loops (list): List of polygons that do not fit any circle.
            - possible_circles (list): List of tuples (center, radius, polygon) for possible circles.
    """
    min_heap = []
    possible_circles = []
    unused_loops = []

    for polygon in self.unique_cycles:
        center, radius, mse = self.best_fit_circle(polygon)
        heapq.heappush(min_heap, (mse, center, radius, polygon))

    while min_heap:
        mse, center, radius, polygon = heapq.heappop(min_heap)

        contains_marked_side = False
        for i in range(len(polygon)):
            side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
            if side in self.marked_sides:
                contains_marked_side = True
                break

        if contains_marked_side:
            unused_loops.append(polygon)
            continue

        self.plot_polygon_and_circle(polygon, center, radius, label='Best Fit Circle')
        self.plot_remaining_sides(self.unique_cycles, self.marked_sides)
        plt.legend()
        plt.axis('equal')
        plt.show()
        print(f'Mean Square Fitting Error: {mse:.4f}')

        if mse < 25:
            possible_circles.append((center, radius, polygon))

            for i in range(len(polygon)):
                side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
                self.marked_sides.add(side)
        else:
            unused_loops.append(polygon)

    filtered_unused_loops = []
    for loop in unused_loops:
        all_sides_unused = True
        for i in range(len(loop)):
            side = tuple(sorted([tuple(loop[i]), tuple(loop[(i + 1) % len(loop)])]))
            if side in self.marked_sides:
                all_sides_unused = False
                break
        if all_sides_unused:
            filtered_unused_loops.append(loop)

    plt.figure()

    self.plot_remaining_sides(self.unique_cycles, self.marked_sides)
    for polygon in self.unique_cycles:
        polygon = np.array(polygon)
        for i in range(len(polygon)):
            side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
            if side not in self.marked_sides:
                self.remaining_sides.add(side)

    self.remaining_sides = set([
        ((float(x1), float(y1)), (float(x2), float(y2)))
        for ((x1, y1), (x2, y2)) in self.remaining_sides
    ])

    print("Final Possible Circles are : ")
    for center, radius, polygon in possible_circles:
        self.plot_polygon_and_circle(polygon, center, radius, label='Best Fit Circle')

    plt.legend()
    plt.axis('equal')
    plt.show()

    return filtered_unused_loops, possible_circles
def are_approximately_equal(p1, p2, tol=5):
"""
Check if two points are approximately equal within a given tolerance.


Args:
    p1 (tuple): The first point as (x, y).
    p2 (tuple): The second point as (x, y).
    tol (float): The tolerance for approximation (default is 5).

Returns:
    bool: True if the distance between the points is less than the tolerance, False otherwise.
"""
return np.linalg.norm(np.array(p1) - np.array(p2)) < tol
def compute_slope(p1, p2):
"""
Compute the slope of the line passing through two points.


Args:
    p1 (tuple): The first point as (x, y).
    p2 (tuple): The second point as (x, y).

Returns:
    float: The slope of the line. Returns np.inf if the line is vertical.
"""
if p2[0] == p1[0]:
    return np.inf
return (p2[1] - p1[1]) / (p2[0] - p1[0])
class SegmentProcessor:
def init(self, segments, tol=1):
"""
Initialize the SegmentProcessor.


    Args:
        segments (list): A list of segments where each segment is represented as a tuple of two points ((x1, y1), (x2, y2)).
        tol (float): Tolerance for considering segments as collinear (default is 1).
    """
    self.segments = segments
    self.tol = tol
    self.merged_segments = []
    self.map = {}
    self.common_vertex_segments = []
    self.filtered_merged_segments = []

def merge_collinear_segments(self):
    """
    Merge collinear segments into single segments based on proximity and slope similarity.
    """
    merged_segments = []
    used = set()
    segment_map = {}

    while len(used) < len(self.segments):
        for i, (start1, end1) in enumerate(self.segments):
            if i not in used:
                break

        collinear_group = [(start1, end1)]
        used.add(i)

        while True:
            merged = False
            for j, (start2, end2) in enumerate(self.segments):
                if j in used:
                    continue

                if (np.abs(compute_slope(start1, end1) - compute_slope(start2, end2)) < self.tol):
                    if any(are_approximately_equal(start, start2) or
                           are_approximately_equal(start, end2) or
                           are_approximately_equal(end, start2) or
                           are_approximately_equal(end, end2) for start, end in collinear_group):
                        collinear_group.append((start2, end2))
                        used.add(j)
                        merged = True

            if not merged:
                break

        all_points = np.array([point for segment in collinear_group for point in segment])
        min_idx = np.argmin(all_points[:, 0])
        max_idx = np.argmax(all_points[:, 0])
        min_y_idx = np.argmin(all_points[:, 1])
        max_y_idx = np.argmax(all_points[:, 1])

        if np.abs(compute_slope(start1, end1)) < self.tol:
            merged_segments.append((tuple(all_points[min_idx]), tuple(all_points[max_idx])))
            segment_map[(tuple(all_points[min_idx]), tuple(all_points[max_idx]))] = collinear_group
        else:
            segment_map[(tuple(all_points[min_y_idx]), tuple(all_points[max_y_idx]))] = collinear_group
            merged_segments.append((tuple(all_points[min_y_idx]), tuple(all_points[max_y_idx])))

    self.merged_segments = merged_segments
    self.map = segment_map

def find_segments_with_common_vertices(self):
    """
    Identify segments from the merged segments that share common vertices.

    Returns:
        list: A list of segments that have common vertices.
    """
    common_vertex_segments = []
    for i, (start1, end1) in enumerate(self.merged_segments):
        for j, (start2, end2) in enumerate(self.merged_segments):
            if i != j:
                if are_approximately_equal(start1, start2, self.tol) or are_approximately_equal(start1, end2, self.tol) or \
                   are_approximately_equal(end1, start2, self.tol) or are_approximately_equal(end1, end2, self.tol):
                    common_vertex_segments.append((start1, end1))
                    break
    self.common_vertex_segments = common_vertex_segments

def revert_to_original_segments(self):
    """
    Revert the merged segments back to their original segments.

    Returns:
        list: A list of original segments corresponding to the common vertex segments.
    """
    reverted_segments = []
    for segment in self.common_vertex_segments:
        reverted_segments += self.map[segment]
    return reverted_segments

def filter_merged_segments(self):
    """
    Filter out the merged segments that are also in the list of common vertex segments.
    """
    self.filtered_merged_segments = [seg for seg in self.merged_segments if seg not in self.common_vertex_segments]

def plot_segments(self, segments, title, highlight_segments=None):
    """
    Plot segments with an optional highlight for specific segments.

    Args:
        segments (list): A list of segments where each segment is represented as a tuple of two points ((x1, y1), (x2, y2)).
        title (str): Title of the plot.
        highlight_segments (list, optional): Segments to be highlighted in a different color (default is None).
    """
    plt.figure(figsize=(10, 8))
    for (start, end) in segments:
        plt.plot([start[0], end[0]], [start[1], end[1]], marker='o', color='blue')
    if highlight_segments:
        for (start, end) in highlight_segments:
            plt.plot([start[0], end[0]], [start[1], end[1]], marker='o', color='red')
    plt.title(title)
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.grid(True)
    plt.show()
class PolygonDetection:
def init(self, error_threshold=150):
self.error_threshold = error_threshold
self.polygons = []


def sample_points_along_segment(self, p1, p2, num_points=10):
    return np.linspace(p1, p2, num_points)

def calculate_angle_between_lines(self, line1, line2):
    def vector_from_line(line):
        (x1, y1), (x2, y2) = line
        return (x2 - x1, y2 - y1)

    common_points = set(line1) & set(line2)
    if len(common_points) == 0:
        return None

    vec1 = vector_from_line(line1)
    vec2 = vector_from_line(line2)

    magnitude1 = math.sqrt(vec1[0]**2 + vec1[1]**2)
    magnitude2 = math.sqrt(vec2[0]**2 + vec2[1]**2)

    if magnitude1 == 0 or magnitude2 == 0:
        return None

    dot_product = vec1[0] * vec2[0] + vec1[1] * vec2[1]
    cos_theta = dot_product / (magnitude1 * magnitude2)

    cos_theta = min(1, max(-1, cos_theta))

    angle = math.degrees(math.acos(cos_theta))
    return angle

def are_points_close(self, p1, p2, tolerance=1):
    return np.linalg.norm(np.array(p1) - np.array(p2)) < tolerance

def calculate_area_contribution(self, p1, p2, p3):
    """Calculate the signed area of the triangle formed by p1, p2, p3."""
    return 0.5 * abs(p1[0] * (p2[1] - p3[1]) + p2[0] * (p3[1] - p1[1]) + p3[0] * (p1[1] - p2[1]))

def calculate_internal_angle(self, p1, p2, p3):
    vec1 = np.array(p2) - np.array(p1)
    vec2 = np.array(p3) - np.array(p2)
    dot_product = np.dot(vec1, vec2)
    magnitude1 = np.linalg.norm(vec1)
    magnitude2 = np.linalg.norm(vec2)
    if magnitude1 == 0 or magnitude2 == 0:
        return 0
    cos_theta = dot_product / (magnitude1 * magnitude2)
    cos_theta = min(1, max(-1, cos_theta))
    return math.degrees(math.acos(cos_theta))

def process_polygons(self, polygons):
    vertices_arr = []
    lines_arr = []

    for points in polygons:
        lines = [(points[i], points[(i + 1) % len(points)]) for i in range(len(points))]

        concave_vertices = []
        for i in range(len(points)):
            p_prev = points[i - 1]
            p_curr = points[i]
            p_next = points[(i + 1) % len(points)]
            angle = self.calculate_internal_angle(p_prev, p_curr, p_next)
            if angle > 180:
                concave_vertices.append(p_curr)

        points = [p for p in points if p not in concave_vertices]

        points = self.filter_points(np.array(points))
        heap = []
        for i in range(len(points)):
            p_prev = points[i - 1]
            p_curr = points[i]
            p_next = points[(i + 1) % len(points)]
            area_contribution = self.calculate_area_contribution(p_prev, p_curr, p_next)
            heapq.heappush(heap, (-area_contribution, p_curr))

        vertices = set()
        while heap:
            score, vertex = heapq.heappop(heap)
            vertex_tuple = tuple(vertex)  # Convert numpy array to a tuple
            if not any(self.are_points_close(vertex, np.array(v)) for v in vertices):
                vertices.add(vertex_tuple)

        vertices = np.array([np.array(v) for v in vertices])
        vertices_arr.append(vertices)
        lines_arr.append(lines)
    return vertices_arr, lines_arr

def calculate_distance(self, p1, p2):
    """Calculate the Euclidean distance between two points."""
    return np.linalg.norm(p2 - p1)

def filter_points(self, points):
    distances = [self.calculate_distance(points[i], points[(i+1) % len(points)]) for i in range(len(points))]
    avg_distance = np.mean(distances)
    threshold = 0.85 * avg_distance
    i = 0
    while i < len(points):
        if distances[i] < threshold:
            dist_prev = self.calculate_distance(points[(i-1 + len(points)) % len(points)], points[i])
            dist_next = self.calculate_distance(points[(i+1) % len(points)], points[(i+2) % len(points)])

            if dist_prev <= dist_next:
                points = np.delete(points, i, axis=0)
            else:
                points = np.delete(points, (i+1) % len(points), axis=0)

            distances = [self.calculate_distance(points[j], points[(j+1) % len(points)]) for j in range(len(points))]
        else:
            i += 1

    return points

def calculate_angle(self, point, centroid):
    return np.arctan2(point[1] - centroid[1], point[0] - centroid[0])

def get_best_fit_polygon(self, vertices, lines):
    centroid = np.mean(vertices, axis=0)
    distances = np.linalg.norm(vertices - centroid, axis=1)
    average_radius = np.mean(distances)
    n_vertices = len(vertices)

    def generate_regular_polygon(centroid, radius, n_vertices, rotation_angle=0):
        angles = np.linspace(0, 2 * np.pi, n_vertices, endpoint=False) + rotation_angle
        return np.array([
            [centroid[0] + radius * np.cos(angle), centroid[1] + radius * np.sin(angle)]
            for angle in angles
        ])

    def fit_score(vertices, approx_vertices):
        return np.sum(cdist(vertices, approx_vertices, 'euclidean').min(axis=1))

    def line_fit_score(lines, approx_vertices):
        total_distance = 0
        total_samples = 0

        for line in lines:
            p1, p2 = line
            segment_points = self.sample_points_along_segment(p1, p2)
            distances = cdist(segment_points, approx_vertices, 'euclidean').min(axis=1)
            total_distance += np.sum(distances)
            total_samples += len(distances)

        return total_distance / total_samples

    best_fit_polygon = None
    best_fit_score = float('inf')
    best_rotation_angle = 0
    best_radius = average_radius

    radii = np.linspace(average_radius - 1, average_radius + 1, 3)
    for radius in radii:
        for angle in np.linspace(0, 2 * np.pi, 360):
            approx_vertices = generate_regular_polygon(centroid, radius, n_vertices, rotation_angle=angle)
            vertex_score = fit_score(vertices, approx_vertices)
            line_score = line_fit_score(lines, approx_vertices)
            score = vertex_score + line_score
            if score < best_fit_score:
                best_fit_score = score
                best_fit_polygon = approx_vertices
                best_rotation_angle = angle
                best_radius = radius

    return best_fit_polygon, best_rotation_angle, best_radius

def get_best_fit_rectangle(self, vertices, lines):
    if len(vertices) != 4:
        return None, None, None
    side_lengths = np.array([np.linalg.norm(vertices[i] - vertices[(i+1) % 4]) for i in range(4)])

    if not (np.isclose(side_lengths[0], side_lengths[2], rtol=0.1) and np.isclose(side_lengths[1], side_lengths[3], rtol=0.1)):
        return None, None, None

    min_adjacent_ratio = np.min(side_lengths) / np.max(side_lengths)
    if min_adjacent_ratio > 0.5:
        return None, None, None

    centroid = np.mean(vertices, axis=0)

    best_rotation_angle = 0  # Placeholder for rotation calculation

    rectangle_vertices = np.array([
        centroid + np.array([-side_lengths[0] / 2, -side_lengths[1] / 2]),
        centroid + np.array([side_lengths[0] / 2, -side_lengths[1] / 2]),
        centroid + np.array([side_lengths[0] / 2, side_lengths[1] / 2]),
        centroid + np.array([-side_lengths[0] / 2, side_lengths[1] / 2])
    ])

    return rectangle_vertices, best_rotation_angle, None

def get_best_fit_star_shape(self, vertices, lines):
    centroid = np.mean(vertices, axis=0)

    distances = np.linalg.norm(vertices - centroid, axis=1)

    angles = np.arctan2(vertices[:, 1] - centroid[1], vertices[:, 0] - centroid[0])
    sorted_indices = np.argsort(angles)
    sorted_vertices = vertices[sorted_indices]

    outer_vertices = sorted_vertices[::2]
    inner_vertices = sorted_vertices[1::2]

    star_points = np.empty((len(vertices), 2))
    star_points[::2] = outer_vertices
    star_points[1::2] = inner_vertices

    best_rotation_angle = 0
    best_radius = np.mean(distances[sorted_indices[::2]])
    return star_points, best_rotation_angle, best_radius


def process_polygons_with_fit(self, vertices_list, lines_list):
    valid_polygons = []
    rejected_polygons = []
    remaining_segments = []

    for vertices, lines in zip(vertices_list, lines_list):
        best_fit_polygon, best_rotation_angle, best_radius = None, None, None
        if len(vertices) == 0 or len(lines) == 0:
            continue

        polygon_type = "polygon"

        if len(vertices) % 2 == 0 and len(vertices) >= 8:
            best_fit_polygon, best_rotation_angle, best_radius = self.get_best_fit_star_shape(vertices, lines)
            if best_fit_polygon is None:
                best_fit_polygon, best_rotation_angle, best_radius = self.get_best_fit_polygon(vertices, lines)
            else:
                polygon_type = "star"

        elif len(vertices) == 4:
            best_fit_polygon, best_rotation_angle, _ = self.get_best_fit_rectangle(vertices, lines)
            if best_fit_polygon is not None:
                polygon_type = "rectangle"
            else:
                best_fit_polygon, best_rotation_angle, best_radius = self.get_best_fit_polygon(vertices, lines)

        else:
            best_fit_polygon, best_rotation_angle, best_radius = self.get_best_fit_polygon(vertices, lines)

        line_errors = []
        for line in lines:
            p1, p2 = line
            segment_points = self.sample_points_along_segment(p1, p2)
            distances = cdist(segment_points, best_fit_polygon, 'euclidean').min(axis=1)
            line_errors.append(np.mean(distances))

        if any(error > self.error_threshold for error in line_errors):
            rejected_polygons.append((vertices, lines))
            for line in lines:
                remaining_segments.append(line)
            continue

        # Store the valid polygon with its type
        valid_polygons.append((best_fit_polygon, best_rotation_angle, best_radius, polygon_type))

    self.polygons = valid_polygons  # Store the valid polygons
    return valid_polygons, rejected_polygons, remaining_segments

def plot_all_polygons_with_symmetry(self):
    """
    Plot all the stored polygons along with their lines of symmetry.
    """
    for polygon_data in self.polygons:
        polygon_vertices, rotation_angle, radius, polygon_type = polygon_data
        self.plot_lines_of_symmetry(polygon_vertices, polygon_type=polygon_type)

def plot_lines_of_symmetry(self, polygon_vertices, polygon_type="polygon"):
    """
    Plot lines of symmetry for regular polygons and rectangles.

    :param polygon_vertices: Array of vertices for the polygon/rectangle.
    :param polygon_type: Type of polygon, either "polygon" or "rectangle".
    """
    centroid = np.mean(polygon_vertices, axis=0)

    plt.figure(figsize=(6, 6))
    plt.plot(*polygon_vertices.T, 'r-', label='Polygon')
    plt.plot([polygon_vertices[-1, 0], polygon_vertices[0, 0]],
            [polygon_vertices[-1, 1], polygon_vertices[0, 1]], 'r-')

    plt.plot(centroid[0], centroid[1], 'ro', label='Centroid')

    if polygon_type == "polygon" or polygon_type == "star":
        for vertex in polygon_vertices:
            plt.plot([vertex[0], centroid[0]], [vertex[1], centroid[1]], 'g--', label='Line of Symmetry')

        if len(polygon_vertices) % 2 == 0 and polygon_type == "polygon":
            num_vertices = len(polygon_vertices)
            for i in range(num_vertices // 2):
                opposite_vertex = polygon_vertices[(i + num_vertices // 2) % num_vertices]
                plt.plot([polygon_vertices[i][0], opposite_vertex[0]],
                        [polygon_vertices[i][1], opposite_vertex[1]], 'b--', label='Bisecting Line')

            for i in range(num_vertices):
                next_i = (i + 1) % num_vertices
                midpoint = (polygon_vertices[i] + polygon_vertices[next_i]) / 2
                opposite_midpoint = (polygon_vertices[(i + num_vertices // 2) % num_vertices] +
                                    polygon_vertices[(next_i + num_vertices // 2) % num_vertices]) / 2
                plt.plot([midpoint[0], opposite_midpoint[0]], [midpoint[1], opposite_midpoint[1]], 'r--', label='Perpendicular Bisector')

    elif polygon_type == "rectangle":
        diagonals = [
            (polygon_vertices[0], polygon_vertices[2]),
            (polygon_vertices[1], polygon_vertices[3])
        ]
        for diagonal in diagonals:
            plt.plot([diagonal[0][0], diagonal[1][0]], [diagonal[0][1], diagonal[1][1]], 'g--', label='Diagonal')

        for i in range(2):
            midpoint = (polygon_vertices[i] + polygon_vertices[(i+2) % 4]) / 2
            plt.plot([midpoint[0], centroid[0]], [midpoint[1], centroid[1]], 'b--', label='Midline')

    plt.axis('equal')
    plt.legend()
    plt.title('Lines of Symmetry')
    plt.show()


def handle_remaining_segments(self, remaining_segments):
    heapq.heapify(remaining_segments)
    final_polygons = []
    utilized_segments = set()

    while remaining_segments:
        current_segment = heapq.heappop(remaining_segments)
        if any(tuple(map(tuple, segment)) in utilized_segments for segment in current_segment):
            continue

        remaining_polygons = [segment for segment in current_segment if tuple(map(tuple, segment)) not in utilized_segments]

        if len(remaining_polygons) == 0:
            continue

        vertices = np.array([point for segment in remaining_polygons for point in segment])
        lines = remaining_polygons
        best_fit_polygon, best_rotation_angle, best_radius = self.get_best_fit_polygon(vertices, lines)

        line_errors = []
        for line in lines:
            p1, p2 = line
            segment_points = self.sample_points_along_segment(p1, p2)
            distances = cdist(segment_points, best_fit_polygon, 'euclidean').min(axis=1)
            line_errors.append(np.mean(distances))

        if any(error > self.error_threshold for error in line_errors):
            for line in lines:
                remaining_segments.append(line)
            continue

        final_polygons.append((best_fit_polygon, best_rotation_angle, best_radius))
        for segment in lines:
            utilized_segments.add(tuple(map(tuple, segment)))

    return final_polygons
def regularize(path, xlim=None, ylim=None):
csv_processor = CSVProcessor(csv_file=path)
csv_processor.load_points_from_csv()


curves = defaultdict(list)
for point in csv_processor.all_points:
    curve_index, _, x, y = point
    curves[curve_index].append((x, y))

curve_processor = CurveProcessor(curves)
curve_processor.process()
curves = curve_processor.segment_points_dict

cycle_detector = CycleDetector(curve_processor.updated_curves)
cycles, non_cycle_lines = cycle_detector.process_cycles()

circle_detector = CircleDetector(cycles)
print("\ntry to fit circle in loops : \n")
remaining_sides = circle_detector.remaining_sides
filtered_unused_loops, possible_circles = circle_detector.detect_circles()


unique_cycles = circle_detector.unique_cycles

for loop in filtered_unused_loops:
    for polygon in unique_cycles:
        for i in range(len(polygon)):
            side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
            remaining_sides.add(side)

# Remove sides in possible_circles
for _, _, polygon in possible_circles:
    for i in range(len(polygon)):
        side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
        if side in remaining_sides:
            remaining_sides.remove(side)

# Remove sides in filtered_unused_loops
for polygon in filtered_unused_loops:
    for i in range(len(polygon)):
        side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
        if side in remaining_sides:
            remaining_sides.remove(side)

segments = list(non_cycle_lines)

segment_processor = SegmentProcessor(segments)
segment_processor.merge_collinear_segments()
segment_processor.find_segments_with_common_vertices()
rem = segment_processor.revert_to_original_segments()
segment_processor.filter_merged_segments()

segment_processor.plot_segments(segment_processor.filtered_merged_segments, "Merged Segments Excluding Common Vertices")
segment_processor.plot_segments(rem, "Original Segments with Common Vertices")
remaining_sides = remaining_sides.union(set(rem))
polygon_detection = PolygonDetection()
vertices_arr, lines_arr = polygon_detection.process_polygons(filtered_unused_loops)
if xlim!=None and ylim!= None:
        plt.xlim(-50, ylim)
        plt.ylim(-50, ylim)
if len(vertices_arr) > 0 or len(lines_arr) > 0 or len(filtered_unused_loops) > 0 :
    print("Impact Points in current loop are : ")
    plt.figure(figsize=(10, 10))
    for vertices in vertices_arr:
        if len(vertices) > 0:
            plt.plot(vertices[:, 0], vertices[:, 1], 'o', label='Processed Vertices')

    for lines in lines_arr:
        for line in lines:
            x_values, y_values = zip(*line)
            plt.plot(x_values, y_values, 'k--', alpha=0.5)

    for points in filtered_unused_loops:
        plt.plot([p[0] for p in points], [p[1] for p in points], 'x--', label='Original Points')

    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title('Impact Points')
    plt.legend()
    plt.grid(True)
    plt.show()

valid_polygons, rejected_polygons, remaining_segments = polygon_detection.process_polygons_with_fit(vertices_arr, lines_arr)
polygon_detection.plot_all_polygons_with_symmetry()
final_polygons = polygon_detection.handle_remaining_segments(remaining_segments)

if len(valid_polygons) > 0 or len(final_polygons) > 0 :
    plt.figure(figsize=(10, 10))
    for best_fit_polygon, best_rotation_angle, best_radius, polygon_type in valid_polygons:
        plt.plot(np.append(best_fit_polygon[:, 0], best_fit_polygon[0, 0]),
                np.append(best_fit_polygon[:, 1], best_fit_polygon[0, 1]), 'ro-', label='Best Fit Polygon')

    for best_fit_polygon, best_rotation_angle, best_radius in final_polygons:
        plt.plot(np.append(best_fit_polygon[:, 0], best_fit_polygon[0, 0]),
                np.append(best_fit_polygon[:, 1], best_fit_polygon[0, 1]), 'go-', label='Final Polygon')

    if xlim!=None and ylim!= None:
        plt.xlim(-50, ylim)
        plt.ylim(-50, ylim)
    plt.legend()
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title('Approximated Polygons')
    plt.grid(True)
    plt.show()


def plot_all(valid_polygons, possible_circles, remaining_sides, curves, inverse_dict, merged_segments):
    plt.figure(figsize=(12, 12))
    remaining_sides = set([
        ((float(x1), float(y1)), (float(x2), float(y2)))
        for ((x1, y1), (x2, y2)) in remaining_sides
    ])

    all_x = []
    all_y = []

    for best_fit_polygon, best_rotation_angle, best_radius, polygon_type in valid_polygons:
        all_x.extend(best_fit_polygon[:, 0])
        all_y.extend(best_fit_polygon[:, 1])
        plt.plot(np.append(best_fit_polygon[:, 0], best_fit_polygon[0, 0]),
                np.append(best_fit_polygon[:, 1], best_fit_polygon[0, 1]), 'ro-', label='Best Fit Polygon')

    for center, radius, circle_points in possible_circles:
        circle = plt.Circle(center, radius, color='b', fill=False)
        plt.gca().add_artist(circle)
        all_x.append(center[0] + radius)
        all_x.append(center[0] - radius)
        all_y.append(center[1] + radius)
        all_y.append(center[1] - radius)
        circle_points = np.array(circle_points)
        all_x.extend(circle_points[:, 0])
        all_y.extend(circle_points[:, 1])

    plotted_curves = set()

    for side in remaining_sides:
        if side in inverse_dict:
            curve_num = inverse_dict[side]
        elif (side[1], side[0]) in inverse_dict:
            curve_num = inverse_dict[(side[1], side[0])]
        else:
            continue

        if curve_num not in plotted_curves:
            if curve_num in curves:
                points = curves[curve_num]
                x_values, y_values = zip(*points)
                all_x.extend(x_values)
                all_y.extend(y_values)
                plt.plot(x_values, y_values, label=f'Curve {curve_num}')
                plotted_curves.add(curve_num)



    for segment in merged_segments:
        (x1, y1), (x2, y2) = segment
        all_x.extend([x1, x2])
        all_y.extend([y1, y2])
        plt.plot([x1, x2], [y1, y2], 'g-', label='Merged Segment')

    if xlim!=None and ylim!= None:
        plt.xlim(-50, ylim)
        plt.ylim(-50, ylim)
    plt.legend()
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title('Regularized Shapes')
    plt.grid(True)
    plt.show()

if xlim!=None and ylim!= None:
        plt.xlim(-50, ylim)
        plt.ylim(-50, ylim)
plot_all(valid_polygons, possible_circles, remaining_sides, curves, curve_processor.inverse_dict, segment_processor.filtered_merged_segments)
regularize("/content/isolated.csv")


this is my final script (combination of both)
import csv
import math
import heapq
from collections import defaultdict
from typing import List, Tuple

import cv2
import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial.distance import cdist

# --------------------------- Helper Functions --------------------------- #

def are_approximately_equal(p1, p2, tol=5):
    """
    Check if two points are approximately equal within a given tolerance.
    """
    return np.linalg.norm(np.array(p1) - np.array(p2)) < tol

def compute_slope(p1, p2):
    """
    Compute the slope of the line passing through two points.
    """
    if p2[0] == p1[0]:
        return np.inf
    return (p2[1] - p1[1]) / (p2[0] - p1[0])

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
    dot_product = np.dot(ab, cb)
    magnitude1 = np.linalg.norm(ab)
    magnitude2 = np.linalg.norm(cb)
    if magnitude1 == 0 or magnitude2 == 0:
        return 0
    cos_theta = dot_product / (magnitude1 * magnitude2)
    cos_theta = min(1, max(-1, cos_theta))
    return math.degrees(math.acos(cos_theta))

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)
        symmetry_lines = []
        if shape_type == "Circle" or shape_type == "Square":
            symmetry_lines.append(([centroid[0], np.min(XY[:, 1])],
                                   [centroid[0], np.max(XY[:, 1])]))  # Vertical
            symmetry_lines.append(([np.min(XY[:, 0]), centroid[1]],
                                   [np.max(XY[:, 0]), centroid[1]]))  # Horizontal
        elif shape_type == "Ellipse" or shape_type == "Rectangle":
            symmetry_lines.append(([np.min(XY[:, 0]), centroid[1]],
                                   [np.max(XY[:, 0]), centroid[1]]))  # Horizontal
        elif shape_type == "Star":
            num_points = len(XY) // 2
            for i in range(num_points):
                start_point = XY[i]
                end_point = XY[(i + num_points) % len(XY)]
                symmetry_lines.append((start_point, end_point))
        return symmetry_lines
    return []

# --------------------------- Intern-1 Classes --------------------------- #

class CSVProcessor:
    """
    Processes and loads data points from a CSV file.
    """

    def __init__(self, csv_file: str):
        """
        Initializes with the CSV file path.
        """
        self.csv_file = csv_file
        self.all_points = []

    def load_points_from_csv(self):
        """
        Loads data points from the CSV file into all_points.
        Expected CSV Format: curve_index, ..., x, y
        """
        with open(self.csv_file, newline='') as csvfile:
            reader = csv.reader(csvfile)
            for row in reader:
                if len(row) < 4:
                    continue  # Skip invalid rows
                curve_index = row[0]
                try:
                    x = float(row[2])
                    y = float(row[3])
                    self.all_points.append((curve_index, x, y))
                except ValueError:
                    continue  # Skip rows with invalid float conversion

class CurveProcessor:
    """
    Processes curves for simplification and refinement based on given parameters.
    """

    def __init__(self, curves: dict[str, List[Tuple[float, float]]], epsilon: float = 5.0, threshold: float = 5.0):
        """
        Initializes with curves, epsilon for simplification, and threshold for endpoint adjustment.
        """
        self.curves = curves
        self.epsilon = epsilon
        self.threshold = threshold
        self.simplified_curves = {}
        self.inverse_dict = {}
        self.segment_points_dict = {}
        self.updated_curves = {}

    def point_line_distance(self, point, start, end):
        """
        Calculates the perpendicular distance from a point to a line segment.
        """
        point = np.array(point)
        start = np.array(start)
        end = np.array(end)

        if np.array_equal(start, end):
            return np.linalg.norm(point - start)
        else:
            n = abs((end[1] - start[1]) * point[0] -
                    (end[0] - start[0]) * point[1] +
                    end[0] * start[1] - end[1] * start[0])
            d = np.linalg.norm(end - start)
            return n / d

    def ramer_douglas_peucker(self, points, epsilon):
        """
        Simplifies a list of points using the Ramer-Douglas-Peucker algorithm.
        """
        if len(points) < 3:
            return points, list(range(len(points)))

        dmax = 0.0
        index = 0
        for i in range(1, len(points) - 1):
            d = self.point_line_distance(points[i], points[0], points[-1])
            if d > dmax:
                index = i
                dmax = d

        if dmax > epsilon:
            rec_results1, idx1 = self.ramer_douglas_peucker(points[:index + 1], epsilon)
            rec_results2, idx2 = self.ramer_douglas_peucker(points[index:], epsilon)
            result = rec_results1[:-1] + rec_results2
            result_indices = idx1[:-1] + [i + index for i in idx2]
        else:
            result = [points[0], points[-1]]
            result_indices = [0, len(points) - 1]

        return result, result_indices

    def simplify_curves(self):
        """
        Simplifies all curves and stores segment information.
        """
        simplified_curves = {}
        inverse_dict = {}
        segment_points_dict = {}
        segment_id = 0

        for index, points in self.curves.items():
            simplified_points, indices = self.ramer_douglas_peucker(points, self.epsilon)

            for i in range(len(simplified_points) - 1):
                start_point = tuple(simplified_points[i])
                end_point = tuple(simplified_points[i + 1])
                line_key = (start_point, end_point)

                segment_indices = indices[i:i + 2]
                segment_start_idx = segment_indices[0]
                segment_end_idx = segment_indices[1]

                simplified_curves[segment_id] = [start_point, end_point]
                inverse_dict[line_key] = segment_id
                segment_points_dict[segment_id] = points[segment_start_idx:segment_end_idx + 1]

                segment_id += 1

        self.simplified_curves = simplified_curves
        self.inverse_dict = inverse_dict
        self.segment_points_dict = segment_points_dict

    def distance(self, point1, point2):
        """
        Computes the Euclidean distance between two points.
        """
        return np.linalg.norm(np.array(point1) - np.array(point2))

    def midpoint(self, point1, point2):
        """
        Returns the midpoint between two points.
        """
        return ((point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2)

    def round_point(self, point):
        """
        Rounds the coordinates of a point to one decimal place.
        """
        return (round(point[0], 1), round(point[1], 1))

    def update_endpoints_with_midpoints(self):
        """
        Adjusts segment endpoints based on proximity and rounds the results.
        """
        endpoints = {}
        updated_curves = {k: v[:] for k, v in self.simplified_curves.items()}
        inverse_dict = {}

        for key, points in self.simplified_curves.items():
            endpoints[key] = (points[0], points[-1])

        for key, (start, end) in endpoints.items():
            if self.distance(start, end) < self.threshold:
                mid = self.midpoint(start, end)
                updated_curves[key][0] = mid
                updated_curves[key][-1] = mid

        # Adjust endpoints based on proximity
        for key1, (start1, end1) in endpoints.items():
            for key2, (start2, end2) in endpoints.items():
                if key1 < key2:
                    if self.distance(start1, start2) < self.threshold:
                        mid = self.midpoint(start1, start2)
                        updated_curves[key1][0] = mid
                        updated_curves[key2][0] = mid
                    if self.distance(start1, end2) < self.threshold:
                        mid = self.midpoint(start1, end2)
                        updated_curves[key1][0] = mid
                        updated_curves[key2][-1] = mid
                    if self.distance(end1, start2) < self.threshold:
                        mid = self.midpoint(end1, start2)
                        updated_curves[key1][-1] = mid
                        updated_curves[key2][0] = mid
                    if self.distance(end1, end2) < self.threshold:
                        mid = self.midpoint(end1, end2)
                        updated_curves[key1][-1] = mid
                        updated_curves[key2][-1] = mid

        # Round points
        for key, points in updated_curves.items():
            updated_curves[key] = [self.round_point(point) for point in points]

        # Reconstruct inverse_dict
        for index, points in updated_curves.items():
            for i in range(len(points) - 1):
                start_point = tuple(points[i])
                end_point = tuple(points[i + 1])
                line_key = (start_point, end_point)
                inverse_dict[line_key] = index

        self.updated_curves = updated_curves
        self.inverse_dict = inverse_dict

    def process(self):
        """
        Executes curve simplification and endpoint updates.
        """
        self.simplify_curves()
        self.update_endpoints_with_midpoints()

class CycleDetector:
    """Detects cycles in a set of updated curves."""

    def __init__(self, updated_curves: dict[int, List[Tuple[float, float]]]):
        """Initialize with updated curves and construct adjacency list."""
        self.adj_list, self.segments = self.construct_adj_list(updated_curves)

    def construct_adj_list(self, updated_curves):
        """Create an adjacency list and segment list from the updated curves."""
        adj_list = defaultdict(list)
        segments = []
        for polyline in updated_curves.values():
            for i in range(len(polyline) - 1):
                start, end = polyline[i], polyline[i + 1]
                adj_list[start].append(end)
                adj_list[end].append(start)
                segments.append((start, end))
        return adj_list, segments

    def find_cycles(self, graph):
        """Find all cycles in the graph using depth-first search (DFS)."""
        def dfs(node, start, visited, path):
            visited.add(node)
            path.append(node)

            for neighbor in graph[node]:
                if neighbor == start and len(path) > 2:
                    cycle = path[:] + [start]
                    cycles.append(cycle)
                elif neighbor not in visited:
                    dfs(neighbor, start, visited, path)

            path.pop()
            visited.remove(node)

        cycles = []
        visited = set()

        for node in graph:
            if node not in visited:
                dfs(node, node, visited, [])

        # Remove duplicate cycles (cycles with same set of nodes)
        unique_cycles = []
        seen = []
        for cycle in cycles:
            cycle_set = set(cycle)
            if not any(cycle_set == s for s in seen):
                unique_cycles.append(cycle)
                seen.append(cycle_set)

        return unique_cycles

    def separate_non_cycle_lines(self, segments, cycles):
        """Separate non-cycle lines from the segments."""
        cycle_edges = set()
        for cycle in cycles:
            for i in range(len(cycle) - 1):
                edge = tuple(sorted([cycle[i], cycle[i + 1]]))
                cycle_edges.add(edge)

        non_cycle_lines = []
        for start, end in segments:
            edge = tuple(sorted([start, end]))
            if edge not in cycle_edges:
                non_cycle_lines.append((start, end))

        return non_cycle_lines

    def process_cycles(self):
        """Detect cycles and separate non-cycle lines."""
        cycles = self.find_cycles(self.adj_list)
        non_cycle_lines = self.separate_non_cycle_lines(self.segments, cycles)
        return cycles, non_cycle_lines

class CircleDetector:
    """Detects circles that best fit polygons and identifies remaining sides."""

    def __init__(self, unique_cycles):
        """Initialize with a list of unique cycles."""
        self.unique_cycles = unique_cycles
        self.remaining_sides = set()
        self.marked_sides = set()

    def point_to_segment_dist(self, p, v, w):
        """Compute the squared distance from point p to the segment vw."""
        p = np.array(p)
        v = np.array(v)
        w = np.array(w)
        l2 = np.sum((v - w) ** 2)
        if l2 == 0:
            return np.sum((p - v) ** 2)
        t = max(0, min(1, np.dot(p - v, w - v) / l2))
        projection = v + t * (w - v)
        return np.sum((p - projection) ** 2)

    def mean_square_circle_error(self, polygon, center, radius, num_points=10):
        """Calculate the mean squared error of fitting a circle to a polygon."""
        total_error = 0
        total_samples = 0

        for i in range(len(polygon)):
            p1 = polygon[i]
            p2 = polygon[(i + 1) % len(polygon)]

            segment_points = np.linspace(p1, p2, num_points)

            for point in segment_points:
                closest_point = self.closest_point_on_circle(point, center, radius)
                dist = np.linalg.norm(point - closest_point)
                error = dist ** 2
                total_error += error

            total_samples += len(segment_points)

        mse = total_error / total_samples
        return mse

    def closest_point_on_circle(self, point, center, radius):
        """Find the closest point on the circle to a given point."""
        direction = point - center
        norm = np.linalg.norm(direction)
        if norm == 0:
            return center + np.array([radius, 0])
        direction /= norm
        return center + direction * radius

    def best_fit_circle(self, polygon):
        """Find the best fit circle for a polygon using random sampling."""
        polygon = np.array(polygon)

        best_mse = float('inf')
        best_center = None
        best_radius = None

        for _ in range(50):
            sample_indices = np.random.choice(len(polygon), 3, replace=False)
            sample_points = polygon[sample_indices]

            A = sample_points[0]
            B = sample_points[1]
            C = sample_points[2]

            D = 2 * (A[0] * (B[1] - C[1]) +
                     B[0] * (C[1] - A[1]) +
                     C[0] * (A[1] - B[1]))
            if D == 0:
                continue

            Ux = ((A[0] ** 2 + A[1] ** 2) * (B[1] - C[1]) +
                  (B[0] ** 2 + B[1] ** 2) * (C[1] - A[1]) +
                  (C[0] ** 2 + C[1] ** 2) * (A[1] - B[1])) / D
            Uy = ((A[0] ** 2 + A[1] ** 2) * (C[0] - B[0]) +
                  (B[0] ** 2 + B[1] ** 2) * (A[0] - C[0]) +
                  (C[0] ** 2 + C[1] ** 2) * (B[0] - A[0])) / D
            center = np.array([Ux, Uy])
            radius = np.linalg.norm(center - A)

            mse = self.mean_square_circle_error(polygon, center, radius)
            if mse < best_mse:
                best_mse = mse
                best_center = center
                best_radius = radius

        return best_center, best_radius, best_mse

    def plot_polygon_and_circle(self, polygon, center, radius, label):
        """Plot the polygon and the best fit circle."""
        polygon = np.array(polygon)
        theta = np.linspace(0, 2 * np.pi, 100)
        circle_x = center[0] + radius * np.cos(theta)
        circle_y = center[1] + radius * np.sin(theta)

        plt.plot(circle_x, circle_y, 'r--', label=label)
        plt.scatter(*center, color='green', zorder=5, label='Center')

    def plot_remaining_sides(self, unique_cycles, marked_sides):
        """Plot remaining sides of polygons that are not marked."""
        for polygon in unique_cycles:
            polygon = np.array(polygon)
            for i in range(len(polygon)):
                side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
                if side not in marked_sides:
                    plt.plot(*np.array([polygon[i], polygon[(i + 1) % len(polygon)]]).T, 'k-', label='Remaining Side' if i == 0 else "")

    def detect_circles(self, threshold_mse=25):
        """Detect circles fitting the polygons and plot results."""
        min_heap = []
        possible_circles = []
        unused_loops = []

        for polygon in self.unique_cycles:
            center, radius, mse = self.best_fit_circle(polygon)
            heapq.heappush(min_heap, (mse, center, radius, polygon))

        while min_heap:
            mse, center, radius, polygon = heapq.heappop(min_heap)

            contains_marked_side = False
            for i in range(len(polygon)):
                side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
                if side in self.marked_sides:
                    contains_marked_side = True
                    break

            if contains_marked_side:
                unused_loops.append(polygon)
                continue

            self.plot_polygon_and_circle(polygon, center, radius, label='Best Fit Circle')
            self.plot_remaining_sides(self.unique_cycles, self.marked_sides)
            plt.legend()
            plt.axis('equal')
            plt.show()
            print(f'Mean Square Fitting Error: {mse:.4f}')

            if mse < threshold_mse:
                possible_circles.append((center, radius, polygon))

                for i in range(len(polygon)):
                    side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
                    self.marked_sides.add(side)
            else:
                unused_loops.append(polygon)

        filtered_unused_loops = []
        for loop in unused_loops:
            all_sides_unused = True
            for i in range(len(loop)):
                side = tuple(sorted([tuple(loop[i]), tuple(loop[(i + 1) % len(loop)])]))
                if side in self.marked_sides:
                    all_sides_unused = False
                    break
            if all_sides_unused:
                filtered_unused_loops.append(loop)

        plt.figure()

        self.plot_remaining_sides(self.unique_cycles, self.marked_sides)
        for polygon in self.unique_cycles:
            polygon = np.array(polygon)
            for i in range(len(polygon)):
                side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
                if side not in self.marked_sides:
                    self.remaining_sides.add(side)

        self.remaining_sides = set([
            ((float(x1), float(y1)), (float(x2), float(y2)))
            for ((x1, y1), (x2, y2)) in self.remaining_sides
        ])

        print("Final Possible Circles:")
        for center, radius, polygon in possible_circles:
            self.plot_polygon_and_circle(polygon, center, radius, label='Best Fit Circle')

        plt.legend()
        plt.axis('equal')
        plt.show()

        return filtered_unused_loops, possible_circles

class SegmentProcessor:
    """
    Processes line segments by merging collinear segments and identifying common vertices.
    """

    def __init__(self, segments, tol=1):
        """
        Initialize the SegmentProcessor.

        Args:
            segments (list): A list of segments where each segment is represented as a tuple of two points ((x1, y1), (x2, y2)).
            tol (float): Tolerance for considering segments as collinear (default is 1).
        """
        self.segments = segments
        self.tol = tol
        self.merged_segments = []
        self.map = {}
        self.common_vertex_segments = []
        self.filtered_merged_segments = []

    def merge_collinear_segments(self):
        """
        Merge collinear segments into single segments based on proximity and slope similarity.
        """
        merged_segments = []
        used = set()
        segment_map = {}

        for i, (start1, end1) in enumerate(self.segments):
            if i in used:
                continue

            collinear_group = [(start1, end1)]
            used.add(i)
            slope1 = compute_slope(start1, end1)

            for j, (start2, end2) in enumerate(self.segments):
                if j in used:
                    continue

                slope2 = compute_slope(start2, end2)
                if np.abs(slope1 - slope2) < self.tol:
                    # Check if endpoints are approximately equal
                    if any(are_approximately_equal(start1, start2) or
                           are_approximately_equal(start1, end2) or
                           are_approximately_equal(end1, start2) or
                           are_approximately_equal(end1, end2) for start1, end1 in collinear_group):
                        collinear_group.append((start2, end2))
                        used.add(j)

            # Merge the collinear group
            all_points = np.array([point for segment in collinear_group for point in segment])
            # Sort points along the direction of the slope
            if slope1 != np.inf:
                sorted_indices = all_points[:, 0].argsort()
            else:
                sorted_indices = all_points[:, 1].argsort()
            sorted_points = all_points[sorted_indices]
            merged_segment = (tuple(sorted_points[0]), tuple(sorted_points[-1]))
            merged_segments.append(merged_segment)
            segment_map[merged_segment] = collinear_group

        self.merged_segments = merged_segments
        self.map = segment_map

    def find_segments_with_common_vertices(self):
        """
        Identify segments from the merged segments that share common vertices.
        """
        common_vertex_segments = []
        for i, (start1, end1) in enumerate(self.merged_segments):
            for j, (start2, end2) in enumerate(self.merged_segments):
                if i != j:
                    if are_approximately_equal(start1, start2, self.tol) or \
                       are_approximately_equal(start1, end2, self.tol) or \
                       are_approximately_equal(end1, start2, self.tol) or \
                       are_approximately_equal(end1, end2, self.tol):
                        common_vertex_segments.append((start1, end1))
                        break
        self.common_vertex_segments = common_vertex_segments

    def revert_to_original_segments(self):
        """
        Revert the merged segments back to their original segments.

        Returns:
            list: A list of original segments corresponding to the common vertex segments.
        """
        reverted_segments = []
        for segment in self.common_vertex_segments:
            reverted_segments += self.map[segment]
        return reverted_segments

    def filter_merged_segments(self):
        """
        Filter out the merged segments that are also in the list of common vertex segments.
        """
        self.filtered_merged_segments = [seg for seg in self.merged_segments if seg not in self.common_vertex_segments]

    def plot_segments(self, segments, title, color='blue'):
        """
        Plot segments.

        Args:
            segments (list): List of segments to plot.
            title (str): Title of the plot.
            color (str): Color of the segments.
        """
        plt.figure(figsize=(10, 8))
        for (start, end) in segments:
            plt.plot([start[0], end[0]], [start[1], end[1]], marker='o', color=color)
        plt.title(title)
        plt.xlabel('X')
        plt.ylabel('Y')
        plt.grid(True)
        plt.axis('equal')
        plt.show()

# --------------------------- Intern-2 Functions --------------------------- #

def read_csv_intern2(csv_path):
    """
    Reads a CSV file and organizes the points into paths for shape detection.

    Args:
        csv_path (str): Path to the CSV file.

    Returns:
        list: List of paths with organized XY points.
    """
    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.

    Args:
        XY (ndarray): Array of points.

    Returns:
        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.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.

    Args:
        contour (ndarray): Contour points.
        tolerance (float): Tolerance for aspect ratio and area fill.

    Returns:
        bool: True if contour is a square, False otherwise.
    """
    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])
    if height == 0:
        return False
    aspect_ratio = min(width, height) / max(width, height)

    contour_area = cv2.contourArea(contour)
    rect_area = width * height
    if rect_area == 0:
        return False
    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.

    Args:
        approx (ndarray): Approximated contour points.

    Returns:
        bool: True if contour is a star, False otherwise.
    """
    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 is_straight_line(XY, tolerance=0.05):
    """
    Checks if the points form a straight line.

    Args:
        XY (ndarray): Array of points.
        tolerance (float): Tolerance for line straightness.

    Returns:
        bool: True if points form a straight line, False otherwise.
    """
    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.

    Args:
        XY (ndarray): Array of points.

    Returns:
        ndarray: Regularized circle points.
    """
    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.

    Args:
        XY (ndarray): Array of points.

    Returns:
        tuple: (xc, yc, r) where (xc, yc) is the center and r is the radius.
    """
    x = XY[:, 0]
    y = XY[:, 1]
    A = np.c_[2 * x, 2 * y, np.ones(len(x))]
    b = x ** 2 + y ** 2
    try:
        c, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
        xc, yc, c = c
        r = math.sqrt(c + xc ** 2 + yc ** 2)
        return xc, yc, r
    except np.linalg.LinAlgError:
        return 0, 0, 0  # Fallback to default values

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

    Args:
        XY (ndarray): Array of points.

    Returns:
        ndarray: Regularized ellipse points.
    """
    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.

    Args:
        XY (ndarray): Array of points.

    Returns:
        ndarray: Regularized rectangle points.
    """
    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.

    Args:
        XY (ndarray): Array of points.

    Returns:
        ndarray: Regularized square points.
    """
    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.

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

    Returns:
        ndarray: Regularized star points.
    """
    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])  # Close the star

    return np.array(star_points)

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

    Args:
        XY (ndarray): Array of points.

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

def detect_and_regularize_shape(XY):
    """
    Detects the shape and returns the regularized points.

    Args:
        XY (ndarray): Array of points.

    Returns:
        tuple: (shape_type, regularized_XY)
    """
    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 plot_shapes(path_XYs, regularized_XYs):
    """
    Plots original and regularized shapes with symmetry lines, and prints information about detected shapes.

    Args:
        path_XYs (list): List of original paths.
        regularized_XYs (list): List of regularized paths.
    """
    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
            symmetry_lines = calculate_symmetry_lines(reg_XY, shape_type)
            for line in symmetry_lines:
                ax[1].plot([line[0][0], line[1][0]],
                          [line[0][1], line[1][1]], 'k--', linewidth=1, label='Symmetry Line')

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

    # To prevent duplicate labels in the legend
    for axes in ax:
        handles, labels = axes.get_legend_handles_labels()
        by_label = dict(zip(labels, handles))
        axes.legend(by_label.values(), by_label.keys())

    plt.show()

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

    Args:
        paths_XYs (list): List of regularized paths.
        csv_path (str): Path to the output 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())

# --------------------------- Intern-1 Regularize Function --------------------------- #

def regularize_intern1(csv_path, xlim=None, ylim=None):
    """
    Regularizes the shapes using Intern-1's processing.

    Args:
        csv_path (str): Path to the input CSV file.
        xlim (tuple, optional): X-axis limits for plotting.
        ylim (tuple, optional): Y-axis limits for plotting.

    Returns:
        tuple: Contains processed curves, inverse_dict, and merged_segments.
    """
    # Load and process curves
    csv_processor = CSVProcessor(csv_path)
    csv_processor.load_points_from_csv()

    curves = defaultdict(list)
    for point in csv_processor.all_points:
        curve_index, x, y = point
        curves[curve_index].append((x, y))

    curve_processor = CurveProcessor(curves)
    curve_processor.process()
    curves_processed = curve_processor.segment_points_dict

    # Detect cycles
    cycle_detector = CycleDetector(curve_processor.updated_curves)
    cycles, non_cycle_lines = cycle_detector.process_cycles()

    # Detect circles in cycles
    circle_detector = CircleDetector(cycles)
    print("\nFitting circles in loops:\n")
    filtered_unused_loops, possible_circles = circle_detector.detect_circles(threshold_mse=25)

    # Identify unique cycles
    unique_cycles = cycle_detector.find_cycles(cycle_detector.adj_list)

    # Update remaining sides
    for loop in filtered_unused_loops:
        for polygon in unique_cycles:
            for i in range(len(polygon)):
                side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
                circle_detector.remaining_sides.add(side)

    # Remove sides in possible_circles
    for _, _, polygon in possible_circles:
        for i in range(len(polygon)):
            side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
            if side in circle_detector.remaining_sides:
                circle_detector.remaining_sides.remove(side)

    # Remove sides in filtered_unused_loops
    for polygon in filtered_unused_loops:
        for i in range(len(polygon)):
            side = tuple(sorted([tuple(polygon[i]), tuple(polygon[(i + 1) % len(polygon)])]))
            if side in circle_detector.remaining_sides:
                circle_detector.remaining_sides.remove(side)

    segments = list(non_cycle_lines)

    # Process segments
    segment_processor = SegmentProcessor(segments)
    segment_processor.merge_collinear_segments()
    segment_processor.find_segments_with_common_vertices()
    reverted_segments = segment_processor.revert_to_original_segments()
    segment_processor.filter_merged_segments()

    # Plot segments
    segment_processor.plot_segments(segment_processor.filtered_merged_segments,
                                    "Merged Segments Excluding Common Vertices",
                                    color='blue')
    segment_processor.plot_segments(reverted_segments,
                                    "Original Segments with Common Vertices",
                                    color='green')

    # Combine remaining sides
    circle_detector.remaining_sides = circle_detector.remaining_sides.union(set(reverted_segments))

    return curves_processed, curve_processor.inverse_dict, segment_processor.filtered_merged_segments

# --------------------------- Main Function --------------------------- #

def main():
    # Paths
    input_csv = "problems/isolated.csv"  # Update this path as needed
    output_csv = "regularized_shapes.csv"

    # Intern-1 Processing
    curves_processed, inverse_dict, merged_segments = regularize_intern1(input_csv)

    # Prepare for Intern-2 Processing
    # Assuming that Intern-2 works on the original CSV.
    # Alternatively, you can adjust this to work on intern-1's output if desired.
    path_XYs = read_csv_intern2(input_csv)

    # Intern-2 Processing
    regularized_XYs = []
    for path in path_XYs:
        reg_XYs = []
        for XY in path:
            if len(XY) == 0:
                continue
            shape_type, reg_XY = detect_and_regularize_shape(XY)
            reg_XYs.append(reg_XY)
        regularized_XYs.append(reg_XYs)

    # Plot Shapes
    plot_shapes(path_XYs, regularized_XYs)

    # Write regularized shapes to CSV
    polylines2csv(regularized_XYs, output_csv)

    # Final Plotting combining Intern-1 and Intern-2 results
    plot_all_combined(curves_processed, inverse_dict, merged_segments, regularized_XYs, output_csv)

def plot_all_combined(curves, inverse_dict, merged_segments, regularized_XYs, output_csv):
    """
    Plots all processed data from both Intern-1 and Intern-2.

    Args:
        curves (dict): Processed curves.
        inverse_dict (dict): Inverse dictionary for segments.
        merged_segments (list): Merged segments excluding common vertices.
        regularized_XYs (list): Regularized shapes from Intern-2.
        output_csv (str): Path to the output CSV file.
    """
    plt.figure(figsize=(12, 12))
    all_x = []
    all_y = []

    # Plot Intern-1's Processed Curves
    for curve_num, points in curves.items():
        x_values, y_values = zip(*points)
        plt.plot(x_values, y_values, label=f'Curve {curve_num}', alpha=0.7)
        all_x.extend(x_values)
        all_y.extend(y_values)

    # Plot Intern-1's Merged Segments
    for idx, segment in enumerate(merged_segments):
        (x1, y1), (x2, y2) = segment
        plt.plot([x1, x2], [y1, y2], 'g-', label='Merged Segment' if idx == 0 else "", alpha=0.7)
        all_x.extend([x1, x2])
        all_y.extend([y1, y2])

    # Plot Intern-2's Regularized Shapes
    for i, reg_XY in enumerate(regularized_XYs):
        if len(reg_XY) == 0:
            continue
        for j, shape in enumerate(reg_XY):
            plt.plot(shape[:, 0], shape[:, 1], 'ro-', label='Regularized Shape' if (i == 0 and j == 0) else "", linewidth=2)

    plt.title('Combined Regularized Segments and Shapes')
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.legend()
    plt.grid(True)
    plt.axis('equal')
    plt.show()

# --------------------------- Execute the Script --------------------------- #

if __name__ == "__main__":
    main()

please give imporved complete code