Code covered by the BSD License  

Highlights from
Photo Quadrat Selection Tool

image thumbnail

Photo Quadrat Selection Tool

by

 

08 Jun 2012 (Updated )

An interactive tool for manually selecting photo quadrats from a collection of images.

quadratSelection_OneImage(imname, figToKill, quadratSize)
%%function [] = quadratSelection_OneImage(imname, figToKill): Select four corners of
%%the shown image (clockwise from lower left), then click and drag the
%%corners to affect the shown quadrat. Press s to save the quadrat and
%%q to quit the function. This file should be located within the folder of
%%images. It's easier string manipulations with the filenames this way. The
%%figToKill is a non-visible figure that this program closes when it's
%%done.  It was the most straightforward way for me to be done with this
%%function and move on to the next image automatically.  See
%%quadratSelection_AllImages2 and the use of waitfor.

%{
Joshua Stough
W&L, Image Group
June 2012
____
Citation is currency. Please see README. Please cite:
Joshua Stough, Matlab quadrat selection tool, 2012. 
http://www.mathworks.com/matlabcentral/fileexchange or 

Joshua Stough, Lisa Greer, William Benson: Texture and Color Distribution-based
Classification for Live Coral Detection. Proceedings of the 12th International 
Coral Reef Symposium, Cairns, Australia, 9-13 July 2012
____
We're done with the play programs, quadratSelectionTry and mouseCallbacksTry,
and this is the attempt to integrate them.
>> quadratSelection_OneImage('someImageInCurDir', someFigHandleYouJustMade);

Intensely useful reference on the mouse callback functionality:
http://blogs.mathworks.com/pick/2008/05/27/advanced-matlab-capture-mouse-movement/
The trick is knowing which properties are available to 'get' for the given objects.
Also see matlab help on imtransform in the images toolbox.
-Also, given the way I size the figures, I am assuming you have 1000+ rows
on your screen and 1220+ columns. So no modification necessary for 1280x1024
or above (like 1080p, retina display (1440p), etc.).
-Also, I assume one quadrat per image. Sorry, though that wouldn't be hard 
  change.  Basically, put all this business in a while loop and kill the
  callbacks upon 'q' pressed. then reinit the figures. Then offline you could
  go through all the _quadrat%d's you like.
%}

function [] = quadratSelection_OneImage(imname, figToKill, quadratSize)

if nargin < 3
    quadratSize = 2048;
end

if nargin < 2
    figToKill = figure('Visible', 'off');
end

IC = im2double(imread(imname));

%The image figure, with all its callbacks.
imgFigHandle = figure('Name', sprintf('%s, select corners clockwise from lower left (enter to quit)', imname), ...
    'WindowButtonDownFcn', @buttonDownCallback, ...
    'KeyPressFcn', @keyPressCallback, ...
    'OuterPosition', [1 300 700 700]);

imgObj = imagesc(IC); axis equal; axis tight;
set(gca, 'LooseInset', get(gca,'TightInset'))
%This minimizes the figure border so as to maximize the screen area of the
%shown image and quadrat. 
%See:
%http://stackoverflow.com/questions/6685092/how-to-reduce-the-borders-around-subplots-in-matlab


%The quadrat figure
quadratFigHandle = figure('Name', 'Proposed Quadrat', ...
    'OuterPosition', [720 500 500 500]);

fprintf('Select the four corners of your quadrat in the left \nfigure, from bottom left clockwise.\n');
fprintf('To quit this image now, hit return.\n');

figure(imgFigHandle);
[xs,ys] = ginput(4);
%xs = round(xs);
%ys = round(ys);
%I can't see a good reason to keep double precision, but oh well.


%Have to check and see whether the user wanted to quit, by hitting return.
if length(xs) < 4
    fprintf('You have finished.  Thank you.\n');
    close(imgFigHandle);
    close(quadratFigHandle);
    close(figToKill); %hopefully, the figure someone else has a waitfor on.
    return
end

