| # Copyright 2014 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import numpy |
| import numpy.linalg |
| import unittest |
| |
| # Illuminant IDs |
| A = 0 |
| D65 = 1 |
| |
| def compute_cm_fm(illuminant, gains, ccm, cal): |
| """Compute the ColorMatrix (CM) and ForwardMatrix (FM). |
| |
| Given a captured shot of a grey chart illuminated by either a D65 or a |
| standard A illuminant, the HAL will produce the WB gains and transform, |
| in the android.colorCorrection.gains and android.colorCorrection.transform |
| tags respectively. These values have both golden module and per-unit |
| calibration baked in. |
| |
| This function is used to take the per-unit gains, ccm, and calibration |
| matrix, and compute the values that the DNG ColorMatrix and ForwardMatrix |
| for the specified illuminant should be. These CM and FM values should be |
| the same for all DNG files captured by all units of the same model (e.g. |
| all Nexus 5 units). The calibration matrix should be the same for all DNGs |
| saved by the same unit, but will differ unit-to-unit. |
| |
| Args: |
| illuminant: 0 (A) or 1 (D65). |
| gains: White balance gains, as a list of 4 floats. |
| ccm: White balance transform matrix, as a list of 9 floats. |
| cal: Per-unit calibration matrix, as a list of 9 floats. |
| |
| Returns: |
| CM: The 3x3 ColorMatrix for the specified illuminant, as a numpy array |
| FM: The 3x3 ForwardMatrix for the specified illuminant, as a numpy array |
| """ |
| |
| ########################################################################### |
| # Standard matrices. |
| |
| # W is the matrix that maps sRGB to XYZ. |
| # See: http://www.brucelindbloom.com/ |
| W = numpy.array([ |
| [ 0.4124564, 0.3575761, 0.1804375], |
| [ 0.2126729, 0.7151522, 0.0721750], |
| [ 0.0193339, 0.1191920, 0.9503041]]) |
| |
| # HH is the chromatic adaptation matrix from D65 (since sRGB's ref white is |
| # D65) to D50 (since CIE XYZ's ref white is D50). |
| HH = numpy.array([ |
| [ 1.0478112, 0.0228866, -0.0501270], |
| [ 0.0295424, 0.9904844, -0.0170491], |
| [-0.0092345, 0.0150436, 0.7521316]]) |
| |
| # H is a chromatic adaptation matrix from D65 (because sRGB's reference |
| # white is D65) to the calibration illuminant (which is a standard matrix |
| # depending on the illuminant). For a D65 illuminant, the matrix is the |
| # identity. For the A illuminant, the matrix uses the linear Bradford |
| # adaptation method to map from D65 to A. |
| # See: http://www.brucelindbloom.com/ |
| H_D65 = numpy.array([ |
| [ 1.0, 0.0, 0.0], |
| [ 0.0, 1.0, 0.0], |
| [ 0.0, 0.0, 1.0]]) |
| H_A = numpy.array([ |
| [ 1.2164557, 0.1109905, -0.1549325], |
| [ 0.1533326, 0.9152313, -0.0559953], |
| [-0.0239469, 0.0358984, 0.3147529]]) |
| H = [H_A, H_D65][illuminant] |
| |
| ########################################################################### |
| # Per-model matrices (that should be the same for all units of a particular |
| # phone/camera. These are statics in the HAL camera properties. |
| |
| # G is formed by taking the r,g,b gains and putting them into a |
| # diagonal matrix. |
| G = numpy.array([[gains[0],0,0], [0,gains[1],0], [0,0,gains[3]]]) |
| |
| # S is just the CCM. |
| S = numpy.array([ccm[0:3], ccm[3:6], ccm[6:9]]) |
| |
| ########################################################################### |
| # Per-unit matrices. |
| |
| # The per-unit calibration matrix for the given illuminant. |
| CC = numpy.array([cal[0:3],cal[3:6],cal[6:9]]) |
| |
| ########################################################################### |
| # Derived matrices. These should match up with DNG-related matrices |
| # provided by the HAL. |
| |
| # The color matrix and forward matrix are computed as follows: |
| # CM = inv(H * W * S * G * CC) |
| # FM = HH * W * S |
| CM = numpy.linalg.inv( |
| numpy.dot(numpy.dot(numpy.dot(numpy.dot(H, W), S), G), CC)) |
| FM = numpy.dot(numpy.dot(HH, W), S) |
| |
| # The color matrix is normalized so that it maps the D50 (PCS) white |
| # point to a maximum component value of 1. |
| CM = CM / max(numpy.dot(CM, (0.9642957, 1.0, 0.8251046))) |
| |
| return CM, FM |
| |
| def compute_asn(illuminant, cal, CM): |
| """Compute the AsShotNeutral DNG value. |
| |
| This value is the only dynamic DNG value; the ForwardMatrix, ColorMatrix, |
| and CalibrationMatrix values should be the same for every DNG saved by |
| a given unit. The AsShotNeutral depends on the scene white balance |
| estimate. |
| |
| This function computes what the DNG AsShotNeutral values should be, for |
| a given ColorMatrix (which is computed from the WB gains and CCM for a |
| shot taken of a grey chart under either A or D65 illuminants) and the |
| per-unit calibration matrix. |
| |
| Args: |
| illuminant: 0 (A) or 1 (D65). |
| cal: Per-unit calibration matrix, as a list of 9 floats. |
| CM: The computed 3x3 ColorMatrix for the illuminant, as a numpy array. |
| |
| Returns: |
| ASN: The AsShotNeutral value, as a length-3 numpy array. |
| """ |
| |
| ########################################################################### |
| # Standard matrices. |
| |
| # XYZCAL is the XYZ coordinate of calibration illuminant (so A or D65). |
| # See: Wyszecki & Stiles, "Color Science", second edition. |
| XYZCAL_A = numpy.array([1.098675, 1.0, 0.355916]) |
| XYZCAL_D65 = numpy.array([0.950456, 1.0, 1.089058]) |
| XYZCAL = [XYZCAL_A, XYZCAL_D65][illuminant] |
| |
| ########################################################################### |
| # Per-unit matrices. |
| |
| # The per-unit calibration matrix for the given illuminant. |
| CC = numpy.array([cal[0:3],cal[3:6],cal[6:9]]) |
| |
| ########################################################################### |
| # Derived matrices. |
| |
| # The AsShotNeutral value is then the product of this final color matrix |
| # with the XYZ coordinate of calibration illuminant. |
| # ASN = CC * CM * XYZCAL |
| ASN = numpy.dot(numpy.dot(CC, CM), XYZCAL) |
| |
| # Normalize so the max vector element is 1.0. |
| ASN = ASN / max(ASN) |
| |
| return ASN |
| |
| class __UnitTest(unittest.TestCase): |
| """Run a suite of unit tests on this module. |
| """ |
| # TODO: Add more unit tests. |
| |
| if __name__ == '__main__': |
| unittest.main() |
| |