Home | History | Annotate | Download | only in its
      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 
     15 import numpy
     16 import numpy.linalg
     17 import unittest
     18 
     19 # Illuminant IDs
     20 A = 0
     21 D65 = 1
     22 
     23 def 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 
    117 def 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 
    167 class __UnitTest(unittest.TestCase):
    168     """Run a suite of unit tests on this module.
    169     """
    170     # TODO: Add more unit tests.
    171 
    172 if __name__ == '__main__':
    173     unittest.main()
    174 
    175