shurik_pronkin
10th April 2026, 13:17
SpliceRepair v1.0 automatic removal of film splice mark artifacts
When restoring scanned film, cement or tape splices at edit points leave visible horizontal artifacts (bright/dark bands) on the first and last frames of each scene. These are hard to remove with generic filters because they sit right at scene transitions any filter strong enough to fix them destroys the picture.
SpliceRepair takes a different approach: it detects scene boundaries via SCDetectEOD (https://forum.doom9.org/showthread.php?t=XXXXX) frame properties, and on boundary frames replaces the damaged top/bottom rows with clean rows from a nearby frame within the same scene.
How it works
First frame of a scene → top/bottom rows copied from frame n + offset
Last frame of a scene → top/bottom rows copied from frame n − offset
All other frames → passed through untouched
Single-frame scenes → skipped (both donors would be from adjacent scenes)
No resampling, no blurring, no temporal filtering. Just a surgical Crop + StackVertical from a clean donor.
Requirements
AviSynth+
SCDetectEOD (https://forum.doom9.org/showthread.php?t=XXXXX) (provides _scd_boundary frame property)
Parameters
SpliceRepair(clip c, clip sc_clip, int "repair_top", int "repair_bottom", int "offset")
c source clip
sc_clip clip from SCDetectEOD carrying _scd_boundary property
repair_top (default 16) rows to replace at the top. Set to 0 to skip. Must be mod-2.
repair_bottom (default 0) rows to replace at the bottom. Set to 0 to skip. Must be mod-2.
offset (default 2) how many frames into the scene to fetch the donor. Use 2+ to avoid the adjacent frame which may also carry splice residue.
Usage
LWLibavVideoSource("film.mkv")
Greyscale()
# ... your cleanup chain (Vinverse, KillerSpots, Stab, etc.) ...
sd = last.SCDetectEOD(mono=true, downscale=2)
last.SpliceRepair(sd, repair_top=48, repair_bottom=0, offset=2)
Tune repair_top to cover the full extent of the splice artifact including any letterbox above it. On typical 35mm scans with letterbox, values around 4060 work well.
Notes
Place SpliceRepair after your cleanup chain (denoise, stabilize, etc.) so donor rows are already clean.
Place it before any neural upscaling (Topaz, AiUpscale) fixing 16 rows at SD resolution is much cheaper than fixing 64 rows at 4K.
If a scene is shorter than offset frames, the donor may come from the adjacent scene. In practice this is rare most scenes are much longer than 2 frames.
sc_clip is a positional parameter pass it positionally, not as sc_clip=sd.
Download
Single file: SpliceRepair.avsi drop into your plugins64 folder or Import() explicitly.
Source
###############################################################################
# SpliceRepair.avsi automatic film splice artifact removal
#
# On the first and last frame of each scene, replaces top/bottom rows
# with donor material from ฑoffset frames inside the same scene.
#
# Requires: SCDetectEOD (_scd_boundary frame property)
###############################################################################
function _sr_apply(clip cur, clip donor, int rt, int rb)
{
h = cur.Height()
result = cur
result = (rt > 0) \
? StackVertical(donor.Crop(0, 0, 0, -(h - rt)), result.Crop(0, rt, 0, 0)) \
: result
result = (rb > 0) \
? StackVertical(result.Crop(0, 0, 0, -rb), donor.Crop(0, h - rb, 0, 0)) \
: result
return result
}
function _sr_shift_right(clip c, int n)
{
return (n <= 0) ? c : _sr_shift_right(c.DuplicateFrame(0), n - 1)
}
function SpliceRepair(clip c, clip sc_clip, int "repair_top", int "repair_bottom", int "offset")
{
rt = Default(repair_top, 16)
rb = Default(repair_bottom, 0)
off = Default(offset, 2)
Assert(rt >= 0, "SpliceRepair: repair_top must be >= 0")
Assert(rb >= 0, "SpliceRepair: repair_bottom must be >= 0")
Assert(off >= 1, "SpliceRepair: offset must be >= 1")
Assert(rt % 2 == 0, "SpliceRepair: repair_top must be mod-2 (YV12)")
Assert(rb % 2 == 0, "SpliceRepair: repair_bottom must be mod-2 (YV12)")
Assert(rt + rb < c.Height(), "SpliceRepair: repair_top + repair_bottom >= frame height")
fc = c.Framecount()
donor_fwd = c.Trim(off, 0)
donor_bwd = _sr_shift_right(c, off).Trim(0, fc - 1)
sc_next = sc_clip.Trim(1, 0)
ScriptClip(c, """
is_first = (sc_clip.propGetInt("_scd_boundary") == 1)
is_last = (current_frame < Framecount() - 1) \
? (sc_next.propGetInt("_scd_boundary") == 1) : false
(is_first && is_last) ? last \
: is_first ? _sr_apply(last, donor_fwd, rt, rb) \
: is_last ? _sr_apply(last, donor_bwd, rt, rb) \
: last
""", args="sc_clip, sc_next, donor_fwd, donor_bwd, rt, rb", local=true)
}
Changelog
v1.0 initial release
When restoring scanned film, cement or tape splices at edit points leave visible horizontal artifacts (bright/dark bands) on the first and last frames of each scene. These are hard to remove with generic filters because they sit right at scene transitions any filter strong enough to fix them destroys the picture.
SpliceRepair takes a different approach: it detects scene boundaries via SCDetectEOD (https://forum.doom9.org/showthread.php?t=XXXXX) frame properties, and on boundary frames replaces the damaged top/bottom rows with clean rows from a nearby frame within the same scene.
How it works
First frame of a scene → top/bottom rows copied from frame n + offset
Last frame of a scene → top/bottom rows copied from frame n − offset
All other frames → passed through untouched
Single-frame scenes → skipped (both donors would be from adjacent scenes)
No resampling, no blurring, no temporal filtering. Just a surgical Crop + StackVertical from a clean donor.
Requirements
AviSynth+
SCDetectEOD (https://forum.doom9.org/showthread.php?t=XXXXX) (provides _scd_boundary frame property)
Parameters
SpliceRepair(clip c, clip sc_clip, int "repair_top", int "repair_bottom", int "offset")
c source clip
sc_clip clip from SCDetectEOD carrying _scd_boundary property
repair_top (default 16) rows to replace at the top. Set to 0 to skip. Must be mod-2.
repair_bottom (default 0) rows to replace at the bottom. Set to 0 to skip. Must be mod-2.
offset (default 2) how many frames into the scene to fetch the donor. Use 2+ to avoid the adjacent frame which may also carry splice residue.
Usage
LWLibavVideoSource("film.mkv")
Greyscale()
# ... your cleanup chain (Vinverse, KillerSpots, Stab, etc.) ...
sd = last.SCDetectEOD(mono=true, downscale=2)
last.SpliceRepair(sd, repair_top=48, repair_bottom=0, offset=2)
Tune repair_top to cover the full extent of the splice artifact including any letterbox above it. On typical 35mm scans with letterbox, values around 4060 work well.
Notes
Place SpliceRepair after your cleanup chain (denoise, stabilize, etc.) so donor rows are already clean.
Place it before any neural upscaling (Topaz, AiUpscale) fixing 16 rows at SD resolution is much cheaper than fixing 64 rows at 4K.
If a scene is shorter than offset frames, the donor may come from the adjacent scene. In practice this is rare most scenes are much longer than 2 frames.
sc_clip is a positional parameter pass it positionally, not as sc_clip=sd.
Download
Single file: SpliceRepair.avsi drop into your plugins64 folder or Import() explicitly.
Source
###############################################################################
# SpliceRepair.avsi automatic film splice artifact removal
#
# On the first and last frame of each scene, replaces top/bottom rows
# with donor material from ฑoffset frames inside the same scene.
#
# Requires: SCDetectEOD (_scd_boundary frame property)
###############################################################################
function _sr_apply(clip cur, clip donor, int rt, int rb)
{
h = cur.Height()
result = cur
result = (rt > 0) \
? StackVertical(donor.Crop(0, 0, 0, -(h - rt)), result.Crop(0, rt, 0, 0)) \
: result
result = (rb > 0) \
? StackVertical(result.Crop(0, 0, 0, -rb), donor.Crop(0, h - rb, 0, 0)) \
: result
return result
}
function _sr_shift_right(clip c, int n)
{
return (n <= 0) ? c : _sr_shift_right(c.DuplicateFrame(0), n - 1)
}
function SpliceRepair(clip c, clip sc_clip, int "repair_top", int "repair_bottom", int "offset")
{
rt = Default(repair_top, 16)
rb = Default(repair_bottom, 0)
off = Default(offset, 2)
Assert(rt >= 0, "SpliceRepair: repair_top must be >= 0")
Assert(rb >= 0, "SpliceRepair: repair_bottom must be >= 0")
Assert(off >= 1, "SpliceRepair: offset must be >= 1")
Assert(rt % 2 == 0, "SpliceRepair: repair_top must be mod-2 (YV12)")
Assert(rb % 2 == 0, "SpliceRepair: repair_bottom must be mod-2 (YV12)")
Assert(rt + rb < c.Height(), "SpliceRepair: repair_top + repair_bottom >= frame height")
fc = c.Framecount()
donor_fwd = c.Trim(off, 0)
donor_bwd = _sr_shift_right(c, off).Trim(0, fc - 1)
sc_next = sc_clip.Trim(1, 0)
ScriptClip(c, """
is_first = (sc_clip.propGetInt("_scd_boundary") == 1)
is_last = (current_frame < Framecount() - 1) \
? (sc_next.propGetInt("_scd_boundary") == 1) : false
(is_first && is_last) ? last \
: is_first ? _sr_apply(last, donor_fwd, rt, rb) \
: is_last ? _sr_apply(last, donor_bwd, rt, rb) \
: last
""", args="sc_clip, sc_next, donor_fwd, donor_bwd, rt, rb", local=true)
}
Changelog
v1.0 initial release