Log in

View Full Version : Expr() – Predictive Ringing Removal


geometer
5th April 2026, 15:20
This is a fairly short function that targets ringing or echo-artifacts from sharp edges,
it can be applied, when you remaster or recompress old DVDs and try some sharpening, eg. with convolutional sharpeners.

The quality level therefor is DVD-related, but regarding Avisynth processing, it was tested with planar formats like YUV444 and 12-bit resolution.

It should be applied on the Luma signal, but you can experiment as you like.

What it does, it analyzes the signal and predicts one artifact to the left and one to the right of a vertical edge.
The placement of the artifact is set by the offset parameters. This means you look at the pixel distance, and set it to 1, 2, or 3, depending on which one is the most disturbing of several echoes.
In some cases, also in upsampled mode, values of 0 or even 4 or 5 may fit the issue.

Tune in on a strongly visible artifact that is repetitive.
(Actually it has dynamic function and works on all parts of the frame that are prone to ringing or to thin echo lines.)
Find its offset, then nudge the weight of the corrective signal.
Different scenes and different artifacts will ideally require different settings, so you want to find some averaged balance.
The result is, properly tweaked, that the video as a whole looks cleaner and more brilliant.
The algorithm avoids blurring.

The predicted artifact thus is being reengineered, and then subtracted from the original.
In other words, you have to set the weight parameters, and monitor the effect on a particular echo or Gibbs ringing structure.
The task is to zero in weight, not too light, not too dark, to make the artifacts less visible.

When your process does some filtering, you may run into sub-pixel issues. If you can't get the offset right, you can try to resample to a higher resolution and apply the function there, with a higher offset.

You can use it freely, but please when you store it somewhere, add the line "Hans' ringing remover".

Example:
(read data with MPEG2Source (), convert to 12-bit planar, whatever Expr() can handle)
last = AntiRingLR(last, "luma", weightL=0.2,offsetL=1,weightR=0.17,offsetR=1,knee=0.7,pr2=0.5,pr3=0.5)

The "output" parameter when specified, is either a format string, or the literal "out=in" (try this in case of error messages), within the limits of Expr()
It was tested with a recent "avisynth plus 64bit" version, please make sure that you run a compatible version.

Thanks, please enjoy, have fun, have success!


function AntiRingLR(clip cl, string "planes", string "output", float "weightL", int "offsetL", float "weightR", int "offsetR", float "knee", float "pr2", float "pr3")
{ # --- Hans' Ringing Remover ---

weightL = Default(weightL, 0.25) # 0.15 .. 0.7, left intensity; 0.0 = inactive
offsetL = Default(offsetL,1) # 0..5, distance from an edge
weightR = Default(weightR, 0.25) # right side
offsetR = Default(offsetR,1)
knee = Default(knee,0.7) # 0.2 .. 1.5 lower = softer, more linear. higher = more on-off like, useful for printed text
pr2 = Default(pr2,0.5) # 0.0 .. 1.5 protection for signal components with radius=2
pr3 = Default(pr3,0.5) # 0.0 .. 1.5 protection for signal components with radius=3
# the higher you set the pr parameters, the less often the artifact remover will get triggered. it depends on the content of the frame.
#
# you invoke the function for one particular distance of an artifact to an edge, left, or right, or both.
# you can invoke it again with a different offset, if there is more than one strong artifact on the same side, but the instances may interact in unexpected ways.

expr = "x[2,0] x[1,0] - dup * .75 * x[1,0] x - dup * + x x[-1,0] - dup * + x[-1,0] x[-2,0] - dup * .75 * + sqrt _F1@ " +\
"x[3,0] x[1,0] - dup * .66 * x[2,0] x[0,0] - dup * + x[1,0] x[-1,0] - dup * + x[0,0] x[-2,0] - dup * .66 * + sqrt " + String(pr2*0.36) + " * - " +\
"x[3,0] x[0,0] - dup * .5 * x[2,0] x[-1,0] - dup * + x[1,0] x[-2,0] - dup * + x[0,0] x[-3,0] - dup * .66 * + sqrt " + String(pr3*0.3) + " * - " +\
String(10.0/knee) + " / " + String(0.5*knee) + " - 0.0 max dup 1.0 - swap 1.0 + / 1.0 + 128.0 * 255.0 min " +\
"x["+ String(offsetL) +",0] x["+ String(2+offsetL) +",0] - 256.0 / * "+ String(weightL) +" * -64.0 max 127.0 min " +\
" _F1 " +\
"x[-3,0] x[-1,0] - dup * .66 * x[-2,0] x[0,0] - dup * + x[-1,0] x[1,0] - dup * + x[0,0] x[2,0] - dup * .66 * + sqrt " + String(pr2*0.36) + " * - " +\
"x[-3,0] x[-0,0] - dup * .5 * x[-2,0] x[1,0] - dup * + x[-1,0] x[2,0] - dup * + x[0,0] x[3,0] - dup * .66 * + sqrt " + String(pr3*0.3) + " * - " +\
String(10.0/knee) + " / " + String(0.5*knee) + " - 0.0 max dup 1.0 - swap 1.0 + / 1.0 + 128.0 * 255.0 min " +\
"x["+ String(-offsetR) +",0] x["+ String(-2-offsetR) +",0] - 256.0 / * "+ String(weightR) +" * -64.0 max 127.0 min + x + "

planes = Default(planes, "luma")
Y = (planes == "luma" || planes == "all") ? 3 : 1
U = (planes == "chroma" || planes == "all") ? 3 : 1
V = (planes == "chroma" || planes == "all") ? 3 : 1

output = Default(output, "YUV444P12")
output = (output=="out=in") ? "" : output

if (output=="") {
return cl.Expr( (Y==3 ? expr : "x"), (U==3 ? expr : "x"), (V==3 ? expr : "x"), scale_inputs = "allf")
} else {
return cl.Expr( (Y==3 ? expr : "x"), (U==3 ? expr : "x"), (V==3 ? expr : "x"), scale_inputs = "allf", format=output)
}
}

