Spaces:
Build error
Build error
Download src/editable_image/masking.py from tonigi/make_editable_image: direct link, hf CLI and curl.
- Browser
- Download file 3.86 kB
-
https://huggingface.co/spaces/tonigi/make_editable_image/resolve/main/src/editable_image/masking.py
- Command line
-
hf download hf://spaces/tonigi/make_editable_image/src/editable_image/masking.py
-
curl -L -o masking.py https://huggingface.co/spaces/tonigi/make_editable_image/resolve/main/src/editable_image/masking.py
3.86 kB
| from __future__ import annotations | |
| import cv2 | |
| import numpy as np | |
| from .models import DocumentSpec, MaskStroke, Point, TextRegion | |
| from .shape_recognition import shape_source_mask | |
| def build_mask(rgba: np.ndarray, document: DocumentSpec) -> np.ndarray: | |
| height, width = rgba.shape[:2] | |
| combined = np.zeros((height, width), dtype=np.uint8) | |
| for region in document.regions: | |
| if region.remove_text: | |
| combined = cv2.bitwise_or(combined, region_mask(rgba, region)) | |
| for shape in document.shapes: | |
| if shape.remove_source: | |
| shape_mask = shape_source_mask(shape, width, height) | |
| padding = max(0, int(round(shape.mask_padding))) | |
| if padding: | |
| kernel = cv2.getStructuringElement( | |
| cv2.MORPH_ELLIPSE, | |
| (padding * 2 + 1, padding * 2 + 1), | |
| ) | |
| shape_mask = cv2.dilate(shape_mask, kernel) | |
| combined = cv2.bitwise_or(combined, shape_mask) | |
| for stroke in document.mask_strokes: | |
| apply_stroke(combined, stroke) | |
| return combined | |
| def region_mask(rgba: np.ndarray, region: TextRegion) -> np.ndarray: | |
| height, width = rgba.shape[:2] | |
| polygon = np.array([[point.x, point.y] for point in region.source_quad], dtype=np.int32) | |
| bounds = cv2.boundingRect(polygon) | |
| x, y, w, h = bounds | |
| x0, y0 = max(0, x), max(0, y) | |
| x1, y1 = min(width, x + w), min(height, y + h) | |
| output = np.zeros((height, width), dtype=np.uint8) | |
| if x1 <= x0 or y1 <= y0: | |
| return output | |
| crop = rgba[y0:y1, x0:x1, :3] | |
| lab = cv2.cvtColor(crop, cv2.COLOR_RGB2LAB).astype(np.float32) | |
| local_polygon = polygon - np.array([x0, y0]) | |
| polygon_mask = np.zeros(crop.shape[:2], dtype=np.uint8) | |
| cv2.fillPoly(polygon_mask, [local_polygon], 255) | |
| perimeter = cv2.morphologyEx(polygon_mask, cv2.MORPH_GRADIENT, np.ones((3, 3), np.uint8)) | |
| border_pixels = lab[perimeter > 0] | |
| background = np.median(border_pixels, axis=0) if border_pixels.size else np.median(lab.reshape(-1, 3), axis=0) | |
| delta = np.linalg.norm(lab - background, axis=2) | |
| inside = delta[polygon_mask > 0] | |
| if inside.size: | |
| otsu_threshold, _ = cv2.threshold( | |
| np.clip(inside, 0, 255).astype(np.uint8), | |
| 0, | |
| 255, | |
| cv2.THRESH_BINARY + cv2.THRESH_OTSU, | |
| ) | |
| threshold = max(8.0, float(otsu_threshold) * 0.72) | |
| else: | |
| threshold = 8.0 | |
| foreground = ((delta >= threshold) & (polygon_mask > 0)).astype(np.uint8) * 255 | |
| count, labels, stats, _ = cv2.connectedComponentsWithStats(foreground, 8) | |
| filtered = np.zeros_like(foreground) | |
| max_area = max(9, int(w * h * 0.22)) | |
| min_area = 1 | |
| for index in range(1, count): | |
| area = stats[index, cv2.CC_STAT_AREA] | |
| component_height = stats[index, cv2.CC_STAT_HEIGHT] | |
| if min_area <= area <= max_area and component_height >= 1: | |
| filtered[labels == index] = 255 | |
| if cv2.countNonZero(filtered) < max(3, w * h * 0.005): | |
| filtered = foreground | |
| padding = max(1, int(round(region.mask_padding))) | |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (padding * 2 + 1, padding * 2 + 1)) | |
| filtered = cv2.dilate(filtered, kernel) | |
| output[y0:y1, x0:x1] = filtered | |
| return output | |
| def apply_stroke(mask: np.ndarray, stroke: MaskStroke) -> None: | |
| value = 255 if stroke.operation == "add" else 0 | |
| radius = max(1, int(round(stroke.radius))) | |
| points = [(int(round(point.x)), int(round(point.y))) for point in stroke.points] | |
| if len(points) == 1: | |
| cv2.circle(mask, points[0], radius, value, -1, lineType=cv2.LINE_AA) | |
| return | |
| for start, end in zip(points, points[1:]): | |
| cv2.line(mask, start, end, value, radius * 2, lineType=cv2.LINE_AA) | |
| cv2.circle(mask, end, radius, value, -1, lineType=cv2.LINE_AA) | |