179 lines
5.8 KiB
Python
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)
|