Selur
5th April 2026, 16:24
Here's (https://pastebin.com/hLxQ3GY8) a Vapoursynth port, which doesn't use output parameter, internally uses YUV444PS and also has a AntiRingLRUD-wrapper which calls, AntiRingLR two time, where the second one is called on a 90° rotated version.

geometer
6th April 2026, 05:53
Thanks Selur,
I was planning to create an AntiRingUD() at a later time, as the systemic structure of halo and ringing is different for horizontal edges and lines.

On pre-blending, and regarding halo and ringing along horizontal bars and lines:
Currently, my own projects use different technologies to tackle that, the deinterlacer has a lot of influence in this. But hand-crafted convolutional impulses work quite well.
The issue is that the cameras deliver an interlaced video stream, but then later there is an attempt to sharpen it during the mastering, which does not work well.
Newer DVDs are naturally better with this.
You have to remove the sharpening in field mode (which I call pre-blending), then deinterlace, then sharpen again, and the halo to the bigger part will be gone.

I would be thankful to hear about usage experience, and am glad to answer questions.


This tool has quite some learning curve, though starting should be easy. See the artifact, find out its pixel distance from the edge, set weight to a high value like 0.8, then try to switch "offset" until the artifact gets covered by the predicted signal. Then, reduce the weight parameter and nudge, until this area looks as neutral as possible.
If you found good settings, the dynamics will find the same artifact through the whole frame, the whole scene, perhaps the whole video, and do the right thing.


You can save away presets, like
last = AntiRingLR(last, "luma", weightL=0.6,offsetL=1,weightR=0.6,offsetR=1,knee=0.5,pr2=0.3,pr3=0.6)
this one will create an effect halo around blurred objects.
You can clean up your signal, but you can also do very crazy things with it, when you pull the parameters out of the comfort zone or sweet spot.

I have added two examples where the sharpening had triggered massive ringing.

procedure:
...
open d2v file
deinterlace to 60fps
convolutional sharpening and corrections
...
apply the antiring function on 720x480 resolution like this:
last = AntiRingLR(last, "luma", weightL=0.11,offsetL=2,weightR=0.11,offsetR=3,knee=0.5,pr2=0.8,pr3=1.2)
...
do some more refinements with the help of upsampling
...
apply the antiring function on 960x640 resolution, use different offsets:
last = AntiRingLR(last, "luma", weightL=0.25,offsetL=1,weightR=0.2,offsetR=1,knee=0.5,pr2=0.5,pr3=0.5)
please note that this one was inserted and tuned first. it removes the most disturbing artifacts,
then the other one got inserted above, to tackle some extra issues for certain scenes.
---
the goal is also to run through the whole video with one or two instances of AntiRing,
and not change settings, because it is practically non-destructive,
but of course it is possible to cut and fine-tune by the particular scenes.
...
surrounding conditions:
pictures were screen shots from AvsPmod preview window (has poor filtering)
x264 plays a role with denoiser set to 25 and compression set to 19 or 20.
deinterlacing uses nnedi3 in double fps mode means the resulting video plays at 60fps.

