Skip to content
AkarinVS edited this page Oct 14, 2021 · 29 revisions

Expr

akarin.Expr is an enhanced version of std.Expr.

Enhancements

  • New math operators
    • sin, cos: also present in std.Expr version R54 (merged PR #693)
    • %: fmod operator
    • clip and clamp: x 16 235 clip and x 16 235 clamp are both equivalent to x 16 max 235 min. clip is used by AVS+ Expr filter.
    • trunc, round, floor
  • New constant operators
    • N to access the current 0-based frame number
    • X and Y to access the current pixel's absolute coordinate (note: in chroma planes, the number includes subsampling, i.e. for YUV420, X in chroma planes only ranges from 0 to width/2-1.
    • width and height: access the plane width and height.
    • Frame property access: use x.PropertyName to access clip x's frame property PropertyName.
  • Temporary variables: these variable does not preserve across pixels though, as limited by the SIMD nature of the implementation.
    • var@ to load a local variable onto stack.
    • var! to store the top of the stack to variable var and pop it from the stack. (Equivalent to AVS+'s A^, or A@ pop)
  • Stack operations
    • dropN that removes the top N items. drop is an alias of drop1.
  • sortN sorts the top N elements of the stack.
  • Relative Pixel Access: x[relX,relY].
    • -width < relX < width and -height < relY < height
    • by default, out of bound pixel accesses are clamped to the edge. i.e., x[relX,relY] access the pixel at location [clamp(X+relX,0,width-1),clamp(Y+relY,0,height-1)].
    • mirror boundary condition enabled with boundary=1 or x[relX,relY]:m.

Implementations

There are two implementations in the repository, which is selected at the build time.

  • legacy version: using the same JIT framework as std.Expr
    • Feature limits to: sin,cos, N, X, Y, pi, trunc, round, frame property access features.
    • No longer actively developed. As the LLVM version is better in every aspect.
  • LLVM version (lexpr): using LLVM framework.
    • Actively developed
    • Works on more than x86 platforms
    • Supports all enhancements
    • Can handle much larger/complicated expressions than the legacy version.

Translating AVS+ Expr expressions

AVS+ Expr Meaning lexpr
W read local variable W W@
W@ write local variable W, without pop dup W!
W^ write local variable W W!
sx or sy absolute X / Y X or Y
sxr or syr relative X / Y X width / or Y height /
frameno frame number N
^ power pow

Rationales and Examples

Frame Property Access

One example for this is to implement Adaptive Graining (aka adg) with fully customized formula.

stat = core.std.PlaneStats(src)
grain = core.grain.Add(core.std.BlankClip(src), var=1)
# simple example adapting grain strength to average brightness of the frame
adged = core.akarin.Expr([stat,grain], ['x.PlaneStatsAverage y * x +', ''])

It could be used to optimize some uses of std.FrameEval that needs to access the frame statistics, for example: https://github.com/AmusementClub/mvsfunc/blob/0b2c6a84de789d9483762fa88a813680f151980c/mvsfunc.py#L1293-L1301

Temporary Variable Access

The syntax of this feature is modeled after the Forth programming language. And it's not compatible with the Internal Variable feature of AviSynth+'s Expr filter for a reason: I have already settled on N, X and Y and the AVS+ limits variables to A-Z only, which is also too limiting (I regard the use of variable names as a form documentation, so longer names definitely help.) This implementation does not limit the length of the name, as long as it does not contain space, any identifiers can be used (in fact, you can use a+b as identifier.)

This feature is not essential in the sense that you can always use swap to access temporary variables stored at the bottom of the stack. But that approach does not scale well: it is hard to add a new variable (potentially all swap indices need to be changed) and too hard to read.

A complete example plotting the sin function:

import vapoursynth as vs
core = vs.core
core.std.BlankClip(format=vs.GRAY8).akarin.Expr(
    'height 2 / halfh! '  # save half height for later reuse
    'X width / N 20 / + pi 2 / * '  # compute input to sin: (X/width + N/20) * pi/2
    'sin -1 * '          # flip sign of sin (in math, we want lower left corner to be the origin, not the upper left corner)
    'halfh@ * halfh@ + ' # adjust result to height of the clip
    'round Y = 255 0 ?'  # round to integer and see if it's equal to current Y
).set_output()

Relative Pixel Access

We can easily use the relative pixel access feature to implement custom convolution kernels. In fact, benchmark showed that 3x3 convolution implemented this way has performance on par with std.Convolution.

boxfilter3x3 = lambda c: c.akarin.Expr(
    'x[-1,-1] x[-1,0] x[-1,1] + + ' # left column
    'x[0,-1]  x[0,0]  x[0,1]  + + ' # middle column
    'x[1,-1]  x[1,0]  x[1,1]  + + ' # right column
    '+ + 9 /'
)

If you want the edge to wrap around instead of clamping, add boundary=1 to the akarin.Expr call.

Reimplementation of std.FlipHorizontal and std.FlipVertical:

flipH = lambda c: c.akarin.Expr(f'x[-{c.width},0]:m')
flipV = lambda c: c.akarin.Expr(f'x[0,-{c.height}]', boundary=1)  # either use :m or set boundary=1 for mirrored boundary
flipHV = lambda c: c.akarin.Expr(f'x[-{c.width},-{c.height}]:m')  # faster than c.std.FlipHorizontal.std.FlipVertical

Please note that the relative coordinates [relX,rely] must be static constants and they have to satisfy: -width <= relX <= width and -height <= relY <= height (the filter will not enforce this constraint, but if it's violated, funny thing might happen.)

Using sortN to implement rank order filters

For example, core.rgvs.RemoveGrain(1) can be implemented as follows:

#core.rgvs.RemoveGrain(c, 1).set_output(0)
core.akarin.Expr(c,
        'x[-1,-1] x[0,-1] x[1,-1] ' # previous row
        'x[-1,0]          x[1,0] '  # current row
        'x[-1,1]  x[0,1]  x[1,1] '  # next row
        'sort8 '  # sort the top 8 items, now the stack becomes (bot) 7 6 5 4 3 2 1 0 (top)
        'dup7 '   # make a copy of the max value to the top, 7 6 5 4 3 2 1 0 7
        'max! '   # store it to the `max` variable and pop it, 7 6 5 4 3 2 1 0
        'dup0 min! ' # similarly store the minimum value to the `min` variable
        'drop8 '     # remove the remaining 8 items
        'x min@ max@ clamp'  # clamp the current pixel to be within the range [min, max]
).set_output(1)

(To implement RemoveGrain(i) 0<i<5, just replace the dup7 with dup(8-i) and dup0 with dup(i-1).)

Besides allowing arbitrary stencil, this implementation also supports int24/int32 and float32 pixel types.

Additional Examples

See my weird scripts collection https://github.com/AkarinVS/weird-scripts/tree/master/lexpr.