Add right-edge spine shadow detection for book scans
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Failing after 1m56s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 22s

Mirror the left-edge shadow detection for the right side: analyze
brightness gradient in the right 25% to find scanner gray strips
from book spines. Cuts at the last bright column before the shadow
dip. Fixes cropping of book scans where the next page bleeds in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-19 07:41:13 +01:00
parent a3e2a7f994
commit e56391b0c3

View File

@@ -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)