Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
170 changes: 170 additions & 0 deletions code/apps/+imviewer/+plugin/RegionSelector.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
classdef RegionSelector < imviewer.ImviewerPlugin
%RegionSelector Plugin for placing rectangular regions on an image
%
% This plugin lets the user place rectangular regions on the image of an
% imviewer app, and returns them as rectangles, for example to pick the
% area to crop a stack to or the parts of the field of view that a
% method should work on.
%
% h = imviewer.plugin.RegionSelector(hImviewer) adds the plugin to the
% imviewer app hImviewer.
%
% h = imviewer.plugin.RegionSelector(hImviewer, Name, Value) also
% specifies the RegionSize and the Regions to start with.
%
% The regions are represented as rectangular polygon rois in a roi map,
% which gives the same display and editing as roimanager provides for
% rois. Regions are placed with the rectangleSelect pointer tool and
% edited with the selectObject pointer tool.
%
% See also nansen.stack.selectRectangularRegions,
% roimanager.pointerTool.rectangleSelect

properties (Constant) % Inherited from applify.mixin.AppPlugin
Name = 'Region Selector'
end

properties
RegionSize (1,2) double {mustBePositive} = [64, 64] % Size of the region to place, as [width, height]
Regions (:,4) double = zeros(0, 4) % Regions to start with, as rectangles [xmin, ymin, width, height]
end

properties (Dependent, SetAccess = private)
RegionCount % Number of regions that are currently placed
end

properties (Access = private)
RoiGroup roimanager.roiGroup
RoiMap roimanager.roiMap
PointerManager uim.interface.pointerManager
end

methods % Constructor

function obj = RegionSelector(varargin)
obj@imviewer.ImviewerPlugin(varargin{:})

if ~nargout; clear obj; end
end

function delete(obj)
if ~isempty(obj.RoiMap) && isvalid(obj.RoiMap)
delete(obj.RoiMap)
end
if ~isempty(obj.RoiGroup) && isvalid(obj.RoiGroup)
delete(obj.RoiGroup)
end
end
end

methods % Set/get

function regionCount = get.RegionCount(obj)
if isempty(obj.RoiGroup) || ~isvalid(obj.RoiGroup)
regionCount = 0;
else
regionCount = obj.RoiGroup.roiCount;
end
end
end

methods

function regions = getRegions(obj)
%getRegions Get the regions that are currently placed
%
% regions = getRegions(obj) returns an N-by-4 array of rectangles
% [xmin, ymin, width, height] of pixel indices, in the order the
% regions were placed.

regions = rois2rectangles(obj.RoiGroup.roiArray);
end
end

methods (Access = protected)

function onPluginActivated(obj)
onPluginActivated@imviewer.ImviewerPlugin(obj)

obj.initializeRoiMap()
obj.initializePointerTools()
end
end

methods (Access = private)

function initializeRoiMap(obj)
%initializeRoiMap Create a roi map to hold and display the regions

imageSize = [obj.ImviewerObj.imHeight, obj.ImviewerObj.imWidth];

obj.RoiGroup = roimanager.roiGroup();
if ~isempty(obj.Regions)
obj.RoiGroup.addRois( rectangles2rois(obj.Regions, imageSize) )
end

obj.RoiMap = roimanager.roiMap(obj.ImviewerObj, ...
obj.ImviewerObj.Axes, obj.RoiGroup);
end

function initializePointerTools(obj)
%initializePointerTools Add the roi pointer tools to the image viewer

obj.PointerManager = obj.PrimaryApp.PointerManager;
hAxes = obj.PrimaryApp.Axes;

obj.PointerManager.initializePointers(hAxes, ...
@roimanager.pointerTool.rectangleSelect)
obj.PointerManager.initializePointers(hAxes, ...
@roimanager.pointerTool.selectObject)

obj.PointerManager.pointers.rectangleSelect.RoiDisplay = obj.RoiMap;
obj.PointerManager.pointers.selectObject.RoiDisplay = obj.RoiMap;
obj.PointerManager.pointers.rectangleSelect.setRectangleSize(obj.RegionSize)

% Placing regions is the default mode, so that toggling the
% select tool off with the s key returns to placing regions.
obj.PointerManager.defaultPointerTool = ...
obj.PointerManager.pointers.rectangleSelect;
obj.PointerManager.togglePointerMode('rectangleSelect')
end
end
end

function rectangles = rois2rectangles(roiArray)
%rois2rectangles Get the bounding rectangle of each roi

numRois = numel(roiArray);
rectangles = zeros(numRois, 4);

for i = 1:numRois
xCoordinates = roiArray(i).coordinates(:, 1);
yCoordinates = roiArray(i).coordinates(:, 2);

xMin = round( min(xCoordinates) );
yMin = round( min(yCoordinates) );
width = round( max(xCoordinates) ) - xMin + 1;
height = round( max(yCoordinates) ) - yMin + 1;

rectangles(i, :) = [xMin, yMin, width, height];
end
end

function roiArray = rectangles2rois(rectangles, imageSize)
%rectangles2rois Create one rectangular polygon roi per rectangle

numRectangles = size(rectangles, 1);
roiArray = RoI.empty;

