kerf-engine/engine/pipeline/vectorize.py
jlightner a12646de89 test: Implemented potrace_trace() function that converts preprocessed b…
- engine/pipeline/vectorize.py
- engine/tests/test_vectorize.py

GSD-Task: S01/T03
2026-03-26 04:15:01 +00:00

74 lines
2.3 KiB
Python

"""Vectorization pipeline — converts preprocessed binary images to SVG."""
import numpy as np
import potrace
def potrace_trace(
binary_img: np.ndarray,
turdsize: int = 2,
alphamax: float = 1.0,
opticurve: bool = True,
opttolerance: float = 0.2,
) -> str:
"""Trace a binary image using Potrace and return an SVG string.
Args:
binary_img: 2D numpy array — nonzero pixels are foreground.
turdsize: Despeckle threshold; curves with enclosed area below this are removed.
alphamax: Corner detection threshold (0.0 = polygon, 1.3333 = no corners).
opticurve: Whether to optimize curves by reducing Bezier segments.
opttolerance: Tolerance for curve optimization.
Returns:
Well-formed SVG string.
"""
if binary_img.ndim != 2:
raise ValueError(f"Expected 2D binary image, got shape {binary_img.shape}")
h, w = binary_img.shape
# Potrace interprets nonzero pixels as foreground.
# Convert to uint32 — pypotrace needs values that fit in a C int.
data = (binary_img > 0).astype(np.uint32)
bmp = potrace.Bitmap(data)
path = bmp.trace(
turdsize=turdsize,
alphamax=alphamax,
opticurve=int(opticurve),
opttolerance=opttolerance,
)
return _path_to_svg(path, w, h)
def _path_to_svg(path, width: int, height: int) -> str:
"""Convert a potrace Path object to an SVG string."""
parts = []
for curve in path:
sx, sy = curve.start_point
parts.append(f"M {sx:.3f},{sy:.3f}")
for segment in curve.segments:
if segment.is_corner:
cx, cy = segment.c
ex, ey = segment.end_point
parts.append(f"L {cx:.3f},{cy:.3f} L {ex:.3f},{ey:.3f}")
else:
c1x, c1y = segment.c1
c2x, c2y = segment.c2
ex, ey = segment.end_point
parts.append(
f"C {c1x:.3f},{c1y:.3f} {c2x:.3f},{c2y:.3f} {ex:.3f},{ey:.3f}"
)
parts.append("Z")
d = " ".join(parts)
return (
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'width="{width}" height="{height}" '
f'viewBox="0 0 {width} {height}">'
f'<path d="{d}" fill="black" fill-rule="evenodd" stroke="none"/>'
f"</svg>"
)