From 464855f365dcd15049e201497230380cb1ae3386 Mon Sep 17 00:00:00 2001 From: lm Date: Fri, 17 Oct 2025 17:11:18 +0200 Subject: [PATCH] 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. --- app/gui/exclusions.py | 26 ++++++++++++++++---------- app/logic/image_processing.py | 20 +++++++++----------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/app/gui/exclusions.py b/app/gui/exclusions.py index cad4c96..30701d3 100644 --- a/app/gui/exclusions.py +++ b/app/gui/exclusions.py @@ -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"] diff --git a/app/logic/image_processing.py b/app/logic/image_processing.py index 51e5a8f..b4357d5 100644 --- a/app/logic/image_processing.py +++ b/app/logic/image_processing.py @@ -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",