75 lines
3.1 KiB
Python
75 lines
3.1 KiB
Python
"""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 <page_png> [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)
|