wetadigital / physlight

Apache License 2.0
184 stars 11 forks source link

How was the `physlight_camera_model` comparison matrix extracted from DCRAW? #5

Closed hotgluebanjo closed 1 year ago

hotgluebanjo commented 1 year ago

This matrix is used for comparison:

# This is the matrix extracted from dcraw
rgb_to_xyz_5d_dcraw = [
    [.84582686764400, -0.124656064433,  0.2292851967890], 
    [0.4151331895030,  0.668390503440, -0.0835236929433], 
    [0.0693495723842, -0.222697019629,  1.2421014472500]
]

However it doesn't match the Adobe matrix from DCRAW or LibRaw.

[[0.4716,  0.0603, -0.083 ],
 [0.7798,  1.5474,  0.248 ],
 [-0.1496,  0.1937,  0.6651]]

Is there a simple explanation for how to prepare them? Or an alternative method? The annotated code hasn't helped much, and it seems they're obfuscated. Any help would be appreciated.

hotgluebanjo commented 1 year ago

@KelSolaar if you have time or any suggestions; don't know if this repo is monitored much.

hotgluebanjo commented 1 year ago

Never mind, got it.

# Usage of Adobe DNG ColorMatrix2 matrices from DCRAW.
# - https://www.cloudynights.com/topic/529426-dslr-processing-the-missing-matrix/?p=7088475
# - https://www.dpreview.com/forums/thread/3973636#forum-post-57370152
# - https://ninedegreesbelow.com/files/dcraw-c-code-annotated-code.html#E

import colour
import numpy as np

example_matrix = [
    [4716, 603, -830],
    [-7798, 15474, 2480],
    [-1496, 1937, 6651],
]

# ColorMatrix2 is XYZ to camera native.
colormatrix2 = np.asarray(example_matrix, dtype=float)
colormatrix2 /= 10_000

bt709_to_xyz = colour.RGB_COLOURSPACES["ITU-R BT.709"].matrix_RGB_to_XYZ

bt709_to_camnat = np.matmul(colormatrix2, bt709_to_xyz)

# Scale so all rows sum to one. This does not change ratios
# between each coefficient; only scales all three. The key and
# weird bit is that it's on BT.709.
bt709_to_camnat[0] = bt709_to_camnat[0] / np.sum(bt709_to_camnat[0])
bt709_to_camnat[1] = bt709_to_camnat[1] / np.sum(bt709_to_camnat[1])
bt709_to_camnat[2] = bt709_to_camnat[2] / np.sum(bt709_to_camnat[2])

# Comparable to DXOMark matrices.
camnat_to_bt709 = np.linalg.inv(bt709_to_camnat)

# Finally.
camnat_to_xyz = np.matmul(bt709_to_xyz, camnat_to_bt709)

print("Camera to BT.709:")
print(camnat_to_bt709)
print("Camera to XYZ:")
print(camnat_to_xyz)

Matrices match

dcraw -4 -T -o 0

to -o 1 and -o 5.

The output somewhat matches the PhysLight example, with slight differences. I guess there needs to be some kind of adaptation.

[[ 0.84577580328554 -0.12466479613679  0.22934491990293]
 [ 0.41510812703395  0.66843732176224 -0.08354544879619]
 [ 0.06934538560373 -0.22271261874517  1.24242498390132]]
KelSolaar commented 1 year ago

Hey @hotgluebanjo,

Glad you figured it out! We should probably include the derivation for future code archeologists!

Cheers,

Thomas

hotgluebanjo commented 1 year ago

Hey Thomas,

That would be great! Might be interesting to compare.

A related question I've been meaning to ask: I happened to find a comment of yours about manufacturer camera matrices and was wondering about this statement:

They can be derived with some work or obtained with a NDA

What kind of derivation might that be? If a manufacturer's SDK or software has a "Camera RGB" debayer option, it's a pretty straightforward solve. Is it possible without that kind of option?

I guess accuracy wouldn't be possible without a baseline (I only ask because of the wonderous existence of this), but perhaps approximating might help to remove the bogus values these matrices sometimes introduce -- almost as a camera-specific gamut mapping solution, from which a more complex response could be formed.

My casual idea was making a diffraction grating setup and then solving from there. I know this is a really specific and strange scenario. :D