Global page assembly with loop closures and lens undistortion
The frame-by-frame stitcher accumulated drift, so later passes landed rotated and offset, doubling text. myscan/pagegraph.py instead solves all frame poses together: ECC odometry between consecutive frames (bridging single corrupt frames), SIFT+RANSAC loop closures between overlapping passes, and a robust Gauss-Newton pose graph. Loop matches must agree over the whole overlap, which rejects matches on repeated text. Off-page frames and frames that disagree with the mosaic are left out of the render. calib.Undistort applies the EEPROM lens model (OpenCV k1..k3, p1, p2 plus the tilt homography, as decoded in EEPROM.md); up to 8.8 px at the corners. scan.py registers frames on a worker thread so capture no longer drops frames, then assembles the page after the stop press (~11 s for a full page). stitch.py uses the new assembly by default; --live keeps the old one. EEPROM.md documents the full calibration blob layout, checksum and signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
-4
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stitch a directory recorded by tools/record.py into a page image."""
|
||||
"""Stitch a directory recorded by tools/record.py (or scan.py --save-frames) into a page image."""
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
@@ -11,6 +11,7 @@ import cv2 # noqa: E402
|
||||
import numpy as np # noqa: E402
|
||||
|
||||
from myscan import calib # noqa: E402
|
||||
from myscan.pagegraph import PageGraph # noqa: E402
|
||||
from myscan.stitch import ROT_GAIN, Stitcher # noqa: E402
|
||||
|
||||
|
||||
@@ -18,9 +19,11 @@ def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("dir")
|
||||
ap.add_argument("-o", "--out", help="output image (default <dir>/page.png)")
|
||||
ap.add_argument("--no-refine", action="store_true", help="lasers only, no image registration")
|
||||
ap.add_argument("--live", action="store_true", help="use the live frame-by-frame stitcher instead of the global one")
|
||||
ap.add_argument("--no-refine", action="store_true", help="with --live: lasers only, no image registration")
|
||||
ap.add_argument("--rot-gain", type=float, default=ROT_GAIN)
|
||||
ap.add_argument("--no-flat", action="store_true", help="skip the EEPROM flat-field correction")
|
||||
ap.add_argument("--no-undistort", action="store_true", help="skip the EEPROM lens correction")
|
||||
ap.add_argument("--no-wb", action="store_true", help="skip white-patch balancing")
|
||||
ap.add_argument("--start", type=int, default=0)
|
||||
ap.add_argument("--end", type=int)
|
||||
@@ -31,12 +34,15 @@ def main():
|
||||
frames = [int(r["frame"]) for r in rows]
|
||||
rng = range(a.start, a.end if a.end is not None else len(rows))
|
||||
|
||||
flat = None if a.no_flat else calib.FlatField(calib.load())
|
||||
cal = calib.load()
|
||||
flat = None if a.no_flat else calib.FlatField(cal)
|
||||
undistort = None if a.no_undistort else calib.Undistort(cal)
|
||||
st = None
|
||||
for i in rng:
|
||||
im = cv2.imread(os.path.join(a.dir, f"f{frames[i]:05d}.png"), cv2.IMREAD_UNCHANGED)
|
||||
if st is None:
|
||||
st = Stitcher(im.shape, refine=not a.no_refine, rot_gain=a.rot_gain, flat=flat)
|
||||
st = (Stitcher(im.shape, refine=not a.no_refine, rot_gain=a.rot_gain, flat=flat) if a.live
|
||||
else PageGraph(im.shape, flat=flat, undistort=undistort, rot_gain=a.rot_gain))
|
||||
st.add(im, v[i])
|
||||
out = a.out or os.path.join(a.dir, "page.png")
|
||||
cv2.imwrite(out, st.image(white_balance=not a.no_wb))
|
||||
|
||||
Reference in New Issue
Block a user