1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
|
function [safemaxFALL, safemaxCLL, maxFALL, maxCLL, errmsg] = ComputeHDRStaticMetadataType1ContentLightLevels(images, integerScalesToSDRRange, nanflag, continueOnError)
% [safemaxFALL, safemaxCLL, maxFALL, maxCLL, errmsg] = ComputeHDRStaticMetadataType1ContentLightLevels(images [, integerScalesToSDRRange=80Nits][, nanflag='omitnan'][, continueOnError=0]);
%
% Compute CTA-861-G static content light levels for an image or a cell array of images.
%
% Compute the maximum frame average light level in nits and return it in
% 'maxFALL'. Compute the maximum content light level in nits and return it
% in 'maxCLL'. Return diagnostic error messages when appropriate in
% 'errmsg', or an empty 'errmsg' if there weren't any notable issues.
% 'safemaxFALL' and 'safemaxCLL' are clamped versions of 'maxFALL' and
% 'maxCLL', ie. as long as calculated values are within the specified range
% for HDR static metadata type 1, they are identical. Otherwise, the safe
% variants are clamped to the limits of the safe range, ie. the range from
% 0 to 65535 nits. NaN values for maxFALL or maxCLL result in safemaxFALL
% or safemaxCLL values of zero, as these signal "unknown" according to the
% standard.
%
% The returned maxFALL and maxCLL values are computed according to standard
% CTA-861-G, Annex P. 'maxCLL' the "maximum content light level" is defined
% as the luminance value in units of nits of the brightest pixel color
% component over all pixels in all frames of the image sequence. 'maxFALL'
% the "maximum frame average light level" is defined as the maximum over
% the average luminance values of all frames in the image sequence, where
% the average luminance of each frame is calculated over all pixels, but
% for each pixel only the value of the brightest color component is taken
% into account for calculation of the average, not the combined luminance
% of the whole pixel! One consequence of these definitions is that the computed
% maxFALL, maxCLL luminance values are only strictly correct under the
% assumption of an achromatic pixel, where the chromaticity coordinates of
% the pixel would correspond to those of the white-point of the display.
% Therefore, for an actual color image, maxFALL and maxCLL as measured by a
% colorimeter would likely be (much) lower than the maxFALL and maxCLL
% calculated by this function according to the standard.
%
% The maxFALL and maxCLL values calculated here can be used as arguments to
% a call to PsychHDR('HDRMetadata', ...) to define what is transmitted to a
% connected HDR display device. They are meant as guidance and conservative
% estimates of what content light levels to expect, to aid potential vendor
% and display-model specific algorithms for tone-mapping and thermal-/power-/
% backlight-dimming management which may be implemented in the display device.
% Those algorithms try to reproduce HDR content which is outside the
% operating range of the display device in a faithful manner, using maxCLL,
% maxFALL and other static HDR metadata to help with the tradeoff between
% proper image reproduction and not damaging or overheating the display device.
%
%
% Input arguments:
%
% 'images' can be a single image matrix of height-by-width-by-channels
% pixel color components, for a height-by-width pixels image, with channels
% being 1 for a luminance image, 2 for a luminance + alpha channel image, 3
% for a RGB color image, or 4 for a RGBA image with additional alpha
% channel. The alpha channel of a 2 channel matrix or a 4 channel matrix is
% ignored for light level computations. 'images' will usually be a floating
% point HDR image, but integer images are also accepted, in which case
% returned 'maxFALL' and 'maxCLL' values are adapted according to the
% optional parameter 'integerScalesToSDRRange'. 'images' can also be a
% cell() array of image matrices, representing a whole image sequence. In
% case of a cell array, each image in the array contributes to light level
% property calculations and you get the maxCLL over the whole sequence, and
% maxFALL over the whole sequence.
%
% 'integerScalesToSDRRange' Optional: How to treat 'images' of integer
% type, instead of floating point type. Currently the Screen('MakeTexture')
% command for turning image matrices into displayable image textures only
% accepts integer image matrices of type uint8, and floating point matrices
% of type double(). double() matrices are used "as is" for HDR display.
%
% Integer uint8 matrices are by default converted into HDR images with a
% value range that corresponds to typical standard dynamic range (SDR).
% Values from 0 to 255 are assumed to represent the HDR subrange [0; 80]
% nits. Iow. by default 0 maps to 0 nits and the maximum uint8 value of
% 255 maps to 80 nits. See the help text of "Screen MakeTexture?" and for
% the PsychImaging() HDR setup functions for details on how this behaviour
% can be customized. 'integerScalesToSDRRange' defines how to adapt the
% returned 'maxFALL' and 'maxCLL' values if a integer image matrix is
% passed in: A value of 0 will just treat it like a floating point matrix.
% A value > 0 will map the possible value range in the integer matrix to
% the range 0 - integerScalesToSDRRange. The default setting if this
% parameter is omitted, is to map the integer range to a range of 0 - 80
% nits, iow. returned maxCLL and maxFALL will be in the range 0 - 80 nits.
% This is the most reasonable behaviour if HDR display mode is requested
% with all parameters at their default and Screen('MakeTexture') is used
% with all precision related parameters at their defaults.
%
% 'nanflag' Optional: How to treat NaN "not a number" values in pixel color
% components: By default 'nanflag' is 'omitnan' - Only non-NaN elements
% contribute to maximum and mean calculations for maxCLL, maxFALL. The
% other valid setting would be 'includenan' - If any pixel color component
% in any of the passed in 'images' is NaN, the returned maxCLL and maxFALL
% will be NaN.
%
% 'continueOnError' Optional: If set to 0 (the default), abort with an
% error message on invalid input. If set to 1, try to continue if possible,
% just printing a warning message and return a user comprehensible error
% string in 'errmsg'.
%
% History:
% 24-Sep-2020 mk Written.
% Default to no error:
errmsg = [];
if nargin < 1 || isempty(images)
error('None, or empty images parameter provided instead of image matrix or cell array of images.');
end
% If omitted, integer images [0 ; maximum integer value] shall be mapped to
% [0 ; 80 nits], as this is the default of PsychHDR() for setting up HDR
% display modes if all optional parameters are omitted:
if nargin < 2 || isempty(integerScalesToSDRRange)
integerScalesToSDRRange = 80;
else
if ~isscalar(integerScalesToSDRRange) || ~isnumeric(integerScalesToSDRRange) || ~isreal(integerScalesToSDRRange) || integerScalesToSDRRange < 0
error('Invalid integerScalesToSDRRange parameter specified. Not a scalar greater than or equal to zero.');
end
end
% Omit NaN values in all calculations by default:
if nargin < 3 || isempty(nanflag)
nanflag='omitnan';
else
switch nanflag
case {'omitnan', 'includenan'}
% All good, nothing to do.
otherwise
error('Invalid nanflag provided.');
end
end
if nargin < 4 || isempty(continueOnError)
continueOnError = 0;
else
if ~ismember(continueOnError, [0, 1])
error('Invalid continueOnError flag specified. Not 0 or 1.');
end
end
% maxCLL is value of the brightest pixel color component of the brightest
% pixel over all images:
if iscell(images)
% Cell array with each cell containing one image. Iterate over all of
% them and keep track of global sequence maxCLL, maxFALL:
maxCLL = 0;
maxFALL = 0;
for i=1:length(images)
[curCLL, curFALL, msg] = computeLLProps(images{i}, integerScalesToSDRRange, nanflag, continueOnError, i);
if (isnan(curCLL) || isnan(curFALL)) && strcmp(nanflag, 'includenan')
% NaN's found in at least one image, and supposed to return NaN
% for whole sequence, do it, we're done here:
maxCLL = nan;
maxFALL = nan;
% NaN's are not safe for use in PsychHDR(). Choose 0 as the
% defined "i don't know" value for the HDR monitor:
safemaxFALL = 0;
safemaxCLL = 0;
errmsg = msg;
return;
end
maxFALL = max(maxFALL, curFALL);
maxCLL = max(maxCLL, curCLL);
if ~isempty(msg)
errmsg = [errmsg, ' : ', msg]; %#ok<AGROW>
end
end
else
% Single image matrix: Directly compute props of the one image:
[maxCLL, maxFALL, errmsg] = computeLLProps(images, integerScalesToSDRRange, nanflag, continueOnError, 1);
end
% Clamp safe (for PsychHDR to transmit HDR metadata) values to allowable
% range of 0 - 65535 nits:
safemaxFALL = min(max(maxFALL, 0), 65535);
safemaxCLL = min(max(maxCLL, 0), 65535);
% Return final maxFALL, maxCLL:
return;
% Compute light level props of one image matrix:
function [maxCLL, FALL, errmsg] = computeLLProps(img, integerScalesToSDRRange, nanflag, continueOnError, index)
% Safe return values on error:
errmsg = [];
maxCLL = nan;
FALL = nan;
% Validate size and dimensions of the image:
if ~ismember(ndims(img), [2, 3]) || size(img, 1) < 1 || size(img, 2) < 1 || ~ismember(size(img, 3), [1,2,3,4])
errmsg = sprintf('At least one provided image (index %i) is not a valid 2D or 3D image matrix of minimum size 1-by-1 pixel, with 1, 2, 3 or 4 color channels.', index);
if continueOnError
warning(errmsg); %#ok<*SPWRN>
return;
else
error(errmsg); %#ok<*SPERR>
end
end
% Is it a numeric matrix?
if ~isnumeric(img) || ~isreal(img)
errmsg = sprintf('At least one provided image (index %i) is not a valid numeric 2D or 3D image matrix.', index);
if continueOnError
warning(errmsg); %#ok<*SPWRN>
return;
else
error(errmsg); %#ok<*SPERR>
end
end
% Throw away alpha channel of 2 layer LA and 4 layer RGBA images:
if ismember(size(img, 3), [1,3])
img = img(:,:,:);
else
img = img(:,:,1:end-1);
end
% Want to include NaN's, ie. return NaN if any elements are NaN?
if strcmp(nanflag, 'includenan') && any(isnan(img(:)))
% At least one image pixel component somewhere is NaN. Return NaN:
maxCLL = nan;
FALL = nan;
return;
end
% At this point, either img does not contain NaN's, only valid values,
% or the nanflag is 'omitnan', so we should compute stuff, but ignore
% any NaN elements for the computations.
% Matlab and Octave default behaviour for max() is to ignore NaN's for
% maximum search, exactly what we want.
% Fixed Octave behaviour for mean() is to return NaN if anything is
% NaN, therefore we must make sure that at least for mean()
% computations we will have scrubbed the input from NaN's.
% maxCLL is the maximum over all color components of all pixels in the
% luminance layer or RGB layers of the image:
maxCLL = max(img(:));
% FALL is the average over all pixels in the image, but for each pixel,
% only the pixel color component with maximum value will contribute to
% the average, so blue dominated regions would use the blue color
% channel values to participate in the mean computation, whereas in
% green dominated regions, only green components would be used:
%
% First build a single channel image, which contains the maximum
% component value for each pixel, iow. each entry (y,x) is the maximum
% over the color components of pixel (y,x). For a pure luminance
% matrix, maximg is naturally == img:
maximg = max(img, [], 3);
% Now compute the arithmetic mean over all values in 2D matrix maximg
% which are not NaN:
FALL = mean(maximg(~isnan(maximg(:))));
% Make sure we only output double() values, as expected for further
% math with high precision, and especially for PsychHDR('HDRMetaData', ...)
% arguments:
maxCLL = double(maxCLL);
FALL = double(FALL);
% uint image instead of float image, and treatment as SDR requested?
if integerScalesToSDRRange && ~isfloat(img)
% Yes: Values [0 ; intmax(class(img))] are mapped to [0; 1] unorm
% range by Psychtoolbox/Screen(), which is in turn assumed to be
% mapped to [0; integerScalesToSDRRange] for HDR onscreen windows:
integerScalesToSDRRange = double(integerScalesToSDRRange) / double(intmax(class(img)));
maxCLL = maxCLL * integerScalesToSDRRange;
FALL = FALL * integerScalesToSDRRange;
end
return;
|