1# Copyright 2014 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import numpy
16import numpy.linalg
17import unittest
18
19# Illuminant IDs
20A = 0
21D65 = 1
22
23def compute_cm_fm(illuminant, gains, ccm, cal):
24    """Compute the ColorMatrix (CM) and ForwardMatrix (FM).
25
26    Given a captured shot of a grey chart illuminated by either a D65 or a
27    standard A illuminant, the HAL will produce the WB gains and transform,
28    in the android.colorCorrection.gains and android.colorCorrection.transform
29    tags respectively. These values have both golden module and per-unit
30    calibration baked in.
31
32    This function is used to take the per-unit gains, ccm, and calibration
33    matrix, and compute the values that the DNG ColorMatrix and ForwardMatrix
34    for the specified illuminant should be. These CM and FM values should be
35    the same for all DNG files captured by all units of the same model (e.g.
36    all Nexus 5 units). The calibration matrix should be the same for all DNGs
37    saved by the same unit, but will differ unit-to-unit.
38
39    Args:
40        illuminant: 0 (A) or 1 (D65).
41        gains: White balance gains, as a list of 4 floats.
42        ccm: White balance transform matrix, as a list of 9 floats.
43        cal: Per-unit calibration matrix, as a list of 9 floats.
44
45    Returns:
46        CM: The 3x3 ColorMatrix for the specified illuminant, as a numpy array
47        FM: The 3x3 ForwardMatrix for the specified illuminant, as a numpy array
48    """
49
50    ###########################################################################
51    # Standard matrices.
52
53    # W is the matrix that maps sRGB to XYZ.
54    # See: http://www.brucelindbloom.com/
55    W = numpy.array([
56        [ 0.4124564,  0.3575761,  0.1804375],
57        [ 0.2126729,  0.7151522,  0.0721750],
58        [ 0.0193339,  0.1191920,  0.9503041]])
59
60    # HH is the chromatic adaptation matrix from D65 (since sRGB's ref white is
61    # D65) to D50 (since CIE XYZ's ref white is D50).
62    HH = numpy.array([
63        [ 1.0478112,  0.0228866, -0.0501270],
64        [ 0.0295424,  0.9904844, -0.0170491],
65        [-0.0092345,  0.0150436,  0.7521316]])
66
67    # H is a chromatic adaptation matrix from D65 (because sRGB's reference
68    # white is D65) to the calibration illuminant (which is a standard matrix
69    # depending on the illuminant). For a D65 illuminant, the matrix is the
70    # identity. For the A illuminant, the matrix uses the linear Bradford
71    # adaptation method to map from D65 to A.
72    # See: http://www.brucelindbloom.com/
73    H_D65 = numpy.array([
74        [ 1.0,        0.0,        0.0],
75        [ 0.0,        1.0,        0.0],
76        [ 0.0,        0.0,        1.0]])
77    H_A = numpy.array([
78        [ 1.2164557,  0.1109905, -0.1549325],
79        [ 0.1533326,  0.9152313, -0.0559953],
80        [-0.0239469,  0.0358984,  0.3147529]])
81    H = [H_A, H_D65][illuminant]
82
83    ###########################################################################
84    # Per-model matrices (that should be the same for all units of a particular
85    # phone/camera. These are statics in the HAL camera properties.
86
87    # G is formed by taking the r,g,b gains and putting them into a
88    # diagonal matrix.
89    G = numpy.array([[gains[0],0,0], [0,gains[1],0], [0,0,gains[3]]])
90
91    # S is just the CCM.
92    S = numpy.array([ccm[0:3], ccm[3:6], ccm[6:9]])
93
94    ###########################################################################
95    # Per-unit matrices.
96
97    # The per-unit calibration matrix for the given illuminant.
98    CC = numpy.array([cal[0:3],cal[3:6],cal[6:9]])
99
100    ###########################################################################
101    # Derived matrices. These should match up with DNG-related matrices
102    # provided by the HAL.
103
104    # The color matrix and forward matrix are computed as follows:
105    #   CM = inv(H * W * S * G * CC)
106    #   FM = HH * W * S
107    CM = numpy.linalg.inv(
108            numpy.dot(numpy.dot(numpy.dot(numpy.dot(H, W), S), G), CC))
109    FM = numpy.dot(numpy.dot(HH, W), S)
110
111    # The color matrix is normalized so that it maps the D50 (PCS) white
112    # point to a maximum component value of 1.
113    CM = CM / max(numpy.dot(CM, (0.9642957, 1.0, 0.8251046)))
114
115    return CM, FM
116
117def compute_asn(illuminant, cal, CM):
118    """Compute the AsShotNeutral DNG value.
119
120    This value is the only dynamic DNG value; the ForwardMatrix, ColorMatrix,
121    and CalibrationMatrix values should be the same for every DNG saved by
122    a given unit. The AsShotNeutral depends on the scene white balance
123    estimate.
124
125    This function computes what the DNG AsShotNeutral values should be, for
126    a given ColorMatrix (which is computed from the WB gains and CCM for a
127    shot taken of a grey chart under either A or D65 illuminants) and the
128    per-unit calibration matrix.
129
130    Args:
131        illuminant: 0 (A) or 1 (D65).
132        cal: Per-unit calibration matrix, as a list of 9 floats.
133        CM: The computed 3x3 ColorMatrix for the illuminant, as a numpy array.
134
135    Returns:
136        ASN: The AsShotNeutral value, as a length-3 numpy array.
137    """
138
139    ###########################################################################
140    # Standard matrices.
141
142    # XYZCAL is the  XYZ coordinate of calibration illuminant (so A or D65).
143    # See: Wyszecki & Stiles, "Color Science", second edition.
144    XYZCAL_A = numpy.array([1.098675, 1.0, 0.355916])
145    XYZCAL_D65 = numpy.array([0.950456, 1.0, 1.089058])
146    XYZCAL = [XYZCAL_A, XYZCAL_D65][illuminant]
147
148    ###########################################################################
149    # Per-unit matrices.
150
151    # The per-unit calibration matrix for the given illuminant.
152    CC = numpy.array([cal[0:3],cal[3:6],cal[6:9]])
153
154    ###########################################################################
155    # Derived matrices.
156
157    # The AsShotNeutral value is then the product of this final color matrix
158    # with the XYZ coordinate of calibration illuminant.
159    #   ASN = CC * CM * XYZCAL
160    ASN = numpy.dot(numpy.dot(CC, CM), XYZCAL)
161
162    # Normalize so the max vector element is 1.0.
163    ASN = ASN / max(ASN)
164
165    return ASN
166
167class __UnitTest(unittest.TestCase):
168    """Run a suite of unit tests on this module.
169    """
170    # TODO: Add more unit tests.
171
172if __name__ == '__main__':
173    unittest.main()
174
175