# B5 baseline (plan 2026-10-06 §2.1): the textbook OpenCV document finder on the frames the SmartDoc run scored, cut to
# the same crops — from the original SmartDoc 2015 Challenge 1 frames and ground truth (metadata.csv.gz) and the crop
# windows in the published per-frame results — so anyone with the dataset can rerun it. On each crop: greyscale,
# Gaussian blur, Canny, the largest contours, the first that approximates to four corners (approxPolyDP at 2 % of its
# perimeter) and covers at least 10 % of the crop. Three common recipes, none tuned to the dataset:
#   tutorial — resized to 500 px tall, Canny 75 / 200 (the widely copied «document scanner» tutorial settings)
#   auto     — long side 960, Canny thresholds from the median grey (0.67 × and 1.33 × the median)
#   dilated  — auto, then the edges dilated with a 3 × 3 kernel so small gaps in the page outline close
# A frame counts as right when every corner is within 2 % of the crop's diagonal of the true corner.
#   python opencv-baseline.py <SmartDoc frames dir: background0N/…, metadata.csv.gz> <smartdoc-results.csv> <out.json>
import csv, gzip, json, math, sys, pathlib
import cv2, numpy as np
frames, results, out = pathlib.Path(sys.argv[1]), sys.argv[2], sys.argv[3]
truth = {}
with gzip.open(frames / 'metadata.csv.gz', 'rt') as f:
    for r in csv.DictReader(f):
        key = f"{r['bg_name']}/{r['model_name']}/frame_{int(r['frame_index']):04d}.jpeg"
        # top-left, top-right, bottom-right, bottom-left
        truth[key] = [[float(r['tl_x']), float(r['tl_y'])], [float(r['tr_x']), float(r['tr_y'])], [float(r['br_x']), float(r['br_y'])], [float(r['bl_x']), float(r['bl_y'])]]
def corner_error(found, true, w, h):
    best = math.inf
    for r in range(4):
        worst = max(math.hypot(found[(i + r) % 4][0] - true[i][0], found[(i + r) % 4][1] - true[i][1]) for i in range(4))
        best = min(best, worst)
    return best / math.hypot(w, h)
def order(points):  # top-left, top-right, bottom-right, bottom-left
    p = np.array(points, dtype=float); s = p.sum(1); d = p[:, 1] - p[:, 0]
    return [p[np.argmin(s)].tolist(), p[np.argmin(d)].tolist(), p[np.argmax(s)].tolist(), p[np.argmax(d)].tolist()]
def find(img, recipe):
    h, w = img.shape[:2]
    k = 500 / h if recipe == 'tutorial' else 960 / max(w, h)
    small = cv2.resize(img, (round(w * k), round(h * k)), interpolation=cv2.INTER_AREA)
    grey = cv2.GaussianBlur(cv2.cvtColor(small, cv2.COLOR_BGR2GRAY), (5, 5), 0)
    if recipe == 'tutorial':
        edges = cv2.Canny(grey, 75, 200)
    else:
        m = float(np.median(grey))
        edges = cv2.Canny(grey, int(max(0, 0.67 * m)), int(min(255, 1.33 * m)))
        if recipe == 'dilated': edges = cv2.dilate(edges, np.ones((3, 3), np.uint8))
    contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    for c in sorted(contours, key=cv2.contourArea, reverse=True)[:5]:
        approx = cv2.approxPolyDP(c, 0.02 * cv2.arcLength(c, True), True)
        if len(approx) == 4 and cv2.contourArea(approx) >= 0.10 * small.shape[0] * small.shape[1]:
            return order([[x / k, y / k] for [[x, y]] in approx])
    return None
RECIPES = ['tutorial', 'auto', 'dilated']
rows = []
for item in csv.DictReader(open(results)):
    key = f"{item['bg']}/{item['model']}/{item['frame']}"
    left, top, w, h = (int(item[k]) for k in ('cropLeft', 'cropTop', 'cropWidth', 'cropHeight'))
    img = cv2.imread(str(frames / key))[top:top + h, left:left + w]
    true = [[x - left, y - top] for x, y in truth[key]]
    row = {'file': f"{item['bg']}_{item['model']}_{item['frame'].replace('.jpeg', '')}.jpg", 'bg': item['bg']}
    for recipe in RECIPES:
        quad = find(img, recipe)
        row[recipe] = corner_error(quad, true, w, h) if quad else None
    rows.append(row)
json.dump({'method': 'opencv-canny-largest-quad', 'recipes': RECIPES, 'rows': rows}, open(out, 'w'))
for recipe in RECIPES:
    ok = sum(1 for r in rows if r[recipe] is not None and r[recipe] <= 0.02)
    found = sum(1 for r in rows if r[recipe] is not None)
    print(f"{recipe:8} frames {len(rows)}  within 2 % {ok} ({100 * ok / len(rows):.1f} %)  found but off {found - ok}")
