Log in

View Full Version : Out of gamut detection


kolak
23rd January 2018, 19:57
Anyone willing to write something to detect out of gamut (with some threshold) script? I may be able to pay some money for it.

This is the spec which it should follow:

https://tech.ebu.ch/docs/r/r103.pdf

Video would be piped through ffmpeg I just need some indication if file has passed or not check.

kolak
23rd January 2018, 21:42
Hmm...looks to me like it's exactly what I want.

I know bit of Python itself, but struggle link vs with it.

Understand 2nd half to the script.
How do I check every frame while it's processed?
Or do I need script to process all frames and then check result in stored values?

ChaosKing
24th January 2018, 00:45
Use FrameEval to check on each frame http://www.vapoursynth.com/doc/functions/frameeval.html

kolak
24th January 2018, 10:31
Thank you.
I've looked at vs engine's doc yesterday and I was close, very close.
Myrsloik said that vspipe is better approach and should be faster.

Now everything makes perfect sense, except
c = core.std.Convolution(c, matrix=[1, 2, 3, 4, 3, 2, 1], mode='h')
c = core.std.Convolution(c, matrix=[1, 2, 1], mode='v')

which I assume is low pass filtering. Looking at numbers I get them, but I don't fully understand it.

What if file is interlaced? We need to process each filed separately?
Where is Rec.709 or Rec.601 here which I assumed needs to be specified when you go from YUV back to RGB?

kolak
24th January 2018, 11:40
Script itself works, but actual out of gamut detection doesn't.
Where is RGB here as when I do:
f.format it shows me YUV?

There has to be somewhere place where we convert it to RGB and then check if RGB planes are out of range.
Gamut errors are detected when YUV is converted back to RGB and creates illegal RGB values.

kolak
24th January 2018, 11:52
After converting to RGB now it seems to work.
Should I convert it before or after low pass filtering?

Is simple:
c = core.resize.Bicubic(clip=c,format=vs.RGB30)

good enough or should I use something better? ( this is for 10bit video).

I also realised that I have to check each RGB plane separately.

I've made files which have issues only on R, G or B channel only and it seems to be detecting it fine. Now I have to simulate below and above 1% frame coverage.

kolak
24th January 2018, 17:24
I've verified that all seems to be working fine for 8bit.
For 10bit there may be an issue as reported max plane values are 32768, not like expected e.g. 768 (some strange scaling is happening- I assume to 16bit?).

My test file with 920x80 pixels bad area did produce perfect score of 3.549% of bad pixels for HD frame.
With low pass filtering it's 3.493% which suggest it's all working well (I expected bit lower value).

kolak
24th January 2018, 17:30
std.Convolution implements the "Video Signal Filtering" section of the document. As described in the document, interlaced images are filtered per-field. You are right that in-range YUV signals can produce out-of-range RGB signals. To handle this, convert the YUV to RGB with limited range. When applying std.PlaneStats, repeat it once with each plane as the "plane" argument and a different prefix for "prop".


c = ... # Source.

c = core.std.CropAbs(c, ...) # Crop to active region.
c = core.resize.Bicubic(c, format=vs.RGB24, range='limited')



I have issue with range='limited'. Using old or latest ffms2 throws error. I have to use range=0.

File "src\cython\vapoursynth.pyx", line 528, in vapoursynth.typedDictToMap
ValueError: invalid literal for int() with base 10: 'limited'

Looks like a bug.

kolak
24th January 2018, 23:16
It was PlaneStatMax when I used RGB30 in resize, but without std.Expr stage. I was checking what values are provided by vs. I was expecting 10bit levels (e.g. 64-940), but saw things like 32768.

I was using old vs, so will try again with current one.

My other note is that:

std.Expr(c, 'x 5 < x 246 > or 255 0 ?')
and PlaneStats

operate on integers? This means precision is lost a bit.

kolak
24th January 2018, 23:34
Where do you observe the max plane value to reach 32768? After the std.Expr stage, only 0 and 255 (or 1023) should exist in the image. If std.PlaneStats after std.Expr reports this, it may be a VS bug.

