Code covered by the BSD License  

Highlights from
Dicom Import GUI

image thumbnail

Dicom Import GUI

by

 

23 May 2013 (Updated )

Sorts DICOM files into series and lets you select the series you want to load in a GUI

DicomImport(sPath, lRecursive, csAdditionalTags)
function SDataOut = DicomImport(sPath, lRecursive, csAdditionalTags)
% DICOMIMPORT Import DICOM series with preview.
%
%   SDATA = DICOMIMPORT(sPATH, lRECURSIVE) Scans the folder specified by
%   sPATH for DICOM files. If lRECURSIVE is true, DICOMIMPORT
%   hierarchically scans all subolders of sPATH. Valid DICOM files are
%   sorted into series according the DICOM tag SeriesInstanceUID and the
%   folder in which the files were found. For each series, a thumbnail
%   image is created and an overview of the obtained series is displayed in
%   a GUI that lets you select the series to load. Select series as if you
%   would in a file browser by klicking and using the SHIFT and CNTL keys
%   to select multiple series Close the GUI by pressing <ENTER> or <ESCAPE>
%   (the latter returns an empty array). Output SDATA is a struct where
%   length(SDATA) equals the amout of selected series. SDATA contains at
%   least the following fields:
%
%       Img:                The image volume
%       SeriesDescription:  The contents of the corresponding DICOM tag
%       Orientation:        The most prominent image volume orientation
%       Aspect:             The pixel spacing in all 3 directions
%       ImageOrientation:   The direction cosines (compare DICOM standard)
%       ImagePosition:      The coordinates of the upper left corner of
%                           each image slice (compare DICOM standard)
%
%   The tags specified in csADDITIONALTAGS (see below) are appended to this
%   structure.
%
%
%   DICOMIMPORT lets you select the base folder in a dialog and asks if the
%   sobfolders are to be scanned.
%
%   DICOMIMPORT(sPATH, lRECURSIVE, csADDITIONALTAGS) Lets you specify
%   additional DICOM tags you can use for further processing in the cell
%   array of strings csADDITIONALTAGS
%
%
% Copyright 2013 Christian Wuerslin, University of Tuebingen and
% University of Stuttgart, Germany.
% Contact: christian.wuerslin@med.uni-tuebingen.de

% =========================================================================
% *** FUNCTION DicomImport
% ***
% *** Main GUI function. See above for description.
% ***
% =========================================================================

% -------------------------------------------------------------------------
% Control the figure's appearence
SAp.sTITLE              = 'Dicom Import [press enter to confirm]';
SAp.iTHUMBNAILSIZE      = 200;
SAp.iINFOBARHEIGHT      = 125;
SAp.dBGCOLOR            = [0.2 0.3 0.4];
SAp.iNROWS              = 2;
SAp.iNCOLS              = 4;
SAp.iTITLEHEIGHT        = 16;
SAp.dOPACITY            = 0.7;
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Define a list of DICOM tags to read (those that I really need)
csTags = {'SeriesInstanceUID',       'Rows',                 'Columns', ...
          'ImageOrientationPatient', 'ImagePositionPatient', 'PixelSpacing', ...
          'SeriesDescription', 'Modality', 'SliceThickness'};
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Parse input arguments
if nargin
    if ~ischar(sPath), error('Input argument must be a string!'); end
else
    sPath = uigetdir;
    if isnumeric(sPath)
        SDataOut = [];
        return
    end
end

if nargin < 2, lRecursive = strcmp(questdlg('Search subdirectories for DICOM files?', 'Stupid question', 'Yup', 'Noop', 'Yup'), 'Yup'); end
if nargin > 2, csTags = [csTags, csAdditionalTags]; end
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Get all DICOM files and their headers
fprintf(1, 'Scanning for DICOM files...');
SData = fGetDicomFiles(sPath, csTags, lRecursive);
fprintf(1, 'done.\n');
if isempty(SData), return, end
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Sort the series into a struct and get a thumbnail of each set
fprintf(1, 'Creating thumbnails...');
iHashs = [SData.iHash];
iSeriesHash = fUnique(iHashs);
iNDatasets = length(iSeriesHash);
iThumbnails = zeros(SAp.iTHUMBNAILSIZE, SAp.iTHUMBNAILSIZE, iNDatasets);
for iDataset = 1:iNDatasets
    iThisSeries = find([SData.iHash] == iSeriesHash(iDataset));
    SSeries(iDataset).SData = SData(iThisSeries);

    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    % Load the center image and create thumbnail
    iInd = iThisSeries(round(length(iThisSeries)./2));
    dImg = double(dicomread(SData(iInd).sFilename));
    if isempty(dImg), continue, end
    
    dImg = dImg - min(dImg(:));
    dX = linspace(1, length(dImg), SAp.iTHUMBNAILSIZE);
    [dYY, dXX] = meshgrid(dX, dX);
    dImg = interp2(dImg, dYY, dXX, 'linear*', 0);
    if max(dImg(:)), dImg = dImg./max(dImg(:)); end
    iThumbnails(:,:,iDataset) = dImg;
    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
