""" Curve rasterization """ import math import numpy as np import cv2 from typing import Dict, List def sample_smooth_curve(points: List[List[float]], point_types: List[str], closed: bool = False, samples_per_segment: int = 15) -> List[List[float]]: """ Convert smooth curve to polygon by sampling using Catmull-Rom style interpolation. Args: points: List of [x, y] point coordinates point_types: List of "smooth" or "hard" for each point closed: Whether curve is closed samples_per_segment: Number of samples per smooth segment Returns: List of sampled [x, y] points """ # Validate inputs if not isinstance(points, list) or len(points) < 2: return points if not isinstance(point_types, list) or len(point_types) != len(points): return points # No valid pointTypes, return original # Validate all point types are valid valid_types = {"smooth", "hard"} if not all(pt in valid_types for pt in point_types): return points # Invalid types, return original if all(pt == "hard" for pt in point_types): return points # All hard, return original n = len(points) # Helper to get point with wrap-around for closed curves def get_point(index: int) -> List[float]: if closed: return points[index % n] else: if index < 0: return points[0] if index >= n: return points[n - 1] return points[index] # Helper to get point type with wrap-around def get_type(index: int) -> str: if closed: return point_types[index % n] else: if index < 0: return point_types[0] if index >= n: return point_types[n - 1] return point_types[index] sampled = [points[0]] # Start with first point # For each segment segment_count = n if closed else n - 1 for i in range(segment_count): next_i = (i + 1) % n p0 = points[i] p1 = points[next_i] type0 = get_type(i) type1 = get_type(next_i) # Validate coordinates are finite if not (all(isinstance(coord, (int, float)) and math.isfinite(coord) for coord in p0 + p1)): sampled.append(p1) # Invalid coordinates, skip smoothing continue # Check segment length (zero-length segments) dx = p1[0] - p0[0] dy = p1[1] - p0[1] segment_length = math.sqrt(dx*dx + dy*dy) if segment_length < 0.1: # Very short segment, treat as hard sampled.append(p1) continue if type0 == "hard" and type1 == "hard": sampled.append(p1) else: # Use cubic curve with Catmull-Rom style control points prev_p = get_point(i - 1) next_next_p = get_point(i + 2) # Tension factor (0.5 is Catmull-Rom default) tension = 0.5 # Calculate control point 1 (based on p0's tangent if smooth) if type0 == "smooth": cp1x = p0[0] + (p1[0] - prev_p[0]) * tension / 2 cp1y = p0[1] + (p1[1] - prev_p[1]) * tension / 2 else: cp1x = p0[0] + (p1[0] - p0[0]) * 0.25 cp1y = p0[1] + (p1[1] - p0[1]) * 0.25 # Calculate control point 2 (based on p1's tangent if smooth) if type1 == "smooth": cp2x = p1[0] - (next_next_p[0] - p0[0]) * tension / 2 cp2y = p1[1] - (next_next_p[1] - p0[1]) * tension / 2 else: cp2x = p1[0] - (p1[0] - p0[0]) * 0.25 cp2y = p1[1] - (p1[1] - p0[1]) * 0.25 # Adaptive sampling based on segment length actual_samples = max(4, min(samples_per_segment, int(segment_length / 5))) # Sample cubic curve for j in range(1, actual_samples + 1): t = j / actual_samples # Cubic curve: (1-t)³P0 + 3(1-t)²tCP1 + 3(1-t)t²CP2 + t³P1 t2 = t * t t3 = t2 * t mt = 1 - t mt2 = mt * mt mt3 = mt2 * mt x = mt3 * p0[0] + 3 * mt2 * t * cp1x + 3 * mt * t2 * cp2x + t3 * p1[0] y = mt3 * p0[1] + 3 * mt2 * t * cp1y + 3 * mt * t2 * cp2y + t3 * p1[1] # Validate sampled point is finite if math.isfinite(x) and math.isfinite(y): sampled.append([x, y]) return sampled def rasterize_curve( mask: np.ndarray, curve: Dict, height: int, width: int ) -> None: """ Rasterize a polygon into the mask. Modifies mask in-place. Args: mask: Mask array to draw into curve: Curve dictionary with points as polygon vertices height: Mask height width: Mask width """ points = curve["points"] closed = curve.get("closed", False) point_types = curve.get("pointTypes", []) # Convert smooth curves to polygons by sampling sampled_points = sample_smooth_curve(points, point_types, closed) # Convert points to integer coordinates and clamp to bounds polygon = [] for point in sampled_points: x, y = point[0], point[1] # Clamp to mask bounds px = int(np.clip(x, 0, width - 1)) py = int(np.clip(y, 0, height - 1)) polygon.append([px, py]) # Convert to numpy array format for cv2 polygon = np.array(polygon, dtype=np.int32) # Fill polygon if closed and len(polygon) >= 3: cv2.fillPoly(mask, [polygon], 255) else: # Draw polyline if not closed cv2.polylines(mask, [polygon], False, 255, 2)