Bit Depth Analysis
Sometimes its useful to know what the actual dynamic range of a particular combination of codec, profile and pixel format is, since it may not necessarily be what you think. i.e. you think you are getting 10-bits (i.e. 1024 values) when in fact you could be getting quite a bit less.
This work is inspired by https://github.com/ColorlabMD/Prores-BitDepth.
These tests generate a luminance YCrCb flat image, where each frame increments the luminance by one using the geq ffmpeg filter.
For example for the prores_ks codec we would run:
ffmpeg -y -r 24 -f lavfi -i nullsrc=s=720x480,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v prores_ks -profile:v 4444xq ./colors-prores_ks_10_4444xq.mov
We then read the resulting movie file, to see if the luminance that we expect.
The columns are defined as:
- Test Name - which codec we are testing.
- Bit Depth - What bit-depth the codec is being evaluated to (which determines how many unique values to test to).
- Unique values - How many resulting values are unique, but may not be exactly the same value.
- STDDEV > 0.0001 - Standard deviation of the frame is > 0.0001 which means what should be a flat color, isnt.
- Off by 1 - The color is a flat color, but is “off by 1” (i.e. what should be 3 is 4), again not ideal.
- Other invalid - is a flat color, but is some other value than expected.
Running the tests
To run the tests you need three additional python libraries as well as ffmpeg:
pip install yuvio numpy pyseq
Warning: The file sizes can get large, so expect it to generate 8GB of data.
Note, the one thing the ColorlabMD test does that this does not currently do is to also do a comparison of chroma variation.
Results
Prores_ks
Tests - prores_ks_10_4444xq, prores_ks_10_proxy, prores_ks_10_hq
It shows that the ffmpeg_ks encoder is only encoding to the legal 10-bit range (i.e. 877 values), which is pretty much what we expect.
The tests https://github.com/ColorlabMD/Prores-BitDepth tests seem a little unfair on prores_ks, which clearly defines itself as a 10-bit codec, and I suspect that the apple encoding is setting full range values which Prores (in this case) is not.
Note, Prores_ks will read 12 bit files, just not generate them.
Apple Videotoolbox Prores
Tests - prores_videotoolbox_10_proxy, prores_videotoolbox_10_hq, prores_videotoolbox_10_4444, prores_videotoolbox_12_xq
Interestingly, we are getting better results for prores_ks for proxy and HQ, in that the values are at least consistent, which for videotoolbox they are often off by 1.
However, for 4444 and XQ they are good.
DNxHD
All the values are as expected.
h264
All the values are as expected.
x265 HEVC
x265 HEVC is struggling with flat color with quite a few values not generating a uniform value, unless lossless is used.
VP9
All the values are as expected.
libsvtav1
All the values are as expected.
libaom-av1
Is slightly better, with 866 unique values, but many of them are off by one or more. At 12-bit the results are similar. Using lossless will maintain all values (as expected).
Click here to see the full page table.
Test Name | Bit Depth | Unique Values | Range of Valid Values | a.std() > 0.0001 | Off by 1 | Other Invalid | Fffmpeg command |
---|---|---|---|---|---|---|---|
prores_ks_10_4444xq | 10 | 1023 / 1024 | [1-1022] | [0, 1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v prores_ks -profile:v 4444xq ./colors-prores_ks_10_4444xq.mov | ||
prores_ks_10_proxy | 10 | 1016 / 1024 | [4-1019] | [0-3, 1020-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv422p10le -frames:v 1024 -vf geq=N:512:512 -c:v prores_ks -profile:v proxy ./colors-prores_ks_10_proxy.mov | ||
prores_ks_10_hq | 10 | 1016 / 1024 | [4-1019] | [0-3, 1020-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v prores_ks -profile:v hq ./colors-prores_ks_10_hq.mov | ||
prores_videotoolbox_10_proxy | 10 | 1015 / 1024 | [4-527, 1019] | [3, 528-1018] | [0-3, 528-1018, 1020-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v prores_videotoolbox -profile:v proxy ./colors-prores_videotoolbox_10_proxy.mov | |
prores_videotoolbox_10_hq | 10 | 1015 / 1024 | [4-446, 1019] | [3, 447-1018] | [0-3, 447-1018, 1020-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v prores_videotoolbox -profile:v hq ./colors-prores_videotoolbox_10_hq.mov | |
prores_videotoolbox_10_4444 | 10 | 1023 / 1024 | [1-1022] | [0, 1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v prores_videotoolbox -profile:v 4444 ./colors-prores_videotoolbox_10_4444.mov | ||
prores_videotoolbox_12_xq | 12 | 4088 / 4096 | [4-4091] | [0-3, 4092-4095] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p12le -frames:v 4096 -vf geq=N:2048:2048 -c:v prores_videotoolbox -profile:v xq ./colors-prores_videotoolbox_12_xq.mov | ||
dnxhd_10_dnxhr_444 | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v dnxhd -profile:v dnxhr_444 ./colors-dnxhd_10_dnxhr_444.mov | |||
dnxhd_10_dnxhqx_422 | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv422p10le -frames:v 1024 -vf geq=N:512:512 -c:v dnxhd -profile:v dnxhr_hqx ./colors-dnxhd_10_dnxhqx_422.mov | |||
dnxhd_8_422 | 8 | 256 / 256 | [0-255] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=1920x1080,format=yuv422p -frames:v 256 -vf geq=N:128:128 -c:v dnxhd -b:v 36M ./colors-dnxhd_8_422.mov | |||
dnxhd_8_dnxhqx_422 | 8 | 256 / 256 | [0-255] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv422p -frames:v 256 -vf geq=N:128:128 -c:v dnxhd -profile:v dnxhr_hq ./colors-dnxhd_8_dnxhqx_422.mov | |||
h264-placebo | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v h264 -preset placebo -qp 0 ./colors-h264-placebo.mov | |||
h264-slower | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v h264 -preset slower -crf 15 ./colors-h264-slower.mov | |||
hevc-slower-444-10 | 10 | 1024 / 1024 | [0-1000, 1002-1003, 1005-1023] | [1001, 1004] | [1001, 1004] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v hevc -profile:v main444-10 -preset slower -crf 10 ./colors-hevc-slower-444-10.mov | |
hevc-slower-444-12 | 12 | 4096 / 4096 | [0-4, 6, 8, 10 ... 4085, 4087, 4089, 4091-4095] | [5, 7, 9, 11 ... 4083-4084, 4086, 4088, 4090] | [5, 7, 9, 11 ... 4083-4084, 4086, 4088, 4090] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p12le -frames:v 4096 -vf geq=N:2048:2048 -c:v hevc -profile:v main444-12 -preset slower -crf 2 ./colors-hevc-slower-444-12.mov | |
hevc-slower-444-12-lossless | 12 | 4096 / 4096 | [0-4095] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p12le -frames:v 4096 -vf geq=N:2048:2048 -c:v hevc -profile:v main444-12 -preset ultrafast -x265-params lossless=1 ./colors-hevc-slower-444-12-lossless.mov | |||
vp9-slower-444-10 | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v libvpx-vp9 -quality good -crf 5 -b:v 0 ./colors-vp9-slower-444-10.mp4 | |||
vp9-slower-444-12 | 12 | 4096 / 4096 | [0-4095] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p12le -frames:v 4096 -vf geq=N:2048:2048 -c:v libvpx-vp9 -quality good -crf 5 -b:v 0 ./colors-vp9-slower-444-12.mp4 | |||
libsvtav1-420-8 | 8 | 256 / 256 | [0-255] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p -frames:v 256 -vf geq=N:128:128 -c:v libsvtav1 -preset 5 -crf 3 ./colors-libsvtav1-420-8.mp4 | |||
libsvtav1-420-10 | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v libsvtav1 -preset 3 -crf 2 ./colors-libsvtav1-420-10.mp4 | |||
libaom-444-10 | 10 | 894 / 1024 | [0-10, 12-14, 16-18, 20-22 ... 988-990, 992-994, 996-998, 1000-1023] | [111, 115, 119, 123 ... 932, 936, 940, 999] | [11, 15, 19, 23 ... 987, 991, 995, 999] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v libaom-av1 -cpu-used 4 -usage good -crf 15 -row-mt 1 ./colors-libaom-444-10.mp4 | |
libaom-444-12 | 12 | 3975 / 4096 | [32-47, 55-70, 88-120, 127-151 ... 4048-4080, 4087, 4090, 4093] | [0-31, 48-54, 71-79] | [80-87, 121-126, 152-159, 193-198 ... 3968-3975, 4009-4014, 4040-4047, 4081-4086] | [0-31, 48-54, 71-87, 121-126 ... 4081-4086, 4088-4089, 4091-4092, 4094-4095] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p12le -frames:v 4096 -vf geq=N:2048:2048 -c:v libaom-av1 -cpu-used 4 -usage good -crf 15 -row-mt 1 ./colors-libaom-444-12.mp4 |
libaom-444-10-lossless | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p10le -frames:v 1024 -vf geq=N:512:512 -c:v libaom-av1 -aom-params: lossless=1 -cpu-used 4 -usage good -crf 20 -row-mt 1 ./colors-libaom-444-10-lossless.mp4 | |||
libaom-444-12-lossless | 12 | 4096 / 4096 | [0-4095] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv444p12le -frames:v 4096 -vf geq=N:2048:2048 -c:v libaom-av1 -aom-params: lossless=1 -cpu-used 4 -usage good -crf 20 -row-mt 1 ./colors-libaom-444-12-lossless.mp4 | |||
hevc_videotoolbox_8_main | 8 | 256 / 256 | [0-255] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=yuv420p -frames:v 256 -vf geq=N:128:128 -c:v hevc_videotoolbox -profile:v main -q:v 100 ./colors-hevc_videotoolbox_8_main.mov | |||
hevc_videotoolbox_10_main10 | 10 | 1024 / 1024 | [0-1023] | ffmpeg -y -r 24 -f lavfi -i nullsrc=s=256x120,format=p010le -frames:v 1024 -vf geq=N:512:512 -c:v hevc_videotoolbox -profile:v main10 -q:v 100 ./colors-hevc_videotoolbox_10_main10.mov |