end
lSelected = false(iNDatasets, 1);
fprintf(1, 'done.\n');
fprintf(1, 'Found %u files in %u datasets!\n', length(SData), iNDatasets);
clear iDataset iThisSeries iInd dImg dX dXX dYY
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Create the figure.
lStartup = true;
dFigureWidth  = (SAp.iTHUMBNAILSIZE + 1).*SAp.iNCOLS + 14;
dFigureHeight = (SAp.iTHUMBNAILSIZE + SAp.iTITLEHEIGHT + 1).*SAp.iNROWS + SAp.iINFOBARHEIGHT + 1;
dScreenSize = get(0, 'MonitorPositions');
try
    hF = figure(...
        'Position'             , [(dScreenSize(1, 3) - dFigureWidth )./2, ...
        (dScreenSize(1, 4) - dFigureHeight)./2, ...
        dFigureWidth, dFigureHeight], ...
        'Units'                , 'pixels', ...
        'Color'                , SAp.dBGCOLOR, ...
        'Resize'               , 'off', ...
        'DockControls'         , 'off', ...
        'MenuBar'              , 'none', ...
        'Name'                 , SAp.sTITLE, ...
        'NumberTitle'          , 'off', ...
        'BusyAction'           , 'cancel', ...
        'KeyPressFcn'          , @fKeyPressFcn, ...
        'CloseRequestFcn'      , @fCloseGUI, ...
        'WindowButtonDownFcn'  , @fWindowButtonDownFcn, ...
        'WindowButtonMotionFcn', @fWindowMouseMoveFcn, ...
        'WindowButtonUpFcn'    , @fWindowButtonUpFcn, ...
        'WindowScrollWheelFcn' , @fWindowScrollWheelFcn);
catch %#ok<CTCH>
    hF = figure(...
        'Position'             , [(dScreenSize(1, 3) - dFigureWidth )./2, ...
        (dScreenSize(1, 4) - dFigureHeight)./2, ...
        dFigureWidth, dFigureHeight], ...
        'Units'                , 'pixels', ...
        'Color'                , SAp.dBGCOLOR, ...
        'Resize'               , 'off', ...
        'DockControls'         , 'off', ...
        'MenuBar'              , 'none', ...
        'Name'                 , SAp.sTITLE, ...
        'NumberTitle'          , 'off', ...
        'BusyAction'           , 'cancel', ...
        'KeyPressFcn'          , @fKeyPressFcn, ...
        'CloseRequestFcn'      , @fCloseGUI, ...
        'WindowButtonDownFcn'  , @fWindowButtonDownFcn, ...
        'WindowButtonUpFcn'    , @fWindowButtonUpFcn, ...
        'WindowButtonMotionFcn', @fWindowMouseMoveFcn);
end
clear dScreenSize
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Create the axes
hAxes = zeros(SAp.iNCOLS.*SAp.iNROWS, 1);
hImg  = zeros(SAp.iNCOLS.*SAp.iNROWS, 1);
hText = zeros(SAp.iNCOLS.*SAp.iNROWS, 1);
for iM = 1:SAp.iNROWS
    for iN = 1:SAp.iNCOLS
        iLinInd = (iM - 1).*SAp.iNCOLS + iN;
        dVertPos = dFigureHeight - iM.*(SAp.iTHUMBNAILSIZE + SAp.iTITLEHEIGHT + 1) + 1;
        iPosition = [(iN - 1).*(SAp.iTHUMBNAILSIZE + 1) + 2, dVertPos, SAp.iTHUMBNAILSIZE, SAp.iTHUMBNAILSIZE];
        hAxes(iLinInd) = axes('Parent' , hF, 'Units', 'pixels', 'Position', iPosition);
        hImg(iLinInd) = image(zeros(SAp.iTHUMBNAILSIZE, SAp.iTHUMBNAILSIZE, 3), 'Parent', hAxes(iLinInd));
        hText(iLinInd) = uicontrol(...
            'Parent'            , hF, ...
            'Style'             , 'text', ...
            'Units'             , 'pixels', ...
            'Position'          , [iPosition(1), iPosition(2) + iPosition(4), SAp.iTHUMBNAILSIZE, SAp.iTITLEHEIGHT], ...
            'BackgroundColor'   , SAp.dBGCOLOR./2, ...
            'ForegroundColor'   , 'w', ...
            'HorizontalAlign'   , 'left', ...
            'FontUnits'         , 'pixels', ...
            'FontSize'          , 12);
    end
