Repository navigation
Conversation
added 2 commits
October 17, 2026 13:26
…rection The inverse-MTF chroma servo rewrites the decoded samples until the burst reaches 20 IRE, so what a .tbc carries depended on a guess about the disc's nominal chroma level. Discs are not mastered to one level: with the correction off, five NTSC captures from different discs read 15.5 to 17.0 IRE at the burst, and the servo pulls all of them to 20, erasing a real difference of about 10 %. On a nine frame NTSC capture it raises the subcarrier by 31 % and lowers the black PSNR by 1.2 to 2.1 dB and the white SNR by up to 2.9 dB. The correction is now opt-in with --auto_chroma_correction. Without it the samples are those of the fixed chain, and the burst level that was measured is already in the metadata (field_record.median_burst_ire), so a chroma decoder can apply the gain itself. With it the output is what the default produced before. --deemp_strength still switches the correction off, as it did, and still wins when both are given. The only way to stop the 2T MTF servo was -m other than 1.0, which also changes the level it is meant to leave alone. --no_mtf_servo turns it off by itself; the servo still runs by default. The VITS conformance decodes (NTSC and PAL cvbs, the radius sweep and the capture cuts) judge the corrected output against the standard, so they pass --auto_chroma_correction and keep their gates: on ve-snw-cut the conformance check passes with the flag and fails 7 of 34 without. Unit tests cover the option mapping. The decoder's use of mtf_servo is exercised by decoding, not by a unit test.
With the chroma correction off by default, the burst of the written samples sits where the fixed filter chain leaves it, 15 to 16.5 IRE on five NTSC discs against 20 nominal. Nothing in a .tbc.db or a .cvbs.meta said what gain brings it to nominal: the .tbc.db had each field's median_burst_ire and the .meta had nothing, so a chroma decoder could not apply it without recomputing it from the fields. capture.chroma_gain is the nominal burst level over the median of the fields' median_burst_ire, so multiplying the chroma by it puts the burst at nominal. It is 1.0 for a decode made with --auto_chroma_correction, whose samples already carry it, and NULL when no field had a usable burst. It is a new nullable column at the end of capture; ld-decode-tools reads capture by column name and already tolerates an optional column there (blanking_16b_ire), so older readers ignore it. The CVBS metadata has no field for it and the format is specified outside this repository, so the writer puts it in capture_notes, which the format reserves for source-specific facts: chroma_gain=1.2768; median_burst_ire=15.66; nominal_burst_ire=20.00 The PAL note that was already there is kept in front of it. FieldInfo keeps only the last few fields, so the burst levels are collected as the fields are committed. chroma_gain_to_nominal() does the arithmetic, ignoring fields with no usable burst, and is unit tested. On a nine frame NTSC capture the new default records 1.3232 and --auto_chroma_correction 1.0001, and cvbs_verify.py still passes on a CVBS decode.
happycube
added a commit
that referenced
this pull request
Oct 19, 2026
NTSC video equalisation now follows the disc's optical readout: the IEC 60856/60857 optics (632.8 nm, NA 0.40, 23% rim) at a cutoff the decoder measures from the colour burst's two first FM sidebands, whose ratio is independent of the recorded level, falling back to the carrier tilt and then the black/white ratio. The compensation is taken against the player chain inside the video band and tapers back outside it, so the band-pass stopband (and the audio-carrier notches) stay as they were; the inverse-MTF chroma boost holds above 4.2 MHz. PAL stays on its pole model; its 3.75 MHz pilot sideband estimator is measured and pooled but not used. The inverse-MTF chroma correction (issue #1072) stays on by default and can be turned off with --no_auto_chroma. The .meta capture_notes record the correction (strength, gain at fsc) and the median burst level with and without it, against the nominal, so a reader can undo or redo it; each committed field's burst level is collected at commit time. --no_mtf_servo stops the 2T servo without changing the MTF level. Also: the NTSC chroma differential-gain corrector applied to CVBS output with an NTC-7 line-13 fallback, and --cvbs-legacy-origin to keep the decoder's own line origin for readers built to that layout. The NTSC known deviations are re-recorded from a sweep of this decoder.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Title: Fix the quality loss from the chroma calibration: leave the decoded samples unmodified and record the chroma gain in the metadata
Checklist
Description
Problem. A decode looks worse than it should: the chroma level written to the
.tbcand to the.cvbsis not what the fixed filter chain produces but what a per-disc calibration forces, and that calibration adds noise to the luminance. Nothing in the output says it was done, and it cannot be undone afterwards. This is the quality problem reported in decode-orc#N (a decode looks degraded when the.cvbsand.tbcpaths are compared).Cause. By default
ld-decoderuns a burst-driven "auto inverse-MTF chroma" calibration. It filters the samples with the inverse of the disc's MTF, with a strength tracked from the colour burst, until the burst reaches 20 IRE. That filter boosts a wide band, not only the chroma, so the luminance noise rises with it; and it is applied to the samples themselves, for both.tbcand.cvbs, with no record in the metadata.Fix.
--auto_chroma_correctionruns it and gives the previous output (checked below).capture.chroma_gainin the.tbc.db(new nullable column, appended), and incapture_notesof the.cvbs.meta, the one field the format reserves for source-specific facts. The decoder, not the sample file, then decides what to do with it.--no_mtf_servostops the 2T MTF servo on its own. It still runs by default. Until now the only way to stop it was-mother than 1.0, which also changes the level.What this fixes, measured on NTSC (details under Motivation): the extra luminance noise (black PSNR 1.2 to 2.1 dB, white SNR up to 2.9 dB) and the loss of the real differences in chroma level between discs.
What this changes for someone running
ld-decodewith no extra option: the chroma in the.tbcand the.cvbsis lower than before. On NTSC the burst is 15.5 to 17.0 IRE instead of about 20. PAL was not measured. To get the previous output, add--auto_chroma_correction.Not fixed here: the chroma level also changed with the NTSC RF band-pass split of ca97e78 (the burst fell from 16.2 to 14.5 IRE on one capture), and the MTF servo changes the output (see Additional Notes). Neither is touched.
.tbcand.cvbs)--auto_chroma_correction--deemp_strength X--auto_chroma_correction.tbc.dbfield_record.median_burst_ireonlycapture.chroma_gain: the gain that brings the burst to nominal.cvbs.metacapture_notescarrieschroma_gain=...; median_burst_ire=...; nominal_burst_ire=...(the existing PAL note is kept in front)-mother than 1.0--no_mtf_servostops it on its ownchroma_gainis the nominal burst level over the median of the fields'median_burst_ire: multiplying the chroma by it puts the burst at nominal. It is about 1.0 for a decode made with--auto_chroma_correction, and NULL when no field had a usable burst.Motivation
Why the calibration degrades the output, in four points.
--deemp_strength, whose name suggests the fixed de-emphasis, and nothing in the output recorded that the calibration had run.Evidence for point 2: burst level of five NTSC captures from different discs (
ld-decode-testdata-ci, first 14 frames,-t 1 --tbc, this branch):chroma_gainrecorded--auto_chroma_correctionWithout the calibration the five discs differ by about 10 % (15.5 to 17.0 IRE); with it they all end at 19.7 to 20.1, so a real difference between the discs is gone from the samples.
The correction also has a cost in the luminance. On a nine-frame NTSC capture (not in the test data), default against
--auto_chroma_correction, over its 19 fields:In the chroma band the signal and the noise are both multiplied by the same factor (burst 5622 to 7351, noise 1734 to 2267, measured on the left margin of that capture, which is not perfectly flat, so only the ratio is meaningful), so the chroma SNR does not change (10.2 dB both ways). The extra noise is in the luminance. A gain applied by a decoder to its separated chroma leaves the luminance as it is.
Related Issues
Addresses decode-orc#N: "compare a
.tbc... and a.cvbsin decode orc ... the.cvbsversion is worse; expected: the result should look the same". At equal options the.tbcand the.cvbsthatld-decodewrites carry the same chroma level (burst 14.98 and 14.92 IRE on one capture), so a difference between the two is a difference between the decodes (version or options), not between the formats. This PR corrects one source of such a difference, the chroma calibration; the parts listed under "Not fixed here" remain. decode-orc/decode-orc#330Changes Made
lddecode/main.py:--auto_chroma_correctionand--no_mtf_servo;auto_deempis nowargs.auto_chroma_correction and args.deemp_strength is None; the--deemp_strengthhelp text corrected (it said 1.0; the NTSC default is 0.96).lddecode/decoder.py:mtf_servooption feedsmtf_2t_servo;capture.chroma_gaincolumn and its upsert; the burst levels are collected as fields are committed (FieldInfoonly keeps the last few fields);_chroma_gain()and_chroma_gain_note(); the CVBS note is set just before the writer closes.lddecode/metrics.py:chroma_gain_to_nominal(), pure, ignoring non-finite levels and levels at or belowMIN_USABLE_BURST_IRE(5.0, the floorcheckAutoDeempalready uses).cmake_modules/LdDecodeTests.cmake: the decodes that feed the VITS conformance checks (NTSC and PAL cvbs, the radius sweep, the capture cuts) and the PAL serial/threaded comparison pass--auto_chroma_correction, so they keep judging the corrected output.docs/user-guide/command.md,docs/technical/vits-servos.md,docs/technical/metadata-format.md.Testing
Unit lane,
python -m pytest -q tests/unit: 2258 passed, 3 failed, 3 skipped. The 3 failures are identical on unmodified 6a5c3a1 in the same environment (Python 3.13, numpy 2.5.3, scipy 1.18.1, numba 0.68.0) and are unrelated to this change:test_demod_half_rate.py::test_the_full_rate_delays_are_unchanged[PAL](a 1e-8 tolerance),test_pulses_burst_lines.py::test_burst_levels_at_the_buffer_edges_and_on_bad_samples[NTSC],test_start_finder.py::test_sync_only_demodulation_matches_the_normal_sync_path.pytest --output-on-failure): see above, 3 pre-existing failures.-t 1 --tbc:capture.chroma_gainis 1.3232 by default and 1.0001 with--auto_chroma_correction(burst 15.30 and 19.97 IRE). With the flag, the burst and the VIRS line 19 (19.8 and 21.3 IRE) match the previous default.ve-snw-cut.ldfas CVBS (-l 6), as the CTest does:vits_conformance.pypasses with--auto_chroma_correction(34 checks, 17 skipped) and fails 7 of 34 without it, which is why the CMake decodes above pass the flag.cvbs_verify.pypasses in both cases, and the CVBS note is written (chroma_gain=1.2768; ...).chroma_gain_to_nominal(tests/unit/test_metrics_chroma_gain.py, 11 tests: median rather than mean, unusable levels ignored, no usable level gives None, per-system nominal); the options (tests/unit/test_main_cli_mapping.py: default off, flag arms it,--deemp_strengthwins,--no_mtf_servo); the collection of burst levels at commit (tests/unit/test_decoder_output_stage.py). All markedunit; none touches the filesystem; no random data.Not run: the CTest functional lanes, including the serial/threaded comparisons and the radius sweep (their decode command lines were edited as described but not executed); anything on PAL.
mtf_servoreachingmtf_2t_servoinLDdecode.__init__has no unit test (it needs a full decoder); it was exercised by decoding.Screenshots (if applicable)
Not attached.
Additional Notes
chroma_gainyet. Reading the recorded value is a change for the decoders, not for this repository, and is the reason for recording it.user_versionis not changed. The reader inld-decode-tools(src/library/tbc/sqliteio.cpp) selectscaptureby column name and already tolerates an optional column there (blanking_16b_ire), so older readers ignore it.