VS R42 is fine- for RGB30 I see correct values for planestats.

kolak
24th January 2018, 23:57
Quite interesting.
Even ProRes files with bars can show as low as e.g. 14 values where you would expect 64. Then if you turn on filtering this is not anymore detected and lowest values is e.g. 56.
I was never expecting ProRes to affect simple things like bars that much :)

In the same time YUV values are almost perfect (some 60 values can be found for 10bit). This suggests that issue for RGB is at bars edges and it's due to chroma up sampling during conversion to RGB (at least in my opinion). It also shows that low pass filtering does well its role :)

kolak
25th January 2018, 00:19
c = core.std.PlaneStats(c, plane=0, props='PlaneStatsR') # Once for each plane if RGB.
c = core.std.PlaneStats(c, plane=1, props='PlaneStatsG')
c = core.std.PlaneStats(c, plane=2, props='PlaneStatsB')


There should be prop here not props.

kolak
25th January 2018, 11:46
This operation is exact. The purpose is to binarize the image at the thresholds specified in the document. PlaneStats then counts the number of pixels that were outside the threshold.

What do you mean by exact (x is float)?

For example my file has area which R is coming at eg. 8.56.

Looks like I can't have:

std.Expr(c, 'x 8.57 < x 246 > or 255 0 ?')

it's either x < 8 or x <9

I understand logic behind and I like this logic :)

kolak
25th January 2018, 13:46
Another question.

This R103 check requires RGB to be in specified values, but also Y out of YUV signal.

Can we do both checks in one go (RGB and original Y)?

How can we pass Y into checking function?
Looks like this has to be 2nd pass, no?

Also, where low pass filtering should happen: on YUY or RGB? ( I would say YUV)

Myrsloik
25th January 2018, 14:07
It's so vague about things. Is it talking about limited range rgb as well? I know you've mentioned other products that check these things and throwing carefully constructed samples at once of those solutions may be the only way we'll ever find out...

kolak
25th January 2018, 14:19
It's not end of the world as difference between low pass filtered on YUV v RGB is very small.
I assume it's limited range as otherwise it would not make sense?

