Log in

View Full Version : Script to set saturation to zero in dark areas of the image


alexh110
10th January 2025, 18:03
Hi all!

Just wanted some advice on how to write a simple AviSynth script.
I wish to set the saturation to zero for those parts of the frame where luminance falls below a user defined threshold.

The purpose is to eliminate unwanted colour from very dark objects that should essentially be black.

The input source would be either an MPEG2, an mkv, or a vob file copied from DVD.

The desired output format would be either MPEG2 or avi.

Many thanks in advance,
Alex

Sharc
10th January 2025, 19:37
Basically you may want to build a luma mask and then remove from those areas the chroma (U,V).
Did you try regular avisynth filters? Sample of your video?

alexh110
10th January 2025, 19:48
Basically you may want to build a luma mask and then remove from those areas the chroma (U,V).
Did you try regular avisynth filters? Sample of your video?

Not yet no.
I've rather forgotten how to use AviSynth, having not touched it in over a decade!

A luma mask may be an unnecessary step, as I simply want to set U and V to zero whenever the luma falls below the threshold given in the script.
The luma levels need to be sampled across the entire frame (or sequence of frames), and the chroma adjusted accordingly.

Can't provide a sample for copyright reasons.

DTL
10th January 2025, 20:03
For best performance it expected to be a compiled binary for simple math like

UV_out = UV_in (with neutral to zero) * Y (in full-range 0..max).

Also Y may be tweaked in some range like only very low Y near zero passed unchanged and any other set to 1.0f (so UV will be unchanged).

In scripting form simple solution may be using Expr() function.

If no any smoothing required and simple Y-threshold is enough - the simple conditional form may be used instead of multiplication. See documentation for Expr function http://avisynth.nl/index.php/Expr

But simple switching form full saturation to zero may be not as nice as smooth transiton from full saturation to zero using some Function(Y) as multiplier. Like some a*Y+b or a*Y^g+ (can be used Levels() filter or math in Expr).

poisondeathray
10th January 2025, 20:10
A luma mask may be an unnecessary step, as I simply want to set U and V to zero whenever the luma falls below the threshold given in the script.
The luma levels need to be sampled across the entire frame (or sequence of frames), and the chroma adjusted accordingly.




That would be u,v = 128 for 8bit YUV zero saturation. Or 512 for 10bit etc... ie. not zero

Use a zero saturation layer with a mask based on luma values. If you want a hard cut off you can use mt_binarize for the mask. If you want a falloff, you can use the lumamask function by dogway

eg.

#Source Filter e.g. MPEG2Source, LWLibavVideoSource, etc...
src=last

#adjust mask values. Right now values <50 are included in the mask
msk = src.mt_binarize(50, upper=true)

#zero sat
sat = src.tweak(sat=0, coring=false)

#to visualize tweak the mask values, use return msk
overlay(src, sat, mask=msk)

johnmeyer
10th January 2025, 20:17
This is easy if you have access to Vegas. It has a "secondary color corrector" which lets you specify a luminescence threshold, and then you can desaturate above or below that threshold. What's more, you can "feather" the result so you don't get some sort of harsh line of demarcation.

https://i.imgur.com/JxXvI5K.jpeg

alexh110
10th January 2025, 20:18
Many thanks for the lightning fast response!
I knew I could rely on you guys!
Will give the above a try.

johnmeyer
10th January 2025, 20:42
Here is a screen grab of some video:

https://i.imgur.com/BnPysCx.jpeg

Here is what it looks like after I desaturated just the pixels that were below the luminescence threshold I set:

https://i.imgur.com/5tBp8UH.jpeg

The reason I suggest that you use an NLE is that you really need to interact with the video in order to set the thresholds. For instance, note the shadow on the white wall underneath the tool rack: the blue is entirely gone. I hadn't expected that because that "white" wall looked pretty light to me, but it wasn't. Also, look at the flesh tone on my arms: some of the color was removed, but not all of it, even though I "smoothed" the effect.

The moral of the story is that your eye reacts to the combination of luminescence and saturation in ways you won't expect. The only solution is to play with the controls interactively in an NLE.

poisondeathray
10th January 2025, 21:09
You can do it interactively with avisynth /avspmod sliders too - You can setup user sliders like a NLE, and when you move them, the image refreshes . But there is a slightly longer lag than most NLE's


Personally, I prefer doing in a NLE too. Resolve is very powerful, probably the one of the best (if not the best) tool for color manipulation. Even the free version is better than most paid NLE's for color work. For your case, you'd probably want to start with a luminance vs. saturation curve. There are many tutorials on youtube from beginner to advanced

alexh110
10th January 2025, 21:13
That would be u,v = 128 for 8bit YUV zero saturation. Or 512 for 10bit etc... ie. not zero


Yes of course, I was thinking of the analogue situation, where no saturation corresponds to zero colour differences.

In an 8bit digital system, I guess you can't have negative values: so you have to set the zero point at half the maximum value.

Dogway
10th January 2025, 21:40
I made HSVxHSV precisely for these kinds of operation. Here's a small example with your image with visual feedback.
HSVxHSV(0.0, modeA="VAL", modeB="SAT", Delta=28, Q=0.11, Opacity=1.0, show=0)

Luma X Saturation - AviSynth+ (https://www.youtube.com/watch?v=NWkFptDy4M0)

DTL
11th January 2025, 06:57
"after I desaturated just the pixels that were below the luminescence threshold I set:"

As expected hard mask/threshold not look any nice - it is better to use some transient controlled by curve from full to zero saturation depending on Y values. And really most useful GUI is for this manual curve editor as we have in Photoshop or same looking software. If user is good in math in head - some approximation polynom can be used in text form like f(x)=a*Y^g1+b*Y^g2+... starting from simple gamma-corrected and scaled Y (with Levels()-like processing).

As I remember setting saturation to zero near blacks is some known imaging processing (like Technicolor film simulation ?). So may be some ready to use tools/plugins exist.

johnmeyer
11th January 2025, 07:52
BTW, if you are trying to desaturate just the dark areas, perhaps you are doing that because you have colored shadows. This is somewhat common, especially in the film transfers I do. I find that instead of desaturating, I can use the Color Corrector tool in Vegas (I'm sure other NLEs have a similar tool). It lets me adjust color balance for shadows, midtones, and highlights independently. I can simply click the color subtractor "eyedropper" tool on the darkest shadow I can find, and that shadow, and all other dark areas are immediately corrected to neutral. The midtones and highlights are not affected.

Here is an example from a long "before/after" I posted. I've queued it up to where you can see the dark background behind the screen door on the right side of the image. The red color is gone in the corrected version.

Before/After (https://www.youtube.com/watch?v=tBAHzO7rJS0&t=137s)

DTL
11th January 2025, 09:48
Some example with Expr:

ImageReader("BnPysCx.jpeg")

ConvertToYUV444()

inp=last.Subtitle("inp")

ConvertBits(32)

sat_layerY=ConvertToY()
sat_layerY=Levels(sat_layerY,0.1,1,0.2,0,1)

#sat control channel setup check
#return sat_layerY.ConvertBits(8)

sat_layer=CombinePlanes(sat_layerY, sat_layerY, sat_layerY, "YUV")

Expr(last, sat_layer,"", "x y *", "x y *")

ConvertBits(8)

Interleave(last, inp)


Saturation control channel by Levels(sat_layerY,0.1,1,0.2,0,1) with float32 levels code values in range 0..1. Attempt to desaturame undertable dark area

https://imgsli.com/MzM3MDM3 (looks like some error with zoom at that site ?)