% Transformation of camera sensor's brightness values into sRGB values
% according to Adobe Digital Negative (DNG) Specification (version 1.4.0.0, 2012)
% (https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf)
% both possible methods from the specification are implemented:
% using "Forward Matrices" and using "Color Matrices".
%
% Input variables:
% one parameter of the photo ("As Shot Neutral" or "As Shot White xy"),
% several parameters of the camera (including parameters of individual camera specimen),
% several settings of the script,
% test image and its darkness and saturation levels.
%
% This script needs the function xy2cct.m from OPTPROP toolbox by Jerker Wagberg
% (https://www.mathworks.com/matlabcentral/fileexchange/13788-optprop-a-color-properties-toolbox)
% and the script bradford.m by Marcel Patek (http://www.marcelpatek.com/color.html),
% converted into a function (with DXX as input and Mbfd_nl as output).
%
% This script contains matrices of conversion between sRGB and XYZ using reference white D50
% from Bruce Lindbloom's site: http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html.
%
% Shortcomings: the script works only with cameras, whose color space has dimensionality 3
% (i.e., camera has no differences between G1 and G2). Generalization is welcomed.
%
% Indices in the names of variables:
% "_m1" (method 1): using "Forward Matrices"; "_m2" (method 2): using "Color Matrices",
% "_clean": matrices for images, already corrected for
% white balance, color sensitivity of individual camera and "analog balance";
% "_norm": matrices, normalized to convert corrected sensor values [1;1;1] into sRGB [1;1;1].
%
% Source: https://sites.google.com/site/crossstereo/raw-converting/dng
clear all
close all
clc
% parameters of the photo:
% (only one of these parameters is given for each camera;
% if you have "As Shot Neutral", assign as_shot_switch = 1, if "As Shot White xy" - 2):
as_shot_switch = 1;
switch as_shot_switch
case 1
% "As Shot Neutral" (selected white balance at time of capture;
% coordinates of a perfectly neutral color in linear reference space values):
as_shot_neutral = [0.493256 1 0.535565];
case 2
% "As Shot White xy" (selected white balance at time of capture;
% x-y chromaticity coordinates):
as_shot_white_xy = [0.384296 0.396109];
end
% settings of the script:
% ignore "Camera Calibration" matrices (turned out to be necessary for Sony NEX-5):
ignore_cc = 1;
% allow extrapolation of matrices, if
% color temperature of the illumination is outside corr_col_temp range:
% (DNG specification prescribes not extrapolation, but taking of closest variant)
allow_extr = 0;
% for testing: replace white-balance-expressing and related variables
% with values, corresponding to the lighting by calibration illuminant 1 or 2:
% 0 - no, 1 - to the illuminant 1, 2 - to the illuminant 2:
test_illum = 0;
% for testing: normalize brightnesses of white-balance-expressing variables to 1:
% (this option doesn't affect matrices for images with already applied corrections
% for white balance, color sensitivity of individual camera and similar things.
% For other images (with original colors), it is harmful, expressing in
% increase of discrepancy between matrices, obtained with and without ForwardMatrix.)
normalize_wb_vars = 0;
% test image (mosaiced or RGB) with original raw colors:
im = imread('D:\Tech\преобразование RAW\тестовые картинки\2018-02-09 white\DSC09242.tiff');
% [dark_brightness saturation_brightness]:
dark_sat = [128 4090]; % values for Sony NEX-5
% cropping:
im = im(701:1000, 1701:2000, :);
% parameters of the camera:
% "Analog Balance":
ab = [1 0 0; 0 1 0; 0 0 1];
% temperatures of illuminants 1 and 2:
corr_col_temp = [2856 6504];
% xy of illuminants 1 and 2:
xy_illum_1 = [0.44758 0.40745];
xy_illum_2 = [0.31271 0.32902];
% "Color Matrix" 1 and 2:
% (XYZ values -> reference camera native space values)
% (under illuminants 1 and 2)
cm_1 = [0.671 -0.1934 -0.0172; -0.4303 1.1507 0.3202; -0.0527 0.1307 0.7431];
cm_2 = [0.6549 -0.155 -0.0436; -0.488 1.2435 0.2753; -0.0854 0.1868 0.6976];
% "Camera Calibration" 1 and 2:
% (reference camera native space values -> individual camera native space values)
% (under illuminants 1 and 2)
cc_1 = [0.9415 0 0; 0 1 0; 0 0 1.0057];
cc_2 = [0.9415 0 0; 0 1 0; 0 0 1.0057];
% "Forward Matrix" 1 and 2 (if absent, assign forward_matrix_exists = 0):
% (white balanced reference camera colors -> XYZ D50 colors)
% (under illuminants 1 and 2)
forward_matrix_exists = 1;
if forward_matrix_exists
fm_1 = [0.7587 0.0956 0.11; 0.2653 0.8476 -0.113; 0.0286 -0.3588 1.1554];
fm_2 = [0.7467 0.2686 -0.051; 0.2927 1.0103 -0.303; 0.0074 -0.1697 0.9874];
end
% "Camera Calibration Signature":
cc_sig = 'com.adobe';
% "Profile Calibration Signature":
pc_sig = 'com.adobe';
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% end of settings
% preprocessing of the test image:
im = double(im);
% converting of brightness to [0 - 1] interval:
im = (im - dark_sat(1)) / (dark_sat(2) - dark_sat(1));
im(im<0) = 0;
im(im>1) = 1;
if size(im, 3) == 1
% demosaicing:
im = uint16(65535*im);
im = demosaic(im, 'rggb');
im = double(im)/65535;
end
% function of image transformation according to a 3x3 matrix:
img_trans_mat = @(img, mat) [cat(3, ...
mat(1,1) * img(:,:,1) + mat(1,2) * img(:,:,2) + mat(1,3) * img(:,:,3), ...
mat(2,1) * img(:,:,1) + mat(2,2) * img(:,:,2) + mat(2,3) * img(:,:,3), ...
mat(3,1) * img(:,:,1) + mat(3,2) * img(:,:,2) + mat(3,3) * img(:,:,3))];
% translation of sRGB (based on D65) and XYZ with "Reference white" D50, as DNG algorithm needs:
% (from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html)
xyz2rgb = [...
[ 3.1338561 -1.6168667 -0.4906146];
[-0.9787684 1.9161415 0.0334540];
[ 0.0719453 -0.2289914 1.4052427]];
rgb2xyz = [...
[ 0.4360747 0.3850649 0.1430804];
[ 0.2225045 0.7168786 0.0606169];
[ 0.0139322 0.0971045 0.7141733]];
if ignore_cc
disp(['camera calibration matrices are ignored']);
disp([' ']);
% replacing camera calibration matrices with identity matrices:
cc_1 = [1 0 0; 0 1 0; 0 0 1];
cc_2 = [1 0 0; 0 1 0; 0 0 1];
end
% comparison of camera calibration signature and profile calibration signature:
% (according to p.79 of DNG specification)
if ~isequal(cc_sig, pc_sig) && ~ignore_cc
disp(['camera calibration signature differs from profile calibration signature;']);
disp([' camera calibration matrices are not applied']);
disp([' ']);
% replacing camera calibration matrices with identity matrices:
cc_1 = [1 0 0; 0 1 0; 0 0 1];
cc_2 = [1 0 0; 0 1 0; 0 0 1];
end
switch as_shot_switch
case 1 % if "As Shot Neutral" exists and "As Shot White xy" - no:
as_shot_neutral = as_shot_neutral';
% iterative "translating camera neutral coordinates to white balance xy coordinates"
% with determination of correlated color temperature and interpolating of matrices
% (search of correlated color temperature, at which
% interpolated matrices convert "As Shot Neutral" to xy values,
% which correspond to this corr. col. temperature)
disp('search of correlated color temperature of the illumination and other parameters...');
corr_col_temp_curr = mean(corr_col_temp); % initial value for iterations
% (100 iterations is more than enough)
for s = 1 : 100
coeff_for_interp = (...
(1/corr_col_temp_curr - 1/corr_col_temp(1)) /...
(1/corr_col_temp(2) - 1/corr_col_temp(1)));
% current interpolated matrices:
cc_curr = cc_1 + (cc_2 - cc_1) * coeff_for_interp;
cm_curr = cm_1 + (cm_2 - cm_1) * coeff_for_interp;
if forward_matrix_exists
fm_curr = fm_1 + (fm_2 - fm_1) * coeff_for_interp;
end
% matrix of translation of XYZ values to color space of the given specimen of camera
% at the current trial corr. color temperature:
XYZ_to_camera_curr = ab * cc_curr * cm_curr;
% response of the given specimen of camera to neutral color:
camera_neutral = cc_curr * as_shot_neutral;
if normalize_wb_vars
camera_neutral = camera_neutral / camera_neutral(2); % normalizing to 1 for G
end
% in XYZ system:
XYZ_curr = (XYZ_to_camera_curr^(-1)) * camera_neutral;
if normalize_wb_vars
XYZ_curr = XYZ_curr / XYZ_curr(2); % normalizing to 1 for Y
end
% white balance xy coordinates:
xy = [XYZ_curr(1) XYZ_curr(2)] / sum(XYZ_curr);
disp([' iteration ' num2str(s) blanks(3-length(num2str(s)))...
': coeff. for interp.: ' num2str(coeff_for_interp)...
', corr. color temperature: ' num2str(corr_col_temp_curr, '%.2f')]);
% new value of the correlated color temperature:
corr_col_temp_curr = xy2cct(xy);
end
if ~allow_extr % if extrapolation of matrices is disallowed (following DNG specification):
% if the color temperature falls outside the interval "corr_col_temp(1)...corr_col_temp(2)",
% extrapolated value is replaced by closest base value:
if corr_col_temp_curr < corr_col_temp(1) || corr_col_temp_curr > corr_col_temp(2)
disp(['found corr.col.temp (' num2str(corr_col_temp_curr, '%.2f')...
') is outside corr_col_temp range '...
num2str(corr_col_temp(1)) ' - ' num2str(corr_col_temp(2)) '; closest value is taken']);
if corr_col_temp_curr < corr_col_temp(1)
corr_col_temp_curr = corr_col_temp(1);
end
if corr_col_temp_curr > corr_col_temp(2)
corr_col_temp_curr = corr_col_temp(2);
end
end
% recalculating of interpolation-dependent variables:
coeff_for_interp = (...
(1/corr_col_temp_curr - 1/corr_col_temp(1)) /...
(1/corr_col_temp(2) - 1/corr_col_temp(1)));
cc_curr = cc_1 + (cc_2 - cc_1) * coeff_for_interp;
cm_curr = cm_1 + (cm_2 - cm_1) * coeff_for_interp;
if forward_matrix_exists
fm_curr = fm_1 + (fm_2 - fm_1) * coeff_for_interp;
end
XYZ_to_camera_curr = ab * cc_curr * cm_curr;
camera_neutral = cc_curr * as_shot_neutral;
XYZ_curr = (XYZ_to_camera_curr^(-1)) * camera_neutral;
if normalize_wb_vars
XYZ_curr = XYZ_curr / XYZ_curr(2); % normalizing to 1 for Y
end
xy = [XYZ_curr(1) XYZ_curr(2)] / sum(XYZ_curr);
end
disp(' ');
case 2 % if "As Shot White xy" exists and "As Shot Neutral" - no:
% correlated color temperature:
xy = as_shot_white_xy;
corr_col_temp_curr = xy2cct(xy);
if ~allow_extr % if extrapolation of matrices is disallowed (following DNG specification):
% if the color temperature falls outside the interval "corr_col_temp(1)...corr_col_temp(2)",
% extrapolated value is replaced by closest base value:
if corr_col_temp_curr < corr_col_temp(1) || corr_col_temp_curr > corr_col_temp(2)
disp(['found corr.col.temp (' num2str(corr_col_temp_curr, '%.2f')...
') is outside corr_col_temp range '...
num2str(corr_col_temp(1)) ' - ' num2str(corr_col_temp(2)) '; closest value is taken']);
if corr_col_temp_curr < corr_col_temp(1)
corr_col_temp_curr = corr_col_temp(1);
end
if corr_col_temp_curr > corr_col_temp(2)
corr_col_temp_curr = corr_col_temp(2);
end
end
end
coeff_for_interp = (...
(1/corr_col_temp_curr - 1/corr_col_temp(1)) /...
(1/corr_col_temp(2) - 1/corr_col_temp(1)));
% current interpolated matrices:
cc_curr = cc_1 + (cc_2 - cc_1) * coeff_for_interp;
cm_curr = cm_1 + (cm_2 - cm_1) * coeff_for_interp;
if forward_matrix_exists
fm_curr = fm_1 + (fm_2 - fm_1) * coeff_for_interp;
end
% matrix of translation of XYZ values to the color space of given specimen of the camera:
XYZ_to_camera_curr = ab * cc_curr * cm_curr;
% XYZ of neutral color:
XYZ_curr = [xy(1)/xy(2); 1; (1 - xy(1) - xy(2)) / xy(2)];
% response of the given specimen of camera to neutral color:
camera_neutral = XYZ_to_camera_curr * XYZ_curr;
if normalize_wb_vars
camera_neutral = camera_neutral / camera_neutral(2); % normalizing to 1 for G
end
end
disp(['white balance correlated color temperature: ' num2str(corr_col_temp_curr, '%.2f')]);
disp(['white balance xy coordinates: ' num2str(xy, ' %.6f')]);
disp(['white balance XYZ: ' num2str(XYZ_curr', ' %.6f')]);
disp(['white balance: response of the given camera: ' num2str(camera_neutral', ' %.6f')]);
% for testing: make "as_shot_neutral" and related variables
% to correspond to the lighting by the illuminant number test_illum:
% (Sony NEX-5 has illum.1 = "Standard Light A" and illum.2 = "D65")
if test_illum
disp(' ');
switch test_illum
case 1
cc_curr = cc_1;
cm_curr = cm_1;
if forward_matrix_exists
fm_curr = fm_1;
end
xy = xy_illum_1;
corr_col_temp_curr = corr_col_temp(1);
case 2
cc_curr = cc_2;
cm_curr = cm_2;
if forward_matrix_exists
fm_curr = fm_2;
end
xy = xy_illum_2;
corr_col_temp_curr = corr_col_temp(2);
end
disp([' test work: as in the case of illuminant ' num2str(test_illum) ':']);
XYZ_to_camera_curr = ab * cc_curr * cm_curr;
XYZ_curr = [xy(1)/xy(2); 1; (1 - xy(1) - xy(2)) / xy(2)];
% response of the given specimen of camera to neutral color:
camera_neutral = XYZ_to_camera_curr * XYZ_curr;
if normalize_wb_vars
camera_neutral = camera_neutral / camera_neutral(2); % normalizing to 1 for G
end
% response of the reference camera to neutral color:
as_shot_neutral = (cc_curr^(-1)) * camera_neutral;
disp([' new values of parameters:']);
disp(['white balance correlated color temperature: ' num2str(corr_col_temp_curr, '%.2f')]);
disp(['white balance xy coordinates: ' num2str(xy, ' %.6f')]);
disp(['white balance XYZ: ' num2str(XYZ_curr', ' %.6f')]);
disp(['white balance: response of the given camera: ' num2str(camera_neutral', ' %.6f')]);
clear x t cc_1 cc_2 cm_1 cm_2 fm_1 fm_2 corr_col_temp
end
disp(' ');
% reference_neutral: response of the reference camera to neutral color,
% camera_neutral: response of the given camera specimen to neutral color
reference_neutral = ((ab * cc_curr)^(-1)) * camera_neutral;
reference_neutral = [reference_neutral(1) 0 0; 0 reference_neutral(2) 0; 0 0 reference_neutral(3)];
if forward_matrix_exists
% matrix "xyz2rgb * fm_curr" translates white balanced camera colors to sRGB;
% so, it must translate [1 1 1] into [1 1 1], hence sum of each its row must be 1:
disp(['quality test of intepolated forward matrix: following numbers must be close to 1:']);
disp([' ' num2str(sum(xyz2rgb * fm_curr, 2)', ' %.6f')]);
disp(' ');
disp('===================== using ForwardMatrix ("ForwardMatrix included"), "_m1"')
disp([' (basing on camera_neutral = [' num2str(camera_neutral', ' %.6f') '])']);
% (1st method)
% recommended method, applicable "if the ForwardMatrix tags are included in the camera profile"
%
d_matrix = reference_neutral^(-1);
% matrix for translation of individual camera's sensor values into XYZ_D50:
camera_to_XYZ_D50_m1 = fm_curr * d_matrix * ((ab * cc_curr)^(-1));
% the matrix "camera_to_XYZ_D50_m1", determined as above,
% translates response of a given camera specimen to neutral color
% into XYZ of illuminant D50: [0.964220; 1.000000; 0.825210]
disp(['quality test of "camera_to_XYZ_D50_m1" matrix: following numbers must be close to 0:']);
disp([' ' num2str([(camera_to_XYZ_D50_m1*(camera_neutral))' - [0.964220 1 0.825210]], ' %.6f')]);
disp(' ');
% matrix for translating individual camera's colors into sRGB (first method):
cam2rgb_m1 = xyz2rgb * camera_to_XYZ_D50_m1;
% ("camera_to_XYZ_D50_m1" differs from "fm_curr"
% by presence of corrections for white balance and for color sensitivity of individual camera specimen.
% If these corrections were applied to the image beforehand,
% matrix for such image will use fm_curr):
cam2rgb_m1_clean = xyz2rgb * fm_curr;
% normalizing sum of each row to 1:
cam2rgb_m1_clean_norm = cam2rgb_m1_clean ./ repmat(sum(cam2rgb_m1_clean, 2), [1 3]);
% displaying matrices:
disp(' camera_to_XYZ_D50_m1: cam2rgb_m1:');
for z = 1 : size(camera_to_XYZ_D50_m1, 1)
disp([...
sprintf(' %9.6f', camera_to_XYZ_D50_m1(z, :)) ' '...
sprintf(' %9.6f', cam2rgb_m1(z, :))]);
end
disp(' ');
disp(' camera_to_XYZ_D50_m1_clean: cam2rgb_m1_clean: cam2rgb_m1_clean_norm:');
disp(' ("Forward Matrix")');
for z = 1 : size(camera_to_XYZ_D50_m1, 1)
disp([...
sprintf(' %9.6f', fm_curr(z, :)) ' '...
sprintf(' %9.6f', cam2rgb_m1_clean(z, :)) ' '...
sprintf(' %9.6f', cam2rgb_m1_clean_norm(z, :))]);
end
disp([' sum of rows of cam2rgb_m1_clean (must be [1 1 1]): '...
num2str(sum(cam2rgb_m1_clean, 2)', ' %.6f')]);
disp([' sum of rows of cam2rgb_m1_clean_norm (must be [1 1 1]): '...
num2str(sum(cam2rgb_m1_clean_norm, 2)', ' %.6f')]);
disp(' ');
% transformation of the test image with matrix from the first method:
im_m1 = im;
im_m1 = img_trans_mat(im_m1, cam2rgb_m1);
im_m1(im_m1<0) = 0;
% gamma correction and turning into 3*8-bit image:
im_m1 = im_m1.^(1/2.2);
im_m1 = uint8(255*im_m1);
end
disp('===================== not using ForwardMatrix ("ForwardMatrix not included"), "_m2"');
disp([' (basing on white balance XYZ = [' num2str(XYZ_curr', ' %.6f') '])']);
% (2nd method)
% "If the ForwardMatrix tags are not included in the camera profile:"
% matrix for translation of individual camera's sensor values into XYZ:
camera_to_XYZ_m2 = XYZ_to_camera_curr^(-1);
% matrix of chromatic adaptation:
% (translates "white balance xy value" into XYZ with illuminant D50: [0.964220; 1.000000; 0.825210]):
chrom_adapt_matrix_m2 = bradford(100*XYZ_curr');
disp(['quality test of "chrom_adapt_matrix_m2": following numbers must be close to 0:']);
disp([' ' num2str([(chrom_adapt_matrix_m2*XYZ_curr)' - [0.964220 1 0.825210]], ' %.6f')]);
disp(' ');
% matrix for translation of individual camera's sensor values into XYZ_D50:
camera_to_XYZ_D50_m2 = chrom_adapt_matrix_m2 * camera_to_XYZ_m2;
% matrix for translating individual camera's colors into sRGB:
cam2rgb_m2 = xyz2rgb * camera_to_XYZ_D50_m2;
% if corrections for white balance and for color sensitivity of individual camera specimen
% were applied beforehand,
% cam2rgb_m2_clean = cam2rgb_m2 * reference_neutral
% = xyz2rgb * bradford(100*XYZ_curr') * (cm_curr^(-1)) * reference_neutral
cam2rgb_m2_clean = xyz2rgb * bradford(100*XYZ_curr') * (cm_curr^(-1)) * reference_neutral;
% unlike cam2rgb_m2, this matrix can be and should be normalized for converting [1;1;1] into [1;1;1]:
% (sum of each row is normalized to 1):
cam2rgb_m2_clean_norm = cam2rgb_m2_clean ./ repmat(sum(cam2rgb_m2_clean, 2), [1 3]);
cam2rgb_m2_norm = cam2rgb_m2_clean_norm * reference_neutral^(-1);
% displaying matrices:
disp(' camera_to_XYZ_D50_m2: cam2rgb_m2: cam2rgb_m2_norm:');
for z = 1 : size(camera_to_XYZ_D50_m2, 1)
disp([...
sprintf(' %9.6f', camera_to_XYZ_D50_m2(z, :)) ' '...
sprintf(' %9.6f', cam2rgb_m2(z, :)) ' '...
sprintf(' %9.6f', cam2rgb_m2_norm(z, :)) ' '...
]);
end
disp(' ');
disp(' cam2rgb_m2_clean: cam2rgb_m2_clean_norm:');
for z = 1 : size(camera_to_XYZ_D50_m1, 1)
disp([...
blanks(30) ' '...
sprintf(' %9.6f', cam2rgb_m2_clean(z, :)) ' '...
sprintf(' %9.6f', cam2rgb_m2_clean_norm(z, :))]);
end
disp([' sum of rows of cam2rgb_m2_clean (must be [1 1 1]): '...
num2str(sum(cam2rgb_m2_clean, 2)', ' %.6f')]);
disp([' sum of rows of cam2rgb_m2_clean_norm (must be [1 1 1]): '...
num2str(sum(cam2rgb_m2_clean_norm, 2)', ' %.6f')]);
disp(' ');
% transformation of the test image with matrices from the second method:
im_m2 = [img_trans_mat(im, cam2rgb_m2_norm) img_trans_mat(im, cam2rgb_m2)];
im_m2(im_m2<0) = 0;
im_m2 = im_m2.^(1/2.2);
im_m2 = uint8(255*im_m2);
% image with non-converted colors: gamma correction and turning into 3*8-bit:
im = uint8(255 * im.^(1/2.2));
% displaying images:
% upper row:
% 1: original colors
% 2: processed using ForwardMatrix ( recommended) - if ForwardMatrix exists,
% 3: processed not using ForwardMatrix (not recommended) with matrix normalizing,
% 4: processed not using ForwardMatrix (not recommended) without matrix normalizing
% lower row:
% maps of overexposed places
if forward_matrix_exists
imshow([im im_m1 im_m2; im2uint8([im im_m1 im_m2]>=255)]);
else
imshow([im im_m2; im2uint8([im im_m2]>=255)]);
end