end
axis(hAxes, 'off');
clear iM iN
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Create the info text objects
uicontrol(...
    'Parent'            , hF, ...
    'Style'             , 'text', ...
    'Units'             , 'pixels', ...
    'Position'          , [2, 2, 200, SAp.iINFOBARHEIGHT], ...
    'BackgroundColor'   , SAp.dBGCOLOR, ...
    'ForegroundColor'   , 'w', ...
    'HorizontalAlign'   , 'left', ...
    'FontUnits'         , 'pixels', ...
    'FontSize'          , 14, ...
    'String'            , {'Series Description:', 'Path:', 'Modality:', 'Rows:', 'Columns:', 'Images:', 'Pixel Spacing:'});
hInfoText = uicontrol(...
    'Parent'            , hF, ...
    'Style'             , 'text', ...
    'Units'             , 'pixels', ...
    'Position'          , [202, 2, dFigureWidth - 300, SAp.iINFOBARHEIGHT], ...
    'BackgroundColor'   , SAp.dBGCOLOR, ...
    'ForegroundColor'   , 'w', ...
    'HorizontalAlign'   , 'left', ...
    'FontUnits'         , 'pixels', ...
    'FontSize'          , 14, ...
    'String'            , '');
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Create the two buttons
sPath = fileparts(mfilename('fullpath'));
dI = imread([sPath, filesep, 'icons', filesep, 'ok.png']);
dI = double(dI)./255;
dOKImg = cat(3, dI, dI(:,:,1));
hAOK = axes('Parent', hF, 'Units', 'Pixels', 'Position', [dFigureWidth - 50 3 size(dI, 2) size(dI, 1)]);
dI = dOKImg; dI(:,:,4) = dI(:,:,4).*0.7;
hIOK = image(fBlend(permute(SAp.dBGCOLOR, [1 3 2]), dI, 'normal'), 'Parent', hAOK, 'ButtonDownFcn', @fOKClick);
dI = imread([sPath, filesep, 'icons', filesep, 'abort.png']);
dI = double(dI)./255;
dAbortImg = cat(3, dI, dI(:,:,1));
hAAbort = axes('Parent', hF, 'Units', 'Pixels', 'Position', [dFigureWidth - 100 3 size(dI, 2) size(dI, 1)]);
dI = dAbortImg; dI(:,:,4) = dI(:,:,4).*0.7;
hIAbort = image(fBlend(permute(SAp.dBGCOLOR, [1 3 2]), dI, 'normal'), 'Parent', hAAbort, 'ButtonDownFcn', 'uiresume(gcf);');
axis([hAAbort, hAOK], 'off');
lAbort = false;
lOK = false;
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Create a mask for the selection highlight
dMask = repmat(permute(SAp.dBGCOLOR.*2, [1 3 2]) , [SAp.iTHUMBNAILSIZE, SAp.iTHUMBNAILSIZE, 1]);
dMask = dMask.*repmat(linspace(1, 0.5, SAp.iTHUMBNAILSIZE)', [1, SAp.iTHUMBNAILSIZE, 3]);
dMask = dMask + 0.05*rand(size(dMask));
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Fill the figure
iStartSeries = 1;
iLastSeries = 0;
sResult = 'esc';
iStartPoint = 0;
iStartPos = 0;
fFillPanels();
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Load the scrollbar elements
csGraphics = {'Bar', 'BarEndB', 'BarEndT', 'BarBG', 'BarBGEnd'};

for i = 1:length(csGraphics)
    [dI, temp, dAlpha] = imread([sPath, filesep, 'icons', filesep, csGraphics{i}, '.png']);
    SAp.(['d', csGraphics{i}]) = cat(3, dI, dAlpha);
end
SAp.dBarBGImg = fBlend(SAp.dBGCOLOR, [flipdim(SAp.dBarBGEnd, 1); repmat(SAp.dBarBG, [SAp.iNROWS.*(SAp.iTHUMBNAILSIZE + 1), 1]); SAp.dBarBGEnd], 'normal');

hAPro = axes('Parent', hF, 'Units', 'pixels', 'Position', [SAp.iNCOLS.*(SAp.iTHUMBNAILSIZE + 1) + 2, ...
    SAp.iINFOBARHEIGHT + 3, 12, SAp.iNROWS.*(SAp.iTHUMBNAILSIZE + SAp.iTITLEHEIGHT + 1) - 2]); 
hIPro = image(SAp.dBarBGImg, 'Parent', hAPro);
axis(hAPro, 'off');

iPositions = max([1 ceil(length(SSeries)/SAp.iNCOLS) - SAp.iNROWS + 1]);
iSpace = size(SAp.dBarBGImg, 1) - size(SAp.dBarEndT, 1) - size(SAp.dBarEndB, 1) - 4;
iRepmatLength = ceil(iSpace*min([1.0 SAp.iNROWS/iPositions]));
SAp.dBarImg = padarray([SAp.dBarEndT; repmat(SAp.dBar, [iRepmatLength, 1, 1]); SAp.dBarEndB], [2 0 0], 'both');

fUpdateSlider;
% -------------------------------------------------------------------------

lStartup = false;
uiwait(hF);
% -------------------------------------------------------------------------
% ~~~~~~~~~~~~ A lot of user-friendly GUI interaction going on ~~~~~~~~~~~~
% -------------------------------------------------------------------------


% The GUI was closed in some way, continue execution


% -------------------------------------------------------------------------
% Dialog was aborted: Seriously, who does that?
if strcmp(sResult, 'esc')
    SDataOut = [];
    try delete(hF); end
    return
end
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% No seriers selected? - Return
iInd = find(lSelected);
if isempty(iInd)
    SDataOut = [];
    delete(hF);
    return
end
% -------------------------------------------------------------------------

% -------------------------------------------------------------------------
% Load the selected series
fprintf(1, 'Loading slected DICOM files...');
for i = 1:length(iInd)
    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    % Load the specified images
    SThisData = SSeries(iInd(i)).SData;
    iNImages = length(SThisData);
    iImg = zeros(SThisData(1).Rows, SThisData(1).Columns, iNImages, 'uint16');
    for j = 1:iNImages
        iImg(:,:,j) = dicomread(SThisData(j).sFilename);
    end
    % Now images are in iImg, headers in SThisData
    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    
    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    % Find out the major orientation
    if numel(SThisData(1).ImageOrientationPatient == 6)
        dImageOrientation = reshape(SThisData(1).ImageOrientationPatient, [3, 2])'; % Should be the same for a volume
        dImagePosition = [SThisData.ImagePositionPatient]';

        dOrientIndicator = sum(abs(dImageOrientation));
        [temp, iMinInd] = min(dOrientIndicator); %#ok<ASGLU>
        d3rdDimInd = dImagePosition(:, iMinInd);
        [temp, iSortInd] = sort(d3rdDimInd, 'ascend'); %#ok<ASGLU>
        iImg = iImg(:,:,iSortInd);
        iImageOrientation = round(dImageOrientation);
        switch(iMinInd)
            case 1 % The x coordinate changes least in both directions -> Sagittal
                SDataOut(i).Orientation = 'Sag'; % -> x: a->p=pos, y: h->f=neg
                if iImageOrientation(1, 2) == -1, flipdim(iImg, 2); end % <- y-coordinate was flipped
                if iImageOrientation(2, 3) ==  1, flipdim(iImg, 1); end % <- z-coordinate was flipped

            case 2 % The y coordinate changes least in both directions -> Coronal
                SDataOut(i).Orientation = 'Cor'; % -> x: r->l=pos, y: h->f=neg
                if iImageOrientation(1, 1) == -1, flipdim(iImg, 2); end % <- x-coordinate was flipped
                if iImageOrientation(2, 3) ==  1, flipdim(iImg, 1); end % <- z-coordinate was flipped

            case 3 % The z coordinate changes least in both directions -> Transversal
                SDataOut(i).Orientation = 'Tra'; % -> x: r->l=pos, y: a->p=pos
                if iImageOrientation(1, 1) == -1, flipdim(iImg, 2); end % <- x-coordinate was flipped
                if iImageOrientation(2, 2) == -1, flipdim(iImg, 1); end % <- Y-coordinate was flipped
        end
    end
    SDataOut(i).Img = iImg;
    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        
    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    % Save the most important infos to the ouput variable
    SDataOut(i).SeriesDescriptions = SThisData(1).SeriesDescription;
    SDataOut(i).Aspect = zeros(3, 1);
    SDataOut(i).Aspect(1:2) = SThisData(1).PixelSpacing;
    if isscalar(d3rdDimInd)
        if isempty(SThisData.SliceThickness)
            SDataOut(i).Aspect(3) = [];
        else
            SDataOut(i).Aspect(3) = SThisData.SliceThickness;
        end
    else
        SDataOut(i).Aspect(3)   = abs(d3rdDimInd(2) - d3rdDimInd(1));
    end
    SDataOut(i).ImageOrientation = dImageOrientation;
    SDataOut(i).ImagePosition = dImagePosition(iSortInd, :);
    if nargin == 3
        for j = 1:length(csAdditionalTags) % The additionally requested infos
            sTag = csAdditionalTags{j};
            eval(['xData = [SThisData.', sTag, ']'';']);
            xData = xData(iSortInd);
            eval(['SDataOut(i).', sTag, ' = xData;']);
        end
    end
    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
end
% -------------------------------------------------------------------------
fprintf(1, 'done.\n');

delete(hF);
% =========================================================================
% ***
% *** The 'end' of the DICOMIMPORT main function.
% ***
% =========================================================================


 
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fCloseGUI (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * Closes the figure.
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fCloseGUI(hObject, eventdata) %#ok<*INUSD> eventdata is repeatedly unused
        uiresume(hObject);
        delete(hObject);
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fCloseGUI
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fWindowMouseMoveFcn (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * Displays informations about the series under the mouse cursor in
    % * * the texts at the bottom of the figure.
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fWindowMouseMoveFcn(hObject, eventdata)
        if lStartup, return, end; % Return if called during GUI startup
        
        iAxisInd = fGetAxes();
        iSeriesInd = iAxisInd + iStartSeries -1;
        
        % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        % Return if not over axes or out of data bounds
        if ~iAxisInd
            set(hInfoText, 'String', '');
            dPos = get(hAOK, 'CurrentPoint');
            iPos = get(hAOK, 'Position');
            if dPos(1, 1) > 0 && dPos(1, 2) > 0 && dPos(1, 1) < iPos(3) && dPos(1, 2) < iPos(4)
                if ~lOK
                    dI = dOKImg; dI(:,:,4) = dI(:,:,4);
                    set(hIOK, 'CData', fBlend(permute(SAp.dBGCOLOR, [1 3 2]), dI, 'normal'));
                    lOK = true;
                end
            else
                if lOK
                    dI = dOKImg; dI(:,:,4) = dI(:,:,4).*0.7;
                    set(hIOK, 'CData', fBlend(permute(SAp.dBGCOLOR, [1 3 2]), dI, 'normal'));
                    lOK = false;
                end
            end
            
            dPos = get(hAAbort, 'CurrentPoint');
            iPos = get(hAAbort, 'Position');
            if dPos(1, 1) > 0 && dPos(1, 2) > 0 && dPos(1, 1) < iPos(3) && dPos(1, 2) < iPos(4)
                if ~lAbort
                    dI = dAbortImg; dI(:,:,4) = dI(:,:,4);
                    set(hIAbort, 'CData', fBlend(permute(SAp.dBGCOLOR, [1 3 2]), dI, 'normal'));
                    lAbort = true;
                end
            else
                if lAbort
                    dI = dAbortImg; dI(:,:,4) = dI(:,:,4).*0.7;
                    set(hIAbort, 'CData', fBlend(permute(SAp.dBGCOLOR, [1 3 2]), dI, 'normal'));
                    lAbort = false;
                end
            end
            
            return
        else
            
            if iSeriesInd > length(lSelected),  set(hInfoText, 'String', ''); return, end
            % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            
            % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
            % Print Information
            sText = SSeries(iSeriesInd).SData(1).SeriesDescription;
            if isempty(sText), sText = 'N/A'; end, csText(1) = {sText};
            
            sText = fileparts(SSeries(iSeriesInd).SData(1).sFilename);
            csText(2) = {sText};
            
            sText = SSeries(iSeriesInd).SData(1).Modality;
            if isempty(sText), sText = 'N/A'; end, csText(3) = {sText};
            
            sText = SSeries(iSeriesInd).SData(1).Rows;
            if isempty(sText), sText = 'N/A'; end, csText(4) = {sText};
            
            sText = SSeries(iSeriesInd).SData(1).Columns;
            if isempty(sText), sText = 'N/A'; end, csText(5) = {sText};
            
            sText = num2str(length(SSeries(iSeriesInd).SData));
            if isempty(sText), sText = 'N/A'; end, csText(6) = {sText};
            
            dPixelSpacing =  SSeries(iSeriesInd).SData(1).PixelSpacing;
            if ~isempty(dPixelSpacing)
                sText = sprintf('%2.2f x %2.2f mm', dPixelSpacing(1), dPixelSpacing(2));
            else
                sText = 'N/A';
            end
            csText(7) = {sText};
            
            set(hInfoText, 'String', csText);
            % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        end
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fWindowMouseMoveFcn
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fWindowButtonDownFcn (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * Starting callback for mouse button actions. Manage the selection
    % * * of the series.
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fWindowButtonDownFcn(hObject, eventdata)
        iPanelInd = fGetAxes();
        iSeriesInd = iPanelInd + iStartSeries - 1;
        
        if ~iPanelInd
            dPos = get(hAPro, 'CurrentPoint');
            iPos = get(hAPro, 'Position');
            if dPos(1, 1) > 0 && dPos(1, 2) > 0 && dPos(1, 1) < iPos(3) && dPos(1, 2) < iPos(4)
                iStartPoint = get(hF, 'CurrentPoint');
                set(hF, 'WindowButtonMotionFcn', @fBarMove);
            end
            
        else
            
            if iSeriesInd > length(lSelected), return, end
            
            switch get(hF, 'SelectionType')
                
                % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                % Left mouse button without any key
                case 'normal'
                    iNSelected = sum(lSelected);
                    lState = lSelected(iSeriesInd);
                    lSelected = false(size(lSelected));
                    lSelected(iSeriesInd) = ~lState || iNSelected > 1;
                    if ~lState, iLastSeries = iSeriesInd; end
                    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                    
                    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                    % Right mouse button/shift key
                case 'extend'
                    if iLastSeries
                        iInd = sort([iLastSeries, iSeriesInd], 'ascend');
                        lSelected(iInd(1):iInd(2)) = true;
                    else
                        lSelected(iSeriesInd) = true;
                        iLastSeries = iSeriesInd;
                    end
                    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                    
                    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                    % Middle mouse button/alt key
                case 'alt'
                    lState = lSelected(iSeriesInd);
                    lSelected(iSeriesInd) = ~lState;
                    if ~lState, iLastSeries = iSeriesInd; end
                    % - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                    
            end
            fFillPanels;
        end
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fWindowButtonDownFcn
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fWindowButtonUpFcn (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * Restore defaults after mouse drag action.
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fWindowButtonUpFcn(hObject, eventdata)
        set(hF, 'WindowButtonMotionFcn', @fWindowMouseMoveFcn);
        fUpdateSlider;
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fWindowButtonUpFcn
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fBarMove (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * The draging callback of the slider.
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fBarMove(hObject, eventdata)
        if length(SSeries) <= SAp.iNCOLS*SAp.iNROWS, return, end
        
        iMaxPos = size(SAp.dBarBGImg, 1) - size(SAp.dBarImg, 1);
        
        iD = iStartPoint - get(hF, 'CurrentPoint');
        iPos = iStartPos + iD(2);
        
        if iPos < 0, iPos = 0; end
        if iPos > iMaxPos, iPos = iMaxPos; end
        dI = padarray(SAp.dBarImg, [iPos 0 0], 'pre');
        dI = padarray(dI, [size(SAp.dBarBGImg, 1) - size(dI, 1) 0 0], 'post');
        set(hIPro, 'CData', fBlend(SAp.dBarBGImg, dI, 'normal'));
                    
        iStartRow = round(iPos/iMaxPos * (iPositions - 1));
        iStartSeries = iStartRow.*SAp.iNCOLS + 1;
        fFillPanels;
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fBarMove
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fKeyPressFcn (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * Callback for keyboard actions.
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fKeyPressFcn(hObject, eventdata) %#ok<INUSL>
    
        switch eventdata.Key

            case 'uparrow'
                iStartSeries = max([1, iStartSeries - SAp.iNCOLS]);
                fFillPanels;

            case 'downarrow'
                if iStartSeries + SAp.iNROWS.*SAp.iNCOLS <= length(lSelected),
                    iStartSeries = iStartSeries + SAp.iNCOLS;
                end
                
            case 'return' % Return and load the selected series
                sResult = 'OK';
                uiresume(hF);
                    
            case 'escape' % Return and do nothing
                uiresume(hF);
                
        end % switch
        
        fFillPanels;
        fUpdateSlider;
        fWindowMouseMoveFcn(hF, []); % Make sure the info at the bottom is valid

    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fKeyPressFcn
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fOKClick (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * Callback of the OK button
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fOKClick(hObject, eventdata)
        sResult = 'OK';
        uiresume;
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fOKClick
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fWindowScrollWheelFcn (nested in DicomImport)
    % * * 
    % * * Figure callback
    % * *
    % * * Callback for keyboard actions.
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fWindowScrollWheelFcn(hObject, eventdata)
        if eventdata.VerticalScrollCount > 0
            SEData.Key = 'downarrow';
        else
            SEData.Key = 'uparrow';
        end
        fKeyPressFcn(hObject, SEData); % This is the laszy way: just call the other callback
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fWindowScrollWheelFcn
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
       
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * *
    % * * NESTED FUNCTION fFillPanels (nested in DicomImport)
    % * *
    % * * Display the current data in all axes.
	% * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fFillPanels()
        for iI = 1:length(hImg);
            iSeriesInd = iI + iStartSeries - 1;
            if iSeriesInd > length(lSelected)
                set(hImg(iI), 'CData', dMask./3);
                set(hText(iI), 'String', '<no data>');
            else
                dImg = repmat(iThumbnails(:,:,iSeriesInd), [1 1 3]);
                if lSelected(iSeriesInd),  dImg = 1 - ((1 - dImg).*(1 - SAp.dOPACITY.*dMask)); end
                set(hImg(iI), 'CData', dImg);
                sTitle = SSeries(iSeriesInd).SData(1).SeriesDescription;
                if length(sTitle) > 22, sTitle = [sTitle(1:21), '...']; end
                if isempty(sTitle), sTitle = '<no description>'; end
                set(hText(iI), 'String', sprintf('[%u]: %s', iSeriesInd, sTitle));
            end
        end
        drawnow expose
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fFillPanels
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * *
    % * * NESTED FUNCTION fGetAxes (nested in DicomImport)
    % * *
    % * * Determine the axes number under the mouse cursor. Returns 0 if
    % * * not over an axes at all.
	% * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function iPanelInd = fGetAxes()
        iCursorPos = get(hF, 'CurrentPoint');
        iPanelInd = uint8(0);
        for iI = 1:length(hAxes)
            dPos = get(hAxes(iI), 'Position');
            if ((iCursorPos(1) >= dPos(1)) && (iCursorPos(1) < dPos(1) + dPos(3)) && ...
                (iCursorPos(2) >= dPos(2)) && (iCursorPos(2) < dPos(2) + dPos(4)))
                iPanelInd = uint8(iI);
            end
        end
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fGetAxes
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * *
    % * * NESTED FUNCTION fGetDicomFiles (nested in DicomImport)
    % * *
    % * * Finds and creates a database of DICOM files
	% * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function SD = fGetDicomFiles(sFolder, csTags, lRecursive)
        SD = [];
        SDir = dir(sFolder);
        for iI = 1:length(SDir)
            sName = SDir(iI).name;
                        
            if (SDir(iI).isdir) && (lRecursive)
                if sName(1) == '.', continue, end
                SD = [SD, fGetDicomFiles([sFolder, filesep, SDir(iI).name], csTags, lRecursive)];
            else
                sFilename = [sFolder, filesep, SDir(iI).name];
                try
                    SHeader = dicominfo(sFilename);
                catch %#ok<CTCH>
                    continue
                end
                SRecord = [];
                for iJ = 1:length(csTags)
                    sTag = csTags{iJ};
                    try
                        eval(['SRecord.', sTag, ' = SHeader.', sTag, ';']);
                    catch %#ok<CTCH>
                        eval(['SRecord.', sTag, ' = [];']);
                    end
                end
                if isfield(SHeader, 'SeriesInstanceUID')
                    SRecord.iHash = fHash([sFolder, SHeader.SeriesInstanceUID]);
                    SRecord.sFilename = sFilename;
                    SD = [SD, SRecord];
                end
            end
        end
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fGetDicomFiles
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =



    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * *
    % * * NESTED FUNCTION fHash (nested in DicomImport)
    % * *
    % * * Return a hash number
	% * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function iHashVal = fHash(sString)
        dString = double(sString);
        dHashVal = 5381;
        for iI = 1:length(dString)
            dHashVal = mod(dHashVal * 33 + dString(iI), 2.^32 - 1);
        end
        iHashVal = uint32(dHashVal);
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fHash
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * *
    % * * NESTED FUNCTION fUnique (nested in DicomImport)
    % * *
    % * * Returns unique values in order of appearance
	% * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function xUniqueVals = fUnique(xArray)
        xUniqueVals = zeros(length(xArray), 1, class(xArray));
        iInd = 0;
        while ~isempty(xArray)
            iInd = iInd + 1;
            xUniqueVals(iInd) = xArray(1);
            xArray(xArray == xArray(1)) = [];
        end
        xUniqueVals = xUniqueVals(1:iInd);
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fUnique
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * *
    % * * NESTED FUNCTION fUpdateSlider (nested in DicomImport)
    % * *
    % * * Update the slider position
	% * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function fUpdateSlider
        iSpace = size(SAp.dBarBGImg, 1) - size(SAp.dBarImg, 1);
        iStartRow = (iStartSeries - 1)/SAp.iNCOLS;
        if iPositions == 1
            iStartPos = 0;
        else
            iStartPos = floor(iStartRow.*iSpace./(iPositions - 1));
        end
        dI = padarray(SAp.dBarImg, [iStartPos 0 0], 'pre');
        dI = padarray(dI, [size(SAp.dBarBGImg, 1) - size(dI, 1) 0 0], 'post');
        set(hIPro, 'CData', fBlend(SAp.dBarBGImg, dI, 'normal'));
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fUpdateSlider
	% = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fBlend (nested in DicomImport)
    % * *
    % * * Blend two images with alpha map
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function dOut = fBlend(dBot, dTop, sMode, dAlpha)
        
        % Parse the inputs
        if nargin < 4, dAlpha = 1.0; end % Top is fully opaque
        if nargin < 3, sMode = 'overlay'; end
        if nargin < 2, error('At least 2 input arguments required!'); end
        if isa(dBot, 'uint8'), dBot = double(dBot)./255; end
        if isa(dTop, 'uint8'), dTop = double(dTop)./255; end
        
        % Check Inputs
        dTopSize = [size(dTop, 1), size(dTop, 2), size(dTop, 3), size(dTop, 4)];
        
        % Check if background is monochrome
        if numel(dBot) == 1 % grayscale background
            dBot = dBot.*ones(dTopSize);
        end
        if numel(dBot) == 3 % rgb background color
            dBot = repmat(permute(dBot(:), [2 3 1]), [dTopSize(1), dTopSize(2), 1, dTopSize(4)]);
        end
        
        dBotSize = [size(dBot, 1), size(dBot, 2), size(dBot, 3), size(dBot, 4)];
        if dBotSize(3) ~= 1 && dBotSize(3) ~= 3, error('Bottom layer must be either grayscale or RGB!'); end
        if dTopSize(3) > 4, error('Size of 3rd top layer dimension must not exceed 4!'); end
        if any(dBotSize(1, 2) ~= dTopSize(1, 2)), error('Size of image data does not match'); end
        
        if dBotSize(4) ~= dTopSize(4)
            if dBotSize(4) > 1 && dTopSize(4) > 1, error('4th dimension of image data mismatch!'); end
            
            if dBotSize(4) == 1, dBot = repmat(dBot, [1, 1, 1, dTopSize(4)]); end
            if dTopSize(4) == 1, dTop = repmat(dTop, [1, 1, 1, dBotSize(4)]); end
        end
        
        % Handle the alpha map
        if dTopSize(3) == 2 || dTopSize(3) == 4 % Alpha channel included
            dAlpha = dTop(:,:,end, :);
            dTop   = dTop(:,:,1:end-1,:);
        else
            if isscalar(dAlpha)
                dAlpha = dAlpha.*ones(dTopSize(1), dTopSize(2), 1, dTopSize(4));
            else
                dAlphaSize = [size(dAlpha, 1), size(dAlpha, 2), size(dAlpha, 3), size(dAlpha, 4)];
                if any(dAlphaSize(1:2) ~= dTopSize(1:2)), error('Top layer alpha map dimension mismatch!'); end
                if dAlphaSize(3) > 1, error('3rd dimension of alpha map must have size 1!'); end
                if dAlphaSize(4) > 1
                    if dAlphaSize(4) ~= dTopSize(4), error('Alpha map dimension mismatch!'); end
                else
                    dAlpha = repmat(dAlpha, [1, 1, 1, dTopSize(4)]);
                end
            end
        end
        
        % Bring data into the right format
        dMaxDim = max([size(dBot, 3), size(dTop, 3)]);
        if dMaxDim > 2, lRGB = true; else lRGB = false; end
        
        if lRGB && dBotSize(3) == 1, dBot = repmat(dBot, [1, 1, 3, 1]); end
        if lRGB && dTopSize(3) == 1, dTop = repmat(dTop, [1, 1, 3, 1]); end
        if lRGB, dAlpha = repmat(dAlpha, [1, 1, 3, 1]); end
        
        % Check Range
        dBot = fCheckRange(dBot);
        dTop = fCheckRange(dTop);
        dAlpha = fCheckRange(dAlpha);
        
        % Do the blending
        switch lower(sMode)
            case 'normal',      dOut = dTop;
            case 'multiply',    dOut = dBot.*dTop;
            case 'screen',      dOut = 1 - (1 - dBot).*(1 - dTop);
            case 'overlay'
                lMask = dBot < 0.5;
                dOut = 1 - 2.*(1 - dBot).*(1 - dTop);
                dOut(lMask) = 2.*dBot(lMask).*dTop(lMask);
            case 'hard_light'
                lMask = dTop < 0.5;
                dOut = 1 - 2.*(1 - dBot).*(1 - dTop);
                dOut(lMask) = 2.*dBot(lMask).*dTop(lMask);
            case 'soft_light',  dOut = (1 - 2.*dTop).*dBot.^2 + 2.*dTop.*dBot; % pegtop
            case 'darken',      dOut = min(cat(4, dTop, dBot), [], 4);
            case 'lighten',     dOut = max(cat(4, dTop, dBot), [], 4);
            otherwise,          error('Unknown blend mode ''%s''!', sMode);
        end
        dOut = dAlpha.*dOut + (1 - dAlpha).*dBot;
        
        dOut(dOut > 1) = 1;
        dOut(dOut < 0) = 0;
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fBlend
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    
    
    
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * NESTED FUNCTION fCheckRange (nested in DicomImport)
    % * *
    % * * Checks for fundamental range of data and clips if necessary
    % * *
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    function dData = fCheckRange(dData)
        if any(dData(:) > 1) || any(dData(:) < 0)
            dData(dData < 0) = 0;
            dData(dData > 1) = 1;
        end
    end
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    % * * END NESTED FUNCTION fCheckRange
    % = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

end

Contact us