Repository navigation
Stabilize pass: gate weave and vertical pitch correction (#150) - #152
Conversation
…ave (#149) The comparison drew both sides at the source's display aspect, so any pass that reshaped the frame (resize, borders, crop, rotate, square pixels, a forced DAR) previewed distorted. A 720x576 PAL 4:3 capture resized to 1440x1080 and bordered out to 1920x1080 came out squashed into 4:3. - Worker: ProcessingPipeline::output_aspect is the single aspect decision, used by the encode's setsar/setdar and reported by the preview on its PREVIEW_INFO line as sar=N/D or dar=N/D. - App: PreviewFrameInfo parses it. The processed aspect is stored with the processed image on the QueueItem. BeforeAfterComparisonWidget takes a separate before/after aspect, and 1:1 is measured against the processed one. - The preview job now carries inputSar (via the shared FieldOrderDetector.normalizeSar). Without it {{SOURCE_SAR}} fell back to 1 and square-pixel fitting previewed a 1350-wide picture where the encode makes 1440. Tests: worker unit tests, test_185, preview_frame_info_test, preview_aspect_test, before_after_comparison_test, and a heavy preview group in integration_aspect_ratio_test comparing the preview's picture extent with the encode's.
… previews - Move the comparison's per-side aspect choice into MainViewModel (sourceDisplayAspect, comparisonAfterAspect) and the preview-arrival storage into _acceptProcessedPreview, so both are testable without a worker. - preview_aspect_test: an anamorphic source only denoised keeps 16:9 on both sides; the processed aspect survives a scrubber move (fails if computed from previewFrameInfo); falls back to the source's before a processed frame; cleared with a failed preview. - integration_aspect_ratio_test (heavy): denoise-only 16:11 source and a rotated 16:15 source, preview aspect against the encode's.
…150) Brings #152's frame-height "breathing" correction for pull-through film scans into main's existing Stabilize pass instead of adding a second one. The pass keeps its id, its position (last before framing) and its havsfunc Stab weave correction (dxmax/dymax/mirror). Weave stays on main's haf.Stab rather than #152's own copy of the Stab technique: #152 carried RGB through 4:4:4 and float through 16-bit for Depan/Repair, but at this pass's position the clip is YUV integer, and Stab is already verified at 8-16 bit there. New, all defaulting to today's behaviour: - weave (default true) so pitch-only is possible - pitchMode off (default) / auto / file, maxPitchDeviation, smoothingRadius, invertFactors, pitchEdgeFill - VideoJob.pitchFactorsPath: per-job factor file (never per preset), with <video>.pitch.txt / <stem>.pitch.txt / <stem>_pitch.txt sidecar pickup in the app and a Choose/Clear row in the pass settings Pitch logic lives in templates/stabilize.py (correct_pitch), imported by both templates. A pipeline saved before this change deserialises to weave-only and produces the same Stabilize block byte for byte (inner tags are removed with their lines); test_212 pins it. Frame bookkeeping: - per-source-frame lookups add the preview window start (PreviewParams::source_frame_offset), the trim start, and the rate change of the passes ahead of Stabilize (rate_before_stabilize, composed from their FrameMaps) - Stab's measured reach (n-8..n+9) is now declared as the pass's radius (it was treated as spatial), plus radius+1 for auto pitch; this sizes the preview window and pipe_source's cache (pipe_cache_frames, floor 40) - file mode without a usable file is an error naming the bad line - pitch on a still-interlaced clip clears _FieldBased; weave alone does not Tests: Rust test_211-test_218 plus unit tests; Flutter converter, pitch file, script-generation and high-bit-depth parity cases, and the heavy integration_stabilize_test.dart ported from #152.
9ef6b61 to
6cea635
Compare
|
Hello. I probably missed it. What does the .txt file content look like? Or is that auto-populated if the file exists alongside the video file? Thanks |
|
Hi, you didn't miss much. The PR description never shows the format, so here it is. VapourBox doesn't create the file. You supply it, for example from your own analysis script. "Auto-populated" only means VapourBox finds it for you: when you add Format: the format from your proposal works as is: plain text, one factor per line, in frame order starting at frame 0. Or give the frame number with each factor. Any frame you don't list gets no correction:
One thing to check: what the number means. By default, VapourBox reads each factor as the frame's measured height. Two small differences from the proposal's script:
If you don't have a factor file, set Pitch Correction to Auto. VapourBox then measures the breathing itself by comparing each frame with its neighbours. It doesn't look at the frame borders the way you suggested. In our tests Auto removed about half of the breathing, and a good factor file about three quarters. I haven't done the two-zone curl correction yet. That one needs some thought about how to keep a seam from showing at the split line. |
Closes #150.
Summary
Adds a Stabilize pass for film gate stabilisation of 8mm / Super 8 / 16mm scans. It runs after Deinterlace and before DeScratch. The logic lives in
worker/templates/stabilize.py, which both templates import, so the preview and the encode can't drift apart.It makes two independent corrections:
Stabtechnique (mv.DepanEstimate/DepanCompensateagainst a 15-frame average). RGB is carried through 4:4:4 YUV and float through 16-bit, because Depan rejects RGB andrgvs.Repairrejects float.src_top/src_height). The approach in the issue rounded to even pixels and padded back, which turns any real breathing correction into a no-op.auto: measures scale and shift between adjacent frames by grid search on a luma strip, then corrects each frame to the mean of its ±radius window. Pairs that still differ by more than 3/255 after the best match are treated as cuts.file: one factor per source frame. It's per job (VideoJob.pitchFactorsPath), never per pipeline, so it can't leak into presets.<video>.pitch.txt,<video ext>.pitch.txtand<stem>_pitch.txtsidecars are picked up automatically. A missing or malformed file is an error that names the bad line.It also wires up every step of the CLAUDE.md "Adding a New Filter" checklist: schema and manifest, Rust and Dart models, converter, the four UI sites, both templates and both test suites.
stabilize.pyis added to all five packaging and debug scripts, and CLAUDE.md and README are updated.Testing
cargo test: all pass, including the newtest_92…test_98(script generation, frame offsets, errors, cache size).flutter test --exclude-tags heavy: 273 pass.flutter test --tags heavy test/integration_stabilize_test.dart: 5 pass. The test injects known breathing using file mode, then measures the error against the undistorted original:flutter analyzereports no new problems. The one existing error, innoise_reduction_settings_panel.dart, is in a file this branch doesn't touch.Review closely
first, a trimmed encode'sstart_frame, and the rate change from double-rate QTGMC or IVTC (rate_before_stabilize). If any of these is wrong, every frame silently gets another frame's factor.StabilizeParameters::radius()). The review measured the frames actually requested under vspipe: translation reads n−8…n+9, soTRANSLATE_RADIUSis now 9 (commit 9ef6b61). This value sizes both the preview decode window andpipe_source's cache (pipe_cache_frames). If it's too small,pipe_sourcesilently returns the wrong frame.QueueItem.copyWith(pitchFactorsPath: null)can't clear the path. Nothing relies on it.