# 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()