Contents

OpenEXR images HDR Retina tone mapping demo

High Dynamic Range retina tone mapping with the help of the Gipsa/Listic's retina.

Retina demonstration for High Dynamic Range compression (tone-mapping): demonstrates the use of wrapper class of the Gipsa/Listic Labs retina model.

This retina model allows spatio-temporal image processing (applied on a still image).

This demo focuses demonstration of the dynamic compression capabilities of the model. The main application is tone mapping of HDR images (i.e. see on a 8bit display a more than 8bits coded (up to 16bits) image with details in high and low luminance ranges.

The retina model still have the following properties:

Sources:

function varargout = retina_hdr_tonemapping_demo_gui(fname)
    % load HDR image
    if nargin < 1
        % http://www.pauldebevec.com/Research/HDR/memorial.hdr
        % http://www.pauldebevec.com/Research/HDR/memorial.exr
        fname = fullfile(mexopencv.root(),'test','memorial.hdr');
    end
    [imgs, isz] = loadInputImage(fname);
    showInputImage(imgs);

    % create retina object
    opts = defaultOptions();
    retina = createRetina(fliplr(isz), opts);

    % create the UI, hook event handlers, and trigger timer
    h = buildGUI(imgs, isz, opts);
    props = {'Interruptible','off', 'BusyAction','cancel'};
    set(h.fig, 'WindowKeyPressFcn',@onType, props{:});
    set(h.slid(1), 'Callback',@onChangeClipping, props{:});
    set(h.slid(2), 'Callback',@onChangeSaturation, props{:});
    set(h.slid(3:5), 'Callback',@onChangeParams, props{:});
    set(h.t, 'TimerFcn',@onTimer, 'BusyMode','drop');
    start(h.t);

    if nargout > 0, varargout{1} = h; end
    if nargout > 1, varargout{2} = retina; end
=> image size (w,h) = 484,714
Input image rescaling with histogram edges cutting
(in order to eliminate bad pixels in HDR image creation):
=> Histogram limits
	0% index = 0
		normalized hist value = 0.354646
		input gray level = 0
	100% index = 256
		normalized hist value = 0.999998
		input gray level = 255
Current Retina instance setup :
OPLandIPLparvo{
	 colorMode : 1
	 normalizeParvoOutput :1
	 photoreceptorsLocalAdaptationSensitivity : 0.985
	 photoreceptorsTemporalConstant : 0.5
	 photoreceptorsSpatialConstant : 0.43
	 horizontalCellsGain : 40
	 hcellsTemporalConstant : 1
	 hcellsSpatialConstant : 7
	 parvoGanglionCellsSensitivity : 0.95}
Current Retina instance setup :
IPLmagno{
	 normaliseOutput : 1
	 parasolCells_beta : 0
	 parasolCells_tau : 0
	 parasolCells_k : 7
	 amacrinCellsTemporalCutFrequency : 2
	 V0CompressionParameter : 0.95
	 localAdaptintegration_tau : 0
	 localAdaptintegration_k : 7}

Event handlers

    function onType(~, e)
        %ONTYPE  Event handler for key press on figure

        % handle keys
        switch e.Key
            case 'h'
                helpdlg({
                    'Hot keys:'
                    'h - this help dialog'
                    'q - quit the program'
                    'c - clear all retina buffers (open your eyes)'
                });

            case {'q', 'escape'}
                stop(h.t);
                delete(h.t);
                close(h.fig);

            case 'v'
                % toggle verbose mode
                verbose(~verbose());

            case 'c'
                % clear retina buffers
                if ~opts.fastMethod
                    retina.clearBuffers();
                end

            case 'f'
                % switch between retina and fast retina tone mapper
                opts.fastMethod = ~opts.fastMethod;
                retina = createRetina(fliplr(isz), opts);
                % update plot title
                str = {'Retina tonemapping output', 'Parvocellular pathway'};
                if opts.fastMethod
                    str{2} = 'fast tone mapping';
                end
                title(h.ax(2), str, 'FontSize',10, 'Interpreter','none');

            case 'l'
                % toggle retina log sampling
                opts.useLogSampling = ~opts.useLogSampling;
                retina = createRetina(fliplr(isz), opts);

            case 'r'
                % reset default options
                opts = defaultOptions();
                retina = createRetina(fliplr(isz), opts);
                % update UI
                set(h.slid(1), 'Value',opts.histogramClippingValue);
                set(h.slid(2), 'Value',opts.colorSaturationFactor);
                set(h.slid(3), 'Value',opts.retinaHcellsGain);
                set(h.slid(4), 'Value',opts.localAdaptation_photoreceptors);
                set(h.slid(5), 'Value',opts.localAdaptation_Gcells);
                onChangeClipping([],[]);
                onChangeSaturation([],[]);
                onChangeParams([],[]);
                drawnow;

            case {'s', 'space'}
                % start/stop the timer
                if strcmp(h.t.Running, 'on')
                    stop(h.t);
                else
                    start(h.t);
                end
        end
    end

    function onChangeClipping(~, ~)
        %ONCHANGECLIPPING  Event handler for UI controls

        % retrieve current values from UI controls
        opts.histogramClippingValue = round(get(h.slid(1), 'Value'));
        set(h.txt(1), 'String',sprintf('hist edges clip limit: %2d', ...
            opts.histogramClippingValue));

        % apply new parameters
        [imgs.rescaled, imgs.histo] = rescaleGrayLevelMat(imgs.src, ...
            opts.histogramClippingValue/100);

        % manually run one step if timer if off
        if strcmp(h.t.Running, 'off'), onTimer([], []); end
    end

    function onChangeSaturation(~, ~)
        %ONCHANGESATURATION  Event handler for UI controls

        % retrieve current values from UI controls
        opts.colorSaturationFactor = round(get(h.slid(2), 'Value'));
        set(h.txt(2), 'String',sprintf('Color saturation: %1d', ...
            opts.colorSaturationFactor));

        % apply new parameters
        if ~opts.fastMethod
            retina.setColorSaturation('SaturateColors',true, ...
                'ColorSaturationValue',opts.colorSaturationFactor);

            % manually run one step if timer if off
            if strcmp(h.t.Running, 'off'), onTimer([], []); end
        end
    end

    function onChangeParams(~, ~)
        %ONCHANGEPARAMS  Event handler for UI controls

        % retrieve current values from UI controls
        opts.retinaHcellsGain = round(get(h.slid(3), 'Value'));
        opts.localAdaptation_photoreceptors = round(get(h.slid(4), 'Value'));
        opts.localAdaptation_Gcells = round(get(h.slid(5), 'Value'));
        set(h.txt(3), 'String',sprintf('Hcells gain: %3d', ...
            opts.retinaHcellsGain));
        set(h.txt(4), 'String',sprintf('Ph sensitivity: %3d', ...
            opts.localAdaptation_photoreceptors));
        set(h.txt(5), 'String',sprintf('Gcells sensitivity: %3d', ...
            opts.localAdaptation_Gcells));

        % apply new parameters
        if ~opts.fastMethod
            retina.setupOPLandIPLParvoChannel(...
                'PhotoreceptorsLocalAdaptationSensitivity',...
                    opts.localAdaptation_photoreceptors/200, ...
                'PhotoreceptorsSpatialConstant',0.43, ...
                'HorizontalCellsGain',opts.retinaHcellsGain, ...
                'GanglionCellsSensitivity',opts.localAdaptation_Gcells/200);

            % manually run one step if timer if off
            if strcmp(h.t.Running, 'off'), onTimer([], []); end
        end
    end

    function onTimer(~, ~)
        %ONTIMER  Timer callback function

        % stop timer if figure was closed
        if ~ishghandle(h.fig)
            stop(h.t);
            delete(h.t);
            return;
        end

        % run retina filter
        if opts.fastMethod
            % apply the simplified hdr tone mapping method
            imgs.dst = retina.applyFastToneMapping(imgs.rescaled);
        else
            % run and retrieve retina output
            retina.run(imgs.rescaled);
            imgs.dst = retina.getParvo();
        end

        % display retina HDR input and LDR output, and clipped histogram
        set(h.img(1), 'CData',imgs.rescaled./255);
        set(h.img(2), 'CData',imgs.dst);
        set(h.img(3), 'CData',imgs.histo);
        drawnow;
    end
end

function b = verbose(varargin)
    %VERBOSE  Get/set verbosity

    persistent verbose
    if isempty(verbose), verbose = true; end

    if nargin > 0
        verbose = logical(varargin{1});
    end
    b = verbose;
end

function [imgs, isz] = loadInputImage(fname)
    %LOADIMAGE  Load HDR image

    % load retina input image
    img = cv.imread(fname, 'Unchanged',true);
    assert(~isempty(img), 'could not load image');
    isz = [size(img,1) size(img,2)];
    if verbose()
        fprintf('=> image size (w,h) = %d,%d\n', isz(2), isz(1));
    end

    % rescale between 0 and 1, still floating-point
    img = cv.normalize(img, 'Alpha',0, 'Beta',1, 'NormType','MinMax');
    img = min(max(img, 0), 1);  % avoid tiny negative values

    % output structure of images:
    %   src      -> single, [0,1]   = input image
    %   gamma    -> single, [0,1]   = gamme transformed image
    %   rescaled -> single, [0,255] = input image rescaled
    %   dst      -> uint8,  [0,255] = retina output image
    %   histo    -> uint8,  [0,255] = histogram image
    imgs = struct();
    imgs.src = img;
    imgs.gamma = img .^ (1/5);  % apply gamma curve
    [imgs.rescaled, imgs.histo] = rescaleGrayLevelMat(img, 0);
    imgs.dst = zeros(size(img), 'uint8');
end

function showInputImage(imgs)
    %SHOWINPUTIMAGES  Show input HDR images

    % overlay titles
    props = {'FontScale',0.5, 'Color',[0 255 0]};
    srcImg = cv.putText(imgs.src, ...
        'EXR original (linear rescaling)', [10 20], props{:});
    gammaImg = cv.putText(imgs.gamma, ...
        'EXR w/ basic processing (gamma correction)', [10 20], props{:});

    % show image montage
    im = cat(2, srcImg, gammaImg);
    fig = figure('Name','HDR', 'NumberTitle','off', 'Menubar','none', ...
        'Position',[200 200 size(im,2) size(im,1)]);
    if ~mexopencv.isOctave()
        movegui(fig, 'center');
    end
    ax = axes('Parent',fig, 'Units','normalized', 'Position',[0 0 1 1]);
    if ~mexopencv.isOctave()
        imshow(im, 'Parent',ax);
    else
        %HACK: https://savannah.gnu.org/bugs/index.php?45473
        axes(ax); imshow(im);
    end
end

function opts = defaultOptions()
    %DEFAULTOPTIONS  Create default options structure

    opts = struct();
    opts.useLogSampling = false;               % retina log sampling processing
    opts.fastMethod = false;                   % fast method (no spectral whitening)
    opts.histogramClippingValue = 0;           % histogram edges clipping limit
    opts.colorSaturationFactor = 3;            % Color saturation
    opts.retinaHcellsGain = 40;                % Hcells gain
    opts.localAdaptation_photoreceptors = 197; % Ph sensitivity
    opts.localAdaptation_Gcells = 190;         % Gcells sensitivity
end

function retina = createRetina(isz, opts)
    %CREATERETINA  Create retina instance

    if opts.useLogSampling
        % activate log sampling
        % (favour foveal vision and subsamples peripheral vision)
        props = {'UseRetinaLogSampling',true, 'ReductionFactor',2.0};
    else
        % allocate "classical" retina
        props = {};
    end

    if opts.fastMethod
        %TODO: untested
        % create a fast retina tone mapper (Meylan et al. algorithm)
        retina = cv.RetinaFastToneMapping(isz);
        retina.setup('PhotoreceptorsNeighborhoodRadius',3, ...
            'GanglioncellsNeighborhoodRadius',1.5, ...
            'MeanLuminanceModulatorK',1);
    else
        % create a retina instance with default parameters setup
        retina = cv.Retina(isz, props{:});

        % desactivate Magnocellular pathway processing
        % (motion information extraction) since it is not useful here
        retina.activateMovingContoursProcessing(false);

        % set retina params
        retina.setColorSaturation('SaturateColors',true, ...
            'ColorSaturationValue',opts.colorSaturationFactor);
        retina.setupOPLandIPLParvoChannel(...
            'PhotoreceptorsLocalAdaptationSensitivity',...
                opts.localAdaptation_photoreceptors/200, ...
            'PhotoreceptorsSpatialConstant',0.43, ...
            'HorizontalCellsGain',opts.retinaHcellsGain, ...
            'GanglionCellsSensitivity',opts.localAdaptation_Gcells/200);

        % display params
        if verbose()
            disp(retina.printSetup());
        end
    end
end

function [dst, histoImg] = rescaleGrayLevelMat(src, clipLimit)
    %RESCALEGRAYLEVELMAT  Get the gray level map of the input image and rescale it to the range [0-255]
    %
    % Performs histogram stretching.
    %
    % See also: imadjust, stretchlim, cv.SimpleWB
    %

    % rescale to [0,255], keeping floating-point values
    dst = cv.normalize(src, 'Alpha',0.0, 'Beta',255.0, 'NormType','MinMax');

    % extract a 8bit image that will be used for histogram edge cut
    gray = uint8(dst);
    if size(dst,3) == 3
        gray = cv.cvtColor(gray, 'RGB2GRAY');
    end

    % get histogram density probability in order to cut values under/above
    % edges limits (e.g 5-95%)... useful for HDR pixel errors cancellation
    HIST_SIZE = 256;  % resolution of histogram
    histo = cv.calcHist(gray, linspace(0,255+1,HIST_SIZE+1));
    % normalize histogram so that its sum equals 1
    histo = cv.normalize(histo, ...
        'Alpha',1.0, 'Beta',0.0, 'NormType','L1', 'DType','single');

    % compute density probability
    denseProb = cumsum(histo);

    % deduce min and max admitted gray level indices
    if clipLimit == 0
        lowerLimit = 0;
        upperLimit = 256;
    else
        lowerLimit = nnz(denseProb < clipLimit);
        upperLimit = nnz(denseProb < (1-clipLimit));
    end

    % corresponding gray level values
    mn = 255.0 * lowerLimit / HIST_SIZE;
    mx = 255.0 * upperLimit / HIST_SIZE;

    if verbose()
        disp('Input image rescaling with histogram edges cutting');
        disp('(in order to eliminate bad pixels in HDR image creation):');
        disp('=> Histogram limits');
        fprintf('\t%g%% index = %d\n', clipLimit*100, lowerLimit);
        fprintf('\t\tnormalized hist value = %g\n', ...
            denseProb(find(denseProb >= clipLimit, 1, 'first')));
        fprintf('\t\tinput gray level = %g\n', mn);
        fprintf('\t%g%% index = %d\n', (1-clipLimit)*100, upperLimit);
        fprintf('\t\tnormalized hist value = %g\n', ...
            denseProb(find(denseProb <= (1-clipLimit), 1, 'last')));
        fprintf('\t\tinput gray level = %g\n', mx);
    end

    % draw clipped histogram image
    if nargout > 1
        [~, hsz] = calcSizes(size(gray));
        histoImg = drawHistogram(histo, hsz, lowerLimit, upperLimit);
    end

    % rescale image range [mn,mx] to [0,255]
    dst = (dst - mn) * 255.0 / (mx - mn);

    % cut original histogram and back project to original image
    if true
        % clips values above 255, and values below 0
        dst = cv.threshold(dst, 255.0, 'MaxValue',255.0, 'Type','Trunc');
        dst = cv.threshold(dst, 0.0, 'MaxValue',0.0, 'Type','ToZero');
    else
        dst = max(min(dst, 255.0), 0.0);
    end

    dst = cv.normalize(dst, 'Alpha',0.0, 'Beta',255.0, 'NormType','MinMax');
end

function histoImg = drawHistogram(histo, hsz, lowerLimit, upperLimit)
    %DRAWHISTOGRAM  Simple procedure for 1D curve tracing
    %
    % See also: imhist, hist, bar, histogram
    %

    % bar width
    len = numel(histo);
    binW = round(hsz(2) / len);

    % normalize histogram to fit output image height
    histo = round(cv.normalize(histo, ...
        'Alpha',0, 'Beta',hsz(1), 'NormType','MinMax', 'DType','single'));

    % output image on which to draw histogram
    histoImg = 255 * ones([hsz 3], 'uint8');  % white background

    % draw histogram bars
    % (BG is white, we simply draw from top to bar height in black)
    for i=1:len
        histoImg = cv.rectangle(histoImg, ...
            [(i-1)*binW, hsz(1)], [i*binW, hsz(1) - histo(i)], ...
            'Color',[0 0 0], 'Thickness','Filled');
    end

    % show lower/upper clip limits
    if nargin > 2
        histoImg = cv.rectangle(histoImg, ...
            [0, 0], [lowerLimit*binW, hsz(1)], ...
            'Color',[128 128 128], 'Thickness','Filled');
    end
    if nargin > 3
        histoImg = cv.rectangle(histoImg, ...
            [hsz(2), 0], [upperLimit*binW, hsz(1)], ...
            'Color',[128 128 128], 'Thickness','Filled');
    end

    % title
    histoImg = cv.putText(histoImg, 'Histogram', [10 20], ...
        'FontScale',0.5, 'Color',[0 255 0]);
end

function [sz, hsz] = calcSizes(isz)
    %CALCSIZE  Calculate axes sizes to maximally fit screen

    % isz = input image size [h,w]
    % sz  = image axis size [h,w]
    % hsz = histogram axis size [h,w]
    % ss  = screen size [1,1,w,h]

    %NOTE: width divided between 2 equal size axes
    %TODO: more portable height calculation
    %  ss(4) = screen height
    % hsz(1) = histogram height
    %    130 = sliders height
    %     35 = axis-title height (2 lines)
    %     40 = window frame height
    %     40 = windows taskbar height
    hsz = [120 2*isz(2)];
    ss = get(0, 'ScreenSize');
    sz = round(min([ss(4)-hsz(1)-130-35-40-40 ss(3)./2] ./ isz) .* isz);
    hsz(2) = 2*sz(2);
end

function h = buildGUI(imgs, isz, opts)
    %BUILDGUI  Creates the UI

    % calculate axes sizes to best fit to screen
    [sz, hsz] = calcSizes(isz);

    % build the user interface (no resizing to keep it simple)
    h = struct();
    h.fig = figure('Name','Retina HDR Tonemapping Demo', ...
        'NumberTitle','off', 'Menubar','none', 'Resize','off', ...
        'Position',[200 200 sz(2)*2 sz(1)+hsz(1)+130+35-1]);
    if ~mexopencv.isOctave()
        %HACK: not implemented in Octave
        movegui(h.fig, 'center');
    end
    im = {imgs.rescaled, imgs.dst, imgs.histo};
    pos = {
        [1       130+hsz(1)  sz(2)  sz(1)]
        [1+sz(2) 130+hsz(1)  sz(2)  sz(1)]
        [1       130        hsz(2) hsz(1)]
    };
    titles = {
        {'Retina input', '(w/ clipped histogram)'}
        {'Retina tonemapping output', 'Parvocellular pathway'}
        ''  % Histogram
    };
    if opts.fastMethod, titles{2}{2} = 'fast tone mapping'; end
    for i=1:numel(im)
        h.ax(i) = axes('Parent',h.fig, 'Units','pixels', 'Position',pos{i});
        if ~mexopencv.isOctave()
            h.img(i) = imshow(im{i}, 'Parent',h.ax(i));
        else
            %HACK: https://savannah.gnu.org/bugs/index.php?45473
            axes(h.ax(i)); h.img(i) = imshow(im{i});
        end
        if ~isempty(titles{i})
            title(h.ax(i), titles{i}, 'FontSize',10, 'Interpreter','none');
        end
    end
    props = {'FontSize',11, 'HorizontalAlignment','right'};
    h.txt(1) = uicontrol('Parent',h.fig, 'Style','text', props{:}, ...
        'Position',[5 5 150 20], ...
        'String',sprintf('hist edges clip limit: %2d', opts.histogramClippingValue));
    h.txt(2) = uicontrol('Parent',h.fig, 'Style','text', props{:}, ...
        'Position',[5 30 150 20], ...
        'String',sprintf('Color saturation: %1d', opts.colorSaturationFactor));
    h.txt(3) = uicontrol('Parent',h.fig, 'Style','text', props{:}, ...
        'Position',[5 55 150 20], ...
        'String',sprintf('Hcells gain: %3d', opts.retinaHcellsGain));
    h.txt(4) = uicontrol('Parent',h.fig, 'Style','text', props{:}, ...
        'Position',[5 80 150 20], ...
        'String',sprintf('Ph sensitivity: %3d', opts.localAdaptation_photoreceptors));
    h.txt(5) = uicontrol('Parent',h.fig, 'Style','text', props{:}, ...
        'Position',[5 105 150 20], ...
        'String',sprintf('Gcells sensitivity: %3d', opts.localAdaptation_Gcells));
    h.slid(1) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',opts.histogramClippingValue, ...
        'Min',0, 'Max',50, 'SliderStep',[1 5]./(50-0), ...
        'Position',[155 5 sz(2)*2-155-20 20]);
    h.slid(2) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',opts.colorSaturationFactor, ...
        'Min',0, 'Max',5, 'SliderStep',[1 2]./(5-0), ...
        'Position',[155 30 sz(2)*2-155-20 20]);
    h.slid(3) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',opts.retinaHcellsGain, ...
        'Min',0, 'Max',100, 'SliderStep',[1 10]./(100-0), ...
        'Position',[155 55 sz(2)*2-155-20 20]);
    h.slid(4) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',opts.localAdaptation_photoreceptors, ...
        'Min',0, 'Max',200, 'SliderStep',[1 20]./(200-0), ...
        'Position',[155 80 sz(2)*2-155-20 20]);
    h.slid(5) = uicontrol('Parent',h.fig, 'Style','slider', ...
        'Value',opts.localAdaptation_Gcells, ...
        'Min',0, 'Max',200, 'SliderStep',[1 20]./(200-0), ...
        'Position',[155 105 sz(2)*2-155-20 20]);

    % create timer
    h.t = timer('ExecutionMode','fixedSpacing', 'Period',0.1);
end