diff --git a/klausur-service/backend/page_crop.py b/klausur-service/backend/page_crop.py index 531b000..28d3fd1 100644 --- a/klausur-service/backend/page_crop.py +++ b/klausur-service/backend/page_crop.py @@ -201,8 +201,8 @@ def detect_and_crop_page( # --- Left edge: spine-shadow detection --- left_edge = _detect_left_edge_shadow(gray, binary, w, h) - # --- Right edge: binary vertical projection --- - right_edge = _detect_right_edge(binary, w, h) + # --- Right edge: spine-shadow detection --- + right_edge = _detect_right_edge_shadow(gray, binary, w, h) # --- Top / bottom edges: binary horizontal projection --- top_edge, bottom_edge = _detect_top_bottom_edges(binary, w, h) @@ -323,8 +323,50 @@ def _detect_left_edge_shadow( return _detect_edge_projection(binary, axis=0, from_start=True, dim=w) -def _detect_right_edge(binary: np.ndarray, w: int, h: int) -> int: - """Detect right content edge via binary vertical projection.""" +def _detect_right_edge_shadow( + gray: np.ndarray, + binary: np.ndarray, + w: int, + h: int, +) -> int: + """Detect right content edge, accounting for book-spine shadow. + + Mirror of _detect_left_edge_shadow: look at the right 25% of the image + for a brightness dip (scanner gray strip at book spine). + The darkest point in the gradient marks the spine center; crop there. + """ + search_w = max(1, w // 4) + right_start = w - search_w + + # Column-mean brightness in the right quarter + col_means = np.mean(gray[:, right_start:], axis=0).astype(np.float64) + + # Smooth with boxcar kernel (width = 1% of image width, min 5) + kernel_size = max(5, w // 100) + if kernel_size % 2 == 0: + kernel_size += 1 + kernel = np.ones(kernel_size) / kernel_size + smoothed = np.convolve(col_means, kernel, mode="same") + + # Determine brightness threshold: midpoint between darkest and brightest + val_min = float(np.min(smoothed)) + val_max = float(np.max(smoothed)) + shadow_range = val_max - val_min + + # Only use shadow detection if there is a meaningful brightness gradient (> 20 levels) + if shadow_range > 20: + threshold = val_min + shadow_range * 0.6 + # Find LAST column (from right) where brightness exceeds threshold + # = first column from right that drops below threshold marks the spine + above = np.where(smoothed >= threshold)[0] + if len(above) > 0: + # The last bright column before it drops into shadow + shadow_edge = right_start + int(above[-1]) + logger.debug("Right edge: shadow detected at x=%d (range=%.0f)", + shadow_edge, shadow_range) + return shadow_edge + + # Fallback: binary vertical projection return _detect_edge_projection(binary, axis=0, from_start=False, dim=w)