Initial support for user general remarks

This commit is contained in:
sebastien committed 2026-10-28 13:51:59 +01:00
1 parent a38c1ff4b3
commit 2c9dc65640
16 files changed
+606 -20

No files matched your search

+47 -8
View File
@@ -13,6 +13,10 @@ WIDTH_TOLERANCE = .08
# Start below it with clearance, including on later pages with repeated names.
BODY_TOP = .30
BODY_BOTTOM = .82
LABEL_BOTTOM = .92
LABEL_LEFT_FRACTION = .05
COHERENT_MARGIN_TOLERANCE_MM = 10
LABEL_MARGIN_GAP_MM = 10
DISCARD_LEFT_FRACTION = .10
STANDARD_MARGIN_MM = 40
MARGIN_CLEARANCE_MM = 20
@@ -47,21 +51,28 @@ def handwriting_mask(rgb: np.ndarray) -> np.ndarray:
return keep[labels] & diagnostics['masks']['foreground']
def body_edge(mask: np.ndarray) -> dict:
"""Estimate a left edge from occupied 4 mm bands within 30–82% height."""
height = mask.shape[0]
def occupied_band_starts(mask: np.ndarray, bottom_fraction: float) -> list[float]:
"""Ignore empty bands and writing starting beyond the page midpoint."""
height, width = mask.shape
band_height = max(1, round(4 * ANALYSIS_DPI / 25.4))
bottom = round(BODY_BOTTOM * height)
bottom = round(bottom_fraction * height)
starts = []
for y in range(round(BODY_TOP * height), bottom, band_height):
band = mask[y:min(y + band_height, bottom)]
xs = np.flatnonzero(band.sum(axis=0) >= 2)
if band.sum() >= 40 and len(xs) >= 12:
if band.sum() >= 40 and len(xs) >= 12 and xs[0] <= width / 2:
starts.append(float(xs[0]) * 25.4 / ANALYSIS_DPI)
return starts
def body_edge(mask: np.ndarray) -> dict:
"""Keep the body decile; collect a wider 30–92% label-search window."""
starts = occupied_band_starts(mask, BODY_BOTTOM)
return {
'edge_mm': trimmed_left_edge(starts) if len(starts) >= 6 else None,
'bands': len(starts),
'band_starts_mm': starts,
'label_band_starts_mm': occupied_band_starts(mask, LABEL_BOTTOM),
}
@@ -90,7 +101,7 @@ def format_groups(images: list[Image.Image], reference_width: float) -> list[lis
def analyse_margins(images: list[Image.Image], render_dpi: int, reference_width: float) -> dict:
"""Pool occupied bands and use the larger half-margin or 20 mm-clearance cut."""
"""Estimate body margins, then retain left labels supported across coherent pages."""
starts = [0] * len(images)
groups = []
page_estimates = {}
@@ -116,6 +127,24 @@ def analyse_margins(images: list[Image.Image], render_dpi: int, reference_width:
status = 'near-edge' if edge < MIN_MARGIN_MM else 'margin'
left_mm = (0.0 if edge < MIN_MARGIN_MM else
max(edge / 2, edge - MARGIN_CLEARANCE_MM))
# Use ALL coherent pages, not just the four body-margin samples.
# A sparse/low/outlying page cannot supply evidence for this format's
# established body margin. Its normal fallback behaviour remains below.
coherent = [index for index in indices
if edge is not None and page_estimate(index)['edge_mm'] is not None
and page_estimate(index)['edge_mm'] >= MIN_MARGIN_MM
and abs(page_estimate(index)['edge_mm'] - edge) <= COHERENT_MARGIN_TOLERANCE_MM]
label_starts = [start for index in coherent for start in
page_estimate(index).get('label_band_starts_mm',
page_estimate(index)['band_starts_mm'])]
label_edge = (round(sorted(label_starts)[int(len(label_starts) * LABEL_LEFT_FRACTION)], 1)
if label_starts else None)
labels_in_margin = (status == 'margin' and len(coherent) >= min(2, len(indices))
and label_edge is not None and edge - label_edge >= LABEL_MARGIN_GAP_MM)
body_left_mm = left_mm
if labels_in_margin:
status = 'labels-in-margin'
left_mm = 0.0
# Secondary subpages can start in the middle of an answer. Preserve
# their left edge even when the only visible writing is indented.
if images[indices[0]].width < .8 * reference_width:
@@ -129,6 +158,11 @@ def analyse_margins(images: list[Image.Image], render_dpi: int, reference_width:
'status': status,
'edge_mm': edge,
'left_mm': round(left_mm, 1),
'body_left_mm': round(body_left_mm, 1),
'labels_in_margin': labels_in_margin,
'coherent_pages': [index + 1 for index in coherent],
'label_edge_mm': label_edge,
'label_bands': len(label_starts),
'bands': len(band_starts),
'discarded_bands': int(len(band_starts) * DISCARD_LEFT_FRACTION),
'samples': [{'page': index + 1, **item} for index, item in estimates.items()],
@@ -154,9 +188,14 @@ def analyse_margins(images: list[Image.Image], render_dpi: int, reference_width:
'status': 'unknown' if edge is None else
('low' if edge < MIN_MARGIN_MM else 'normal')})
return {'version': 8, 'pages': pages, 'starts_px': starts, 'groups': groups,
return {'version': 9, 'pages': pages, 'starts_px': starts, 'groups': groups,
'no_margin_from_page': no_margin_from_page,
'discard_left_fraction': DISCARD_LEFT_FRACTION, 'margin_clearance_mm': MARGIN_CLEARANCE_MM,
'cut_fraction_of_margin': .5, 'cut_rule': 'maximum',
'standard_margin_mm': STANDARD_MARGIN_MM, 'minimum_margin_mm': MIN_MARGIN_MM,
'body_height_fraction': [BODY_TOP, BODY_BOTTOM]}
'body_height_fraction': [BODY_TOP, BODY_BOTTOM],
'label_height_fraction': [BODY_TOP, LABEL_BOTTOM],
'label_left_fraction': LABEL_LEFT_FRACTION,
'coherent_margin_tolerance_mm': COHERENT_MARGIN_TOLERANCE_MM,
'label_margin_gap_mm': LABEL_MARGIN_GAP_MM,
'maximum_band_start_width_fraction': .5}