How can I check original Y (if it's in limits) during the same check as RGB?

Myrsloik
25th January 2018, 14:20
It's not end of the world as difference between low pass filtered on YUV v RGB is very small.

How can I check original Y (if it's in limits) during the same check as RGB?

Post the script so far and I'll show you, it's a quite simple addition...

kolak
25th January 2018, 14:25
def check_gamut(n, f, c):
if f.props['PlaneStatsRAverage'] > 0.01 or f.props['PlaneStatsGAverage'] > 0.01 or f.props['PlaneStatsBAverage'] > 0.01:
print("Bad frame: "+str(n))
exit()
return c

c = core.ffms2.Source()
c = core.std.Convolution(c, matrix=[1, 2, 3, 4, 3, 2, 1], mode='h')
c = core.std.Convolution(c, matrix=[1, 2, 1], mode='v')

c = core.resize.Bicubic(c, format=vs.RGB30, range_s="limited")

c = core.std.Expr(c, 'x 20 < x 984 > or 1023 0 ?')

c = core.std.PlaneStats(c, plane=0, prop='PlaneStatsR')
c = core.std.PlaneStats(c, plane=1, prop='PlaneStatsG')
c = core.std.PlaneStats(c, plane=2, prop='PlaneStatsB')

c = core.std.FrameEval(c, functools.partial(check_gamut, c=c), prop_src=c)
c.set_output()

Myrsloik
25th January 2018, 14:25
Basically the idea is to convert the input to yuv/rgb (opposite of what it is).

Then run the original and opposite format clips through the appropriate checks.

Use stackhorizontal last to make both branches of the script be fetched. Done. Probably.

kolak
25th January 2018, 14:28
I need to somehow do this:




c = core.ffms2.Source()
c = core.std.Convolution(c, matrix=[1, 2, 3, 4, 3, 2, 1], mode='h')
c = core.std.Convolution(c, matrix=[1, 2, 1], mode='v')
...

y=c
y = core.std.Expr(y, 'x 20 < x 984 > or 1023 0 ?')
y = core.std.PlaneStats(y, plane=0, prop='PlaneStatsY')

c = core.std.FrameEval(c, functools.partial(check_gamut, c=c), prop_src=c)
c.set_output()

but I have no way to pass it to FrameEval as it takes prop_src=c only?

I need this:
c = core.std.FrameEval(c, functools.partial(check_gamut, c=c, y=y), prop_src=[c,y])

Myrsloik
25th January 2018, 14:46
import vapoursynth as vs
from vapoursynth import core
import functools

def check_gamut_rgb(n, f, c):
if f.props['PlaneStatsRAverage'] > 0.01 or f.props['PlaneStatsGAverage'] > 0.01 or f.props['PlaneStatsBAverage'] > 0.01:
raise RuntimeError("Bad frame (RGB): "+str(n))
return c

def check_gamut_y(n, f, c):
if f.props['PlaneStatsYAverage'] > 0.01:
raise RuntimeError("Bad frame (Y): "+str(n))
return c

c = core.std.BlankClip(format=vs.YUV420P10)
if c.format.color_family != vs.YUV:
raise RuntimeError("only yuv input supported")

rgb_format_id = core.register_format(vs.RGB, c.format.sample_type, c.format.bits_per_sample, 0, 0)

c = core.std.Convolution(c, matrix=[1, 2, 3, 4, 3, 2, 1], mode='h')
c = core.std.Convolution(c, matrix=[1, 2, 1], mode='v')

crgb = core.resize.Bicubic(c, format=rgb_format_id, range_s="limited")

expression = 'x ' +str(5 * (2**(c.format.bits_per_sample - 8)))+ ' < x ' +str(246 * (2**(c.format.bits_per_sample - 8)))+ ' > or ' + str(2**(c.format.bits_per_sample) - 1) + ' 0 ?'

crgb = core.std.Expr(crgb, expression)

crgb = core.std.PlaneStats(crgb, plane=0, prop='PlaneStatsR')
crgb = core.std.PlaneStats(crgb, plane=1, prop='PlaneStatsG')
crgb = core.std.PlaneStats(crgb, plane=2, prop='PlaneStatsB')

crgb = core.std.FrameEval(crgb, functools.partial(check_gamut_rgb, c=crgb), prop_src=crgb)

c = core.std.Expr(c, [expression, ''])
c = core.std.PlaneStats(c, plane=0, prop='PlaneStatsY')
c = core.std.FrameEval(c, functools.partial(check_gamut_y, c=c), prop_src=c)

c = core.std.StackVertical([c, crgb.resize.Bilinear(format=c.format, matrix_s="709")])

c.set_output()

kolak
25th January 2018, 15:11
Looks a little bit complicated :)

Problem:

NameError: name 'opposite_cf' is not defined

Myrsloik
25th January 2018, 15:14
Look a little bit complicated :)

Problem:

NameError: name 'opposite_cf' is not defined

Doh, should be vs.RGB there instead.

kolak
25th January 2018, 16:45
Seems to work well now :)

kolak
25th January 2018, 18:09
If the resulting RGB is in-range, it is not possible for Y to be out of range, because Y = a * R + b * G + c * B. Since a, b, c are between 0 and 1, Y must be less than max(R, G, B) and greater than min(R, G, B). The converse is not true.

Hm...I was thinking about it, but since they recommend and all equipment also checks Y I thought I also have to. Maybe this is for cases when RGB gamut is not checked, just YUV values.

Are sure that it's enough to check RGB and if this is fine Y HAS to be also fine?

I've tried to make such a file where Y is out of range and RGB still in but I didn't manage :) If this is the math then it has to be true :)

We just wasted time to make this script :)