Initial support for user general remarks
This commit is contained in:
1 parent
a38c1ff4b3
commit
2c9dc65640
16 files changed
+606
-20
No files matched your search
@@ -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}
|
||||
Reference in new issue
Block a user