- ScanSession: scan-mode context manager with bright-pixel auto-exposure and sensor colour gains; tools/scan.py scans on the scan button with live stitching - Read-only EEPROM access and calib.py parser (laser geometry, intrinsics, colour gains, black/white flat-field samples); flat-field correction in Stitcher - OV7740 sensor access via the MCU proxy; colour gains from EEPROM fix green cast - Stitcher: colour support, high-passed registration (robust on blank paper) - RE notes on the Mac app's colour, exposure, stitching and button handling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
3.3 KiB
Python
81 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Scan mode for a few seconds while logging every place the scan button might
|
|
show up (bridge reg 0xBC, HID report bit 7, record flags) plus colour statistics.
|
|
Saves a colour frame every second to <out>/. Mouse mode is restored on exit."""
|
|
import argparse
|
|
import os
|
|
import select
|
|
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=12)
|
|
ap.add_argument("-o", "--out", default="btnprobe")
|
|
ap.add_argument("--video", default="/dev/video4")
|
|
ap.add_argument("--hid", default="/dev/hidraw8")
|
|
a = ap.parse_args()
|
|
os.makedirs(a.out, exist_ok=True)
|
|
x = XU(a.video)
|
|
hid = os.open(a.hid, os.O_RDONLY | os.O_NONBLOCK)
|
|
log = open(os.path.join(a.out, "log.csv"), "w")
|
|
log.write("t,src,value\n")
|
|
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, 27)
|
|
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"scan mode for {a.duration:.0f}s: press and release the scan button a few times", flush=True)
|
|
t0, n, last, state = time.time(), 0, -1.0, {}
|
|
while time.time() - t0 < a.duration:
|
|
ok, f = cap.read()
|
|
t = time.time() - t0
|
|
while select.select([hid], [], [], 0)[0]:
|
|
r = os.read(hid, 64)
|
|
state["hid"] = r[0] >> 7
|
|
log.write(f"{t:.3f},hid,{r.hex()}\n")
|
|
if not ok:
|
|
continue
|
|
clean, rec = fix_and_extract(f.tobytes())
|
|
im = cv2.imdecode(np.frombuffer(clean, np.uint8), cv2.IMREAD_COLOR)
|
|
if im is None or rec is None:
|
|
continue
|
|
bc = x.bridge_read(0xBC)
|
|
log.write(f"{t:.3f},bc,{bc}\n{t:.3f},flags,{rec.flags}\n{t:.3f},b16,{rec.b16}\n")
|
|
changes = [k for k, v in (("bc", bc), ("flags", rec.flags), ("b16", rec.b16)) if state.get(k) != v]
|
|
for k, v in (("bc", bc), ("flags", rec.flags), ("b16", rec.b16)):
|
|
state[k] = v
|
|
if changes:
|
|
print(f"t={t:5.2f} " + " ".join(f"{k}={state.get(k)}" for k in ("bc", "flags", "b16", "hid")), flush=True)
|
|
if t - last >= 1:
|
|
m = im.reshape(-1, 3).mean(0)
|
|
sat = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)[..., 1].mean()
|
|
print(f"t={t:5.2f} colour mean BGR={m.round(1)} saturation={sat:.1f}", flush=True)
|
|
cv2.imwrite(f"{a.out}/c{n:03d}.png", im)
|
|
last = t
|
|
n += 1
|
|
cap.release()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
print("restore mouse mode:", x.enable_mouse()[0])
|
|
log.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|