I have seen @elle and @ggbutcher do screen evaluations of some image processing algorithms (including mine) based on display-referred Lch values and histogram likeliness, so maybe it’s time to provide some methodic way to stop saying whatever, because it’s bad for my nerves. This is a framework based on Python and darktable output to measure systematic deviations.
What ?
You. Can. Measure. Deviations. Only. On. Synthetic. Imagery.
Help yourself here : https://hdrihaven.com/hdri/?h=aerodynamics_workshop
The problem of real-world photos is nobody knows their true spectral values, because they have been filtered by a sensor and various corrections and adjustements, so you have to trust too many black boxes. 3D renders and sythetic imagery are the only way to get “true”, uncorrected pictures that can be used as references.
Then, you ideally need files encoded in 32 bits floats to avoid every quantization error.
How ?
In darktable, save your files in PFM 32 bits encoded in XYZ space (there is an hidden option to enable Lab and XYZ outputs). To get from the HDRi files to darktable, you have to convert them through Blender in PFM full precision.
Then, there is a small lib I have put together to open the PFM file and store it as a numpy matrix. We convert the XYZ data to xyY, center the xyY space on the equi-energetic white so that we can express the saturation as the euclidian norm of the (x, y) vector and the hue as its angle. Then, we compute the root mean of the square error/deviation over the whole picture, the error being the difference between reference and output hue and saturation.
import numpy as np
import matplotlib.pyplot as plt
def load_pfm(fn):
if fn.endswith(".pfm"):
fid = open(fn, "rb")
else:
print("No pfm file! \n")
return
raw_data = fid.readlines()
fid.close()
cols = int(raw_data[1].strip().split(b" ")[0])
rows = int(raw_data[1].strip().split(b" ")[1])
del raw_data[2]
del raw_data[1]
del raw_data[0]
image = np.frombuffer(b"".join(raw_data), dtype=np.float32)
del raw_data
image = image.reshape(cols, rows, 3).T
return image
hue = 0
sat = 1
def convert_2_hsY(image):
"""
Takes an XYZ input image
Outputs a [hue, saturation, Y] image
"""
sum_channels = np.sum(image, axis=2)
tmp = np.empty_like(image)
# Convert to xyz centered in (x, y) = (1/3, 1/3)
# which is the equi-energetic white in xyY
# https://en.wikipedia.org/wiki/CIE_1931_color_space#/media/File:CIE1931_rgxy.png
for i in range(2):
tmp[:, :, i] = image[:, :, i] / sum_channels - 1/3
out = np.empty_like(image)
# Copy Y
out[:, :, 2] = image[:, :, 2]
# The hue is the angle of the (x, y) vector
out[:, :, hue] = np.arctan2(tmp[:, :, 1], tmp[:, :, 0])
# The saturation is the norm of the (x, y) vector
out[:, :, sat] = (tmp[:, :, 1]**2 + tmp[:, :, 0]**2)**0.5
return out
def RMSE(x, y):
# https://en.wikipedia.org/wiki/Root-mean-square_deviation
SE = (x-y)**2
# Output the RMSE and the max of the error
return (np.sum(SE)/ x.size)**0.5 , np.amax(SE)
Results
If you want to compare a set of pictures in a systematic way, you can loop over an array of files, that’s more convenient:
files = [
"Téléchargements/aerodynamics_workshop_16k-filmic-chroma.pfm",
"Téléchargements/aerodynamics_workshop_16k-filmic-non-chroma.pfm",
"Téléchargements/aerodynamics_workshop_16k-filmic-chroma-desat.pfm",
"Téléchargements/aerodynamics_workshop_16k-basecurve-hdr.pfm",
"Téléchargements/aerodynamics_workshop_16k-basecurve.pfm",
]
reference = load_pfm("Téléchargements/aerodynamics_workshop_16k-reference.pfm")
reference = convert_2_hsY(reference )
for f in files:
print(f)
print("---------------------------------------------------")
img = load_pfm(f)
img = convert_2_hsY(img)
print("Hue \tRMSE = %.4g \tmax(SE) = %.4g" % RMSE(reference[:, :, hue], img[:, :, hue]))
print("Sat \tRMSE = %.4g \tmax(SE) = %.4g" % RMSE(reference[:, :, sat], img[:, :, sat]))
print("All \tRMSE = %.4g \tmax(SE) = %.4g" % RMSE(reference[:, :, hue:sat], img[:, :, hue:sat]))
print("---------------------------------------------------\n")
Then, the output is nice:
éléchargements/aerodynamics_workshop_16k-filmic-chroma.pfm
---------------------------------------------------
Hue RMSE = 1.111e-07 max(SE) = 3.638e-12
Sat RMSE = 3.939e-05 max(SE) = 6.815e-09
All RMSE = 1.111e-07 max(SE) = 3.638e-12
---------------------------------------------------
Téléchargements/aerodynamics_workshop_16k-filmic-non-chroma.pfm
---------------------------------------------------
Hue RMSE = 1.576e-07 max(SE) = 3.638e-12
Sat RMSE = 4.674e-05 max(SE) = 6.162e-09
All RMSE = 1.576e-07 max(SE) = 3.638e-12
---------------------------------------------------
Téléchargements/aerodynamics_workshop_16k-filmic-chroma-desat.pfm
---------------------------------------------------
Hue RMSE = 1.086e-07 max(SE) = 2.785e-12
Sat RMSE = 4.029e-05 max(SE) = 4.094e-09
All RMSE = 1.086e-07 max(SE) = 2.785e-12
---------------------------------------------------
Téléchargements/aerodynamics_workshop_16k-basecurve-hdr.pfm
---------------------------------------------------
Hue RMSE = 4.627e-07 max(SE) = 3.029e-10
Sat RMSE = 9.426e-05 max(SE) = 1.463e-08
All RMSE = 4.627e-07 max(SE) = 3.029e-10
---------------------------------------------------
Téléchargements/aerodynamics_workshop_16k-basecurve.pfm
---------------------------------------------------
Hue RMSE = 2.699e-07 max(SE) = 1.455e-11
Sat RMSE = 9.373e-05 max(SE) = 1.549e-08
All RMSE = 2.699e-07 max(SE) = 1.455e-11
---------------------------------------------------
So, the filmic-non-chroma
is the current filmic version in darktable master branch. The filmic-non-chroma
is a variant I’m working on with chroma handcuffs, and the filmic-chroma-desat
is the same as the previous with a - 50 % desaturation, performed in color balance.
You see that you get roughly half the RMSE with filmic variants than with the basecurve, and basecurve + exposure fusion is ever worse, hence me calling that thing silly.
This what the filmic-chroma-desat
looks like:
Another version of filmic-chroma-desat
And its metrics:
Téléchargements/aerodynamics_workshop_16k-filmic-chroma-desat-2.pfm
---------------------------------------------------
Hue RMSE = 1.663e-07 max(SE) = 4.604e-12
Sat RMSE = 9.019e-05 max(SE) = 1.786e-08
All RMSE = 1.663e-07 max(SE) = 4.604e-12
---------------------------------------------------
Now, basecurve-hdr
:
Recall its metrics:
Téléchargements/aerodynamics_workshop_16k-basecurve-hdr.pfm
---------------------------------------------------
Hue RMSE = 4.627e-07 max(SE) = 3.029e-10
Sat RMSE = 9.426e-05 max(SE) = 1.463e-08
All RMSE = 4.627e-07 max(SE) = 3.029e-10
---------------------------------------------------
The original is here: https://hdrihaven.com/hdri/?h=aerodynamics_workshop