UVC extension-unit control (mouse/scan mode, LEDs, exposure), JPEG header fix and embedded laser record decoding, session recorder, and a stitcher that combines laser dead reckoning with phase correlation and ECC refinement. FINDINGS.md holds the reverse-engineered protocol and laser calibration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.4 KiB
Python
84 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Record a scan session: scan mode + auto-exposure, saving every frame (fixed
|
|
640x296 grayscale PNG) and its embedded sensor record to records.csv.
|
|
Mouse mode is restored on exit (also on Ctrl-C)."""
|
|
import argparse
|
|
import csv
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
sys.stderr = os.fdopen(os.dup(2), "w")
|
|
os.dup2(os.open(os.devnull, os.O_WRONLY), 2) # silence libjpeg warnings
|
|
|
|
import cv2 # noqa: E402
|
|
import numpy as np # noqa: E402
|
|
|
|
from myscan.frame import fix_and_extract # noqa: E402
|
|
from myscan.xu import XU # noqa: E402
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("-d", "--duration", type=float, default=20)
|
|
ap.add_argument("-o", "--out", default="scan")
|
|
ap.add_argument("--video", default="/dev/video4")
|
|
ap.add_argument("--target", type=float, default=150, help="auto-exposure target mean brightness")
|
|
ap.add_argument("--exposure", type=int, help="fixed E6 value instead of auto-exposure")
|
|
a = ap.parse_args()
|
|
os.makedirs(a.out, exist_ok=True)
|
|
|
|
x = XU(a.video)
|
|
e6 = a.exposure or 40
|
|
rows = []
|
|
try:
|
|
if not (x.disable_mouse()[0] and x.mcu_write_verified(0x2D, 0x60)
|
|
and x.mcu_write_verified(0xE7, 0x28) and x.mcu_write_verified(0xE6, e6)
|
|
and x.start_scan()[0]):
|
|
sys.exit("failed to enter scan mode")
|
|
cap = cv2.VideoCapture(a.video, cv2.CAP_V4L2)
|
|
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
|
|
cap.set(cv2.CAP_PROP_CONVERT_RGB, 0)
|
|
print(f"scanning for {a.duration:.0f}s -> {a.out}/ (Ctrl-C to stop early)", flush=True)
|
|
t0, n, last = time.time(), 0, 0.0
|
|
while time.time() - t0 < a.duration:
|
|
ok, f = cap.read()
|
|
if not ok:
|
|
continue
|
|
t = time.time() - t0
|
|
clean, rec = fix_and_extract(f.tobytes())
|
|
im = cv2.imdecode(np.frombuffer(clean, np.uint8), cv2.IMREAD_GRAYSCALE)
|
|
if im is None or rec is None:
|
|
continue
|
|
cv2.imwrite(f"{a.out}/f{n:05d}.png", im)
|
|
mean = float(im.mean())
|
|
rows.append([round(t, 4), n, rec.counter, *rec.v, rec.b14, rec.b15, rec.b16, rec.flags, e6,
|
|
round(mean, 1), rec.raw.hex()])
|
|
# Proportional auto-exposure every 3 frames (brightness is ~linear in E6).
|
|
if a.exposure is None and n % 3 == 0 and mean > 1:
|
|
new = int(min(255, max(2, round(e6 * (a.target / mean) ** 0.7))))
|
|
if new != e6 and x.mcu_write_verified(0xE6, new):
|
|
e6 = new
|
|
if t - last >= 1:
|
|
print(f"t={t:5.1f}s frames={n:4d} E6={e6:3d} bright={mean:5.1f} "
|
|
f"l1=({rec.v[0]:#06x},{rec.v[1]:#06x}) l2=({rec.v[3]:#06x},{rec.v[4]:#06x}) "
|
|
f"sq={rec.b14},{rec.b15} flags={rec.flags:05b}", flush=True)
|
|
last = t
|
|
n += 1
|
|
cap.release()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
print("restore mouse mode:", x.enable_mouse()[0])
|
|
with open(f"{a.out}/records.csv", "w", newline="") as fh:
|
|
w = csv.writer(fh)
|
|
w.writerow(["t", "frame", "counter", "v0", "v1", "v2", "v3", "v4", "v5",
|
|
"b14", "b15", "b16", "flags", "e6", "brightness", "raw"])
|
|
w.writerows(rows)
|
|
print(f"{len(rows)} frames saved to {a.out}/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|