Spaces:
Running
Running
File size: 4,808 Bytes
92a5683 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from features import _ensure_ink_true, binarize
@dataclass(frozen=True)
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
|