Refine freehand exclusion polygons

Store freehand paths as closed polygons with thin outlines and fill the interior when masking, so free-drawn shapes behave like custom rectangles.
This commit is contained in:
lm 2025-10-17 17:11:18 +02:00
parent f678c403b7
commit 464855f365
2 changed files with 25 additions and 21 deletions

View File

@ -14,7 +14,6 @@ class ExclusionMixin:
y = max(0, min(self.preview_img.height - 1, int(event.y)))
if mode == "free":
self._current_stroke = [(x, y)]
width = int(getattr(self, "free_draw_width", 14))
preview_id = getattr(self, "_stroke_preview_id", None)
if preview_id:
try:
@ -27,15 +26,13 @@ class ExclusionMixin:
x,
y,
fill="yellow",
width=width,
width=2,
smooth=True,
capstyle="round",
joinstyle="round",
)
self._rubber_start = None
return
x = max(0, min(self.preview_img.width - 1, int(event.x)))
y = max(0, min(self.preview_img.height - 1, int(event.y)))
self._rubber_start = (x, y)
if self._rubber_id:
try:
@ -70,13 +67,12 @@ class ExclusionMixin:
mode = getattr(self, "exclude_mode", "rect")
if mode == "free":
stroke = getattr(self, "_current_stroke", None)
if stroke and len(stroke) > 1:
cleaned = self._compress_stroke(stroke)
if len(cleaned) > 1:
if stroke and len(stroke) > 2:
polygon = self._close_polygon(self._compress_stroke(stroke))
if len(polygon) >= 3:
shape = {
"kind": "stroke",
"points": cleaned,
"width": int(getattr(self, "free_draw_width", 14)),
"kind": "polygon",
"points": polygon,
}
self.exclude_shapes.append(shape)
stamper = getattr(self, "_stamp_shape_on_mask", None)
@ -190,5 +186,15 @@ class ExclusionMixin:
compressed.append(point)
return compressed
@staticmethod
def _close_polygon(points: list[tuple[int, int]]) -> list[tuple[int, int]]:
"""Ensure the polygon is closed by repeating the start if necessary."""
if len(points) < 3:
return points
closed = list(points)
if closed[0] != closed[-1]:
closed.append(closed[0])
return closed
__all__ = ["ExclusionMixin"]

View File

@ -266,12 +266,12 @@ class ImageProcessingMixin:
if shape.get("kind") == "rect":
x0, y0, x1, y1 = shape["coords"] # type: ignore[index]
outline.rectangle([x0, y0, x1, y1], outline=(255, 215, 0, 200), width=3)
elif shape.get("kind") == "stroke":
elif shape.get("kind") == "polygon":
points = shape.get("points", [])
if len(points) < 2:
continue
width_px = int(shape.get("width", 8))
outline.line(points, fill=(255, 215, 0, 200), width=width_px, joint="round")
path = points if points[0] == points[-1] else points + [points[0]]
outline.line(path, fill=(255, 215, 0, 200), width=2, joint="round")
return merged
def compute_stats_preview(self):
@ -423,14 +423,12 @@ class ImageProcessingMixin:
],
fill=255,
)
elif kind == "stroke":
elif kind == "polygon":
points = shape.get("points")
if not points or len(points) < 2:
return
base_width = float(shape.get("width", 8)) # type: ignore[arg-type]
width_px = max(1, int(round(base_width * (scale_x + scale_y) / 2.0)))
scaled = [(px * scale_x, py * scale_y) for px, py in points] # type: ignore[misc]
draw.line(scaled, fill=255, width=width_px, joint="round")
draw.polygon(scaled, fill=255)
def _render_exclusion_overlays(self) -> None:
if not hasattr(self, "canvas_orig"):
@ -449,16 +447,16 @@ class ImageProcessingMixin:
x0, y0, x1, y1, outline="yellow", width=3
)
self._exclude_canvas_ids.append(item)
elif kind == "stroke":
elif kind == "polygon":
points = shape.get("points")
if not points or len(points) < 2:
continue
width_px = int(shape.get("width", 8))
coords = [coord for point in points for coord in point] # type: ignore[misc]
closed = points if points[0] == points[-1] else points + [points[0]] # type: ignore[operator]
coords = [coord for point in closed for coord in point] # type: ignore[misc]
item = self.canvas_orig.create_line(
*coords,
fill="yellow",
width=width_px,
width=2,
smooth=True,
capstyle="round",
joinstyle="round",