shurik_pronkin
27th March 2026, 13:44
RIFE scene change detection hack via AviSynth border signaling
The problem
RIFE's built-in sc_threshold is based on raw frame-to-frame SAD without motion compensation. It works okay for hard cuts with obvious brightness changes, but fails badly on:
Shot/reverse shot dialogues — same lighting, same background, only the face changes. RIFE doesn't see a scene change and produces ugly morph frames.
If you lower sc_threshold to catch these, RIFE starts duplicating frames on fast motion instead of interpolating them.
One threshold can't solve both problems.
Meanwhile, AviSynth has MVTools with MSCDetection which does motion-compensated SAD — it counts blocks where SAD exceeds thSCD1 after motion compensation and signals scene change if that count exceeds thSCD2. Not magic, still fails on fades and non-translational transforms, but the motion compensation step handles the "fast movement vs scene change" distinction noticeably better than raw frame comparison.
The proper solution would be adding a signal clip input to the RIFE plugin (e.g. an sc_clip parameter) so you could feed any external SCD decision directly. Until that exists, here's a workaround that uses the only channel RIFE currently understands — the pixels themselves.
The idea
Encode scene change metadata directly into the image using a border signal.
SCDetect adds a border strip on top of the frame — black normally, white on scene change
RIFE sees a massive brightness change in the border region → its sc_threshold fires reliably
Crop removes the border after RIFE — clean output
The single-pulse problem
Naive approach: white border on SC frame only.
...black, black, WHITE, black, black...
RIFE sees two transitions (black→white and white→black) and duplicates frames around both — you get ~3 duplicate frames instead of a clean cut.
The solution: toggle
Instead of a single pulse, alternate the border color between scenes:
Scene 1: black black black [SC] Scene 2: white white white [SC] Scene 3: black black black
Now there's exactly one border transition per scene change, coinciding with the actual content change. Result: ~2 duplicate frames at 50fps (40ms, invisible) instead of morph artifacts.
Improved SCDetect function
The standard SCDetect was improved with several features:
"Previous frame calm" check: if frame N-1 also had high YDifferenceFromPrevious, it's continuous motion, not a scene change. Real scene changes have a calm frame before the cut.
border parameter — adds borders and handles toggle internally
blksize — larger MAnalyse blocks average out noise, reducing false SCD on noisy footage (tip from DTL2020)
preblur_strength — three levels of pre-analysis denoise to suppress noise influence on SAD
downscale — BicubicResize before MSuper/MAnalyse, speeds up analysis and further suppresses noise (full resolution not needed for SCD)
Toggle state via global variable — works correctly with linear frame access (encoding)
scdetect.avsi:
Function SCDetect(clip c, string "scFile", int "thSCD1", int "thSCD2", float "mDiff", int "pel", int "search", int "searchparam", bool "preblur", int "preblur_strength", bool "info", int "border", int "blksize", int "downscale"){
scFile = Default(scFile, "scFile.log")
thSCD1 = Default(thSCD1, 360)
thSCD2 = Default(thSCD2, 120)
mDiff = Default(mDiff, 2.5)
pel = Default(pel, 1)
search = Default(search, 4)
searchparam = Default(searchparam, 2)
preblur = Default(preblur, true)
preblur_strength = Default(preblur_strength, 1)
info = Default(info, false)
border = Default(border, 0)
blksize = Default(blksize, 8)
downscale = Default(downscale, 1)
last = preblur ? (
\ preblur_strength == 3 ? c.RemoveGrain(20, 0).RemoveGrain(20, 0).Blur(1.0) :
\ preblur_strength == 2 ? c.RemoveGrain(20, 0).RemoveGrain(20, 0) :
\ c.RemoveGrain(20, 0)
\ ) : c
Assert( IsYV12 , "SCDetect needs YV12 input!" )
mv_clip = (downscale > 1) ? last.BicubicResize(width/downscale, height/downscale) : last
super = mv_clip.MSuper(pel=pel, chroma=false)
vector = super.MAnalyse(pelsearch=pel, search=search, searchparam=searchparam, chroma=false, blksize=blksize)
global scMask = MSCDetection(vector, thSCD1=thSCD1, thSCD2=thSCD2).Crop(0,0,16,16)
global SC_mD = mDiff
global mean = Histogram(mode="levels").Crop(width+64, 64, 128, 128)
global SC_state = 0
borderWhite = c.AddBorders(0, border, 0, 0, color=$FFFFFF)
borderBlack = c.AddBorders(0, border, 0, 0, color=$000000)
(border > 0) ? ConditionalFilter( last, borderWhite, borderBlack, "SC_state", "==", "1" )
\ : nop
FrameEvaluate( """
\ global boolSC =
\ (
\ ( scMask.YPlaneMax == 255 ) &&
\ ( mean.loop(0,0,-1).YDifferenceFromPrevious < SC_mD*2 ) &&
\ (
\ (mean.YDifferenceFromPrevious > SC_mD*3) ||
\ (
\ (mean.YDifferenceFromPrevious > SC_mD) &&
\ (mean.YDifferenceFromPrevious > (mean.YDifferenceToNext+mean.loop(0, 0, -1).YDifferenceToNext+mean.loop(0, 0, 1).YDifferenceToNext)/1.2)
\ )
\ )
\ )
\ global SC_state = boolSC ? (1 - SC_state) : SC_state""" )
( scFile != "nul" ) ? WriteFileStart( scFile, """ "# Scene change frame list" """ ).WriteFileIf( scFile, "boolSC==true", "current_frame" ) : nop
return last
}
Usage example (24p → 50fps):
Clean source:
SCDetect(scFile="nul", search=4, searchparam=2, pel=2, thSCD1=200, thSCD2=100, mDiff=0.5, border=180)
z_ConvertFormat(pixel_type="RGBPS", colorspace_op="709:709:709:l=>rgb:709:709:f")
Rife(gpu_thread=1, model=73, sc_threshold=0.15, sc1=false, sc=true, fps_num=50, fps_den=1, uhd=true, skip=true)
z_ConvertFormat(pixel_type="YUV420P10", colorspace_op="rgb:709:709:f=>709:709:709:l")
Prefetch(2)
Crop(0, 180, -0, -0)
Noisy source:
SCDetect(scFile="nul", search=4, searchparam=2, pel=2, thSCD1=200, thSCD2=100, mDiff=0.5, border=180, blksize=16, preblur_strength=2, downscale=2)
z_ConvertFormat(pixel_type="RGBPS", colorspace_op="709:709:709:l=>rgb:709:709:f")
Rife(gpu_thread=1, model=73, sc_threshold=0.15, sc1=false, sc=true, fps_num=50, fps_den=1, uhd=true, skip=true)
z_ConvertFormat(pixel_type="YUV420P10", colorspace_op="rgb:709:709:f=>709:709:709:l")
Prefetch(2)
Crop(0, 180, -0, -0)
Parameters to tune
thSCD1 MVTools SC sensitivity. Lower = more sensitive. Start: 200
thSCD2 MVTools secondary threshold. Start: 100
mDiff Brightness difference threshold. Start: 0.5
pel Subpixel precision (2 = better for subtle changes). Start: 2
border Border height in pixels. Start: 180
blksize MAnalyse block size. Larger = less noise sensitivity. Start: 8 (clean), 16-32 (noisy)
preblur_strength Denoise before analysis. 1/2/3. Start: 1 (clean), 2-3 (noisy)
downscale Resolution divider for MAnalyse. 1/2/4. Start: 1 (clean), 2 (noisy/speed)
sc_threshold RIFE's own threshold (don't go above 0.15). Start: 0.15
If missing soft scene changes (dialogue shot/reverse shot): lower thSCD1 by 25.
If false positives on fast motion: raise mDiff by 0.25.
If false positives on noisy footage: increase blksize, preblur_strength, or downscale.
Caveats
Toggle relies on linear frame access — works for encoding, may glitch in preview (AvsPmod/VirtualDub seeking). For preview/debugging use info=true without border to see where SC fires.
Crop must come after RIFE and Prefetch.
~2 duplicate frames per scene change at 50fps (40ms) — invisible in playback.
Border height should be large enough for RIFE to react reliably. 180px works well for 1080p.
How it works (TL;DR)
MVTools does motion-compensated SAD which separates fast motion from scene changes better than RIFE's raw frame comparison → the result is encoded as border color (toggle between scenes) → RIFE reads the signal through its own sc_threshold → border is cropped away. An out-of-band workaround until RIFE gets a proper signal clip input.
Credits
Thanks to DTL2020 for the technical breakdown of MSCDetection internals and suggestions on noise handling (blksize, downscale, preblur).
The problem
RIFE's built-in sc_threshold is based on raw frame-to-frame SAD without motion compensation. It works okay for hard cuts with obvious brightness changes, but fails badly on:
Shot/reverse shot dialogues — same lighting, same background, only the face changes. RIFE doesn't see a scene change and produces ugly morph frames.
If you lower sc_threshold to catch these, RIFE starts duplicating frames on fast motion instead of interpolating them.
One threshold can't solve both problems.
Meanwhile, AviSynth has MVTools with MSCDetection which does motion-compensated SAD — it counts blocks where SAD exceeds thSCD1 after motion compensation and signals scene change if that count exceeds thSCD2. Not magic, still fails on fades and non-translational transforms, but the motion compensation step handles the "fast movement vs scene change" distinction noticeably better than raw frame comparison.
The proper solution would be adding a signal clip input to the RIFE plugin (e.g. an sc_clip parameter) so you could feed any external SCD decision directly. Until that exists, here's a workaround that uses the only channel RIFE currently understands — the pixels themselves.
The idea
Encode scene change metadata directly into the image using a border signal.
SCDetect adds a border strip on top of the frame — black normally, white on scene change
RIFE sees a massive brightness change in the border region → its sc_threshold fires reliably
Crop removes the border after RIFE — clean output
The single-pulse problem
Naive approach: white border on SC frame only.
...black, black, WHITE, black, black...
RIFE sees two transitions (black→white and white→black) and duplicates frames around both — you get ~3 duplicate frames instead of a clean cut.
The solution: toggle
Instead of a single pulse, alternate the border color between scenes:
Scene 1: black black black [SC] Scene 2: white white white [SC] Scene 3: black black black
Now there's exactly one border transition per scene change, coinciding with the actual content change. Result: ~2 duplicate frames at 50fps (40ms, invisible) instead of morph artifacts.
Improved SCDetect function
The standard SCDetect was improved with several features:
"Previous frame calm" check: if frame N-1 also had high YDifferenceFromPrevious, it's continuous motion, not a scene change. Real scene changes have a calm frame before the cut.
border parameter — adds borders and handles toggle internally
blksize — larger MAnalyse blocks average out noise, reducing false SCD on noisy footage (tip from DTL2020)
preblur_strength — three levels of pre-analysis denoise to suppress noise influence on SAD
downscale — BicubicResize before MSuper/MAnalyse, speeds up analysis and further suppresses noise (full resolution not needed for SCD)
Toggle state via global variable — works correctly with linear frame access (encoding)
scdetect.avsi:
Function SCDetect(clip c, string "scFile", int "thSCD1", int "thSCD2", float "mDiff", int "pel", int "search", int "searchparam", bool "preblur", int "preblur_strength", bool "info", int "border", int "blksize", int "downscale"){
scFile = Default(scFile, "scFile.log")
thSCD1 = Default(thSCD1, 360)
thSCD2 = Default(thSCD2, 120)
mDiff = Default(mDiff, 2.5)
pel = Default(pel, 1)
search = Default(search, 4)
searchparam = Default(searchparam, 2)
preblur = Default(preblur, true)
preblur_strength = Default(preblur_strength, 1)
info = Default(info, false)
border = Default(border, 0)
blksize = Default(blksize, 8)
downscale = Default(downscale, 1)
last = preblur ? (
\ preblur_strength == 3 ? c.RemoveGrain(20, 0).RemoveGrain(20, 0).Blur(1.0) :
\ preblur_strength == 2 ? c.RemoveGrain(20, 0).RemoveGrain(20, 0) :
\ c.RemoveGrain(20, 0)
\ ) : c
Assert( IsYV12 , "SCDetect needs YV12 input!" )
mv_clip = (downscale > 1) ? last.BicubicResize(width/downscale, height/downscale) : last
super = mv_clip.MSuper(pel=pel, chroma=false)
vector = super.MAnalyse(pelsearch=pel, search=search, searchparam=searchparam, chroma=false, blksize=blksize)
global scMask = MSCDetection(vector, thSCD1=thSCD1, thSCD2=thSCD2).Crop(0,0,16,16)
global SC_mD = mDiff
global mean = Histogram(mode="levels").Crop(width+64, 64, 128, 128)
global SC_state = 0
borderWhite = c.AddBorders(0, border, 0, 0, color=$FFFFFF)
borderBlack = c.AddBorders(0, border, 0, 0, color=$000000)
(border > 0) ? ConditionalFilter( last, borderWhite, borderBlack, "SC_state", "==", "1" )
\ : nop
FrameEvaluate( """
\ global boolSC =
\ (
\ ( scMask.YPlaneMax == 255 ) &&
\ ( mean.loop(0,0,-1).YDifferenceFromPrevious < SC_mD*2 ) &&
\ (
\ (mean.YDifferenceFromPrevious > SC_mD*3) ||
\ (
\ (mean.YDifferenceFromPrevious > SC_mD) &&
\ (mean.YDifferenceFromPrevious > (mean.YDifferenceToNext+mean.loop(0, 0, -1).YDifferenceToNext+mean.loop(0, 0, 1).YDifferenceToNext)/1.2)
\ )
\ )
\ )
\ global SC_state = boolSC ? (1 - SC_state) : SC_state""" )
( scFile != "nul" ) ? WriteFileStart( scFile, """ "# Scene change frame list" """ ).WriteFileIf( scFile, "boolSC==true", "current_frame" ) : nop
return last
}
Usage example (24p → 50fps):
Clean source:
SCDetect(scFile="nul", search=4, searchparam=2, pel=2, thSCD1=200, thSCD2=100, mDiff=0.5, border=180)
z_ConvertFormat(pixel_type="RGBPS", colorspace_op="709:709:709:l=>rgb:709:709:f")
Rife(gpu_thread=1, model=73, sc_threshold=0.15, sc1=false, sc=true, fps_num=50, fps_den=1, uhd=true, skip=true)
z_ConvertFormat(pixel_type="YUV420P10", colorspace_op="rgb:709:709:f=>709:709:709:l")
Prefetch(2)
Crop(0, 180, -0, -0)
Noisy source:
SCDetect(scFile="nul", search=4, searchparam=2, pel=2, thSCD1=200, thSCD2=100, mDiff=0.5, border=180, blksize=16, preblur_strength=2, downscale=2)
z_ConvertFormat(pixel_type="RGBPS", colorspace_op="709:709:709:l=>rgb:709:709:f")
Rife(gpu_thread=1, model=73, sc_threshold=0.15, sc1=false, sc=true, fps_num=50, fps_den=1, uhd=true, skip=true)
z_ConvertFormat(pixel_type="YUV420P10", colorspace_op="rgb:709:709:f=>709:709:709:l")
Prefetch(2)
Crop(0, 180, -0, -0)
Parameters to tune
thSCD1 MVTools SC sensitivity. Lower = more sensitive. Start: 200
thSCD2 MVTools secondary threshold. Start: 100
mDiff Brightness difference threshold. Start: 0.5
pel Subpixel precision (2 = better for subtle changes). Start: 2
border Border height in pixels. Start: 180
blksize MAnalyse block size. Larger = less noise sensitivity. Start: 8 (clean), 16-32 (noisy)
preblur_strength Denoise before analysis. 1/2/3. Start: 1 (clean), 2-3 (noisy)
downscale Resolution divider for MAnalyse. 1/2/4. Start: 1 (clean), 2 (noisy/speed)
sc_threshold RIFE's own threshold (don't go above 0.15). Start: 0.15
If missing soft scene changes (dialogue shot/reverse shot): lower thSCD1 by 25.
If false positives on fast motion: raise mDiff by 0.25.
If false positives on noisy footage: increase blksize, preblur_strength, or downscale.
Caveats
Toggle relies on linear frame access — works for encoding, may glitch in preview (AvsPmod/VirtualDub seeking). For preview/debugging use info=true without border to see where SC fires.
Crop must come after RIFE and Prefetch.
~2 duplicate frames per scene change at 50fps (40ms) — invisible in playback.
Border height should be large enough for RIFE to react reliably. 180px works well for 1080p.
How it works (TL;DR)
MVTools does motion-compensated SAD which separates fast motion from scene changes better than RIFE's raw frame comparison → the result is encoded as border color (toggle between scenes) → RIFE reads the signal through its own sc_threshold → border is cropped away. An out-of-band workaround until RIFE gets a proper signal clip input.
Credits
Thanks to DTL2020 for the technical breakdown of MSCDetection internals and suggestions on noise handling (blksize, downscale, preblur).