mkanel
8th September 2004, 12:52
I wrote this function to find the static/snow and blue screen frames that occur during gaps between recordings on an analog tape. It's slow, about 20 fps on my PIII 933mhz computer. You'll need Avisynth 2.55 or newer to use this.
To use it make a file with a text editor called anything.avsi (ex. findjunk.avsi) cut and paste from the code section below to your new avsi file and put the file in the Avisynth plugin directory. When used it will write a bunch of trims to two files, goodframes.txt and badframes.txt. The idea is to use goodframes.txt for processing the video. Badframes.txt is only to verify that the cut frames are really junk frames. The junkframes also include an overlap of 3 goodframes just to make sure the trim is in about the right place. Notepad shouldn't be used to view the text files because notepad doesn't understand the formatting commands. Wordpad works fine and so should most other editors.
The first argument is a prefix for the output files. The following script would output MyVideoGoodframes.txt and MyVideoBadFrames.txt
Avisource=("c:\some.avi")
FindJunk(prefix="MyVideo")
The second argument changes the path, make sure to include the last "\", the defuault is "c:\"
findjunk(path="C:\somefolder\"). It would probably be easier just to change the default value in the avsi.
The last two variables firstgood and lastgood change the frame where the cut between good frames and bad frames occur they're positive or negative intergers, -1 moves the cutframe forward one frame. The default is zero.
It works best if you scroll to the first frame of your video in VirtualDubMod and then do a refresh F5, and then preview output from start ctrl-F5. If you start the preview from somewhere in the middle of the video you'll likely get some unfinished trim commands in the output text files, they should still be useable after a little manual tweaking.
You can put the output files into your AVS script with cut and paste or with import.
Avisource=("c:\some.avi")
import ("C:\GoodFrames.txt")
That should be enough to use it, it works for me but I'm assuming that blue screen and snow/static is similar on most videotape.
Here's the script
function findjunk(clip c, string "prefix", string "path",int "firstgood", int "lastgood")
{
prefix=default(prefix,"")
path=default(path,"c:\") #change your default path here
global firstgood=default(firstgood,0) #change default cut positions
global lastgood=default(lastgood,0) #change default cut positions
c=c.converttoyv12
count = name(2,prefix,path)
BadFile= (exist(path+prefix+"badframes.txt")||exist(path+prefix+"goodframes.txt")) \
?path+prefix+"badframes"+string(count)+".txt" : path+prefix+"badframes.txt"
GoodFile= (exist(path+prefix+"goodframes.txt")||exist(path+prefix+"badframes.txt")) \
?path+prefix+"goodframes"+string(count)+".txt" : path+prefix+"goodframes.txt"
WriteFileStart(c,BadFile,""" "showframenumber" """)
WriteFileStart(last,GoodFile, """ "#Trims of good frames" """)
global needtrim = false
global newtext = ""
global FindNext_Good2_Bad1=0 #1=Find next junk, 2=find next useable, 0=check first frame
global useabletrims = ""
global junktrims = ""
global transitiontrims = ""
global futureblue=last.trim(3,0)
global count=-1
global goodframe = true
Junk=ScriptClip(c,"maketrims(last, current_frame, averageluma, averagechromau, goodframe)")
Conditionalfilter(last, Junk, c, "NeedTrim", "=", "true" , show = false)
WriteFileIf(last,BadFile, "strlen(junktrims)>75 && needtrim==true","junktrims")
FrameEvaluate(last,"NeedTrim = current_frame==framecount-3 \
|| (FindNext_Good2_Bad1 == 1 && GoodFrame==false) \
|| (FindNext_Good2_Bad1 == 2 && GoodFrame==true && AverageChromaU(futureblue)<200) \
|| FindNext_Good2_Bad1 ==0 \
? true : false",after_frame=true)
FrameEvaluate(last,"GoodFrame = AverageChromaU()>200 \
|| (AverageLuma>215 && YDifferenceFromPrevious>30 && YDifferenceToNext>30 && UPlaneMin>20) \
?false :true")
subtitle("") # needed to catch a single frame of smow ???.
WriteFileIf(last,GoodFile, "current_frame==framecount-2","leftstr(useabletrims,strlen(useableTrims)-3)", \
"chr(13)",""" "#Completed " """, """ time(" %c") ""","chr(13)","chr(13)" )
last.converttoyuy2 #speed up the script by removing this conversion if able.
}
function name(int count, string prefix,string path)
{
return exist(path+prefix+"goodframes"+string(count)+".txt")\
||exist(path+prefix+"badframes"+string(count)+".txt") ?name(count+1,prefix,path) : count
}
function maketrims(clip c, int frame, float luma, float chroma, bool goodframe)
{
JunkTrims = (strlen(junktrims)>80) ?" \++" :junktrims
UseableTrims = (FindNext_Good2_Bad1==1 && 2<frame<framecount(c)-2) \
?UseableTrims + "," + string(frame-4+lastgood) + ") ++ ": UseableTrims
JunkTrims = (FindNext_Good2_Bad1==2 && 2<frame<framecount(c)-2) \
?JunkTrims + "," + string(frame-1+firstgood) + ").subtitle("+chr(34) + "JUNK"+chr(34) \
+ ",x=-1,size=34) ++ " + "Trim(" + string(frame+firstgood) + "," + string(frame+2+firstgood) \
+ ").subtitle("+chr(34) + "GOOD" + chr(34) + ",x=-1,size=34)" + " ++ Trim(0,-1).blankclip" \
: JunkTrims
UseableTrims = (FindNext_Good2_Bad1==2 && 8<count<11 && 2<frame<framecount(c)-2 ) \
?chr(13) + " \" + UseableTrims :UseableTrims
UseableTrims = (FindNext_Good2_Bad1==2 && 2<frame<framecount(c)-2) \
?UseableTrims + "Trim(" + string(frame+firstgood) : UseableTrims
JunkTrims = (FindNext_Good2_Bad1==1 && frame<framecount(c)-2) ?JunkTrims + \
"Trim(" + string(frame-6+lastgood)+ "," + string(frame-4+lastgood) + \
").subtitle("+chr(34) + "GOOD"+chr(34) + ",x=-1,size=34) ++ " \
+ "Trim(" + string(frame-3+lastgood) : JunkTrims
UseableTrims = (FindNext_Good2_Bad1==0 && GoodFrame==true) ?UseableTrims + "Trim(0" : UseableTrims
JunkTrims = (FindNext_Good2_Bad1==0 && GoodFrame==false) ?JunkTrims + "Trim(0" : JunkTrims
global UseableTrims = (frame >=framecount(c)-2 && FindNext_Good2_Bad1==1) ?UseableTrims + ",0) ++" \
: UseableTrims
global JunkTrims = (frame >=framecount(c)-2 && FindNext_Good2_Bad1==2) ?JunkTrims + \
",0).subtitle("+chr(34) + "JUNK"+chr(34) + ",x=-1,size=34)" : JunkTrims
FindNext_Good2_Bad1 = (FindNext_Good2_Bad1==2) ?1 :2
FindNext_Good2_Bad1 = (FindNext_Good2_Bad1==0 && GoodFrame==true) ?1 :FindNext_Good2_Bad1
global FindNext_Good2_Bad1 = (FindNext_Good2_Bad1==0 && GoodFrame==false) ?2 :FindNext_Good2_Bad1
global count= (count==10) ?1 : count+1
return c
}
function MoreInfo(clip c)
{
global v=c.ConvertToYV12 # we need YV12
last=v
ScriptClip("Subtitle ((String(AverageLuma(v))),x=150,y=25) ")
subtitle("AverageLuma",y=25)
ScriptClip("Subtitle ((String(AverageChromaU(v))),x=150,y=50) ")
subtitle("AverageChromaU",y=50)
ScriptClip("Subtitle ((String(AverageChromaV(v))),x=150,y=75) ")
subtitle("AverageChromaV",y=75)
ScriptClip("Subtitle ((String(YPlaneMedian(v))),x=150,y=100) ")
subtitle("YPlaneMedian",y=100)
ScriptClip("Subtitle ((String(UPlaneMedian(v))),x=150,y=125) ")
subtitle("UPlaneMedian",y=125)
ScriptClip("Subtitle ((String(VPlaneMedian(v))),x=150,y=150) ")
subtitle("VPlaneMedian",y=150)
##################################################
ScriptClip("Subtitle ((String(YPlaneMax(v))),x=400,y=25) ")
subtitle("YPlaneMax",x=300,y=25)
ScriptClip("Subtitle ((String(UPlaneMax(v))),x=400,y=50) ")
subtitle("UPlaneMax",x=300,y=50)
ScriptClip("Subtitle ((String(VPlaneMax(v))),x=400,y=75) ")
subtitle("VPlaneMax",x=300,y=75)
ScriptClip("Subtitle ((String(YPlaneMin(v))),x=400,y=100) ")
subtitle("YPlaneMin",x=300,y=100)
ScriptClip("Subtitle ((String(UPlaneMin(v))),x=400,y=125) ")
subtitle("UPlaneMin",x=300,y=125)
ScriptClip("Subtitle ((String(VPlaneMin(v))),x=400,y=150) ")
subtitle("VPlaneMin",x=300,y=150)
#####################################################
ScriptClip("Subtitle ((String(YDifferenceFromPrevious(v))),x=185,y=175) ")
subtitle("YDifferenceFromPrev",y=175)
ScriptClip("Subtitle ((String(UDifferenceFromPrevious(v))),x=185,y=200) ")
subtitle("UDifferenceFromPrev",y=200)
ScriptClip("Subtitle ((String(VDifferenceFromPrevious(v))),x=185,y=225) ")
subtitle("VDifferenceFromPrev",y=225)
#####################################################
ScriptClip("Subtitle ((String(YDifferenceToNext(v))),x=455,y=175) ")
subtitle("YDifferenceToNext",x=300,y=175)
ScriptClip("Subtitle ((String(UDifferenceToNext(v))),x=455,y=200) ")
subtitle("UDifferenceToNext",x=300,y=200)
ScriptClip("Subtitle ((String(VDifferenceToNext(v))),x=455,y=225) ")
subtitle("VDifferenceToNext",x=300,y=225)
ConvertToYUY2
}
In case anyone would like to improve this I'm providing more details below, it's pretty messy and if the idea has merit it might be easier to just start from scratch than use my script, but here goes.
First the script decides if the first frame is good or junk. If it finds junk it starts looking for the next good frame or visa versa, once it finds a good frame it notes the location and starts looking for a junk frame and so on. Defining junk is the interesting part and I have added more conditions as I have encountered false positives for junk. So far blue screen has been easiest with no false positives, I look for a frame with an AverageChromaU greater than 200, I haven't had any false positives yet with that setting. To find static/snow I look for an AverageLuma greater than 215, unfortunately beach scenes normally trigger this. YDifferenceToNext is low on beach scenes but high on static/snow so I look for YDifferenceToNext, YDifferenceFromPrevious of greater than 30, I check both next and previous to prevent a false positive if there's a duplicate beach scene. I've also had a few false positive where the AverageLuma is way too high for just a few frames, I don't know if t!
his is caused by the camera or the operator but I don't want to trim these scenes if only because it would chop up the audio, they happen to have a lower UPlaneMin, if it's less than 20 it's deemed not junk. The last trick is not to identify the transition between static/snow and the blue frames as good frames, on my captures as the snow morphs into blue it loses it's "junkiness", so if I'm looking for and find a good frame I peek ahead three frames to see if that frame is blue, if so the current frame is still considered junk and the search for the next good frame continues.
One of the functions with FindJunk is MoreInfo, a horribly slow function but it gives you lots of parameters that you can use when testing a frame for "junkiness", you can use it if you're getting false positives for junk or to find your own way of determining what's junk.
The goodframe trims are stored in the variable goodfile and are written at the end of processing the video. Badframe trims are stored in badfile and written at various times during processing. I tried to use the same method for badfile as goodfile but the variable soon contained to many characters and caused a crash. The same could possibly happen to goodfile in a video with many gaps.
I hope it's useful to someone else. Mike.
To use it make a file with a text editor called anything.avsi (ex. findjunk.avsi) cut and paste from the code section below to your new avsi file and put the file in the Avisynth plugin directory. When used it will write a bunch of trims to two files, goodframes.txt and badframes.txt. The idea is to use goodframes.txt for processing the video. Badframes.txt is only to verify that the cut frames are really junk frames. The junkframes also include an overlap of 3 goodframes just to make sure the trim is in about the right place. Notepad shouldn't be used to view the text files because notepad doesn't understand the formatting commands. Wordpad works fine and so should most other editors.
The first argument is a prefix for the output files. The following script would output MyVideoGoodframes.txt and MyVideoBadFrames.txt
Avisource=("c:\some.avi")
FindJunk(prefix="MyVideo")
The second argument changes the path, make sure to include the last "\", the defuault is "c:\"
findjunk(path="C:\somefolder\"). It would probably be easier just to change the default value in the avsi.
The last two variables firstgood and lastgood change the frame where the cut between good frames and bad frames occur they're positive or negative intergers, -1 moves the cutframe forward one frame. The default is zero.
It works best if you scroll to the first frame of your video in VirtualDubMod and then do a refresh F5, and then preview output from start ctrl-F5. If you start the preview from somewhere in the middle of the video you'll likely get some unfinished trim commands in the output text files, they should still be useable after a little manual tweaking.
You can put the output files into your AVS script with cut and paste or with import.
Avisource=("c:\some.avi")
import ("C:\GoodFrames.txt")
That should be enough to use it, it works for me but I'm assuming that blue screen and snow/static is similar on most videotape.
Here's the script
function findjunk(clip c, string "prefix", string "path",int "firstgood", int "lastgood")
{
prefix=default(prefix,"")
path=default(path,"c:\") #change your default path here
global firstgood=default(firstgood,0) #change default cut positions
global lastgood=default(lastgood,0) #change default cut positions
c=c.converttoyv12
count = name(2,prefix,path)
BadFile= (exist(path+prefix+"badframes.txt")||exist(path+prefix+"goodframes.txt")) \
?path+prefix+"badframes"+string(count)+".txt" : path+prefix+"badframes.txt"
GoodFile= (exist(path+prefix+"goodframes.txt")||exist(path+prefix+"badframes.txt")) \
?path+prefix+"goodframes"+string(count)+".txt" : path+prefix+"goodframes.txt"
WriteFileStart(c,BadFile,""" "showframenumber" """)
WriteFileStart(last,GoodFile, """ "#Trims of good frames" """)
global needtrim = false
global newtext = ""
global FindNext_Good2_Bad1=0 #1=Find next junk, 2=find next useable, 0=check first frame
global useabletrims = ""
global junktrims = ""
global transitiontrims = ""
global futureblue=last.trim(3,0)
global count=-1
global goodframe = true
Junk=ScriptClip(c,"maketrims(last, current_frame, averageluma, averagechromau, goodframe)")
Conditionalfilter(last, Junk, c, "NeedTrim", "=", "true" , show = false)
WriteFileIf(last,BadFile, "strlen(junktrims)>75 && needtrim==true","junktrims")
FrameEvaluate(last,"NeedTrim = current_frame==framecount-3 \
|| (FindNext_Good2_Bad1 == 1 && GoodFrame==false) \
|| (FindNext_Good2_Bad1 == 2 && GoodFrame==true && AverageChromaU(futureblue)<200) \
|| FindNext_Good2_Bad1 ==0 \
? true : false",after_frame=true)
FrameEvaluate(last,"GoodFrame = AverageChromaU()>200 \
|| (AverageLuma>215 && YDifferenceFromPrevious>30 && YDifferenceToNext>30 && UPlaneMin>20) \
?false :true")
subtitle("") # needed to catch a single frame of smow ???.
WriteFileIf(last,GoodFile, "current_frame==framecount-2","leftstr(useabletrims,strlen(useableTrims)-3)", \
"chr(13)",""" "#Completed " """, """ time(" %c") ""","chr(13)","chr(13)" )
last.converttoyuy2 #speed up the script by removing this conversion if able.
}
function name(int count, string prefix,string path)
{
return exist(path+prefix+"goodframes"+string(count)+".txt")\
||exist(path+prefix+"badframes"+string(count)+".txt") ?name(count+1,prefix,path) : count
}
function maketrims(clip c, int frame, float luma, float chroma, bool goodframe)
{
JunkTrims = (strlen(junktrims)>80) ?" \++" :junktrims
UseableTrims = (FindNext_Good2_Bad1==1 && 2<frame<framecount(c)-2) \
?UseableTrims + "," + string(frame-4+lastgood) + ") ++ ": UseableTrims
JunkTrims = (FindNext_Good2_Bad1==2 && 2<frame<framecount(c)-2) \
?JunkTrims + "," + string(frame-1+firstgood) + ").subtitle("+chr(34) + "JUNK"+chr(34) \
+ ",x=-1,size=34) ++ " + "Trim(" + string(frame+firstgood) + "," + string(frame+2+firstgood) \
+ ").subtitle("+chr(34) + "GOOD" + chr(34) + ",x=-1,size=34)" + " ++ Trim(0,-1).blankclip" \
: JunkTrims
UseableTrims = (FindNext_Good2_Bad1==2 && 8<count<11 && 2<frame<framecount(c)-2 ) \
?chr(13) + " \" + UseableTrims :UseableTrims
UseableTrims = (FindNext_Good2_Bad1==2 && 2<frame<framecount(c)-2) \
?UseableTrims + "Trim(" + string(frame+firstgood) : UseableTrims
JunkTrims = (FindNext_Good2_Bad1==1 && frame<framecount(c)-2) ?JunkTrims + \
"Trim(" + string(frame-6+lastgood)+ "," + string(frame-4+lastgood) + \
").subtitle("+chr(34) + "GOOD"+chr(34) + ",x=-1,size=34) ++ " \
+ "Trim(" + string(frame-3+lastgood) : JunkTrims
UseableTrims = (FindNext_Good2_Bad1==0 && GoodFrame==true) ?UseableTrims + "Trim(0" : UseableTrims
JunkTrims = (FindNext_Good2_Bad1==0 && GoodFrame==false) ?JunkTrims + "Trim(0" : JunkTrims
global UseableTrims = (frame >=framecount(c)-2 && FindNext_Good2_Bad1==1) ?UseableTrims + ",0) ++" \
: UseableTrims
global JunkTrims = (frame >=framecount(c)-2 && FindNext_Good2_Bad1==2) ?JunkTrims + \
",0).subtitle("+chr(34) + "JUNK"+chr(34) + ",x=-1,size=34)" : JunkTrims
FindNext_Good2_Bad1 = (FindNext_Good2_Bad1==2) ?1 :2
FindNext_Good2_Bad1 = (FindNext_Good2_Bad1==0 && GoodFrame==true) ?1 :FindNext_Good2_Bad1
global FindNext_Good2_Bad1 = (FindNext_Good2_Bad1==0 && GoodFrame==false) ?2 :FindNext_Good2_Bad1
global count= (count==10) ?1 : count+1
return c
}
function MoreInfo(clip c)
{
global v=c.ConvertToYV12 # we need YV12
last=v
ScriptClip("Subtitle ((String(AverageLuma(v))),x=150,y=25) ")
subtitle("AverageLuma",y=25)
ScriptClip("Subtitle ((String(AverageChromaU(v))),x=150,y=50) ")
subtitle("AverageChromaU",y=50)
ScriptClip("Subtitle ((String(AverageChromaV(v))),x=150,y=75) ")
subtitle("AverageChromaV",y=75)
ScriptClip("Subtitle ((String(YPlaneMedian(v))),x=150,y=100) ")
subtitle("YPlaneMedian",y=100)
ScriptClip("Subtitle ((String(UPlaneMedian(v))),x=150,y=125) ")
subtitle("UPlaneMedian",y=125)
ScriptClip("Subtitle ((String(VPlaneMedian(v))),x=150,y=150) ")
subtitle("VPlaneMedian",y=150)
##################################################
ScriptClip("Subtitle ((String(YPlaneMax(v))),x=400,y=25) ")
subtitle("YPlaneMax",x=300,y=25)
ScriptClip("Subtitle ((String(UPlaneMax(v))),x=400,y=50) ")
subtitle("UPlaneMax",x=300,y=50)
ScriptClip("Subtitle ((String(VPlaneMax(v))),x=400,y=75) ")
subtitle("VPlaneMax",x=300,y=75)
ScriptClip("Subtitle ((String(YPlaneMin(v))),x=400,y=100) ")
subtitle("YPlaneMin",x=300,y=100)
ScriptClip("Subtitle ((String(UPlaneMin(v))),x=400,y=125) ")
subtitle("UPlaneMin",x=300,y=125)
ScriptClip("Subtitle ((String(VPlaneMin(v))),x=400,y=150) ")
subtitle("VPlaneMin",x=300,y=150)
#####################################################
ScriptClip("Subtitle ((String(YDifferenceFromPrevious(v))),x=185,y=175) ")
subtitle("YDifferenceFromPrev",y=175)
ScriptClip("Subtitle ((String(UDifferenceFromPrevious(v))),x=185,y=200) ")
subtitle("UDifferenceFromPrev",y=200)
ScriptClip("Subtitle ((String(VDifferenceFromPrevious(v))),x=185,y=225) ")
subtitle("VDifferenceFromPrev",y=225)
#####################################################
ScriptClip("Subtitle ((String(YDifferenceToNext(v))),x=455,y=175) ")
subtitle("YDifferenceToNext",x=300,y=175)
ScriptClip("Subtitle ((String(UDifferenceToNext(v))),x=455,y=200) ")
subtitle("UDifferenceToNext",x=300,y=200)
ScriptClip("Subtitle ((String(VDifferenceToNext(v))),x=455,y=225) ")
subtitle("VDifferenceToNext",x=300,y=225)
ConvertToYUY2
}
In case anyone would like to improve this I'm providing more details below, it's pretty messy and if the idea has merit it might be easier to just start from scratch than use my script, but here goes.
First the script decides if the first frame is good or junk. If it finds junk it starts looking for the next good frame or visa versa, once it finds a good frame it notes the location and starts looking for a junk frame and so on. Defining junk is the interesting part and I have added more conditions as I have encountered false positives for junk. So far blue screen has been easiest with no false positives, I look for a frame with an AverageChromaU greater than 200, I haven't had any false positives yet with that setting. To find static/snow I look for an AverageLuma greater than 215, unfortunately beach scenes normally trigger this. YDifferenceToNext is low on beach scenes but high on static/snow so I look for YDifferenceToNext, YDifferenceFromPrevious of greater than 30, I check both next and previous to prevent a false positive if there's a duplicate beach scene. I've also had a few false positive where the AverageLuma is way too high for just a few frames, I don't know if t!
his is caused by the camera or the operator but I don't want to trim these scenes if only because it would chop up the audio, they happen to have a lower UPlaneMin, if it's less than 20 it's deemed not junk. The last trick is not to identify the transition between static/snow and the blue frames as good frames, on my captures as the snow morphs into blue it loses it's "junkiness", so if I'm looking for and find a good frame I peek ahead three frames to see if that frame is blue, if so the current frame is still considered junk and the search for the next good frame continues.
One of the functions with FindJunk is MoreInfo, a horribly slow function but it gives you lots of parameters that you can use when testing a frame for "junkiness", you can use it if you're getting false positives for junk or to find your own way of determining what's junk.
The goodframe trims are stored in the variable goodfile and are written at the end of processing the video. Badframe trims are stored in badfile and written at various times during processing. I tried to use the same method for badfile as goodfile but the variable soon contained to many characters and caused a crash. The same could possibly happen to goodfile in a video with many gaps.
I hope it's useful to someone else. Mike.