View Single Post
Old 3rd April 2011, 23:01   #1  |  Link
Mini-Me
Registered User
 
Join Date: Jan 2011
Posts: 121
Help debugging recursive runtime script

I have some interlaced VHS captures with jittery/bouncing fields. The fields randomly move up or down a few [whole] pixels vertically, screwing up the interlacing (and potentially making deinterlacing harder). I've tried an AVT-8710 TBC, but it only makes things worse (in addition to compressing some of the upper luma range). Maybe it's broken. I've also tried solutions like Depan/DepanStabilize, Stab, and the function g-force created that preceded Stab, but they don't work nearly as well as a brute force routine.

My naive brute force version does the following:
  • Create 16 shifted clips by moving the odd and even fields up and down by 1, 2, 3, and 4 pixels.
  • Weave each clip and take a vertical edge mask of each with a "0 0 0 0 -1 0 0 1 0" kernel.
  • Repeatedly use GConditionalFilter and AverageLuma to compare masks, and select the less combed frame.
  • Perform the previous step 8 times to cut the 16 extra clips down to 8, do it 4 more times to cut 8 clips down to 4, do it 2 more times to cut 4 clips down to 2, and do it again to get the best shifted frame. Finally, compare the best shifted clip with the original to get the best frame of all.

It's giving me fantastic results, but there are three weaknesses:
  1. It's slightly suboptimal, because it doesn't mix/match odd/even field shifts. Only one field will be shifted in the final frame. If the best solution was to move the even field up 1 and the odd field down 2, this algorithm will return a frame with only the even field shifted up 3 or the odd field shifted down 3.
  2. It's not entirely robust, since a field might hypothetically be jittered by more than 4 rows.
  3. It's slower than it needs to be. Running on my slow capture computer (Athlon 64 3000+), I'm getting about 3.7 FPS. Most of this computation is wasteful, because most fields are only jittered by 0, 1, or 2 rows.

In response, I'm trying to create a recursive version that starts with small shifts and moves on to progressively larger shifts when the result seems to be improving. Unfortunately, this requires runtime recursion. According to Gavino's GRunT thread, you should be able to call runtime functions within user functions called by runtime functions, but...I'm getting a crash with this version. Any pointers?

Note that this requires Masktools 2 and GRunT. The main function is Stabilize, and it requires planar input due to the mt_edge call. It also assumes TFF order for now, since that's what my captures are...but come to think of it, the AssumeTFF calls are probably unnecessary in this case, regardless of field order.

Code:
### MAIN FUNCTION ###
function Stabilize(clip orig)
{
	# Get the clip's combed edges and weave them with the clip itself.
	origedges = orig.GetCombedEdges()
	origandedgeswoven = Interleave(orig, origedges).AssumeFieldBased.Weave()

	# Stabilize in "clockwise" and "counterclockwise" directions and choose the best result.
	# If orig is best, both clockwise and counterclockwise directions will return it.
	# No additional test is necessary.
	bestandedgeswoven = ChooseLessCombedFramesWovenEdges(StabilizeClockwise(origandedgeswoven), \
		StabilizeCounterclockwise(origandedgeswoven))

	# Extract and return the actual clip
	best = bestandedgeswoven.SeparateFields.SelectEven.AssumeFrameBased()
	return best
}


### STABILIZE SUBROUTINES ###
function GetCombedEdges(clip c)
{
	# Vertical edges between adjacent lines are a decent measure of combing.
	# The Greyscale call is probably useless, so consider removing it.
	# The levels call could probably use more testing and better parameters,
	# but it's working pretty well.
	return c.mt_edge("0 0 0 0 -1 0 0 1 0", 0, 255, 0, 255).Greyscale().Levels(0, 5, 64, 0, 255)
}

function ChooseLessCombedFramesWovenEdges(clip try1andedgeswoven, clip try2andedgeswoven)
{
	# Separate edges from clips, compare brightness of edges, and return the edge-woven clip
	# with the dimmest edges.  Chances are one or both args are redundant, but whatev.
	return GConditionalFilter(try1andedgeswoven, try1andedgeswoven, try2andedgeswoven, """
		AverageLuma(try1andedgeswoven.SeparateFields.SelectOdd()) < AverageLuma(try2andedgeswoven.SeparateFields.SelectOdd())
		""", args = "try1andedgeswoven, try2andedgeswoven", local = true)
}