for i = 1:numRectangles
xMin = rectangles(i, 1);
yMin = rectangles(i, 2);
xMax = xMin + rectangles(i, 3) - 1;
yMax = yMin + rectangles(i, 4) - 1;

xCorners = [xMin, xMax, xMax, xMin];
yCorners = [yMin, yMin, yMax, yMax];

roiArray(i) = RoI('Polygon', [xCorners; yCorners], imageSize);
end
end
124 changes: 124 additions & 0 deletions code/datatypes/+nansen/+stack/selectRectangularRegions.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
function [regions, wasAborted] = selectRectangularRegions(imageStack, options)
%selectRectangularRegions - Select rectangular image regions interactively
%
% regions = nansen.stack.selectRectangularRegions(imageStack) opens the
% nansen.stack.ImageStack object imageStack in imviewer and lets the user
% place rectangular regions on the image by clicking. Pressing enter
% finishes the selection and returns the regions as an N-by-4 array of
% rectangles [xmin, ymin, width, height] of pixel indices, in the order
% the regions were created.
%
% Interaction:
% click : Add a region centered on the point that is clicked
% g / h : Grow / shrink the region in both directions
% shift + g / h : Grow / shrink the region in the y-direction only
% s : Switch between placing and selecting regions.
% A selected region is moved with the arrow keys and
% deleted with the backspace key
% enter : Finish and return the regions
% escape : Abort and return no regions
%
% regions = selectRectangularRegions(imageStack,RegionSize=SIZE) sets the
% initial size of the region that is placed. SIZE is a scalar for a
% square region or a two-element vector [WIDTH, HEIGHT]. By default, the
% region is one sixth of the image size.
%
% regions = selectRectangularRegions(imageStack,Regions=REGIONS) starts
% the selection with the regions in REGIONS already placed, so that an
% earlier selection can be adjusted.
%
% [regions, wasAborted] = selectRectangularRegions(...) also returns
% whether the user aborted the selection, either by pressing escape or by
% closing the window. regions is empty when the selection is aborted.
%
% See also nansen.stack.crop, imviewer.plugin.RegionSelector, imviewer

arguments
imageStack {mustBeA(imageStack, "nansen.stack.ImageStack"), ...
mustBeScalarOrEmpty, mustBeNonempty}
options.RegionSize (1,:) {mustBeNumeric, mustBeReal, mustBePositive} = []
options.Regions (:,4) {mustBeNumeric, mustBeReal} = zeros(0, 4)
end

regionSize = resolveRegionSize(options.RegionSize, imageStack);

% Caching makes browsing the stack in imviewer responsive while the
% regions are placed. Only a virtual stack has a dynamic cache.
useDynamicCache = imageStack.IsVirtual;
if useDynamicCache
initialCacheState = imageStack.DynamicCacheEnabled;
imageStack.DynamicCacheEnabled = 'on';
end

hImviewer = imviewer(imageStack);
hImviewer.ImageDragAndDropEnabled = false;

hRegionSelector = hImviewer.openPlugin(@imviewer.plugin.RegionSelector, ...
[], 'RegionSize', regionSize, 'Regions', options.Regions);

message = ['Click to add a region. Press g/h to resize, s to select ', ...
'and move or delete a region, enter to finish and escape to abort.'];
hImviewer.displayMessage(message, [], 8)

wasAborted = waitForUser(hImviewer, hRegionSelector);

if wasAborted
regions = zeros(0, 4);
else
regions = hRegionSelector.getRegions();
end

delete(hRegionSelector)
if isvalid(hImviewer)
hImviewer.quit()
end
if useDynamicCache && isvalid(imageStack)
imageStack.DynamicCacheEnabled = initialCacheState;
end
end

function regionSize = resolveRegionSize(regionSize, imageStack)
%resolveRegionSize Get the initial region size as [width, height]

if isempty(regionSize)
% One sixth of the image in each direction is large enough to hold
% structures for registration and small enough that several regions
% fit in the field of view.
regionSize = round( [imageStack.ImageWidth, imageStack.ImageHeight] ./ 6 );
elseif isscalar(regionSize)
regionSize = [regionSize, regionSize];
elseif numel(regionSize) > 2
error('NANSEN:Stack:SelectRegions:InvalidRegionSize', ...
['RegionSize must be a scalar or a two-element vector ', ...
'[width, height].'])
end

regionSize = min(regionSize, [imageStack.ImageWidth, imageStack.ImageHeight]);
end

function wasAborted = waitForUser(hImviewer, hRegionSelector)
%waitForUser Block until the user finishes or aborts the selection

hFigure = hImviewer.Figure;
hFigure.UserData.lastKey = '';

while true
uiwait(hFigure) % Resumes on enter or escape

if ~ishghandle(hFigure)
wasAborted = true;
break
elseif strcmp(hFigure.UserData.lastKey, 'escape')
wasAborted = true;
break
elseif strcmp(hFigure.UserData.lastKey, 'return')
if hRegionSelector.RegionCount == 0
hImviewer.displayMessage( ...
'Add at least one region before pressing enter.', [], 4)
else
wasAborted = false;
break
end
end
end
end
Loading