Log in

View Full Version : SpliceRepair v1.0 — automatic removal of film splice mark artifacts


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 40–60 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

johnmeyer
11th April 2026, 01:57
I've done a huge amount of film restoration and have scripts which detect and fix "flash" frames that result from the camera rotating shutter not getting instantly up to speed, resulting in massive over-exposure.

I've also removed thousands of splices, but usually by hand, so I appreciate what you're trying to do. However, having quickly skimmed your code, two things are not clear. First, do you do this repair after every scene transition? Second, it looks like your script is hardwired for a very specific type of splice which uses sort of an "S" cut, where part of the spliced frame is still OK. My experience with spliced film is that you usually have a vertical cut and if the splice uses tape, then both frames on either side of the cut is junk because of the tape. If glue is used, then both adjacent frames are somewhat compromised, but usually not as badly. I have seldom encountered your type of splice.

I will be interested to see some examples of how it works. It is a great idea.

shurik_pronkin
12th April 2026, 08:39
@johnmeyer Thanks for the thoughtful questions.
1. Applied after every scene transition?
Yes — by design. SCDetectEOD flags the first frame of every new scene, and SpliceRepair acts on those (plus the last frame of the previous scene). On clean digital cuts there's no splice to fix, but copying 16–48 rows from a frame 2 steps inside the same scene is visually indistinguishable from the original, so the repair is harmless there. Overhead is negligible. If someone wanted surgical control, it would be easy to add a confidence threshold from SCDetectEOD or a manual frame list — but in practice "repair every boundary" just works on films to DVD scans where splices are everywhere.
2. Hardwired for S-cuts / partial-frame damage?
Correct observation — this is the limitation. SpliceRepair assumes the damage is confined to the top and/or bottom N rows of an otherwise usable frame. That's exactly what I was seeing on my source: horizontal bright bands in the upper ~16–48 lines, rest of the frame intact. Likely an artifact of how the telecine handled the physical splice rather than a classic tape or cement splice of the kind you describe.
For the full-frame-destroyed case you describe (tape splices eating both adjacent frames), SpliceRepair won't help — you'd need frame replacement (RemoveDirt's SCSelect, or motion-compensated interpolation from MVTools/RIFE). Those are complementary, not competing. One could chain: SCSelect for fully destroyed frames, SpliceRepair for partially damaged ones.

https://i.ibb.co/1Gv1czXF/B1-t00-04.png

https://i.ibb.co/1JbnkmmT/B1-t00-03.png

johnmeyer
12th April 2026, 16:08
I need to try your scene detection logic. My experience with scene detection when using it on 16 fps or 18 fps silent film is that when the camera pans there is a massive difference in metrics from one frame to the next because of the low frame rate and you end up with a large number of spurious (i.e., incorrect) scene changes. If you then take some sort of action (like duplicating parts of frames) at those spurious transitions, you will end up with significant artifacts. However, if your scene detection logic is significantly better than what I've been using, that will let me do all sorts of things that up until now has required a lot of manual intervention.