Skip to content
Snippets Groups Projects
curve_path.py 7.53 KiB
Newer Older
  • Learn to ignore specific revisions
  • import numpy as np
    import matplotlib.pyplot as plt
    
    import math
    from scipy.ndimage import gaussian_filter1d
    
    def bezier(t, points):
        """Calculate Bezier curve point for parameter t and given control points."""
        n = len(points) - 1
        return sum(
            (math.comb(n, i) * (1 - t) ** (n - i) * t ** i * points[i] for i in range(n + 1)),
            np.zeros(2)
        )
    
    def smooth_path(points, N=100, alpha=0.25, sigma=1.0):
        smooth_points = []
        control_points = []
        i = 0
        while i < len(points) - 1:
            if i < len(points) - 3 and is_bend(points[i:i+4]):
                # Double bend (cubic bezier with two softened control points)
                p0, p1, p2, p3 = np.array(points[i]), np.array(points[i+1]), np.array(points[i+2]), np.array(points[i+3])
                cp1 = soften_control_point(p1, alpha, p0, p2)  # Soften the first control point
                cp2 = soften_control_point(p2, alpha, p1, p3)  # Soften the second control point
                control_points.extend([cp1, cp2])  # Collect control points for visualization
                for t in np.linspace(0, 1, 20):
                    smooth_points.append(bezier(t, [p0, cp1, cp2, p3]))
                i += 3
            elif i < len(points) - 2 and is_bend(points[i:i+3]):
                # Single bend (quadratic bezier with one softened control point)
                p0, p1, p2 = np.array(points[i]), np.array(points[i+1]), np.array(points[i+2])
                cp = soften_control_point(p1, alpha, p0, p2)  # Use refined softening
                control_points.append(cp)  # Collect control point for visualization
                for t in np.linspace(0, 1, 20):
                    smooth_points.append(bezier(t, [p0, cp, p2]))
                i += 2
            else:
                # No bend, interpolate straight line
    
    
                if points[i][0] == points[i+1][0] and points[i][1] == points[i+1][1]:
                    for t in range(20):
                        smooth_points.append(points[i])
                    i += 1
                    continue
    
                for t in np.linspace(0, 1, 20):
                    smooth_points.append((1 - t) * points[i] + t * points[i + 1])
    
                i += 1
    
        # Ensure start and end points are included
        smooth_points = [points[0]] + smooth_points + [points[-1]]
    
        # Apply Gaussian smoothing to soften remaining sharp transitions
        smooth_points = np.array(smooth_points)
        smooth_points[:, 0] = gaussian_filter1d(smooth_points[:, 0], sigma=sigma)
        smooth_points[:, 1] = gaussian_filter1d(smooth_points[:, 1], sigma=sigma)
    
        # Downsample to N points, preserving start and end points
        indices = np.linspace(0, len(smooth_points) - 1, N).astype(int)
        downsampled_points = smooth_points[indices]
        downsampled_points[0], downsampled_points[-1] = points[0], points[-1]
    
        return downsampled_points, np.array(control_points)
    
    def soften_control_point(middle_point, alpha, prev_point, next_point):
        """Move middle point along the bisector away from the 90-degree angle."""
    
        middle_point = np.array(middle_point, dtype=np.float64)
        prev_point = np.array(prev_point, dtype=np.float64)
        next_point = np.array(next_point, dtype=np.float64)
    
        # Vectors from middle point to adjacent points
        vec1 = prev_point - middle_point
        vec2 = next_point - middle_point
    
        # Normalize the vectors
        vec1 /= np.linalg.norm(vec1)
        vec2 /= np.linalg.norm(vec2)
    
        # Calculate the bisector direction
        bisector = vec1 + vec2
        bisector /= np.linalg.norm(bisector)  # Normalize bisector
    
        # Move middle point along the bisector direction
        adjusted_point = middle_point + alpha * bisector
        return adjusted_point
    
    def is_bend(segment):
        """Check if three or four points form a 90-degree bend."""
        if len(segment) == 3:
            return np.cross(segment[1] - segment[0], segment[2] - segment[1]) != 0
        elif len(segment) == 4:
            return (np.cross(segment[1] - segment[0], segment[2] - segment[1]) != 0 and
                    np.cross(segment[2] - segment[1], segment[3] - segment[2]) != 0)
        return False
    
    def calculate_headings(path_points):
        """
        Calculate headings for each segment in the path, allowing for reverse movement.
    
        Parameters:
            path_points (np.ndarray): Array of (x, y) points representing the smoothed path.
    
        Returns:
            headings (list): List of headings (in radians) for each segment in the path.
        """
        headings = []
        prev_heading = None
    
        for i in range(len(path_points) - 1):
            # Calculate forward and reverse headings for each segment
            p1, p2 = path_points[i], path_points[i + 1]
            forward_heading = np.arctan2(p2[1] - p1[1], p2[0] - p1[0])
    
    
            # print(f"p1 = {p1}, p2 = {p2}")
    
            if prev_heading is None:
                chosen_heading = forward_heading
    
            elif p2[0] == p1[0] and p2[1] == p1[1]:
                # print("same")
                chosen_heading = prev_heading
    
    
    
            # Choose direction based on previous heading to minimize angle change
    
            else:
                reverse_heading = (forward_heading + np.pi) % (2 * np.pi)
    
                forward_diff = np.abs((forward_heading - prev_heading + np.pi) % (2 * np.pi) - np.pi)
                reverse_diff = np.abs((reverse_heading - prev_heading + np.pi) % (2 * np.pi) - np.pi)
    
    
    
                chosen_heading = forward_heading if forward_diff <= reverse_diff else reverse_heading
    
            # plot the two points and the heading
            # import matplotlib.pyplot as plt
            # plt.plot([p1[0], p2[0]], [p1[1], p2[1]], 'ro-')  # Plot the two points
    
            # # dx = 0.1 * np.cos(forward_heading)
            # # dy = 0.1 * np.sin(forward_heading)
            # # plt.arrow(p1[0], p1[1], dx, dy, head_width=0.01, head_length=0.1, fc='blue', ec='blue')
            # dx = 0.1 * np.cos(chosen_heading)
            # dy = 0.1 * np.sin(chosen_heading)
    
            # plt.arrow(p1[0], p1[1], dx, dy, head_width=0.01, head_length=0.1, fc='green', ec='green')
            # plt.show()
    
            headings.append(chosen_heading)
            prev_heading = chosen_heading
    
        return headings
    
    
    if __name__ == "__main__":
    
        # Example points and visualization
        points = np.array([
            [0, 0], [0, 1], [1, 1], [2, 1], [2, 2], [2, 3], [2, 2], [2, 1], [2,0]
        ])
    
        # points = np.array([
        #     [0, 0], [0, 1], [1, 1], [1,0], [0,0]
        # ])
    
        # points = np.array([
            # [0, 0], [0, 1], [1, 1], [2, 1], [2, 2], [2, 3], [3, 3], [3, 2], [2,2], [2,3]
        # ])
    
        points = np.array([
            [3,3], [3,5], [1,5]])
    
    
        # Generate smooth curve and get control points
    
        smooth_points, control_points = smooth_path(points, N, alpha=-.5, sigma=0.8)
    
        print(f"smooth_points = {smooth_points}")
    
        # Example usage with a smooth path
        # path_points, control_points = smooth_path(points, N=100, alpha=0.2, sigma=1.5)
        headings = calculate_headings(smooth_points)
    
        # Displaying the headings
        for i, heading in enumerate(headings):
            print(f"Segment {i}: Heading = {np.degrees(heading):.2f} degrees")
    
        # Plotting
        plt.figure(figsize=(8, 8))
        plt.plot(smooth_points[:, 0], smooth_points[:, 1], 'b-', label="Bezier Smooth Path")
    
        plt.scatter(points[:, 0], points[:, 1], color="purple", marker="x", s=100, label="Control Points")
    
        # Add circles and headings
        for i, (x, y) in enumerate(smooth_points):
            plt.plot(x, y, 'ro')  # Circle at each point
            if i < len(headings):
                heading = headings[i]
                dx = 0.1 * np.cos(heading)
                dy = 0.1 * np.sin(heading)
                plt.arrow(x, y, dx, dy, head_width=0.05, head_length=0.1, fc='green', ec='green')
    
        plt.xlabel("X")
        plt.ylabel("Y")
    
        plt.legend()
        plt.grid(True)
        plt.show()