% 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