"""PROOF-OF-CONCEPT: detect the printed grey separator rules on a newspaper page using classical CV (no ML, no API). Horizontal + vertical thin straight lines are extracted by morphological opening with long thin kernels, then overlaid on the page so we can see whether they recover article boundaries. Usage: venv/bin/python3 _detect_rules.py [out_png] """ import sys from pathlib import Path import cv2 import numpy as np def detect_rules(gray, min_frac=0.10, thickness=9): """Return (h_lines, v_lines) as lists of (x1,y1,x2,y2). min_frac: a rule must be at least this fraction of page width/height long. """ H, W = gray.shape # The rules are mid-grey on white. Invert + threshold so lines become white. # Use a gentle adaptive-ish binarisation: anything clearly darker than paper. blur = cv2.GaussianBlur(gray, (3, 3), 0) bw = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1] def extract(kernel_len, horizontal): if horizontal: k = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_len, 1)) else: k = cv2.getStructuringElement(cv2.MORPH_RECT, (1, kernel_len)) opened = cv2.morphologyEx(bw, cv2.MORPH_OPEN, k, iterations=1) # thicken slightly so near-collinear fragments merge opened = cv2.dilate(opened, cv2.getStructuringElement( cv2.MORPH_RECT, (thickness, thickness))) cnts, _ = cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) lines = [] for c in cnts: x, y, w, h = cv2.boundingRect(c) if horizontal and w >= min_frac * W and h <= 60: lines.append((x, y + h // 2, x + w, y + h // 2, w)) if (not horizontal) and h >= min_frac * H and w <= 60: lines.append((x + w // 2, y, x + w // 2, y + h, h)) return lines h_lines = extract(int(min_frac * W), horizontal=True) v_lines = extract(int(min_frac * H), horizontal=False) return h_lines, v_lines def main(png_path, out_path=None): png_path = Path(png_path) out_path = Path(out_path) if out_path else Path(f"/tmp/rules_{png_path.stem}.png") img = cv2.imread(str(png_path)) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) H, W = gray.shape h_lines, v_lines = detect_rules(gray) print(f"page {png_path.name} {W}x{H}") print(f" horizontal rules: {len(h_lines)}") for x1, y1, x2, y2, ln in sorted(h_lines, key=lambda L: L[1]): print(f" y={y1:>5} x[{x1:>4}..{x2:>4}] len={ln}") print(f" vertical rules: {len(v_lines)}") for x1, y1, x2, y2, ln in sorted(v_lines, key=lambda L: L[0]): print(f" x={x1:>5} y[{y1:>4}..{y2:>4}] len={ln}") for x1, y1, x2, y2, ln in h_lines: cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 6) # red = horizontal for x1, y1, x2, y2, ln in v_lines: cv2.line(img, (x1, y1), (x2, y2), (255, 0, 0), 6) # blue = vertical cv2.imwrite(str(out_path), img) print(f"\noverlay written: {out_path}") if __name__ == "__main__": main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)