ocr / calibrate_fields.py
hanz245's picture
set up
7111e1a
"""
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
# ── state ─────────────────────────────────────────────────────────────────────
drawing = False
ix, iy = -1, -1
ex, ey = -1, -1
boxes = [] # list of (name, rx1, ry1, rx2, ry2)
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()
# form boundary
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)
# live cursor box
if drawing and ix >= 0 and ex >= 0:
cv2.rectangle(vis, (ix, iy), (ex, ey), (255, 255, 255), 1)
# instructions
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()