Python API¶
The vsanalog Python package provides a high-level, type-hinted interface to
the vsanalog VapourSynth plugin. It handles plugin loading automatically and
accepts Python-native types like Path and bool.
Decoding¶
- vsanalog.decode_4fsc_video(composite_or_luma_source, chroma_or_pb_source=None, pr_source=None, *, decoder=None, color_family=None, color_difference_precision=None, broadcast_scaling_precision=None, model_version=None, model_path=None, model_input_scale=None, onnx_provider=None, model_chroma_bandpass=None, reverse_fields=False, chroma_gain=1.0, chroma_phase=0.0, chroma_nr=0.0, luma_nr=0.0, phase_compensation=True, first_active_sample=None, last_active_sample=None, first_active_line=None, last_active_line=None, dropout_correct=False, dropout_overcorrect=False, dropout_intra=False, annotate_dropouts=False, dropout_composite_or_luma_extra_sources=None, dropout_chroma_extra_sources=None)¶
Decode 4𝑓𝑠𝑐 (four times subcarrier frequency) sampled analog video signals to a digital video clip. The signal data must be orthogonal video system lines with well-formed blanking and syncing structure at a stable time base, such as those produced by ld-decode and vhs-decode. These files normally have a
.tbcextension indicating they are time-base-corrected and must have a metadata sidecar file in JSON or SQLite format. The newer CVBS format is also read, detected by extension:.cvbsfor composite, or.cvbsy/.cvbscfor separated luma/chroma (with a.metasidecar); the pre-1.5.0.compositeand.y/.cspellings are still accepted. RAW (unscaled-ADC) CVBS encodings are not supported.Returns a 32-bit float clip whose format depends on color_family:
YUV444PS(default),RGBS, orGRAYS. SECAM decodes toYUV440PS(4:4:0).- Parameters:
composite_or_luma_source (
str|Path) – Path to the composite or luma-only.tbcfile.chroma_or_pb_source (
str|Path| None) – Path to a separate chroma.tbcfile, for Y/C-separated sources such as S-Video or VHS color-under.pr_source (
str|Path| None) – Path to the Pr component.tbcfile (component video).decoder (
str| None) – Chroma decoder to use. Analytical:"ntsc1d","ntsc2d","ntsc3d","ntsc3dnoadapt","pal2d","transform2d","transform3d","secam", or"mono". Neural-network (NTSC only):"nntransform3d","ldzeug2_color_cnn","ldzeug2_luma_sep","ldzeug2_luma_sep_frame". When None, the decoder is chosen automatically based on the video system.color_family (
str| None) – Output family:"yuv"(default;YUV444PS, orYUV440PSfor SECAM),"rgb"(RGBS; not available for SECAM), or"gray"(GRAYSluma only).color_difference_precision (
str| None) – Color-difference matrix precision:"classic"or"modern".broadcast_scaling_precision (
str| None) – Broadcast-safe scaling precision:"classic","modern"or"scientific".model_version (
str| None) –Bundled model to use for a neural-network decoder (defaults per decoder). Ignored when model_path is given. Choices:
"nntransform3d":"v1_202512","v1_202603"(thev1series, predatingv2’s larger input-magnitude scale), or"v2"(default);"ldzeug2_color_cnn":"1031640","denoise_613928_ft22k", or"v2_alot"(default);"ldzeug2_luma_sep":"2dgray_fields"(default, only choice);"ldzeug2_luma_sep_frame":"2d_frame_gray_gray_run2_latest"(default, only choice).On Apple silicon the bundled
nntransform3d"v2"package is converted at fp16, which is what makes it eligible for the Apple Neural Engine — the fastest placement measured, and the reason"v2"’s weights scale their input magnitudes down. Its masks differ from the fp32 reference well below tape noise. Every other bundled package, on every platform, is fp32.model_path (
str|Path| None) – Path to custom model weights (.onnx, or a CoreML.mlpackageon macOS) for a neural-network decoder.model_input_scale (
float| None) – Override the model input magnitude divisor (nntransform3donly).model_precision (
str| None) – Compute precision a neural-network decoder’s backend may use:"fp32"or"fp16". Permission rather than a guarantee. TensorRT acts on it by building a mixed fp16/fp32 engine from the bundled fp32 weights (fp16 and fp32 engines are cached separately, so changing this never reuses an engine built the other way). CUDA and DirectML have no such engine mode, so on those the wheel instead bundles a pre-converted fp16 copy of the weights, used only when onnx_provider is pinned to"cuda"or"directml"— an"auto"request that lands on the CUDA provider stays fp32. Every other backend ignores the setting. Defaults to"fp16"for the bundlednntransform3d"v2"weights and"fp32"for everything else, including any custom model_path (nothing here can inspect an arbitrary model’s training scale, and fp16 on weights that overflow it yields NaN output). Unrelated to the macOS packages’ precision, which is fixed when they are converted.onnx_provider (
str| None) – Execution provider for a neural-network decoder:"auto","cpu","cuda"/"gpu","tensorrt"/"trt","migraphx","directml", or"coreml". An unavailable accelerator falls back to CPU.model_chroma_bandpass (
bool| None) – Toggle the post-demodulation I/Q low-pass (ldzeug2_luma_sepandldzeug2_luma_sep_frameonly).reverse_fields (bool) – Swap field order.
chroma_gain (float) – Chroma gain multiplier for saturation adjustment.
chroma_phase (float) – Chroma phase adjustment in degrees.
chroma_nr (float) – Chroma noise-reduction level. Only applies to NTSC decoders.
luma_nr (float) – Luma noise-reduction level.
phase_compensation (bool) – Burst-locked NTSC chroma demodulation, recovering the subcarrier phase from each line’s colorburst instead of assuming it’s locked to the 4𝑓𝑠𝑐 sample grid. Set to False to force fixed-phase demodulation. The PAL decoders are burst-locked by design and ignore this.
first_active_sample (int)
last_active_sample (int) – Inclusive horizontal crop, in the sample numbering of the 4𝑓𝑠𝑐 interface standards (SMPTE ST 244, EBU Tech 3280-E): sample 0 is the first sample of the digital active line, and negative numbers reach back into the line blanking ahead of it. Defaults to the whole digital active line. See Active Window.
first_active_line (int)
last_active_line (int) – Inclusive vertical crop, in the standards’ field-sequential signal line numbers.
first_active_lineis the window’s topmost line. Defaults to the video system’s standard active picture.dropout_correct (bool) – Enable dropout correction using metadata-identified dropouts.
dropout_overcorrect (bool) – Extend dropout boundaries by +/-24 samples (for heavily damaged sources).
dropout_intra (bool) – Force intra-field-only dropout correction, avoiding inter-field borrowing artifacts on high-motion content.
annotate_dropouts (bool) – Record each frame’s dropout regions in the
AnalogDropoutSpansframe property, forvsanalog.dropout_spans()andvsanalog.create_dropouts_mask(). Independent of dropout_correct; dropout_overcorrect widens what gets reported. See Dropouts.dropout_composite_or_luma_extra_sources (
Sequence[str|Path] | None) – Additional composite or luma.tbcfiles for multi-source dropout correction.dropout_chroma_extra_sources (
Sequence[str|Path] | None) – Additional chroma.tbcfiles for multi-source dropout correction (for color-under formats).
- Return type:
Usage Examples¶
Basic Composite Decode¶
from vsanalog import decode_4fsc_video
clip = decode_4fsc_video("/path/to/capture.tbc")
Y/C-Separated Sources¶
For S-Video or VHS color-under captures produced by vhs-decode:
clip = decode_4fsc_video("luma.tbc", "chroma.tbc")
Choosing a Decoder¶
# Use the 3D adaptive comb filter for NTSC:
clip = decode_4fsc_video("capture.tbc", decoder="ntsc3d")
# Use the Transform PAL frequency-domain filter:
clip = decode_4fsc_video("capture.tbc", decoder="transform3d")
# Luma-only (monochrome) decode:
clip = decode_4fsc_video("capture.tbc", decoder="mono")
Dropout Correction¶
# Basic dropout correction:
clip = decode_4fsc_video("capture.tbc", dropout_correct=True)
# Multi-source dropout correction with extra captures:
clip = decode_4fsc_video(
"capture1.tbc",
dropout_correct=True,
dropout_composite_or_luma_extra_sources=[
"capture2.tbc",
"capture3.tbc",
],
)
Working with the Output¶
The resulting YUV444PS format retains maximum quality from the decode but is
uncommon. You will often want to convert it for downstream filters:
import vapoursynth as vs
# R'G'B' for color/levels adjustment filters:
workable_clip = clip.resize.Point(format=vs.RGBS)
# For mvtools2 pipelines like QTGMC that require an integer format:
workable_clip = clip.resize.Point(format=vs.YUV444P16)
# Reducing chroma resolution closer to analog source aids some NR filters:
workable_clip = clip.resize.Spline36(format=vs.YUV422P16)
Dropouts¶
Correction hides dropouts. annotate_dropouts=True instead reports where
they are, so another filter can act on them — in-painting the damage with a
plugin of your choosing rather than borrowing from neighbouring lines, say.
Annotation is independent of correction: annotate alone to hand the damaged
regions to something else, or alongside dropout_correct=True to see what was
concealed. Note the regions describe detected damage, so with correction on
they mark what has already been repaired, not what still needs repairing.
dropout_overcorrect=True widens what gets reported to the same footprint it
widens correction to.
Because the regions ride along as frame properties, they survive std.Trim,
std.Interleave and splicing — the mask stays aligned whether you build it
before or after editing the decoded clip.
- vsanalog.create_dropouts_mask(clip, origins=None)¶
Rasterise a clip’s annotated dropout regions into a mask clip.
Returns a single-plane clip matching
clip’s dimensions and precision —0for clean samples, full scale for dropped ones — ready to pass tocore.std.MaskedMergeand the otherstdmask functions.clipmust have been decoded withannotate_dropouts=True.Because the mask is full-size, subsampled clips need no special handling:
MaskedMergeresamples the mask for the chroma planes itself, honouring the clip’s_ChromaLocation.originsrestricts the mask to particularDropoutOriginvalues; by default every origin is drawn.- Parameters:
clip (vapoursynth.VideoNode)
origins (Iterable[DropoutOrigin | int] | None)
- Return type:
vapoursynth.VideoNode
- vsanalog.dropout_spans(frame)¶
Read a frame’s dropout regions, as recorded by
annotate_dropouts=True.Returns an empty list for a frame with no dropouts, and raises
ValueErrorif the clip was not decoded withannotate_dropouts.- Parameters:
frame (vapoursynth.VideoFrame)
- Return type:
- class vsanalog.DropoutSpan(y, x_start, x_end, origin)¶
One run of dropped samples on a single output row.
xis half-open —[x_start, x_end)— and both axes are in the decoded clip’s own pixel coordinates.- Parameters:
y (int)
x_start (int)
x_end (int)
origin (DropoutOrigin)
- class vsanalog.DropoutOrigin(*values)¶
Where a reported dropout region came from.
- DECODER_CONCEALMENT = 8¶
Detected and concealed by the decoder itself (SECAM FM click concealment).
- SOURCE_METADATA = 0¶
Flagged upstream by ld-decode / vhs-decode and stored in the sidecar.
import vapoursynth as vs
import vsanalog
# Locate the dropouts without concealing them, then repair them elsewhere.
clip = vsanalog.decode_4fsc_video(
"capture.tbc", dropout_correct=False, annotate_dropouts=True)
mask = vsanalog.create_dropouts_mask(clip)
repaired = vs.core.std.MaskedMerge(clip, inpainted, mask)
The mask is a full-size single-plane clip matching the decoded clip’s precision,
which is exactly what MaskedMerge wants — including for SECAM’s 4:4:0
output, where it resamples the mask for the half-height chroma planes itself
rather than needing a pre-subsampled one. That resampling is bilinear, so a
one-line dropout softens across two chroma rows; std.Binarize the result if
you need the edges kept hard.
Grow the mask to cover the ringing either side of a dropout, or measure damage per frame without repairing anything:
grown = vs.core.std.Maximum(mask).std.Inflate()
with clip.get_frame(0) as f:
spans = vsanalog.dropout_spans(f)
damaged_samples = sum(s.x_end - s.x_start for s in spans)
On SECAM, dropout_spans also reports the FM click concealment the decoder
performed itself, tagged
DECODER_CONCEALMENT rather than
SOURCE_METADATA. Nothing upstream flagged
those regions — they exist only because the frame was decoded — so masking them
alone shows exactly which chroma samples were replaced rather than received:
concealed = vsanalog.create_dropouts_mask(
clip, origins=[vsanalog.DropoutOrigin.DECODER_CONCEALMENT])
Colorimetry¶
Analog-era color often lives in chromaticities and transfer characteristics
that modern playback systems don’t speak natively, and at whatever strength the
decoder gave it. modernize_chromaticity converts the colorimetry and
amplify_chroma adjusts the strength, both on any clip rather than only a
fresh decode: a conventional capture loaded through a source plugin such as
BestSource is as valid an input as either.
modernize_chromaticity converts to a modern target (BT.709, sRGB,
BT.2100 PQ/HLG, BT.2020 SDR) in one color-managed step. It performs no
geometry conversions and no dithering, so feed it high bit depth (e.g. the
32-bit float decode_4fsc_video produces) and dither at the end of your
pipeline. Subsampled Y’CbCr input is fine: chroma is upsampled through the
resize plugin for the
conversion (resample_filter_uv) and returned to
the source’s subsampling on output.
Unlike the resize functions whose parameter names it borrows, the
*_in parameters override frame properties; anything not given is
inferred from the clip’s _Primaries/_Transfer/_Matrix properties,
and the filter errors when neither source is available. This is because the
IEC/ITU code points used by those properties don’t always map to analog specs.
- vsanalog.modernize_chromaticity(clip, *, primaries_in_s=None, transfer_in_s=None, matrix_in_s=None, primaries_s=None, transfer_s=None, matrix_s=None, output_preset=None, resample_filter_uv=None, filter_param_a_uv=None, filter_param_b_uv=None, chromatic_adaptation=False, nominal_luminance=None, contrast_in=None, brightness_in=None, contrast=None, brightness=None)¶
Convert analog-era colorimetry and photometry to a modern target.
- Parameters:
clip (
VideoNode) – Input clip: YUV or RGB, constant format, integer up to 16 bits or 32-bit float.primaries_in_s (str) – Input chromaticity. Broadcast systems:
"ntsc-1953"("bt470m"/"470m"/"fcc"),"bt470-japan"("470m93"/"ntscj"),"bt1700-japan"("170j"),"pal"("ebu"/"bbc"/"470bg"),"smpte-c"("st170"/"170m"),"studio-japan","nederland-proposal","code-point-22"(the mystery H.273 chromaticity). CRT phosphor sets:"ecia-xxa"("p22") through"ecia-xxg","rca-sulfide-8500k","rca-sulfide-9300k-27mpcd","rca-sulfide-c","rca-p22-4-67","rca-p22-5-61","rca-p22-9-65","sony-p22". Japanese entries use ITU-R BT.2035’s reference D93 white point. Inferred from_Primarieswhen omitted.transfer_in_s (str) – Input transfer characteristics:
"linear","ntsc-1953"("bt470m"/"470m"/"fcc"/"gamma22"),"bt470bg"("470bg"/"tube"/"gamma28"),"st170-scene"("st170-oetf"/"bt601"/"601"),"st170-display"("st170-eotf"),"bt1886-annex-1"("1886"/"lcd"/"gamma24"),"bt1886-appendix-1"("1886a"/"crt"), or"srgb"("iec-61966-2-1"). Inferred from_Transferwhen omitted.matrix_in_s (str) – Input Y’CbCr matrix:
"analog-classic"("ntsc-1953"/"fcc", the 0.30/0.11 luma weights) or"analog-modern"("bt470"/"bt1700"/"st170"/"170m"/"bt601"/"601", the 0.299/0.114 weights). Not applicable to RGB input. Inferred from_Matrixwhen omitted.primaries_s (str) – Output chromaticity:
"bt709"("709"),"bt2020"("2020"),"p3dci"("st431-2"),"p3d65"("st432-1"), or"xyz"("st428"; requiresmatrix_s="rgb").transfer_s (str) – Output transfer characteristics:
"linear","bt1886-annex-1"("1886"/"lcd"/"gamma24"; tagged as BT.709, or as the BT.2020 10/12-bit tag when paired with BT.2020 primaries),"srgb"("iec-61966-2-1"),"pq"("st2084"/"2084"), or"hlg"("std-b67").matrix_s (str) –
Output matrix:
"rgb"(produces an RGB clip),"bt709"("709"),"bt2020ncl"("bt2100"/"2020ncl"/"2020"/"2100"),"2020cl"(BT.2020 constant luminance), or"chromacl"("chromaticity-derived-cl"; constant luminance with the luma weights derived from primaries_s, so it pairs with any output chromaticity). Required for YUV output unless output_preset supplies it; RGB input defaults to RGB output.Constant luminance is not a transfer characteristic: it applies the matrix to the linear tristimulus and the transfer curve after it, rather than before, so luminance survives chroma subsampling. Its color-difference normalizers are derived from whichever transfer_s is in play, so the two are independent; naming no transfer falls back to the BT.2020 OETF, the pairing BT.2020’s own table tabulates.
output_preset (str) – Convenience bundle:
"hdtv"/"bt709"(BT.709 primaries, BT.1886 transfer, BT.709 matrix),"uhdtv"/"bt2100-pq"(BT.2020 primaries, PQ, BT.2020 NCL matrix),"bt2100-hlg"(BT.2020 primaries, HLG, BT.2020 NCL matrix),"bt2020-sdr"(BT.2020 primaries, BT.1886, BT.2020 NCL matrix), or"srgb"/"iec-61966-2-1"(BT.709 primaries, sRGB transfer, RGB output — the standard defines no matrix, so add matrix_s for Y’CbCr). Explicit primaries_s/transfer_s/matrix_s override preset members;matrix_s="rgb"keeps the preset’s colorimetry but yields RGB.resample_filter_uv (str) – Kernel for the internal chroma round trip on subsampled input, by the same names
vsanalog.resample_secam()accepts:"point","bilinear","bicubic"(default),"spline16","spline36","spline64", or"lanczos". The upsample sites against the frame’s_ChromaLocation, and Y’CbCr output is returned to the input subsampling with the same kernel. Unused for 4:4:4 and RGB input. 4:4:0 input is rejected: SECAM fromvsanalog.decode_4fsc_video()carries a line-sequential Db/Dr lattice that plain resampling would blend — realign it withvsanalog.resample_secam(), orvsanalog.fill_secam_by_delay()for the classic delay-line treatment.filter_param_a_uv (float)
filter_param_b_uv (float) – Kernel tuning for resample_filter_uv, as in the resize functions: the bicubic b/c coefficients, or the lanczos tap count (filter_param_a_uv only).
chromatic_adaptation (bool) – Apply a Bradford chromatic adaptation between the input and output white points. Off by default (matching
resizebehaviour): whites keep their original tint, e.g. a D93-mastered picture stays cool on a D65 display. Beware that off, extremes can clip unintentionally in SDR output: full-strength white under a non-D65 input white lands outside the output’s unit RGB range (NTSC-1953’s Illuminant C white overshoots BT.709’s red and blue by roughly 5-10% linear), clamping in integer output and deferring the clip downstream in float. PQ and HLG have headroom above SDR reference white and are unaffected. Enable adaptation — or attenuate first — when unclipped SDR highlights matter.nominal_luminance (float) – Physical luminance in cd/m² that linear 1.0 (SDR reference white) maps to for PQ and HLG output. Defaults to 100.
contrast_in (float)
brightness_in (float) – Input-side BT.1886 user controls as 0.0-1.0 fractions of reference white: contrast_in is the screen white luminance LW (Annex 1 user gain; also normalizes Appendix 1) and brightness_in the black lift (Annex 1 LB, Appendix 1
b). Defaults 1.0 and 0.0. Only valid with thebt1886-annex-1/bt1886-appendix-1input transfers.contrast (float)
brightness (float) – Output-side counterparts; only valid with the
bt1886-annex-1output transfer.
- Return type:
Output range follows the output matrix’s convention (studio range for Y’CbCr, full range for RGB); input range is read from the
_ColorRangeframe property with the same convention as the fallback.
import vapoursynth as vs
from vapoursynth import core
import vsanalog
clip = vsanalog.decode_4fsc_video("capture.tbc")
# The decoded clip carries assumed colorimetry in frame properties.
# When correct, a preset might be all that's needed:
hd = vsanalog.modernize_chromaticity(clip, output_preset="hdtv")
# Or override what the properties can't convey:
hdr = vsanalog.modernize_chromaticity(
clip,
primaries_in_s="studio-japan",
transfer_in_s="crt",
output_preset="bt2100-pq",
)
# Take to a delivery format after filtering the modernized clip:
hdr10 = core.resize.Spline36(
hdr,
format=vs.YUV420P10,
dither_type="error_diffusion"
)
# Conventional capture? Serve from a conventional source plugin but raise
# bit depth so we can dither when returning to lower bit depth.
src = core.bs.VideoSource("capture.mkv")
edit_fmt = src.format.replace(bits_per_sample=16)
editable = src.resize.Point(format=edit_fmt)
wide_gamut_sdr = vsanalog.modernize_chromaticity(
editable,
output_preset="bt2020-sdr"
)
output = wide_gamut_sdr.resize.Spline36(
format=src.format,
dither_type="random"
)
# Re-tag for lower bit-depth (BT.2020-specific):
output = output.std.SetFrameProps(_Transfer=vs.TRANSFER_BT2020_10)
- vsanalog.amplify_chroma(clip, gain, *, resample_filter_uv=None, filter_param_a_uv=None, filter_param_b_uv=None)¶
Amplify or attenuate the color-difference signals: a post-decode counterpart of
vsanalog.decode_4fsc_video()’s chroma_gain, which scales the demodulated color differences on their way out of the decoder.A saturation control, but saturation in analog video domain terms (a gain on E’Cb/E’Cr rather than the saturation axis of an HSV or HLS model). Frames with analog-style color difference planes are scaled in place; the rest are converted through a
_Matrix=6intermediate and back. Colorimetry from outside the analog era is rejected, so this belongs upstream ofvsanalog.modernize_chromaticity()rather than after it.- Parameters:
clip (
VideoNode) – Input clip: YUV or RGB, constant format. GRAY carries no chroma and is rejected.gain (float) – Multiplier applied to both color differences. Above
1.0amplifies and below1.0attenuates;0.0leaves a monochrome picture, and1.0hands the clip straight back without the conversion round trip a unity gain could only lose precision to. Must be0.0or greater.resample_filter_uv (str) –
Kernel for the chroma resampling a matrix change entails, by the same names
vsanalog.resample_secam()accepts:"point","bilinear","bicubic"(default),"spline16","spline36","spline64", or"lanczos". Only subsampled frames on non-analog axes use it — the matrix change happens at 4:4:4 and is resampled back to the source subsampling, sited against the frame’s_ChromaLocation. Frames already carrying analog color differences never reach it, in any format or subsampling.4:4:0 frames needing that matrix change are refused rather than resampled: SECAM from
vsanalog.decode_4fsc_video()carries a line-sequential Db/Dr lattice that plain resampling would blend, so realign it withvsanalog.resample_secam()(orvsanalog.fill_secam_by_delay()) first. Analog-matrix 4:4:0 is amplified normally, integer included, and needs no realignment.filter_param_a_uv (float)
filter_param_b_uv (float) – Kernel tuning for resample_filter_uv, as in the resize functions: the bicubic b/c coefficients, or the lanczos tap count (filter_param_a_uv only).
- Return type:
import vsanalog
clip = vsanalog.decode_4fsc_video("capture.tbc")
# A washed-out capture, given back some color before anything else
# touches it:
livelier = vsanalog.amplify_chroma(clip, 1.2)
hd = vsanalog.modernize_chromaticity(livelier, output_preset="hdtv")
# SECAM's 4:4:0 needs no realignment for a per-sample gain, so this is
# safe before resample_secam:
secam = vsanalog.decode_4fsc_video("secam.tbc", "secam_chroma.tbc",
decoder="secam")
calmer = vsanalog.resample_secam(vsanalog.amplify_chroma(secam, 0.85),
format=vs.YUV444PS)
SECAM Chroma¶
SECAM carries one color-difference component per line, so a SECAM decode comes
out as YUV440PS (4:4:0) with each chroma plane holding only the lines its
component was really decoded from. Because the second field of a 625-line frame
sits an odd line count after the first, the components pair up in frame-row
order — Db, Dr, Dr, Db, Db, ... — so neither plane is a fixed-step lattice,
and which plane starts the frame flips frame to frame (the four-field ident
cycle of Rec. ITU-R BR.469, reported per frame as
AnalogSecamFirstRowComponent).
The chroma planes are woven by row parity the way the luma plane is, so separating fields selects the same field on all three planes. A plane is still not a picture, though: on any frame one of the two carries each adjacent row pair spatially swapped, and which plane that is alternates.
These two functions turn the lattice into a conventional raster. Both read the ident per frame, and both want woven frames — they split the fields themselves, so an already-separated clip is rejected rather than silently mishandled. Run them before deinterlacing, so the chroma is aligned before anything resamples it vertically.
- vsanalog.resample_secam(clip, *, filter='bicubic', **resize_kwargs)¶
Resample a SECAM 4:4:0 clip, realigning its line-sequential chroma.
Behaves like
core.resize.<Filter>:filterpicks the kernel by name (asresize.Bob()does) and every other keyword is forwarded to it, soformat,matrix,range,dither_typeand friends work as usual. Clips that aren’t SECAM 4:4:0 are handed straight to that resize.SECAM carries one color-difference component per line, so
decode_4fsc_videoemits 4:4:0 with each plane holding only the lines it was really decoded from. The two fields of a 625-line frame sit an odd line count apart, giving components that pair up in frame-row order (Db, Dr, Dr, Db, Db, ...): each plane alternates between the first and the second luma row of its pair, so neither is a fixed-step lattice and no single_ChromaLocationdescribes them. Split by row parity, though, one plane is top-sited within a field and the other one line lower, the stagger swapping planes between the two fields.The chroma planes are woven by row parity the way the luma plane is, so a plane row’s parity picks the same field on all three. Internally, the clip is marked TFF and resampled whole, leaving resize to split into fields, resample each, and weave the result back. A resize carries one
src_topfor both fields while the stagger flips between them, so each pass comes out right in one field and wrong in the other; four passes cover the two planes’ two offsets and are merged by row parity. Which plane is which is read per frame fromAnalogSecamFirstRowComponent, since that flips frame to frame (sometimes referred to as the BR.469 4-field cycle).Fields are separated only to shuffle rows, never to resample: the row-parity merge is split, select, re-weave, which is a plain copy, and the destination keeps resize’s own interlaced chroma siting. The lattice offsets are whole lines, so unless a fractional
src_topis passed, an interpolating kernel carries surviving samples through bit-for-bit except when specificfilter_param_afilter_param_bsettings soften even a pure realignment.Because the correction is keyed to row parity rather than to temporal field order,
_FieldBasedis ignored for chroma: either field order works, and a clip re-tagged progressive resamples its chroma identically, since the lattice lives in the rows and not in the tag. Only the chroma actually moving would change that, which is why this has to run before any deinterlacing — seefill_secam_by_delay()for the ordering rule.Luma does honor the tag, so a clip marked progressive is scaled frame-wise rather than per field. That only shows up when a vertical scale is asked for; without one, luma comes through untouched either way.
chromaloc_in/chromaloc_in_sare fixed by the lattice and are rejected. The target format must be YUV — matrix a following resize call to reach RGB, so the chroma is realigned before it is mixed in.
- vsanalog.fill_secam_by_delay(clip)¶
Fill line-sequential chroma the way a SECAM receiver’s delay line does.
Take the 4:4:0 video
decode_4fsc_videoemits (or the 4:2:0 a horizontal subsample of it yields) and return 4:4:4 (or 4:2:2): every row keeps the color difference its own line carried and borrows the other from the line before it, which is what the 64 µs delay line in a receiver supplies. Nothing is interpolated — each output sample is a decoded one, copied.That is the canonical picture, vertical chroma error included: the borrowed component is a line stale, so chroma resolves at half the line rate and a color edge lands one line late.
resample_secam()resamples the lattice instead, which is truer to the samples but not to what a receiver would have shown.“The line before” means the previous line of the same field, since that is the one the delay line held. The first line of each field has no predecessor for one of its components and repeats the next one instead.
AnalogSecamFirstRowComponentis dropped from the result, which is no longer line sequential.Run this before deinterlacing, never after.
_FieldBasedis ignored — the lattice is read from row parity, so a clip re-tagged progressive fills identically — but a clip whose fields have actually been separated is rejected, since the fill works from woven frames.- Parameters:
clip (vapoursynth.VideoNode)
- Return type:
vapoursynth.VideoNode
import vsanalog
clip = vsanalog.decode_4fsc_video("secam.tbc", "secam_chroma.tbc",
decoder="secam")
# Resample the lattice — truest to the decoded samples.
resampled = vsanalog.resample_secam(clip, format=vs.YUV444PS)
# Or reproduce what a receiver's delay line showed, copying each line's
# missing component from the previous line of the same field.
as_broadcast = vsanalog.fill_secam_by_delay(clip)
# Deinterlace afterwards, never before.
from vsdeinterlace import QTempGaussMC
progressive = QTempGaussMC(resampled.resize.Point(format=vs.YUV444P16)).deinterlace()
Utility¶
- vsanalog.requires_plugin(func)¶
Decorator ensuring the vsanalog VapourSynth plugin is loaded.
- vsanalog.set_log_level(level)¶
Set the threshold for the decoder’s diagnostic messages.
Decoding diagnostics — an accelerated neural-network backend falling back to CPU, a SECAM field ident that disagrees with the sidecar, a capture that isn’t at a 4𝑓𝑠𝑐 sample rate — are reported as VapourSynth log messages, so
core.add_log_handlerreceives them alongside everything else. Failures are not: those raise.levelis one ofdebug,info(the default),warning,criticaloroff, and applies process-wide.Outside a host that installs its own log handler (
vspipeand the like), VapourSynth’s Python module forwards these to the standard library’sloggingmodule under the logger name"vapoursynth". If nothing has calledlogging.basicConfig()or otherwise attached a handler, onlywarningand above reach stderr;debug/infoare silently discarded.- Parameters:
level (str)
- Return type:
None