Add geometry draft mode and pad silhouette descenders

pre-commit: 1 maintain.sh - not rerun full, ruff + 34 targeted tests passed; 2) /simplify - pad lives in silhouette crop, stable=False reuses probes; 3) docs sync - no README/cli refs to update; 4) CLAUDE.md - no change
EOF
)
This commit is contained in:
Victor Kuznetsov
2026-08-19 19:53:38 -07:00
parent d84fd1a125
commit ee865e95e4
4 changed files with 60 additions and 4 deletions
@@ -185,6 +185,20 @@ def restore_verified_text(
return Image.fromarray(restored)
def _glyph_crop_box(box: tuple[int, int, int, int], width: int, height: int) -> tuple[int, int, int, int]:
"""Widen the detector box so descenders stay inside the silhouette crop.
Paddle line boxes sit 2-5 px above the true ink bottom on the poster
fixtures. The crop is the box itself, so those pixels never reached the
donor composite. Expand only on Y: 8% up, 25% down, clamped to the frame.
"""
x1, y1, x2, y2 = box
line_h = max(1, y2 - y1)
pad_top = max(1, round(line_h * 0.08))
pad_bot = max(2, round(line_h * 0.25))
return max(0, x1), max(0, y1 - pad_top), min(width, x2), min(height, y2 + pad_bot)
def source_silhouette_mask(
source_rgb: NDArray[Any],
box: tuple[int, int, int, int],
@@ -192,7 +206,7 @@ def source_silhouette_mask(
) -> NDArray[Any]:
"""Recover a thresholded glyph shape without retaining source amplitudes."""
height, width = source_rgb.shape[:2]
x1, y1, x2, y2 = _clip_box(box, width, height)
x1, y1, x2, y2 = _glyph_crop_box(box, width, height)
gray = cv2.cvtColor(source_rgb[y1:y2, x1:x2], cv2.COLOR_RGB2GRAY)
support = np.ones(gray.shape, dtype=np.uint8)
if angle:
+16 -3
View File
@@ -223,15 +223,22 @@ def draft_text_lines(
min_score: float = 0.85,
detector: Any | None = None,
engines: dict[str, Any] | None = None,
stable: bool = True,
) -> TextDraft:
"""Propose verified-text manifest lines for ``image``; never verified ones.
Args:
image: path of the source image (draft boxes are in ITS pixel space).
min_score: recognition confidence floor for every jittered read.
min_score: recognition confidence floor. With ``stable=True`` every
jittered read must clear it; with ``stable=False`` the single
probe for the chosen language must.
detector/engines: injectable Paddle objects (tests use fakes); when
None they are built from the ``text-draft`` extra, which must be
installed (``draft_available()`` reports it).
stable: when True (default), accept a line only if three crop paddings
normalize identically. When False, take one recognition trio and
accept on score alone. Automatic restoration uses box/script only,
so the jitter gate is optional there.
Returns:
``TextDraft`` with crop-stable ``accepted`` proposals and unstable
@@ -262,8 +269,14 @@ def draft_text_lines(
probes[language] = _recognize(engine, source_rgb, box, script, 0.1)
language = choose_language(probes)
script = "cjk" if language == "ch" else "alphabetic"
reads = [_recognize(engines[language], source_rgb, box, script, ratio) for ratio in JITTER_RATIOS]
text = stable_recognition(reads, min_score)
if stable:
reads = [_recognize(engines[language], source_rgb, box, script, ratio) for ratio in JITTER_RATIOS]
text = stable_recognition(reads, min_score)
else:
text, score = probes[language]
reads = [(text, score)]
if not text.strip() or score < min_score:
text = None
if text is None:
rejected.append(
RejectedLine(