Examples :: Stack clip frames in a matrix

The purpose of this example script is to create a library routine that stacks a given range of frames of a clip in a (single clip) matrix. Such an operation offers a way to quick preview a clip, in order for example to search for scene changes, cut points, etc.

Since we make a library routine we want for it to be generic, ie to support matrices of any dimension and clip framecounts of any size. How do we proceed to design and implementation then?

At first, one may think to simply Trim signle-frame clips and create an array of the appropriate size to pass afterwards to the Stack filter. Using recursion this process could be repeated for all the requested frame range.

However this is highly inefficient. To see this let f be the total number of frames to stack and s the frames to stack in a single output frame. Then the above approach has a startup cost of ceil(f/s) recursive calls to our new function and setups a filter chain with f Trim's plus ceil(f/s) calls to the Stack filter, which by the way is expensive (approximately s calls to StackHorizontal / StackVertical per call plus a possible overhead for blank clips creation plus a startup overhead for operation on arrays). That is f + ceil(f/s)*s ~ 2*f filters.

Fortunately, there is a faster way: the SelectEvery standard Avisynth filter. Thus, instead of trimming and making arrays every s frames, we make one array of s SelectEvery filters plus s Trim's (because an internal frame range may be requested) plus a single call to Stack. That is s + s + 1*s ~ 3*s filters. Since typically s << f, we get a speed increase of orders of magnitude.

A final culprit to deal with is that if the number of frames, f, is not a multiple of s then the last output frame will contain duplicated frames because some of the s SelectEvery filters will return a shorter by one frame clip, the last frame of which will show 2 times. To make those frames black (our choice) we must perform some additional filter operations, which however since we are dealing with one output frame there will be upper-bounded by s. That is our final filter chain is in the range of [3*s..4*s] filters.

After the above considerations, the implementation of the script is rather straightforward:

LoadModule("avslib", "array", "core")
LoadModule("avslib", "array", "operators")
LoadModule("avslib", "filters", "stack")

Function __select_frames(int ofs, clip c, int total) { 
	return SelectEvery(c, total, ofs)

Function __trim_frames(clip c, int n, int frames, int modulo) { 
	return n < modulo \
		? (frames < c.Framecount ? c.Trim(0, -(frames+1)) : c) \
		: c.Trim(0, -frames) + c.BlankClip(length=1) 

Function StackFrames(clip c, int fstart, int fcount, int rows, int cols)
	Assert(fstart >= 0, "StackFrames: 'fstart' must be zero or greater")
	Assert(fcount > 0, "StackFrames: 'fcount' must be positive")
	Assert(rows > 0 && cols > 0, "StackFrames: 'rows', 'cols' must be positive")
	nlen = rows * cols
	offsets = ArrayRange(fstart, fstart + nlen - 1)
	counts = ArrayRange(0, nlen - 1)
	old = ArrayDelimiterReset()
	extra_args = ArrayCreate(c, nlen)
	div = fcount / nlen
	mod = fcount % nlen
	trim_args = ArrayCreate(div, mod)
	old = ArrayDelimiterSet(old)
	clips = offsets.ArrayOpFunc("__select_frames", extra_args)
	clips = ArrayOpArrayFunc(clips, counts, "__trim_frames", trim_args)
	return Stack(clips, rows, cols)

Since we are making a generic routine, we setup some asserts at the begining to ensure proper input. Afterwards the function creates an array of offsets to pass to SelectEvery (spacing is constant and equal to nlen).

The part of the code surrounded by calls to ArrayDelimiterReset and ArrayDelimiterSet creates the additional argument strings for our two helper "private" functions __select_frames and __trim_frames. Using those functions is mandatory since we use, for convenience, ArrayCreate to create the comma delimited argument lists. In general we do not know what the user will have selected for the default array delimiter when the function will be called; thus we must ensure that a comma will be used and not something else.

The rest of the code is two calls to the appropriate array operators, the first to create the array of SelectEvery filters and the second to trim and pad with blank frames when appropriate the later array. Finally the call to Stack filter is made.

clp = BlankClip(length=166, width=632, height=472, color=$000040)
clp = clp.ShowFrameNumber(scroll=true, size=48).AddBorders(4, 4, 4, 4, $ffffff)
clp = clp.Spline16Resize(Round(clp.Width / 4), Round(clp.Height / 4))
return StackFrames(clp, 0, clp.Framecount, 4, 4)

The rest of the script is a simple test of the new function, to demonstrate its proper operation.