Files
2026-01-28 19:33:35 +13:00

179 lines
5.8 KiB
Python

"""
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)