geometer
22nd April 2026, 17:45
I am sharing what I do, so here is an update, with a limitation that does not bother me.

What it does, it adds the other lobe also, when it generates the predicted error signal.
This became apparent with some applications on videos with a bigger Gibbs ringing problem.
So, this version generates an impulse of one polarity at position offsetR from the edge, and then another impulse of opposite polarity at position offsetR+1.
Weight is currently (0.6 * weightR) fixed, because it works, you can change that easily.

But it adds that lobe only on the right side.
The reason is that I use asymmetric convolution impulses that can generate ringing tails on the right side only. On the left side there would normally be only one artifact.


function AntiRingLR2(clip cl, string "planes", string "output", float "weightL", int "offsetL", float "weightR", int "offsetR", float "knee", float "pr2", float "pr3")
{ # --- Hans' Ringing Remover ---

weightL = Default(weightL, 0.25) # 0.15 .. 0.7, left intensity; 0.0 = inactive
offsetL = Default(offsetL,1) # 0..5, distance from an edge (you can even go negative)
weightR = Default(weightR, 0.25) # right side
offsetR = Default(offsetR,1)
knee = Default(knee,0.7) # 0.2 .. 1.5 lower = softer, more linear. higher = more on-off like, useful for printed text
pr2 = Default(pr2,0.5) # 0.0 .. 1.5 protection for signal components with radius=2
pr3 = Default(pr3,0.5) # 0.0 .. 1.5 protection for signal components with radius=3
# the higher you set the pr parameters, the less often the artifact remover will get triggered. it depends on the content of the frame.
#
# you invoke the function for one particular distance of an artifact to an edge, left, or right, or both.
# you can invoke it again with a different offset, if there is more than one strong artifact on the same side, but the instances may interact in unexpected ways.

expr = "x[2,0] x[1,0] - dup * .75 * x[1,0] x - dup * + x x[-1,0] - dup * + x[-1,0] x[-2,0] - dup * .75 * + sqrt _F1@ " +\
"x[3,0] x[1,0] - dup * .66 * x[2,0] x[0,0] - dup * + x[1,0] x[-1,0] - dup * + x[0,0] x[-2,0] - dup * .66 * + sqrt " + String(pr2*0.36) + " * - " +\
"x[3,0] x[0,0] - dup * .5 * x[2,0] x[-1,0] - dup * + x[1,0] x[-2,0] - dup * + x[0,0] x[-3,0] - dup * .66 * + sqrt " + String(pr3*0.3) + " * - " +\
String(10.0/knee) + " / " + String(0.5*knee) + " - 0.0 max dup 1.0 - swap 1.0 + / 1.0 + 128.0 * 255.0 min " +\
"x["+ String(offsetL) +",0] x["+ String(2+offsetL) +",0] - 256.0 / * "+ String(weightL) +" * -64.0 max 127.0 min " +\
" _F1 " +\
"x[-3,0] x[-1,0] - dup * .66 * x[-2,0] x[0,0] - dup * + x[-1,0] x[1,0] - dup * + x[0,0] x[2,0] - dup * .66 * + sqrt " + String(pr2*0.36) + " * - " +\
"x[-3,0] x[-0,0] - dup * .5 * x[-2,0] x[1,0] - dup * + x[-1,0] x[2,0] - dup * + x[0,0] x[3,0] - dup * .66 * + sqrt " + String(pr3*0.3) + " * - " +\
String(10.0/knee) + " / " + String(0.5*knee) + " - 0.0 max dup 1.0 - swap 1.0 + / 1.0 + 128.0 * 255.0 min " +\
"x["+ String(-offsetR) +",0] x["+ String(-2-offsetR) +",0] - 256.0 / * "+ String(weightR) +" * -64.0 max 127.0 min + " +\
\
"x[1,0] x[0,0] - dup * .75 * x[0,0] x[-1,0] - dup * + x[-1,0] x[-2,0] - dup * + x[-2,0] x[-3,0] - dup * .75 * + sqrt " +\
"x[-4,0] x[-2,0] - dup * .66 * x[-3,0] x[-1,0] - dup * + x[-2,0] x[0,0] - dup * + x[-1,0] x[1,0] - dup * .66 * + sqrt " + String(pr2*0.36) + " * - " +\
"x[-4,0] x[-1,0] - dup * .5 * x[-3,0] x[0,0] - dup * + x[-2,0] x[1,0] - dup * + x[-1,0] x[2,0] - dup * .66 * + sqrt " + String(pr3*0.3) + " * - " +\
String(10.0/knee) + " / " + String(0.5*knee) + " - 0.0 max dup 1.0 - swap 1.0 + / 1.0 + 128.0 * 255.0 min " +\
"x["+ String(-1-offsetR) +",0] x["+ String(-3-offsetR) +",0] - 256.0 / * "+ String(weightR*0.6) +" * -64.0 max 127.0 min - " +\
" x + "

planes = Default(planes, "luma")
Y = (planes == "luma" || planes == "all") ? 3 : 1
U = (planes == "chroma" || planes == "all") ? 3 : 1
V = (planes == "chroma" || planes == "all") ? 3 : 1

output = Default(output, "YUV444P12") ### 420?
output = (output=="out=in") ? "" : output

if (output=="") {
return cl.Expr( (Y==3 ? expr : "x"), (U==3 ? expr : "x"), (V==3 ? expr : "x"), scale_inputs = "allf")
} else {
return cl.Expr( (Y==3 ? expr : "x"), (U==3 ? expr : "x"), (V==3 ? expr : "x"), scale_inputs = "allf", format=output)
}
}



