Code covered by the BSD License  

Highlights from
3D Volume Visualization

image thumbnail

3D Volume Visualization

by

 

22 Jun 2012 (Updated )

3D volume viz with interactive slice selection, colored ortho-planes, windowing, colormap selection.

vis3d(seg, mycmap, figName)
%%vis3d(img, mycmap): a GUI-enabled visualization of the 3D volume img,
%%with an optional colormap argument.  

%{
Joshua Stough
JHU, iacl
June 2012
April 2013

BSD Licence:
Copyright (c) 2013, Joshua Stough
All rights reserved.

Please comment on MATLAB fileexchange if you find this useful. Thank you.

This program offers a visualization of 3D volumes.

Obviously, this is not a new idea. This program might offer over other
programs:
    -change slice via click drag or scroll
    -orthogonal planes are visualized on each projection in a color-coded
    manner, allowing a better sense of where you are in the 3D space.
    -windowing capability
    -changing the colormap
    -neurological coordinate frame

However, after seeing the slice function, I think I must be missing a lot
of built-in functionality, making this program pointless. But I share in any
event as I obviously cannot see all ends (nerd).

References to really good and thoughtful 3D viewer implementations in
matlab (including buttons and all axis flipping that I can't do):
http://www.mathworks.com/matlabcentral/fileexchange/2256-orthoview
http://www.mathworks.com/matlabcentral/fileexchange/764-sliceomatic

For some 3d matrix of double or uint8, call
>> vis3d(img);
or, if you have some multilabel segmentation image, try:
>> vis3d(seg, 'multi');
or, if you want define your own colormap, you can send that instead:
>> mymap = colormap('lines');
>> vis3d(seg, mymap);
lastly, you can customize the figure title:
>> vis3d(seg, '', 'My Figure Title');

Updates/Modifications:
4/13: 
-fixed slice number issue: the out-of-plane slice number was incorrect 
in the initial window.
-changed interaction: now, scrolling changes slice number 
-fixed the 3d volume visualization errors arising from the treatment of 
images versus matrices in matlab.
-fixed tick mark misrepresentation.
-added windowing on the colormap
-added choice of colormap in addition to typical maps.
%}

function [] = vis3d(seg, mycmap, figName)

    %We check for the colormap after we have a figure open.
    if nargin < 3
        figName = 'vis3d: click and scroll/drag vertically (up=closer to eye).'; 
        userSpecFigName = false; %did the user specify the figure name.
    else
        userSpecFigName = true;
    end
    %arg 2 taken care of below.
    

    if ~(isa(seg, 'double') || isa(seg, 'uint8'))
        fprintf('The image is not double or uint8.  You may receive many warnings.\n');
        fprintf('Cast your image to either before sending here.');
    end
   %seg = seg/max(seg(:)); %Make 0-1. 
    segRange = [min(seg(:)) max(seg(:))];
    origSegRange = segRange;
    %segRange is required to ensure that if you view a slice with more or
    %less range, you get colors consistent with other slices.
    
    sizeSeg = size(seg);
    sn = round(sizeSeg/2);
    %slice numbers for the three projections.
    
    titleLines = {'YX view: ', 'YZ view: ', 'XZ view: '};
    %titleColors = {'b', 'r', 'g'};
    titleColors = {[.3 .3 .8], [.8 .3 .3], [.3 .8 .3]};
    
    %The figure handle, with its callbacks.
    mainHandle = figure('Position', [50 50 800 800], ...
        'WindowButtonDownFcn', @buttonDownCallback, ...
        'WindowButtonUpFcn', @buttonUpCallback, ...
        'Name', figName); hold on;
    
    
   
    
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %Adding colormap popupmenu
    colormapText = uicontrol('Style', 'text', 'Position', [300 0 100, 20], ...
        'FontSize', 12, 'FontWeight', 'bold');
    set(colormapText, 'String', 'Colormap:', 'ForegroundColor', [1 1 1]);
    
    
    colormapPopup = uicontrol('Style', 'popupmenu', ...
        'Position', [400 0 200 20], ...
        'Callback', @specifyColormap);
    set(colormapPopup, 'String', {'gray'; 'multi-label'; 'jet'}, ...
        'Value', 1, 'FontSize', 12, 'FontWeight', 'bold');
    
    %define the colormaps.
    %A colormap I find useful for multi-label segmentation images, which is
    %why I made this thing to begin with (and why the image is called seg).
    multi = [0 0 0;
            1 0 1;
            0 .7 1;
            1 0 0;
            .3 1 0;
            0 0 1;
            1 1 1;
            1 .7 0];
    graymap = colormap('gray');
    jetmap = colormap('jet');
    the_cmaps = {graymap; multi; jetmap};
    
    %Is there a user-defined map?
    %Now that we're sure to have a figure open, get a colormap.
    if nargin < 2 || strcmp(mycmap, '')
        mycmap = gray;
    elseif strcmp(mycmap, 'multi')
        mycmap = multi;
        set(colormapPopup, 'Value', 2);
    else
        %User defined a colormap to use.  Set it as current.
        the_cmaps{end+1} =  mycmap;
        set(colormapPopup, 'String', {'gray'; 'multi-label'; 'jet'; 'user-spec'}, ...
        'Value', 4);
    end
    %End of colormap popup menu.
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    
    
    
    
    %flip the matrix to better organize views. This is memory-stupid!!!
    %Please help!
    xyview = seg;
    xzview = seg;
    yzview = seg;
    
    xyview = flipdim(xyview, 1);
    
    xzview = flipdim(xzview, 1);
    xzview = permute(xzview, [3 2 1]);
    %xzview = flipdim(xzview, 2);
    
    yzview = permute(yzview, [1 3 2]);
    yzview = flipdim(yzview, 1);
    
    
    %Now, we plot the three orthogonal slices and try to get the two lines
    %on each slice, which represent the other planes.
    %Notice we are using axis xy, so that the tick marks are correct, that
    %also means we don't need to flip z in yz and xz views...
    handles{1} = subplot(2,2,3); 
    imHandles{1} = imagesc(squeeze(xyview(:,:,sn(3))), segRange); colormap(mycmap); axis image; axis xy;
    %set(handles{1}, 'LooseInset', get(gca,'TightInset')) %made smaller...
    titleHandles{1} = title(handles{1}, 'XY view', 'Color', titleColors{1}, 'FontSize', 14, 'FontWeight', 'bold');
    

    handles{2} = subplot(2,2,1); 
    imHandles{2} = imagesc(squeeze(yzview(sn(1),:,:)), segRange); colormap(mycmap); axis image; axis xy;
    titleHandles{2} = title(handles{2}, 'YZ view', 'Color', titleColors{2}, 'FontSize', 14, 'FontWeight', 'bold');
    %set(handles{2}, 'XDir', 'rev'); 
    %I used reverse here to have appropriate neurological coord, but then I
    %thought, people like to see 'through the eyes of the patient' in
    %coronal (i.e., patient l-r equals l-r in the image).
    
    handles{3} = subplot(2,2,2); 
    imHandles{3} = imagesc(squeeze(xzview(:,sn(2),:)), segRange); colormap(mycmap); axis image; axis xy;
    titleHandles{3} = title(handles{3}, 'XZ view', 'Color', titleColors{3}, 'FontSize', 14, 'FontWeight', 'demi');
    
    
    
    %Making sure the titles all have the initial slice number as well.
    offsliceCoord = [3 1 2];
    for i = 1:3
        set(titleHandles{i}, 'String', sprintf('%s%03d', titleLines{i}, sn(offsliceCoord(i))));
    end
    
    
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %lower and upper bound on the intensity window and text box.
    
    %Adding uicontrol elements would remove the toolbar from the figure
    %without this statement.
    set(mainHandle, 'Toolbar', 'figure');
    
    IntWinSliderHandle{1} = uicontrol('Style', 'slider', ...
        'Position', [0 0 200 20], ...
        'Callback', @IntWinBound);
    set(IntWinSliderHandle{1}, 'Value', segRange(1));
    
    IntWinSliderHandle{2} = uicontrol('Style', 'slider', ...
        'Position', [0 25 200 20], ...
        'Callback', @IntWinBound);
    set(IntWinSliderHandle{2}, 'Value', segRange(2));
    
    for i = 1:2
        set(IntWinSliderHandle{i}, 'Min', segRange(1));
        set(IntWinSliderHandle{i}, 'Max', segRange(2));
    end
    
    IntWinTextBound{1} = uicontrol('Style', 'edit', ...
        'Position', [210 0 60 20], ...
        'Callback', @defineBound);
    set(IntWinTextBound{1}, 'String', segRange(1));
    
    IntWinTextBound{2} = uicontrol('Style', 'edit', ...
        'Position', [210 25 60 20], ...
        'Callback', @defineBound);
    set(IntWinTextBound{2}, 'String', segRange(2));
    
    
    IntWinText = uicontrol('Style', 'text', 'Position', [0 50 210 15], ...
        'FontSize', 12, 'FontWeight', 'bold');
    
    set(IntWinText, 'String', 'Windowing', ...
            'ForegroundColor', [1 1 1]);
    %End of intensity windowing elements.
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    
    
    
    
    
    
    
    
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %And the 3D view.
    handles{4} = subplot(2,2,4);
    
    %We're using the slice visualization function here, but it's screwy
    %because the x and y are transposed and axes must be flipped for the
    %appropriate perspective, 
    
    %I wanted the callback to avoid duplicating code, but the camera position
    %is hard to get before slice is called...buttonUpCallback([]); so here
    %is init.
    %due to confusing coordinates for the slice function, we need to
    %permute and flip dimensions...
    visSeg = seg;
    visSeg = permute(visSeg, [2 1 3]);
    %visSeg = flipdim(visSeg, 1);
    %visSeg = flipdim(visSeg, 2);
    flippedx = inline(sprintf('%d - x + 1', size(visSeg, 2))); 
    %The 2nd dimension of the matrix ends up being 'called' x by matlab.
    %wtf?
    
    
    hslc=slice(visSeg, flippedx(sn(1)) ,sn(2),sn(3));
    axis equal; axis vis3d; set(hslc(1:3),'LineStyle','none');
    xlabel 'p-a' ;ylabel 'l-r' ;zlabel 'i-s';
    %set(gca, 'XDir', 'reverse'); 
    %The above XDir didn't work, without screwing up the data.  So, I'll
    %just change the labels...WAIT!: that's wrong because the slice numbers
    %become wrong then.  Have to flip...
    myx = get(gca, 'XTickLabel');
    set(gca, 'XTickLabel', flippedx(str2num(myx)));
    
    title 'Manually enable/disable Rotate3D.'
    %To maintain the 3D perspective throughout.
    camPos3D = get(handles{4}, 'CameraPosition');
    

    %Too many problems trying to regain control from the rotate3d.
    %rotHandle = rotate3d(handles{4}); so it's up to the user.
    rotHandle = rotate3d;
    setAllowAxesRotate(rotHandle, handles{1}, false);
    setAllowAxesRotate(rotHandle, handles{2}, false);
    setAllowAxesRotate(rotHandle, handles{3}, false);
    %End of the 3D view elements
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    
    
    
    
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %Generate the lines representing the orthogonal planes.  This part is 
    %so confusing, in part because line will use the bottom
    %left, whereas we're thinking of the top left (image) as being 0,0.
    %But it's mostly my fault, because coordinate systems are so HARD...
    
    lines{1}.x = [1 sizeSeg(2); sn(2) sn(2)]'; 
    lines{1}.y = [sn(1) sn(1); 1 sizeSeg(1)]';
    %the x coords of the horiz and vertical line respectively.
    %the y coords of the horiz and vertical line respectively.
    
    lines{2}.x = [1 sizeSeg(2); sn(2) sn(2)]';
    lines{2}.y = [sn(3) sn(3); 1 sizeSeg(3)]';
    
    lines{3}.x = [1 sizeSeg(1); sn(1) sn(1)]';
    lines{3}.y = [sn(3) sn(3); 1 sizeSeg(3)]';
    
    %We now instantiate the lines.  We also want the line colors to reflect
    %the plane they represent, which is given by the titleColors. The trick
    %is weird though: when we're visualizing the XY plane, the two lines on
    %it are the XZ plane (vertical), and the YZ plane, horizontal.  whew.
    %for each, first is horizontal, next is vertical.
    lineHandles = {};
    subplot(handles{1});
    lineHandles{1}(1) = line(lines{1}.x(:,1), lines{1}.y(:,1), 'Color', titleColors{2}, 'LineWidth', 1);
    lineHandles{1}(2) = line(lines{1}.x(:,2), lines{1}.y(:,2), 'Color', titleColors{3}, 'LineWidth', 1);
    
    subplot(handles{2});
    lineHandles{2}(1) = line(lines{2}.x(:,1), lines{2}.y(:,1), 'Color', titleColors{1}, 'LineWidth', 1);
    lineHandles{2}(2) = line(lines{2}.x(:,2), lines{2}.y(:,2), 'Color', titleColors{3}, 'LineWidth', 1);
    
    subplot(handles{3});
    lineHandles{3}(1) = line(lines{3}.x(:,1), lines{3}.y(:,1), 'Color', titleColors{1}, 'LineWidth', 1);
    lineHandles{3}(2) = line(lines{3}.x(:,2), lines{3}.y(:,2), 'Color', titleColors{2}, 'LineWidth', 1);
    %End of the line crap.  This is really confusing.
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    
    
    
    

    %Variables to keep track of mouse motion and which slice is being
    %maniupulated. 
    lastPoint = [];
    whichView = 1;
    
    %At this point, time to define our callbacks.
    
    %This is the callback when the user clicks.  We want to determine which
    %if any projection they clicked on so that further vertical motion can
    %be associated with changing the slice for that projection. 
    function buttonDownCallback(varargin)
        
        p = get(gca, 'CurrentPoint');
        lastPoint = [p(1); p(3)];
        
        %We know where they clicked with respect to the last active axes,
        %determine which axes that was.
        whichView = find(cell2mat(handles) == gca);
        
        if whichView == 4 
            %Can't do anthing with the 3D view, unless it's rotate
            %selection.  The user has to do it themselves.
            %set(rotHandle, 'Enable', 'on', 'ActionPostCallback', @buttonUpOnRotateCallback);
            return
        end
        
        %By getting the camera position on a click, we can maintain the 3d
        %perspective in the case of changing slices.
        camPos3D = get(handles{4}, 'CameraPosition');
        
        %Now determine if they clicked on a valid spot, within the coords
        %of the chosen image/axes.
        xlim = get(gca, 'XLim');
        ylim = get(gca, 'YLim');
        if lastPoint(1) >= xlim(1) && lastPoint(1) <= xlim(2) && ...
                lastPoint(2) >= ylim(1) && lastPoint(2) <= ylim(2)
            %They clicked on a valid point, time to activate the motion
            %callback. Also let's tell them the intensity at the point they
            %click. But we also don't want to remove what they decided to
            %call the main figure. 
            t_img = get(imHandles{whichView}, 'CData');
            p = round(p);
            if userSpecFigName
                set(mainHandle, 'Name', sprintf('%s: Pos: %d %d, Int: %f', ...
                    figName, p(3), p(1), t_img(p(3), p(1))));
            else
                set(mainHandle, 'Name', sprintf('Pos: %d %d, Int: %f', ...
                    p(3), p(1), t_img(p(3), p(1))));
            end
            set(mainHandle, 'WindowButtonMotionFcn', @dragCallback);
            set(mainHandle, 'WindowScrollWheelFcn', @scrollCallback);
            %fprintf('You clicked on %f, %f on gca %f (%d).\n', p(1), p(3), gca, whichView);
        %else
            %fprintf('Oops, you did''t click flippedx(sn(1))a valid point.\n');
        end
    end

    %At this point, it's established that the user clicked on one of the
    %images and is currently dragging.  We will change the image on the
    %relevant slice and update slice info (in the title). 
    %And then, if this works, I'll try the red lines on the other two
    %projections.
    function dragCallback(varargin)
        p = get(gca, 'CurrentPoint');
        motionV = round([p(1); p(3)] - lastPoint);
        if abs(motionV(2)) < 2
            return
        end %Wait until the motion is noticeable.
        
        lastPoint = [p(1); p(3)];
        
        
        newslice = sn(offsliceCoord(whichView)) + round(motionV(2)/2);
        
        updateSliceViz(newslice);
  
    end

    function buttonUpCallback(varargin)
        %Nullify the motion and correct the 3D view.
        %Slice is too easy though, thanks Matlab.  I should be ashamed
        %http://www.mathworks.com/matlabcentral/fileexchange/2256-orthoview
        set(mainHandle, 'WindowButtonMotionFcn', '');
        
        axes(handles{4}); hslc=slice(visSeg, flippedx(sn(1)), sn(2), sn(3));%rotate3d on;
        axis equal; axis vis3d; set(hslc(1:3),'LineStyle','none');
        xlabel 'p-a' ;ylabel 'l-r' ;zlabel 'i-s';
        
        %Same stupid and incorrect label issue.
        myx = get(gca, 'XTickLabel');
        set(gca, 'XTickLabel', flippedx(str2num(myx)));
    
        title 'Manually enable/disable Rotate3D.'
        set(handles{4}, 'CameraPosition', camPos3D);
    end

    %I had problems with 'ActionPostCallback' and so I'm leaving rotation
    %of the 3d plot up to the user clicking on and off the button.
    %function buttonUpOnRotateCallback(varargin)
    %    set(rotHandle, 'ActionPostCallback', '');
    %    set(rotHandle, 'Enable', 'off');
    %end
    
    function scrollCallback(src,evnt)
        %Change whichView's slice by the vertical motion.
        offsliceCoord = [3 1 2];
        %if they're moving xy slice, that's a change in z, etc.
        
        if whichView == 4
            return
        end
        
        if evnt.VerticalScrollCount > 0
            newslice = sn(offsliceCoord(whichView)) - 1;
        else
            newslice = sn(offsliceCoord(whichView)) + 1;
        end
        
        updateSliceViz(newslice);
        
    end


    function updateSliceViz(newslice)
        %Change whichView's slice by the vertical motion.
        
        offsliceCoord = [3 1 2];
        %if they're moving xy slice, that's a change in z, etc.
        
        
        if newslice > 0 && newslice <= sizeSeg(offsliceCoord(whichView))
            sn(offsliceCoord(whichView)) = newslice;
        end
        subplot(handles{whichView});
        
        %This is a tough to comprehend part.  I want to move the lines
        %representing the current plane in the other orthogonal views.
        %I'll be changing line properties accordingly.
        if whichView == 1
            %XY view (axial)
            %imagesc(squeeze(seg(:,:,sn(3))), segRange); axis image; hold on
            %While you don't have to respecify the colormap, you do need to
            %remind imagesc of the appropriate range and ensure the axes
            %are good. In fact, I forgot that with matlab, everything is an
            %object.  Just change the data in the object.
            set(imHandles{1}, 'CData', squeeze(xyview(:,:,sn(3))));
            
            %Moving in z, so change the horizontal lines in the YZ and XZ
            %plots.
            set(lineHandles{2}(1), 'YData', [sn(3) sn(3)]');
            set(lineHandles{3}(1), 'YData', [sn(3) sn(3)]');
            
        elseif whichView == 2
            %YZ view (coronal)
            set(imHandles{2}, 'CData', squeeze(yzview(sn(1),:,:)));
            %imagesc(squeeze(seg(sn(1),:,:)), segRange); axis image
            
            %Moving in x, so change the horizontal line in the XY 
            %(representing front-back) and the vertical in and XZ.
            %plots
            set(lineHandles{1}(1), 'YData', [sn(1) sn(1)]');
            set(lineHandles{3}(2), 'XData', [sn(1) sn(1)]');
        else
            %XZ view (sagittal)
            set(imHandles{3}, 'CData', squeeze(xzview(:,sn(2),:)));
            %imagesc(squeeze(seg(:,sn(2),:)), segRange); axis image 
            
            %Moving in y, so change the vertical line in plot 1 and
            %horizontal line in plot 2.
            set(lineHandles{1}(2), 'XData', [sn(2) sn(2)]');
            set(lineHandles{2}(2), 'XData', [sn(2) sn(2)]');
        end
        %title(handles{whichView}, strcat(titleLines{whichView}, num2str(sn(offsliceCoord(whichView)))),...
        %    'Color', titleColors{whichView});
        %Why recreate the title if all we really want to do is change the
        %string.
        set(titleHandles{whichView}, 'String', ...
            sprintf('%s%03d', titleLines{whichView}, sn(offsliceCoord(whichView))));
    end





    function IntWinBound(varargin)
        %Here we perform the linear intensity windowing on the images.
        %First, determine the window selected by the user and update the
        %text appropriately.
        
        for whichSlider = 1:2
            slider_value = get(IntWinSliderHandle{whichSlider}, 'Value');
            segRange(whichSlider) = slider_value;
            set(IntWinTextBound{whichSlider}, 'String', segRange(whichSlider));
        end
        

        %Now, change the CLim of the four axes.  I couldn't find how to get
        %at just the axes (which have CLim defined), because subplot
        %apparently never returned a handle for my handles cell array.
        %Anyway, looping through the main figure's children did the trick.
        %(For now anyway, until I start using uipanels and such).
        for c = get(mainHandle, 'Children')'
            try
                set(c, 'CLim', segRange);
            catch exception
                continue
            end
        end
        
    end

    function defineBound(varargin)
        for whichBound = 1:2
            bound_str = get(IntWinTextBound{whichBound},'String');
            if strcmp(bound_str, 'max')
                bound_value = max(origSegRange);
            elseif strcmp(bound_str, 'min')
                bound_value = min(origSegRange);
            else
                bound_value = str2double(bound_str);
            end
            set(IntWinTextBound{whichBound},'String', num2str(bound_value));
            segRange(whichBound) = bound_value;
        end
        
        for c = get(mainHandle, 'Children')'
            try
                set(c, 'CLim', segRange);
            catch exception
                continue
            end
        end
    end

    function specifyColormap(hObject, ~)
        choice = get(hObject, 'Value');
        
        colormap(the_cmaps{choice});
        
        
    end




end

Contact us