Spaces:
Running
Running
Download sequence_segmentation.py from loohhoo/detangutify: direct link, hf CLI and curl.
- Browser
- Download file 4.81 kB
-
https://huggingface.co/spaces/loohhoo/detangutify/resolve/main/sequence_segmentation.py
- Command line
-
hf download hf://spaces/loohhoo/detangutify/sequence_segmentation.py
-
curl -L -o sequence_segmentation.py https://huggingface.co/spaces/loohhoo/detangutify/resolve/main/sequence_segmentation.py
4.81 kB
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import numpy as np | |
| from features import _ensure_ink_true, binarize | |
| class GlyphSegment: | |
| bbox: tuple[int, int, int, int] | |
| image: np.ndarray | |
| def _true_runs(values: np.ndarray) -> list[tuple[int, int]]: | |
| padded = np.pad(np.asarray(values, dtype=np.int8), (1, 1)) | |
| changes = np.flatnonzero(np.diff(padded)) | |
| return [(int(start), int(end)) for start, end in changes.reshape(-1, 2)] | |
| def _tight_bbox(mask: np.ndarray) -> tuple[int, int, int, int] | None: | |
| ys, xs = np.where(mask) | |
| if not len(xs): | |
| return None | |
| return int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1 | |
| def _projection_boundaries( | |
| projection: np.ndarray, | |
| orthogonal_extent: int, | |
| max_characters: int, | |
| ) -> list[int]: | |
| extent = int(len(projection)) | |
| if extent < 2: | |
| return [] | |
| # A real gap between glyph cells is normally much wider than an internal | |
| # one- or two-pixel opening. Permit a tiny amount of antialiasing/noise in | |
| # an otherwise blank column or row. | |
| blank_threshold = max(0, int(round(orthogonal_extent * 0.015))) | |
| minimum_gap = max(2, int(round(orthogonal_extent * 0.06))) | |
| boundaries = [ | |
| (start + end) // 2 | |
| for start, end in _true_runs(projection <= blank_threshold) | |
| if start > 0 and end < extent and end - start >= minimum_gap | |
| ] | |
| if boundaries: | |
| return boundaries[: max(0, max_characters - 1)] | |
| # Touching glyphs may have no completely blank divider. If the ink block | |
| # is clearly wider/taller than one square Tangut cell, look near expected | |
| # cell boundaries for the lowest-ink projection instead. | |
| estimated = min(max_characters, max(1, int(round(extent / max(orthogonal_extent, 1))))) | |
| if estimated < 2 or extent < orthogonal_extent * 1.35: | |
| return [] | |
| cell = extent / estimated | |
| minimum_cell = max(2, int(round(cell * 0.55))) | |
| chosen: list[int] = [] | |
| for index in range(1, estimated): | |
| target = cell * index | |
| radius = max(2, int(round(cell * 0.22))) | |
| low = max(minimum_cell, int(round(target)) - radius) | |
| high = min(extent - minimum_cell, int(round(target)) + radius) | |
| if high <= low: | |
| continue | |
| candidates = np.arange(low, high + 1) | |
| best = min( | |
| candidates, | |
| key=lambda value: (float(projection[value]), abs(float(value) - target)), | |
| ) | |
| if not chosen or best - chosen[-1] >= minimum_cell: | |
| chosen.append(int(best)) | |
| return chosen | |
| def segment_glyphs( | |
| gray: np.ndarray, | |
| direction: str = "auto", | |
| max_characters: int = 32, | |
| ) -> tuple[str, list[GlyphSegment]]: | |
| image = np.asarray(gray) | |
| if image.ndim != 2: | |
| raise ValueError("Sequence segmentation expects a two-dimensional grayscale image.") | |
| if image.size == 0 or int(image.max()) == int(image.min()): | |
| return "horizontal", [] | |
| mask = _ensure_ink_true(binarize(image, min_size=2)) | |
| overall = _tight_bbox(mask) | |
| if overall is None: | |
| return "horizontal", [] | |
| left, top, right, bottom = overall | |
| width = right - left | |
| height = bottom - top | |
| if direction == "auto": | |
| resolved_direction = "horizontal" if width >= height else "vertical" | |
| elif direction in {"horizontal", "vertical"}: | |
| resolved_direction = direction | |
| else: | |
| raise ValueError("direction must be 'auto', 'horizontal', or 'vertical'.") | |
| tight_mask = mask[top:bottom, left:right] | |
| if resolved_direction == "horizontal": | |
| projection = tight_mask.sum(axis=0) | |
| relative_boundaries = _projection_boundaries(projection, height, max_characters) | |
| boundaries = [left, *(left + value for value in relative_boundaries), right] | |
| regions = [(boundaries[i], top, boundaries[i + 1], bottom) for i in range(len(boundaries) - 1)] | |
| else: | |
| projection = tight_mask.sum(axis=1) | |
| relative_boundaries = _projection_boundaries(projection, width, max_characters) | |
| boundaries = [top, *(top + value for value in relative_boundaries), bottom] | |
| regions = [(left, boundaries[i], right, boundaries[i + 1]) for i in range(len(boundaries) - 1)] | |
| minimum_ink = max(4, int(round(mask.sum() * 0.005))) | |
| segments: list[GlyphSegment] = [] | |
| for x0, y0, x1, y1 in regions: | |
| region_mask = mask[y0:y1, x0:x1] | |
| if int(region_mask.sum()) < minimum_ink: | |
| continue | |
| local = _tight_bbox(region_mask) | |
| if local is None: | |
| continue | |
| lx0, ly0, lx1, ly1 = local | |
| box = (x0 + lx0, y0 + ly0, x0 + lx1, y0 + ly1) | |
| bx0, by0, bx1, by1 = box | |
| segments.append(GlyphSegment(box, image[by0:by1, bx0:bx1].copy())) | |
| return resolved_direction, segments | |