| """ |
| calibrate_fields.py |
| =================== |
| Click-to-measure tool for recalibrating field ratios in field_extractor.py. |
| |
| Usage: |
| python calibrate_fields.py --image your_scan.png --form birth |
| |
| Controls: |
| β’ Click and drag β draw a field box |
| β’ After releasing β enter the field name in the terminal |
| β’ Press S β save all measured ratios to calibrated_fields.py |
| β’ Press Z β undo last box |
| β’ Press Q / ESC β quit without saving |
| |
| Output: |
| calibrated_fields.py β copy-paste the dict into field_extractor.py |
| """ |
|
|
| import argparse |
| import json |
| import cv2 |
| import numpy as np |
| from pathlib import Path |
|
|
| |
| drawing = False |
| ix, iy = -1, -1 |
| ex, ey = -1, -1 |
| boxes = [] |
| form_name = "birth" |
|
|
| COLOURS = [ |
| (0,200,0),(0,150,255),(200,0,200),(0,200,200),(200,200,0),(220,20,60), |
| (255,140,0),(150,50,200),(0,160,80),(30,144,255),(255,20,147),(100,200,100), |
| ] |
|
|
| def draw_boxes(img, bounds): |
| left, top, right, bottom = bounds |
| fw = right - left |
| fh = bottom - top |
|
|
| vis = img.copy() |
| |
| cv2.rectangle(vis, (left, top), (right, bottom), (0, 140, 255), 2) |
|
|
| for idx, (name, rx1, ry1, rx2, ry2) in enumerate(boxes): |
| x1 = int(left + rx1 * fw) |
| y1 = int(top + ry1 * fh) |
| x2 = int(left + rx2 * fw) |
| y2 = int(top + ry2 * fh) |
| c = COLOURS[idx % len(COLOURS)] |
| cv2.rectangle(vis, (x1, y1), (x2, y2), c, 2) |
| cv2.putText(vis, name[:25], (x1 + 2, max(0, y1 - 3)), |
| cv2.FONT_HERSHEY_SIMPLEX, 0.35, c, 1) |
|
|
| |
| if drawing and ix >= 0 and ex >= 0: |
| cv2.rectangle(vis, (ix, iy), (ex, ey), (255, 255, 255), 1) |
|
|
| |
| cv2.putText(vis, "Drag=draw box | S=save | Z=undo | Q=quit", |
| (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 1) |
| cv2.putText(vis, f"Boxes: {len(boxes)}", |
| (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) |
| return vis |
|
|
|
|
| def detect_bounds(image_bgr): |
| """Simple form boundary detection (reuses logic from FormBoundsDetector).""" |
| h, w = image_bgr.shape[:2] |
| gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY) |
| try: |
| thresh = cv2.adaptiveThreshold( |
| gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, |
| cv2.THRESH_BINARY_INV, 11, 2) |
| hk = cv2.getStructuringElement(cv2.MORPH_RECT, (max(w // 5, 10), 1)) |
| h_lines = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, hk) |
| h_rows = np.where(np.sum(h_lines, axis=1) > w * 0.15)[0] |
| vk = cv2.getStructuringElement(cv2.MORPH_RECT, (1, max(h // 5, 10))) |
| v_lines = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vk) |
| v_cols = np.where(np.sum(v_lines, axis=0) > h * 0.08)[0] |
| if len(h_rows) == 0 or len(v_cols) == 0: |
| return (0, 0, w, h) |
| top_b, bottom_b = int(h_rows.min()), int(h_rows.max()) |
| left_b, right_b = int(v_cols.min()), int(v_cols.max()) |
| if (right_b - left_b) < w * 0.4 or (bottom_b - top_b) < h * 0.4: |
| return (0, 0, w, h) |
| return (left_b, top_b, right_b, bottom_b) |
| except Exception: |
| return (0, 0, w, h) |
|
|
|
|
| def save_calibration(output_path, form): |
| dict_name = { |
| "birth": "BIRTH_FIELDS", |
| "death": "DEATH_FIELDS", |
| "marriage": "MARRIAGE_FIELDS", |
| "marriage_license": "MARRIAGE_LICENSE_FIELDS", |
| }.get(form, "CALIBRATED_FIELDS") |
|
|
| lines = [f"# Auto-calibrated β copy-paste into field_extractor.py\n", |
| f"{dict_name} = {{\n"] |
| for name, rx1, ry1, rx2, ry2 in boxes: |
| lines.append(f' "{name}":{" " * max(1, 34 - len(name))}' |
| f'({rx1:.4f}, {ry1:.4f}, {rx2:.4f}, {ry2:.4f}),\n') |
| lines.append("}\n") |
|
|
| with open(output_path, "w") as f: |
| f.writelines(lines) |
| print(f"\n Saved {len(boxes)} fields β {output_path}") |
|
|
|
|
| def main(): |
| global drawing, ix, iy, ex, ey, form_name |
|
|
| parser = argparse.ArgumentParser() |
| parser.add_argument("--image", required=True) |
| parser.add_argument("--form", default="birth", |
| choices=["birth","death","marriage","marriage_license"]) |
| parser.add_argument("--output", default="calibrated_fields.py") |
| parser.add_argument("--scale", type=float, default=1.0, |
| help="Scale factor to fit image on screen (e.g. 0.5)") |
| args = parser.parse_args() |
| form_name = args.form |
|
|
| img_orig = cv2.imread(args.image) |
| if img_orig is None: |
| print(f"ERROR: Cannot load {args.image}") |
| return |
|
|
| scale = args.scale |
| if scale != 1.0: |
| img_orig = cv2.resize(img_orig, None, fx=scale, fy=scale) |
|
|
| bounds = detect_bounds(img_orig) |
| left, top, right, bottom = bounds |
| fw = right - left |
| fh = bottom - top |
| print(f" Form boundary detected: {bounds} ({fw}Γ{fh} px)") |
| print(f" Scale: {scale}") |
| print("\n Instructions:") |
| print(" Drag β draw a field box") |
| print(" After releasing β type field name in terminal, press Enter") |
| print(" S β save all boxes") |
| print(" Z β undo last box") |
| print(" Q/ESC β quit\n") |
|
|
| win = "Calibrate Fields" |
| cv2.namedWindow(win, cv2.WINDOW_NORMAL) |
|
|
| def mouse(event, x, y, flags, param): |
| global drawing, ix, iy, ex, ey |
| if event == cv2.EVENT_LBUTTONDOWN: |
| drawing = True |
| ix, iy = x, y |
| ex, ey = x, y |
| elif event == cv2.EVENT_MOUSEMOVE and drawing: |
| ex, ey = x, y |
| elif event == cv2.EVENT_LBUTTONUP: |
| drawing = False |
| ex, ey = x, y |
| x1r = (min(ix, ex) - left) / fw |
| y1r = (min(iy, ey) - top) / fh |
| x2r = (max(ix, ex) - left) / fw |
| y2r = (max(iy, ey) - top) / fh |
| x1r, y1r = max(0.0, x1r), max(0.0, y1r) |
| x2r, y2r = min(1.0, x2r), min(1.0, y2r) |
| if (x2r - x1r) > 0.005 and (y2r - y1r) > 0.003: |
| name = input(f" Field name for ({x1r:.3f},{y1r:.3f},{x2r:.3f},{y2r:.3f}): ").strip() |
| if name: |
| boxes.append((name, x1r, y1r, x2r, y2r)) |
| print(f" β '{name}' added (total: {len(boxes)})") |
|
|
| cv2.setMouseCallback(win, mouse) |
|
|
| while True: |
| vis = draw_boxes(img_orig, bounds) |
| cv2.imshow(win, vis) |
| key = cv2.waitKey(20) & 0xFF |
|
|
| if key in (ord('q'), 27): |
| print(" Quit β no file saved.") |
| break |
| elif key == ord('s'): |
| save_calibration(args.output, form_name) |
| break |
| elif key == ord('z') and boxes: |
| removed = boxes.pop() |
| print(f" Undone: '{removed[0]}'") |
|
|
| cv2.destroyAllWindows() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|