|
| 1 | +# `qr-parse` Task List |
| 2 | + |
| 3 | +Tracks implementation progress against [spec-qr-parse.md](spec-qr-parse.md). |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## CLI Options |
| 8 | + |
| 9 | +- [x] `PATH` argument — path to video file or directory |
| 10 | +- [x] `-m / --mode [PARSE|INFO]` — execution mode |
| 11 | +- [x] `-g / --grayscale [none|numpy|opencv]` — frame grayscale conversion method; default `cvtcolor` |
| 12 | +- [x] `-t / --std-threshold FLOAT` — grayscale std-deviation pre-filter; skip decode when std < threshold; disabled when ≤ 0; default `10.0` |
| 13 | +- [x] `-x / --scale FLOAT` — frame downscale factor `(0, 1]`; `1.0` = no resize; default `1.0` |
| 14 | +- [x] `-s / --skip INT` — frames to skip after each processed frame; `0` = every frame; default `0` |
| 15 | +- [x] `-q / --qr-decoder [none|opencv|pyzbar]` — QR backend; `none` skips decode; default `pyzbar` |
| 16 | +- [x] `-v / --video-decoder [opencv]` — video frame backend; only `opencv` supported now; placeholder for `ffmpeg`/`pyav`; default `opencv` |
| 17 | +- [x] `-Q / --qrdet` — enable qrdet-based frame pre-filter; default `False` |
| 18 | +- [x] `-M / --qrdet-model-size [n|s|m|l]` — qrdet model size; default `s`; only used when `--qrdet` is set |
| 19 | +- [x] `-W / --qr-decoder-workers INT` — worker threads for parallel QR decoding; `0`/`1` = sequential (streaming); `N > 1` = parallel (buffered, iframe-ordered) |
| 20 | + |
| 21 | +--- |
| 22 | + |
| 23 | +## Core Logic |
| 24 | + |
| 25 | +### PARSE mode |
| 26 | +- [x] Read frames via `cv2.VideoCapture` |
| 27 | +- [x] Grayscale conversion (`np.mean` — current; candidate for `cv2.cvtColor` optimisation) |
| 28 | +- [x] QR detection via `pyzbar.decode` |
| 29 | +- [x] Deduplicate consecutive identical QR codes |
| 30 | +- [x] Output JSONL (`ParseSummary` + per-code records) to stdout |
| 31 | +- [x] Std-deviation pre-filter: compute grayscale std before decode, skip frame if below `--std-threshold` |
| 32 | +- [x] Replace `np.mean` grayscale with `cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)` (×10 speedup) |
| 33 | +- [x] Replace `np.std` with `cv2.meanStdDev` on grayscale frame (faster, same result) |
| 34 | + |
| 35 | +### INFO mode |
| 36 | +- [x] Enumerate `.mkv` files in directory (or single file) |
| 37 | +- [x] Output `InfoSummary` JSONL (path, duration, size, rate) |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Tests |
| 42 | + |
| 43 | +### CLI option handling |
| 44 | +- [x] `--grayscale none` — verify raw BGR frame is passed through without conversion |
| 45 | +- [x] `--grayscale numpy` — verify `np.mean(frame, axis=2)` path is taken |
| 46 | +- [x] `--grayscale opencv` — verify `cv2.cvtColor` path is taken (default) |
| 47 | +- [x] `--std-threshold 0` — verify pre-filter is disabled, all frames reach decoder |
| 48 | +- [x] `--std-threshold 40` — verify frames below threshold are skipped |
| 49 | +- [x] `--scale 0.5` — verify frames are resized before decode |
| 50 | +- [x] `--scale 1.0` — verify no resize is applied (default, no-op) |
| 51 | +- [x] `--skip 0` — verify every frame is processed |
| 52 | +- [x] `--skip 2` — verify only every 3rd frame is processed |
| 53 | +- [x] `--qr-decoder none` — verify decode is skipped, output still produced |
| 54 | +- [x] `--qr-decoder opencv` — verify `cv2.QRCodeDetector.detectAndDecode` is used |
| 55 | +- [x] `--qr-decoder pyzbar` — verify `pyzbar.decode` is used (default) |
| 56 | +- [x] `--video-decoder opencv` — verify `cv2.VideoCapture` is used (default) |
| 57 | +- [x] `--qrdet` — verify qrdet pre-filter is activated (mocked, no GPU needed) |
| 58 | +- [x] `--qrdet-model-size n/s/m/l` — verify correct model variant is loaded (mocked) |
| 59 | +- [x] `--qr-decoder-workers 0` — verify sequential path is taken (ThreadPoolExecutor not instantiated) |
| 60 | +- [x] `--qr-decoder-workers 1` — verify sequential path is taken (threshold is `> 1`) |
| 61 | +- [x] `--qr-decoder-workers 4` — verify ThreadPoolExecutor instantiated with `max_workers=4` |
| 62 | +- [x] `--qr-decoder-workers 4` — verify `_process_frame` called once per non-skipped frame |
| 63 | +- [x] `--qr-decoder-workers 4` — verify output records match sequential path (data, frame_start, time_start) |
| 64 | + |
| 65 | +### Integration |
| 66 | +- [ ] Combined `--grayscale cvtcolor --std-threshold 40 --skip 1` — verify all three interact correctly on a real video |
| 67 | +- [ ] `--qrdet --qr-decoder pyzbar` — verify qrdet pre-filter feeds into pyzbar decode |
| 68 | +- [ ] `--qr-decoder none --std-threshold 40` — verify std filter still runs even when decode is disabled |
| 69 | +- [ ] QR codes are detected and match expected output on reference test video |
| 70 | + |
| 71 | +### Coverage |
| 72 | +- [x] `qr_parse.py` ≥ 80% — achieved **93%** |
| 73 | +- [x] `cmd_qr_parse.py` ≥ 80% — achieved **100%** |
| 74 | +- [x] `get_video_time_info` edge cases: invalid filename, start-only filename, start ≥ end |
| 75 | +- [x] `_decode_qr_pyzbar` / `_decode_qr_opencv` found-code paths |
| 76 | +- [x] `_qr_state_machine` — two different QR codes in sequence; QR code at end of video |
| 77 | +- [x] `do_parse` — `summary_only=True`; `ignore_errors=True`; `cap.isOpened()=False` |
| 78 | +- [x] `do_info` / `do_info_file` — file, directory, invalid path |
| 79 | +- [x] `do_main` — path not found, invalid scale, invalid skip, INFO mode, unknown mode, PARSE mode success |
| 80 | +- [x] CLI (`cmd_qr_parse`) — basic invocation, `--mode INFO`, option forwarding, invalid path |
| 81 | + |
| 82 | +### Regression |
| 83 | +- [ ] Existing PARSE mode output unchanged when all new options are at their defaults |
| 84 | +- [ ] Existing INFO mode output unchanged |
| 85 | + |
| 86 | +--- |
| 87 | + |
| 88 | +## Performance / Optimisation (from spec benchmarks) |
| 89 | + |
| 90 | +- [x] `cv2.cvtColor` grayscale (proposal 1) — 23.7 → 46.1 fps |
| 91 | +- [x] `cv2.meanStdDev` std deviation (proposal 2) |
| 92 | +- [x] Std pre-filter with `--std-threshold` (proposal 3) |
| 93 | +- [x] Optional frame downscaling `-x / --scale` (proposal 4) |
| 94 | +- [x] Parallel decoding via `ThreadPoolExecutor` + `-W / --qr-decoder-workers` (proposal 5) |
| 95 | +- [ ] GPU / ZXing decoder (proposal 6) |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## Future: Video Decoder Backends |
| 100 | + |
| 101 | +Extend `-v / --video-decoder` with additional backends once `opencv` path is stable. |
| 102 | + |
| 103 | +- [ ] `ffmpeg` — drive frame extraction via `ffmpeg` subprocess or `ffmpeg-python` bindings; useful for formats/codecs OpenCV cannot handle |
| 104 | +- [ ] `pyav` — use `av` (PyAV) bindings for libavcodec/libavformat; lower overhead than subprocess, supports hardware-accelerated decode (VAAPI, NVDEC) |
| 105 | +- [ ] ? `decord` — GPU-accelerated video reader (`decord` package); designed for ML workloads, supports batch frame reads and CUDA tensors |
| 106 | +- [ ] Abstract `_open_video(ctx) -> iterator[frame]` API in `qr_parse.py` so backends are swappable without touching the main frame loop |
0 commit comments