( I left the x[0,0] in for better readability in proof reading )

Here is a usage example.

At 720x480 in RGB planar, I apply a kernel like
C50g5u4="5 -19 33 -43 101 0 0 -4 2 -37 0 1 -3 7 9 0 0 -0 -1 -11 0 0 0 0 5"
last = Conv5x5(last, C50g5u4, "all", "out=in").ConvertToYUV444(matrix="PC.709", interlaced=false,chromaresample="lanczos4")

(this is extreme sharpening, with my own convolution function, very aggressive, normally you would not get away with that.)

then follows the first general ringing buster with
last = AntiRingLR2(last, "luma", weightL=0.23,offsetL=1,weightR=0.3,offsetR=1,knee=0.1,pr2=0.3,pr3=0.25)

then, we do some upsampling and sharpening in the upsampled domain, color correction, sometimes asharp() etc..

aside from the margin handling, the sample format ends up with 960x640.
and there we do a final
last = AntiRingLR2(last, "luma", weightL=0.15,offsetL=2,weightR=0.26,offsetR=5,knee=0.3,pr2=0.4,pr3=0.5)
because there is an extra artifact at the end of the tail.

I was lucky and results were stunning! :cool:
There are very few scenes with certain hatch patterns that would need extra attention, but it happens for a second only on a very small section of the screen.

---
addendum
Please notify me if you have difficulty to get it running.
You may have to take care of the pixel data format. I use 12bit planar.
Change the defaults in the code to your needs.
The following is distilled from my own workflow routine so you can see how I get to the format that works for me.
MPEG2Source (....)

# here would be the deinterlacer

ConvertBits(12)
ConvertToPlanarRGB(matrix="Rec709",interlaced=false)
ConvertToYUV444(matrix="PC.709", interlaced=false,chromaresample="lanczos4")

# ... various processings
# ...

last = AntiRingLR2(last, "luma", weightL=0.23,offsetL=1,weightR=0.3,offsetR=1,knee=0.1,pr2=0.3,pr3=0.25)
# if you use different formats, you may add output="out=in" to disable checking and attempted conversion

# ... upsampling and more processings
# ... maybe another AntiRing instance

ConvertBits(8, dither=1)

ConvertToYV12(chromaresample="lanczos4", matrix="Rec709", interlaced=false)

Prefetch(9)

return last

johnmeyer
23rd April 2026, 19:04
Impressive results.

Selur
24th April 2026, 13:00
AntiRingLR2 for Vapoursynth (https://github.com/Selur/VapoursynthScriptsInHybrid/blob/5d4b3458fd9c0799348ac069aeb11720f6a7a3ef/dering.py#L712).

Cu Selur