%The user did select the four points, so continue. Start by printing the
%user-specified coordinates.
fprintf('The points you selected are:\n');
for i = 1:4
    fprintf('%7.3f %7.3f\n', xs(i), ys(i));
end


%Make the qudrat appear in another figure and put up the original to allow
%the user to manipulate. See matlab help on imtransform. Boy, am I glad I
%didn't have to remember how to do the projective homography math.
%See Multiple View Geometry book: http://www.robots.ox.ac.uk/~vgg/hzbook/
input_points = [xs,ys];
base_points = [1 quadratSize; 1 1; quadratSize 1; quadratSize quadratSize];
%The weird figure coords mean that the upper left is 1,1, with x horizontal
%and y vertical. So base_points here is the four axis corners clockwise
%from lower-left.
quadratTform = cp2tform(input_points, base_points, 'projective');

QImage = imtransform(IC, quadratTform, 'bicubic', 'XData', [1 quadratSize], 'YData', [1 quadratSize], 'XYScale',1);
QImage(QImage > 1) = 1;
QImage(QImage < 0) = 0;
%This business with limitting the range seemed necessary because the
%transformed image would have a few pixels that were bad.  I believe that's
%because of the bicubic interpolation / extrapolation.  The bicubic led to
%better image fidelity in my opinion (sharper edges, better contrast), so I
%keep it and just correct for its vagaries.

%Make the quadrat appear.
figure(quadratFigHandle), imagesc(QImage); axis equal; axis tight;
set(gca, 'LooseInset', get(gca,'TightInset'))

figure(imgFigHandle); %bring this figure to front.

set(imgFigHandle, 'WindowButtonUpFcn', @buttonUpCallback);
%Sets the callback for mouse button release, which is safe now that we have
%selected the four corners.


inds = [1 2 3 4; 2 3 4 1];
%each x and y selected get repeated in order to draw the lines, according
%to inds:
%1 2 3 4
%2 3 4 1
%This is to say, the first line requires the 1 and 2 x and y coordinates chosen,
%the second line requires the 2 and 3 x, y coords chosen, etc.
x_coords = xs(inds);
y_coords = ys(inds);

selectedPoint = 0;
lastPoint = [0; 0];
%Used in the control point click and drag action.

%Okay, now make the lines.  
lineH = line(x_coords, y_coords, 'Color', 'r', 'LineWidth', 3);
%lineH is a vector of handles to the line objects.  So if the box is:
%2 3
%1 4
%Then the line objects are for the lines (1,2), (2,3), (3,4), and (4,1).


%Now change the instructions, on both the figure and the terminal:
set(imgFigHandle, 'Name', sprintf('%s, drag corners to affect quadrat (q to quit)', imname));