# Moving even fields up is almost equivalent to moving odd fields down.
# Do them together, and call them "clockwise."
function StabilizeClockwise(clip currentandedgeswoven)
{
	# Get clips with odd fields shifted down a line and even fields shifted up a line
	current = currentandedgeswoven.SeparateFields.SelectEven.AssumeFrameBased().AssumeTFF.SeparateFields()
	evenup = current.ShiftEvenLumaAndChroma(0.0, 1.0).Weave()
	odddown = current.ShiftOddLumaAndChroma(0.0, -1.0).Weave()

	# Get combed edges of the new clips
	evenupedges = evenup.GetCombedEdges()
	odddownedges = odddown.GetCombedEdges()

	# Weave clips with edges
	evenupandedgeswoven = Interleave(evenup, evenupedges).AssumeFieldBased.Weave()
	odddownandedgeswoven = Interleave(odddown, odddownedges).AssumeFieldBased.Weave()

	# Which is better?
	oddorevenandedgeswoven = ChooseLessCombedFramesWovenEdges(evenupandedgeswoven, odddownandedgeswoven)

	# Which is best?
	bestandedgeswoven = ChooseLessCombedFramesWovenEdges(currentandedgeswoven, oddorevenandedgeswoven)

	# If current is not the best, recurse again using the best
	return GScriptClip(currentandedgeswoven, """
		return AverageLuma(currentandedgeswoven) == AverageLuma(bestandedgeswoven) ? currentandedgeswoven \
		: StabilizeClockwise(bestandedgeswoven)
		""", args = "currentandedgeswoven, bestandedgeswoven", local = True)
}


# Moving even fields down is almost equivalent to moving odd fields up.
# Do them together, and call them "counterclockwise."
function StabilizeCounterclockwise(clip currentandedgeswoven)
{
	# Get clips with odd fields shifted down a line and even fields shifted up a line
	current = currentandedgeswoven.SeparateFields.SelectEven.AssumeFrameBased().AssumeTFF.SeparateFields()
	evendown = current.ShiftEvenLumaAndChroma(0.0, -1.0).Weave()
	oddup = current.ShiftOddLumaAndChroma(0.0, 1.0).Weave()

	# Get combed edges of the new clips
	evendownedges = evendown.GetCombedEdges()
	oddupedges = oddup.GetCombedEdges()

	# Weave clips with edges
	evendownandedgeswoven = Interleave(evendown, evendownedges).AssumeFieldBased.Weave()
	oddupandedgeswoven = Interleave(oddup, oddupedges).AssumeFieldBased.Weave()

	# Which is better?
	oddorevenandedgeswoven = ChooseLessCombedFramesWovenEdges(evendownandedgeswoven, oddupandedgeswoven)

	# Which is best?
	bestandedgeswoven = ChooseLessCombedFramesWovenEdges(currentandedgeswoven, oddorevenandedgeswoven)

	# If current is not the best, recurse again using the best
	return GScriptClip(currentandedgeswoven, """
		return AverageLuma(currentandedgeswoven) == AverageLuma(bestandedgeswoven) ? currentandedgeswoven \
		: StabilizeCounterclockwise(bestandedgeswoven)
		""", args = "currentandedgeswoven, bestandedgeswoven", local = True)
}


### UTILITY FUNCTIONS ###
function SubpixelShift(clip input, float deltax, float deltay, bool "sharp")
{
	sharp = Default(sharp, True)
	return sharp ? Spline36Resize(input, input.Width(), input.Height(), deltax, deltay, input.Width(), input.Height()) \ 
	: BilinearResize(input, input.Width(), input.Height(), deltax, deltay, input.Width(), input.Height())
}

function ShiftOddLumaAndChroma(clip c, float deltax, float deltay, bool "sharp")
{
	deltax = Default(deltax, 0.0)
	deltay = Default(deltay, 0.0)
	sharp = Default(sharp, True)

	return Interleave(c.SelectEven(), c.SubpixelShift(deltax, deltay, sharp).SelectOdd())
}
Sorry for the long lines. :-/ Are you allowed to use '\' for line continuation inside the quotes of a runtime function (particularly with GRunT)?

Last edited by Mini-Me; 4th April 2011 at 02:41. Reason: Fixed infinite loop. It works now! I'll create a thread once I have a release-ready version.
Mini-Me is offline   Reply With Quote