fprintf('Now click and hold/drag the corner points to change the quadrat.\n');
fprintf('-type ''s'' to save the quadrat image.\n');
fprintf('-type ''q'' to quit this function.\n');


    function buttonDownCallback(varargin)
        %Decide which point the user clicked on, so as to allow dragging
        %while the mouse button continues to be pressed. Again,
        %super-useful reference:
        %http://blogs.mathworks.com/pick/2008/05/27/advanced-matlab-capture-mouse-movement/
        
        p = get(gca, 'CurrentPoint');
        lastPoint = [p(1); p(3)];
        
        %Now find the closest control point and allow motion drag.
        [~, m_ind] = min(sum(([x_coords(1,:)', y_coords(1,:)']'- repmat([p(1); p(3)], 1, 4)).^2));
        selectedPoint = m_ind;
        
        fprintf('\tYou selected point %d...hold and drag to change quadrat.\n', selectedPoint);
        
        %When they hit the mouse button, set the motion function so that we
        %can drag the selected point.
        set(imgFigHandle, 'WindowButtonMotionFcn', @dragControlPointCallback);
    end

    function dragControlPointCallback(varargin)
        %The mouse motion callback, active once the user has selected a
        %point to drag.  The goal here is to change the lines associated
        %with the control point (corner point) being dragged.
        
        p = get(gca, 'CurrentPoint');
        motionV = [p(1); p(3)] - lastPoint;
        lastPoint = [p(1); p(3)];
        
        %Now move the selected point by the motionV.  That is, modify the
        %the XData and YData of the affected lines and change x_coords and
        %y_coords.
        affectedLines = [4 1; 1 2; 2 3; 3 4]';
        %4 1 2 3
        %1 2 3 4
        %So point 1 will affect lines 4 and 1, point 2 will affect lines 1
        %and 2, and so on...
        %In fact, the selected point will affect the second x coord of
        %the first affected line and the first x coord of the second
        %affected line (confused?)  
        
        whichCoords = affectedLines(:, selectedPoint);
        x_coords(2, whichCoords(1)) = x_coords(2, whichCoords(1)) + motionV(1);
        x_coords(1, whichCoords(2)) = x_coords(1, whichCoords(2)) + motionV(1);
        y_coords(2, whichCoords(1)) = y_coords(2, whichCoords(1)) + motionV(2);
        y_coords(1, whichCoords(2)) = y_coords(1, whichCoords(2)) + motionV(2);
        
        %Now, set the XData and YData of the affected Lines;
        set(lineH(whichCoords(1)), 'XData', x_coords(:, whichCoords(1)));
        set(lineH(whichCoords(2)), 'XData', x_coords(:, whichCoords(2)));
        set(lineH(whichCoords(1)), 'YData', y_coords(:, whichCoords(1)));
        set(lineH(whichCoords(2)), 'YData', y_coords(:, whichCoords(2)));
        
    end

    function buttonUpCallback(varargin)
        %Correct the quadrat image given the just finished drag motion.
        
        %First, nullify the motion callback, so that continued mouse motion
        %(after the button release) is ignored.
        set(imgFigHandle, 'WindowButtonMotionFcn', ''); 
        
        %Now, recompute the quadrat to show.
        input_points = [x_coords(1,:)', y_coords(1,:)'];
        %x_coords and y_coords' first rows represent the current corner 
        %coordinates in the original (left) image figure.  
        
        quadratTform = cp2tform(input_points, base_points, 'projective');
        QImage = imtransform(IC, quadratTform, 'bicubic', 'XData', [1 quadratSize], 'YData', [1 quadratSize], 'XYScale',1);
        QImage(QImage > 1) = 1;
        QImage(QImage < 0) = 0;

        figure(quadratFigHandle), imagesc(QImage); axis equal; axis tight;
        set(gca, 'LooseInset', get(gca,'TightInset'))
        
        figure(imgFigHandle);
    end

    function keyPressCallback(~, evt)
        %Save the quadrat image.
        
        
        if evt.Key == 's'
            [~, name, ext] = fileparts(imname);
            newName = ['savedQuadrats/' name '_quadrat.png'];
            %pathstr should be empty
            %if ~isempty(pathstr)
            %    newName = [pathstr '/' newName];
            %end
            
            metaString = sprintf('#Matlab Quadrat selection tool metadata. Stough\n');
            metaString = [metaString sprintf('#image: %s\n', imname)];
            metaString = [metaString sprintf('%12.4f ', x_coords(1,:))];
            metaString = [metaString sprintf('\n') sprintf('%12.4f ', y_coords(1,:))];
            metaString = [metaString sprintf('\n')];
            
            fprintf('Saving %s...', newName);
            imwrite(QImage, newName, ...
                'Comment', metaString);
            fprintf('done.\n');
            
            %Now to save meta data.
            newName = ['savedQuadratMetaData/' name '_quadratMeta.txt'];
            f = fopen(newName, 'w'); 
            fprintf(f, metaString);
            fclose(f);
            
        elseif evt.Key == 'q' 
            fprintf('You have finished.  Thank you.\n');
            close(imgFigHandle);
            close(quadratFigHandle);
            close(figToKill); %hopefully, the figure someone else has a waitfor on.
            return
        end
    end

end


Contact us