diff --git a/demos/shape/demoCrop.m b/demos/shape/demoCrop.m new file mode 100644 index 0000000..ddf99f0 --- /dev/null +++ b/demos/shape/demoCrop.m @@ -0,0 +1,55 @@ +% Demonstration of image crop functions +% +% output = demoCrop(input) +% +% Example +% demoCrop +% +% See also +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2022-06-24, using Matlab 9.12.0.1884302 (R2022a) +% Copyright 2022 INRAE. + +%% Input data + +% read sample image +% (provided within the @image/sampleFiles directory) +img = Image.read('wheatGrainSlice.tif'); + +figure; show(img); + + +%% Boxes + +% need to segment image to obtain the region of the grain +seg = img > 160; +seg2 = areaOpening(killBorders(seg), 10); + +% compute boxes +box = regionBoundingBox(seg2); +obox = regionOrientedBox(seg2); + +% display boxes over image +% (requires MatGeom toolbox) +hold on; +drawBox(box, 'color', 'g', 'linewidth', 2); +drawOrientedBox(obox, 'color', 'm', 'linewidth', 2); + + +%% Crop + +resCrop = crop(img, box); +figure; show(resCrop) +title('Crop Box'); +write(resCrop, 'wheatGrainCrop.tif'); + +resCrop2 = cropOrientedBox(img, obox); +figure; show(resCrop2) +title('Crop Oriented Box'); +write(resCrop, 'wheatGrainCropOriented.tif'); + diff --git a/demos/shape/html/demoCrop.html b/demos/shape/html/demoCrop.html new file mode 100644 index 0000000..477a7c3 --- /dev/null +++ b/demos/shape/html/demoCrop.html @@ -0,0 +1,172 @@ + + + + + Codestin Search App

Contents

% Demonstration of image crop functions
+%
+%   output = demoCrop(input)
+%
+%   Example
+%   demoCrop
+%
+%   See also
+%
+
+% ------
+% Author: David Legland
+% e-mail: david.legland@inrae.fr
+% INRAE - BIA Research Unit - BIBS Platform (Nantes)
+% Created: 2022-06-24,    using Matlab 9.12.0.1884302 (R2022a)
+% Copyright 2022 INRAE.
+

Input data

% read sample image
+% (provided within the @image/sampleFiles directory)
+img = Image.read('wheatGrainSlice.tif');
+
+figure; show(img);
+

Boxes

% need to segment image to obtain the region of the grain
+seg = img > 160;
+seg2 = areaOpening(killBorders(seg), 10);
+
+% compute boxes
+box = regionBoundingBox(seg2);
+obox = regionOrientedBox(seg2);
+
+% display boxes over image
+% (requires MatGeom toolbox)
+hold on;
+drawBox(box, 'color', 'g', 'linewidth', 2);
+drawOrientedBox(obox, 'color', 'm', 'linewidth', 2);
+

Crop

resCrop = crop(img, box);
+figure; show(resCrop)
+title('Crop Box');
+write(resCrop, 'wheatGrainCrop.tif');
+
+resCrop2 = cropOrientedBox(img, obox);
+figure; show(resCrop2)
+title('Crop Oriented Box');
+write(resCrop, 'wheatGrainCropOriented.tif');
+
\ No newline at end of file diff --git a/demos/shape/html/demoCrop.png b/demos/shape/html/demoCrop.png new file mode 100644 index 0000000..d9fc51b Binary files /dev/null and b/demos/shape/html/demoCrop.png differ diff --git a/demos/shape/html/demoCrop_01.png b/demos/shape/html/demoCrop_01.png new file mode 100644 index 0000000..071ac19 Binary files /dev/null and b/demos/shape/html/demoCrop_01.png differ diff --git a/demos/shape/html/demoCrop_02.png b/demos/shape/html/demoCrop_02.png new file mode 100644 index 0000000..a6ddb06 Binary files /dev/null and b/demos/shape/html/demoCrop_02.png differ diff --git a/demos/shape/html/demoCrop_03.png b/demos/shape/html/demoCrop_03.png new file mode 100644 index 0000000..6815eef Binary files /dev/null and b/demos/shape/html/demoCrop_03.png differ diff --git a/demos/shape/html/demoCrop_04.png b/demos/shape/html/demoCrop_04.png new file mode 100644 index 0000000..2ebc27d Binary files /dev/null and b/demos/shape/html/demoCrop_04.png differ diff --git a/src/@Image/Image.m b/src/@Image/Image.m index 159b486..cf64457 100644 --- a/src/@Image/Image.m +++ b/src/@Image/Image.m @@ -108,14 +108,20 @@ methods(Access = protected) se = defaultStructuringElement(obj, varargin) + conn = defaultConnectivity(obj); name = createNewName(obj, pattern) end +%% Private utility methods +methods(Static, Access = protected) + weights = directionWeights3d13(delta); +end + %% Constructor declaration methods function obj = Image(varargin) - %IMAGE Constructor for Image object. + % Constructor for Image object. % % Syntax % IMG = Image(MAT); diff --git a/src/@Image/areaOpening.m b/src/@Image/areaOpening.m index 70605dc..bae03a0 100644 --- a/src/@Image/areaOpening.m +++ b/src/@Image/areaOpening.m @@ -47,11 +47,10 @@ elseif isBinaryImage(obj) % if image is binary compute labeling - % first determines connectivity to use - conn = 4; - if ndims(obj) == 3 - conn = 6; - end + % choose default connectivity depending on dimension + conn = defaultConnectivity(obj); + + % case of connectivity specified by user if ~isempty(varargin) conn = varargin{1}; end diff --git a/src/@Image/catChannels.m b/src/@Image/catChannels.m index 2579f6c..387f3da 100644 --- a/src/@Image/catChannels.m +++ b/src/@Image/catChannels.m @@ -7,16 +7,16 @@ % % 'manual' creation of a color image % img = Image.read('cameraman.tif'); % res = catChannels(img, img, invert(img)); -% res.type = 'color'; +% res.Type = 'color'; % show(res); % % See also -% splitChannels, cat, catFrames +% splitChannels, createRGB, cat, catFrames % % ------ % Author: David Legland -% e-mail: david.legland@inra.fr +% e-mail: david.legland@inrae.fr % Created: 2011-11-22, using Matlab 7.9.0.529 (R2009b) % Copyright 2011 INRA - Cepia Software Platform. @@ -31,7 +31,7 @@ end res = Image(... - 'data', data, ... - 'parent', obj, ... - 'type', 'vector', ... - 'name', name); + 'Data', data, ... + 'Parent', obj, ... + 'Type', 'vector', ... + 'Name', name); diff --git a/src/@Image/chamferDistanceMap.m b/src/@Image/chamferDistanceMap.m new file mode 100644 index 0000000..ad2f85a --- /dev/null +++ b/src/@Image/chamferDistanceMap.m @@ -0,0 +1,203 @@ +function map = chamferDistanceMap(obj, varargin) +% Distance map of a binary image computed using chamfer mask. +% +% DISTMAP = chamferDistanceMap(IMG) +% DISTMAP = chamferDistanceMap(IMG, WEIGHTS) +% Computes the distance map of the input image using chamfer weights. The +% aim of this function is similar to that of the "distanceMap" one, with +% the following specificities: +% * possibility to use 5-by-5 chamfer masks +% * possibility to compute distance maps for label images with touching +% regions. +% +% Example +% chamferDistanceMap +% +% See also +% distanceMap, geodesicDistanceMap +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + +%% Process input arguments + +% default weights for orthogonal or diagonal +weights = [5 7 11]; + +normalize = true; + +% extract user-specified weights +if ~isempty(varargin) + weights = varargin{1}; + varargin(1) = []; +end + +% extract verbosity option +verbose = false; +if length(varargin) > 1 + varName = varargin{1}; + if ~ischar(varName) + error('Require options as name-value pairs'); + end + + if strcmpi(varName, 'normalize') + normalize = varargin{2}; + elseif strcmpi(varName, 'verbose') + verbose = varargin{2}; + else + error(['unknown option: ' varName]); + end +end + + +%% Initialisations + +% determines type of output from type of weights +outputType = class(weights); + +% small check up to avoid degenerate cases +w1 = weights(1); +w2 = weights(2); +if w2 < w1 + w2 = 2 * w1; +end + +% shifts in directions i and j for (1) forward and (2) backward iterations +if length(weights) == 2 + nShifts = 4; + di1 = [-1 -1 -1 0]; + dj1 = [-1 0 1 -1]; + di2 = [+1 +1 +1 0]; + dj2 = [-1 0 1 +1]; + ws = [w2 w1 w2 w1]; + +elseif length(weights) == 3 + nShifts = 8; + w3 = weights(3); + di1 = [-2 -2 -1 -1 -1 -1 -1 0]; + dj1 = [-1 +1 -2 -1 0 1 +2 -1]; + di2 = [+2 +2 +1 +1 +1 +1 +1 0]; + dj2 = [-1 +1 +2 +1 0 -1 -2 +1]; + ws = [w3 w3 w3 w2 w1 w2 w3 w1]; +end + +% allocate memory for result +dist = ones(size(obj.Data), outputType); + +% init result: either max value, or 0 for marker pixels +if isinteger(w1) + dist(:) = intmax(outputType); +else + dist(:) = inf; +end +dist(obj.Data == 0) = 0; + +% size of image +[D1, D2] = size(obj.Data); + + +%% Forward iteration + +if verbose + disp('Forward iteration %d'); +end + +for i = 1:D1 + for j = 1:D2 + % computes only for pixels within a region + if obj.Data(i, j) == 0 + continue; + end + + % compute minimal propagated distance + newVal = dist(i, j); + for k = 1:nShifts + % coordinate of neighbor + i2 = i + di1(k); + j2 = j + dj1(k); + + % check bounds + if i2 < 1 || i2 > D1 || j2 < 1 || j2 > D2 + continue; + end + + % compute new value + if obj.Data(i2, j2) == obj.Data(i, j) + % neighbor in same region + % -> add offset weight to neighbor distance + newVal = min(newVal, dist(i2, j2) + ws(k)); + else + % neighbor in another region + % -> initialize with the offset weight + newVal = min(newVal, ws(k)); + end + end + + % if distance was changed, update result + dist(i,j) = newVal; + end + +end % iteration on lines + + + +%% Backward iteration + +if verbose + disp('Backward iteration'); +end + +for i = D1:-1:1 + for j = D2:-1:1 + % computes only for foreground pixels + if obj.Data(i, j) == 0 + continue; + end + + % compute minimal propagated distance + newVal = dist(i, j); + for k = 1:nShifts + % coordinate of neighbor + i2 = i + di2(k); + j2 = j + dj2(k); + + % check bounds + if i2 < 1 || i2 > D1 || j2 < 1 || j2 > D2 + continue; + end + + % compute new value + if obj.Data(i2, j2) == obj.Data(i, j) + % neighbor in same region + % -> add offset weight to neighbor distance + newVal = min(newVal, dist(i2, j2) + ws(k)); + else + % neighbor in another region + % -> initialize with the offset weight + newVal = min(newVal, ws(k)); + end + end + + % if distance was changed, update result + dist(i,j) = newVal; + end + +end % line iteration + +if normalize + dist(obj.Data>0) = dist(obj.Data>0) / w1; +end + +newName = createNewName(obj, '%s-distMap'); + +% create new image +map = Image('Data', dist, ... + 'Parent', obj, ... + 'Name', newName, ... + 'Type', 'intensity', ... + 'ChannelNames', {'distance'}); diff --git a/src/@Image/clone.m b/src/@Image/clone.m index 89c9dbc..0325ba8 100644 --- a/src/@Image/clone.m +++ b/src/@Image/clone.m @@ -1,17 +1,27 @@ function res = clone(obj) % Create a deep-copy of an Image object. % -% output = clone(input) +% RES = clone(IMG) +% Return a new Image, initialized with same data values as in IMG. +% % % Example -% clone +% img = Image.read('rice.png'); +% img2 = clone(img); +% all(size(img) == size(img2)) +% ans = +% 1 +% strcmp(class(img.Data), class(img2.Data)) +% ans = +% 1 % % See also +% read, zeros, ones % % ------ % Author: David Legland -% e-mail: david.legland@inra.fr +% e-mail: david.legland@inrae.fr % Created: 2011-12-15, using Matlab 7.9.0.529 (R2009b) % Copyright 2011 INRA - Cepia Software Platform. diff --git a/src/@Image/colorCloud.m b/src/@Image/colorCloud.m new file mode 100644 index 0000000..f8cb10d --- /dev/null +++ b/src/@Image/colorCloud.m @@ -0,0 +1,46 @@ +function varargout = colorCloud(obj, varargin) +% Display image colors into the 3D space of specified gamut. +% +% colorCloud(IMG) +% colorCloud(IMG, GAMUT) +% See the "colorcloud" function help for options. +% +% Works also for 3D images. +% +% Example +% % Read and display a color image +% img = Image.read('peppers.png'); +% figure; show(img); +% % Display color cloud in a new figure +% colorCloud(img) +% +% See also +% show, isColorImage, colorcloud +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2020-02-18, using Matlab 9.7.0.1247435 (R2019b) Update 2 +% Copyright 2020 INRAE. + +% check input +if ~isColorImage(obj) + error('requires a color image as input'); +end + +% format data +if obj.DataSize(3) == 1 + rgb = permute(obj.Data(:,:,1,:,1), [2 1 4 3 5]); +else + nSlices = obj.DataSize(3); + rgb = zeros([0 3]); + for iz = 1:nSlices + rgb = [rgb ; permute(obj.Data(:,:,iz,:,1), [2 1 4 3 5])]; %#ok + end +end + +% call the IPT function +varargout = cell(1, nargout); +[varargout{:}] = colorcloud(rgb, varargin{:}); diff --git a/src/@Image/componentLabeling.m b/src/@Image/componentLabeling.m index 82fca6a..6a3ca9c 100644 --- a/src/@Image/componentLabeling.m +++ b/src/@Image/componentLabeling.m @@ -12,7 +12,7 @@ % figure; show(rgb); % % See also -% label2rgb, watershed +% label2rgb, watershed, reconstruction % % ------ @@ -26,14 +26,22 @@ error('Requires a binary image'); end +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); + +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; +end + nd = ndims(obj); if nd == 2 % Planar images - data = bwlabel(obj.Data, varargin{:}); + data = bwlabel(obj.Data, conn); elseif nd == 3 % 3D images - data = bwlabeln(obj.Data, varargin{:}); + data = bwlabeln(obj.Data, conn); else error('Function "componentLabeling" is not implemented for image of dim %d', nd); diff --git a/src/@Image/create.m b/src/@Image/create.m index 1da1530..fd748ea 100644 --- a/src/@Image/create.m +++ b/src/@Image/create.m @@ -19,12 +19,12 @@ % % % See also -% read, ones, zeros +% read, ones, zeros, true, false % % ------ % Author: David Legland -% e-mail: david.legland@inra.fr +% e-mail: david.legland@inrae.fr % Created: 2010-07-21, using Matlab 7.9.0.529 (R2009b) % Copyright 2010 INRA - Cepia Software Platform. @@ -59,7 +59,7 @@ end % create empty data buffer - if strcmp(type, 'logical') || strcmp(type, 'binary') + if strcmpi(type, 'logical') || strcmp(type, 'binary') % case of binary image data = false(imageSize); else @@ -83,7 +83,7 @@ continue; end - if strcmp(varargin{i}, 'data') + if strcmpi(varargin{i}, 'data') data = varargin{i+1}; imageSize = size(data); varargin(i:i+1) = []; @@ -120,4 +120,4 @@ % call constructor depending on image dimension nd = length(imageSize); -img = Image(nd, 'data', data, arguments{:}); +img = Image(nd, 'Data', data, arguments{:}); diff --git a/src/@Image/createRGB.m b/src/@Image/createRGB.m index d6348b9..3945e4c 100644 --- a/src/@Image/createRGB.m +++ b/src/@Image/createRGB.m @@ -1,20 +1,22 @@ function rgb = createRGB(red, green, blue) % Create a RGB color image from scalar channels. % -% RGB = createRGB(DATA) +% RGB = Image.createRGB(DATA) % Create a RGB image from data array DATA, using the third dimension as % channel dimension. % Input array DATA can be either a 3D or a 4D array, resulting in a 2D or % 3D image. DATA is ordered using Matlab convention: y, x, channel, z. % -% RGB = createRGB(RED, GREEN, BLUE) +% RGB = Image.createRGB(RED, GREEN, BLUE) % Concatenates the 3 data arrays to form a RGB color image. Inputs can be % either 2D or 3D, but they must have the same dimension. One of them can % be empty. % % % Example -% createRGB +% img = Image.read('peppers.png'); +% img2 = Image.createRGB(channel(img, 3), channel(img, 1), channel(img, 2)); +% figure; show(img2) % % See also % create, Image, catChannels diff --git a/src/@Image/cropOrientedBox.m b/src/@Image/cropOrientedBox.m new file mode 100644 index 0000000..0f26fe3 --- /dev/null +++ b/src/@Image/cropOrientedBox.m @@ -0,0 +1,52 @@ +function res = cropOrientedBox(obj, obox, varargin) +% Crop the content of an image within an oriented box. +% +% RES = cropOrientedBox(IMG, OBOX) +% Crops the content of the image IMG that is contained within the +% oriented box OBOX. The size of the resulting image is approximately +% (due to rounding) the size of the oriented box. +% +% Example +% % open and display input image +% img = Image.read('circles.png') ; +% figure; show(img); hold on; +% % identifies oriented box around the main region +% obox = regionOrientedBox(img > 0); +% drawOrientedBox(obox, 'g'); +% % crop the content of the oriented box +% res = cropOrientedBox(img, obox); +% figure; show(res) +% +% See also +% regionOrientedBox, crop +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2022-06-24, using Matlab 9.12.0.1884302 (R2022a) +% Copyright 2022 INRAE. + +% retrieve oriented box parameters +boxCenter = obox(1:2); +boxSize = obox(3:4); +boxAngle = obox(5); + +% create the transform matrix that maps from box coords to global coords +transfo = createTranslation(boxCenter) * createRotation(deg2rad(boxAngle)); + +% sample points within the box (use single pixel spacing) +lx = -floor(boxSize(1)/2):ceil(boxSize(1)/2); +ly = -floor(boxSize(2)/2):ceil(boxSize(2)/2); + +% map into global coordinate space +[y, x] = meshgrid(ly, lx); +[x, y] = transformPoint(x, y, transfo); + +% evaluate within image, keeping type of original image +resData = zeros(size(x), class(obj.Data)); +resData(:) = interp(obj, x, y); + +% create new image +res = Image('data', resData, 'parent', obj); diff --git a/src/@Image/defaultConnectivity.m b/src/@Image/defaultConnectivity.m new file mode 100644 index 0000000..9638962 --- /dev/null +++ b/src/@Image/defaultConnectivity.m @@ -0,0 +1,28 @@ +function conn = defaultConnectivity(obj) +% Choose the default foreground connectivity for the image. +% +% CONN = defaultConnectivity(IMG) +% Returns the default connectivity for the input image IMG. +% Returns: +% * CONN = 4 for 2D images, +% * CONN = 6 for 3D images +% +% Example +% defaultConnectivity +% +% See also +% killBorders, fillHoles, reconstruction, watershed +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-04-07, using Matlab 9.8.0.1323502 (R2020a) +% Copyright 2021 INRAE. + +if ndims(obj) == 2 %#ok + conn = 4; +elseif ndims(obj) == 3 + conn = 6; +end diff --git a/src/@Image/directionWeights3d13.m b/src/@Image/directionWeights3d13.m new file mode 100644 index 0000000..050a903 --- /dev/null +++ b/src/@Image/directionWeights3d13.m @@ -0,0 +1,547 @@ +function res = directionWeights3d13(varargin) +% Direction weights for 13 directions in 3D. +% +% C = computeDirectionWeights3d13 +% Returns an array of 13-by-1 values, corresponding to directions: +% C(1) = [+1 0 0] +% C(2) = [ 0 +1 0] +% C(3) = [ 0 0 +1] +% C(4) = [+1 +1 0] +% C(5) = [-1 +1 0] +% C(6) = [+1 0 +1] +% C(7) = [-1 0 +1] +% C(8) = [ 0 +1 +1] +% C(9) = [ 0 -1 +1] +% C(10) = [+1 +1 +1] +% C(11) = [-1 +1 +1] +% C(12) = [+1 -1 +1] +% C(13) = [-1 -1 +1] +% The sum of the weights in C equals 1. +% Some values are equal whatever the resolution: +% C(4)==C(5); +% C(6)==C(7); +% C(8)==C(9); +% C(10)==C(11)==C(12)==C(13); +% +% C = computeDirectionWeights3d13(DELTA) +% With DELTA = [DX DY DZ], specifies the resolution of the grid. +% +% Example +% c = computeDirectionWeights3d13; +% sum(c) +% ans = +% 1.0000 +% +% c = computeDirectionWeights3d13([2.5 2.5 7.5]); +% sum(c) +% ans = +% 1.0000 +% +% +% See also +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2010-10-18, using Matlab 7.9.0.529 (R2009b) +% Copyright 2010 INRA - Cepia Software Platform. + + +%% Initializations + +% grid resolution +delta = [1 1 1]; +if ~isempty(varargin) + delta = varargin{1}; +end + +% If resolution is [1 1 1], return the pre-computed set of weights +if all(delta == [1 1 1]) + area1 = 0.04577789120476 * 2; + area2 = 0.03698062787608 * 2; + area3 = 0.03519563978232 * 2; + res = [... + area1; area1; area1; ... + area2; area2; area2; area2; area2; area2;... + area3; area3; area3; area3 ]; + return; +end + +% Define points of interest in the 26 discrete directions +% format is pt[Xpos][Ypos][Zpos], with [X], [Y] or [Z] being one of +% 'N' (for negative), 'P' (for Positive) or 'Z' (for Zero) + +% points below the OXY plane +ptPNN = normalizeVector3d([+1 -1 -1].*delta); +ptPZN = normalizeVector3d([+1 0 -1].*delta); +ptNPN = normalizeVector3d([-1 +1 -1].*delta); +ptZPN = normalizeVector3d([ 0 +1 -1].*delta); +ptPPN = normalizeVector3d([+1 +1 -1].*delta); + +% points belonging to the OXY plane +ptPNZ = normalizeVector3d([+1 -1 0].*delta); +ptPZZ = normalizeVector3d([+1 0 0].*delta); +ptNPZ = normalizeVector3d([-1 +1 0].*delta); +ptZPZ = normalizeVector3d([ 0 +1 0].*delta); +ptPPZ = normalizeVector3d([+1 +1 0].*delta); + +% points above the OXY plane +ptNNP = normalizeVector3d([-1 -1 +1].*delta); +ptZNP = normalizeVector3d([ 0 -1 +1].*delta); +ptPNP = normalizeVector3d([+1 -1 +1].*delta); +ptNZP = normalizeVector3d([-1 0 +1].*delta); +ptZZP = normalizeVector3d([ 0 0 +1].*delta); +ptPZP = normalizeVector3d([+1 0 +1].*delta); +ptNPP = normalizeVector3d([-1 +1 +1].*delta); +ptZPP = normalizeVector3d([ 0 +1 +1].*delta); +ptPPP = normalizeVector3d([+1 +1 +1].*delta); + + +%% Spherical cap type 1, direction [1 0 0] + +% Compute area of voronoi cell for a point on the Ox axis, i.e. a point +% in the 6-neighborhood of the center. +refPoint = ptPZZ; + +% neighbours of chosen point, sorted by CCW angle +neighbors = [ptPNN; ptPNZ; ptPNP; ptPZP; ptPPP; ptPPZ; ptPPN; ptPZN]; + +% compute area of spherical polygon +area1 = sphericalVoronoiDomainArea(refPoint, neighbors); + + +%% Spherical cap type 1, direction [0 1 0] + +% Compute area of voronoi cell for a point on the Oy axis, i.e. a point +% in the 6-neighborhood of the center. +refPoint = ptZPZ; + +% neighbours of chosen point, sorted by angle +neighbors = [ptPPZ; ptPPP; ptZPP; ptNPP; ptNPZ; ptNPN; ptZPN; ptPPN]; + +% compute area of spherical polygon +area2 = sphericalVoronoiDomainArea(refPoint, neighbors); + + +%% Spherical cap type 1, direction [0 0 1] + +% Compute area of voronoi cell for a point on the Oz axis, i.e. a point +% in the 6-neighborhood of the center. +refPoint = ptZZP; + +% neighbours of chosen point, sorted by angle +neighbors = [ptPZP; ptPPP; ptZPP; ptNPP; ptNZP; ptNNP; ptZNP; ptPNP]; + +% compute area of spherical polygon +area3 = sphericalVoronoiDomainArea(refPoint, neighbors); + + +%% Spherical cap type 2, direction [1 1 0] + +% Compute area of voronoi cell for a point on the Oxy plane, i.e. a point +% in the 18-neighborhood +refPoint = ptPPZ; + +% neighbours of chosen point, sorted by angle +neighbors = [ptPZZ; ptPPP; ptZPZ; ptPPN]; + +% compute area of spherical polygon +area4 = sphericalVoronoiDomainArea(refPoint, neighbors); + + +%% Spherical cap type 2, direction [1 0 1] + +% Compute area of voronoi cell for a point on the Oxz plane, i.e. a point +% in the 18-neighborhood +refPoint = ptPZP; +% neighbours of chosen point, sorted by angle +neighbors = [ptPZZ; ptPPP; ptZZP; ptPNP]; + +% compute area of spherical polygon +area5 = sphericalVoronoiDomainArea(refPoint, neighbors); + + +%% Spherical cap type 2, direction [0 1 1] + +% Compute area of voronoi cell for a point on the Oxy plane, i.e. a point +% in the 18-neighborhood +refPoint = ptZPP; +% neighbours of chosen point, sorted by angle +neighbors = [ptZPZ; ptNPP; ptZZP; ptPPP]; + +% compute area of spherical polygon +area6 = sphericalVoronoiDomainArea(refPoint, neighbors); + + +%% Spherical cap type 3 (all cubic diagonals) + +% Compute area of voronoi cell for a point on the Oxyz diagonal, i.e. a +% point in the 26 neighborhood only +refPoint = ptPPP; +% neighbours of chosen point, sorted by angle +neighbors = [ptPZP; ptZZP; ptZPP; ptZPZ; ptPPZ; ptPZZ]; + +% compute area of spherical polygon +area7 = sphericalVoronoiDomainArea(refPoint, neighbors); + + +%% Concatenate results + +% return computed areas, normalized by the area of the unit sphere surface +res = [... + area1 area2 area3 ... + area4 area4 area5 area5 area6 area6... + area7 area7 area7 area7... + ] / (2 * pi); + +function area = sphericalVoronoiDomainArea(refPoint, neighbors) +% Compute area of a spherical voronoi domain +% +% AREA = sphericalVoronoiDomainArea(GERM, NEIGHBORS) +% GERM is a 1-by-3 row vector representing cartesian coordinates of a +% point on the sphere (in X, Y Z order) +% NEIGHBORS is a N-by-3 array representing cartesian coordinates of the +% germ neighbors. It is expected that NEIGHBORS contains only neighbors +% that effectively contribute to the voronoi domain. +% + +% reference sphere +sphere = [0 0 0 1]; + +% number of neigbors, and number of sides of the domain +nbSides = size(neighbors, 1); + +% compute planes containing separating circles +planes = zeros(nbSides, 9); +for i = 1:nbSides + planes(i,1:9) = normalizePlane(medianPlane(refPoint, neighbors(i,:))); +end + +% allocate memory +lines = zeros(nbSides, 6); +intersects = zeros(2*nbSides, 3); + +% compute circle-circle intersections +for i=1:nbSides + lines(i,1:6) = intersectPlanes(planes(i,:), ... + planes(mod(i,nbSides)+1,:)); + intersects(2*i-1:2*i,1:3) = intersectLineSphere(lines(i,:), sphere); +end + +% keep only points in the same direction than refPoint +ind = dot(intersects, repmat(refPoint, [2*nbSides 1]), 2)>0; +intersects = intersects(ind,:); +nbSides = size(intersects, 1); + +% compute spherical area of each triangle [center pt[i+1]%4 pt[i] ] +angles = zeros(nbSides, 1); +for i=1:nbSides + pt1 = intersects(i, :); + pt2 = intersects(mod(i , nbSides)+1, :); + pt3 = intersects(mod(i+1, nbSides)+1, :); + + angles(i) = sphericalAngle(pt1, pt2, pt3); + angles(i) = min(angles(i), 2*pi-angles(i)); +end + +% compute area of spherical polygon +area = sum(angles) - pi*(nbSides-2); + + +function plane = medianPlane(p1, p2) +% Create a plane in the middle of 2 points. + +% unify data dimension +if size(p1, 1) == 1 + p1 = repmat(p1, [size(p2, 1) 1]); +elseif size(p2, 1) == 1 + p2 = repmat(p2, [size(p1, 1) 1]); +elseif size(p1, 1) ~= size(p2, 1) + error('data should have same length, or one data should have length 1'); +end + +% middle point +p0 = (p1 + p2)/2; + +% normal to plane +n = p2 - p1; + +% create plane from point and normal +plane = createPlane(p0, n); + + +function line = intersectPlanes(plane1, plane2, varargin) +% Return intersection line between 2 planes in space. + +tol = 1e-14; +if ~isempty(varargin) + tol = varargin{1}; +end + +% plane normal +n1 = normalizeVector3d(cross(plane1(:,4:6), plane1(:, 7:9), 2)); +n2 = normalizeVector3d(cross(plane2(:,4:6), plane2(:, 7:9), 2)); + +% test if planes are parallel +if abs(cross(n1, n2, 2)) < tol + line = [NaN NaN NaN NaN NaN NaN]; + return; +end + +% Uses Hessian form, ie : N.p = d +% I this case, d can be found as : -N.p0, when N is normalized +d1 = dot(n1, plane1(:,1:3), 2); +d2 = dot(n2, plane2(:,1:3), 2); + +% compute dot products +dot1 = dot(n1, n1, 2); +dot2 = dot(n2, n2, 2); +dot12 = dot(n1, n2, 2); + +% intermediate computations +det = dot1*dot2 - dot12*dot12; +c1 = (d1*dot2 - d2*dot12)./det; +c2 = (d2*dot1 - d1*dot12)./det; + +% compute line origin and direction +p0 = c1*n1 + c2*n2; +dp = cross(n1, n2, 2); + +line = [p0 dp]; + + +function plane = createPlane(p0, n) +% Create a plane in parametrized form +% P = createPlane(P0, N); + +% normal is given by a 3D vector +n = normalizeVector3d(n); + +% ensure same dimension for parameters +if size(p0, 1) == 1 + p0 = repmat(p0, [size(n, 1) 1]); +end +if size(n, 1) == 1 + n = repmat(n, [size(p0, 1) 1]); +end + +% find a vector not colinear to the normal +v0 = repmat([1 0 0], [size(p0, 1) 1]); +inds = vectorNorm3d(cross(n, v0, 2))<1e-14; +v0(inds, :) = repmat([0 1 0], [sum(inds) 1]); + +% create direction vectors +v1 = normalizeVector3d(cross(n, v0, 2)); +v2 = -normalizeVector3d(cross(v1, n, 2)); + +% concatenate result in the array representing the plane +plane = [p0 v1 v2]; + + +function plane2 = normalizePlane(plane1) +% Normalize parametric representation of a plane + +% compute first direction vector +d1 = normalizeVector3d(plane1(:,4:6)); + +% compute second direction vector +n = normalizeVector3d(planeNormal(plane1)); +d2 = -normalizeVector3d(crossProduct3d(d1, n)); + +% compute origin point of the plane +origins = repmat([0 0 0], [size(plane1, 1) 1]); +p0 = projPointOnPlane(origins, [plane1(:,1:3) d1 d2]); + +% create the resulting plane +plane2 = [p0 d1 d2]; + + +function n = planeNormal(plane) +% Compute the normal to a plane + +% plane normal +outSz = size(plane); +outSz(2) = 3; +n = zeros(outSz); +n(:) = crossProduct3d(plane(:,4:6,:), plane(:, 7:9,:)); + +function alpha = sphericalAngle(p1, p2, p3) +% Compute angle between points on the sphere + +% test if points are given as matlab spherical coordinates +if size(p1, 2) == 2 + [x, y, z] = sph2cart(p1(:,1), p1(:,2), ones(size(p1,1), 1)); + p1 = [x y z]; + [x, y, z] = sph2cart(p2(:,1), p2(:,2), ones(size(p2,1), 1)); + p2 = [x y z]; + [x, y, z] = sph2cart(p3(:,1), p3(:,2), ones(size(p3,1), 1)); + p3 = [x y z]; +end + +% normalize points +p1 = normalizeVector3d(p1); +p2 = normalizeVector3d(p2); +p3 = normalizeVector3d(p3); + +% create the plane tangent to the unit sphere and containing central point +plane = createPlane(p2, p2); + +% project the two other points on the plane +pp1 = planePosition(projPointOnPlane(p1, plane), plane); +pp3 = planePosition(projPointOnPlane(p3, plane), plane); + +% compute angle on the tangent plane +pp2 = zeros(max(size(pp1, 1), size(pp3,1)), 2); +alpha = angle3Points(pp1, pp2, pp3); + + +function pos = planePosition(point, plane) +% Compute position of a point on a plane + +% size of input arguments +npl = size(plane, 1); +npt = size(point, 1); + +% check inputs have compatible sizes +if npl ~= npt && npl > 1 && npt > 1 + error('geom3d:planePoint:inputSize', ... + 'plane and point should have same size, or one of them must have 1 row'); +end + +% origin and direction vectors of the plane +p0 = plane(:, 1:3); +d1 = plane(:, 4:6); +d2 = plane(:, 7:9); + +% Compute dot products with direction vectors of the plane +if npl > 1 || npt == 1 + s = dot(bsxfun(@minus, point, p0), d1, 2) ./ vectorNorm3d(d1); + t = dot(bsxfun(@minus, point, p0), d2, 2) ./ vectorNorm3d(d2); +else + % we have npl == 1 and npt > 1 + d1 = d1 / vectorNorm3d(d1); + d2 = d2 / vectorNorm3d(d2); + inds = ones(npt,1); + s = dot(bsxfun(@minus, point, p0), d1(inds, :), 2); + t = dot(bsxfun(@minus, point, p0), d2(inds, :), 2); +end + +% % old version: +% s = dot(point-p0, d1, 2) ./ vectorNorm3d(d1); +% t = dot(point-p0, d2, 2) ./ vectorNorm3d(d2); + +pos = [s t]; + + +function point = projPointOnPlane(point, plane) +% Return the orthogonal projection of a point on a plane + +% Unpack the planes into origins and normals, keeping original shape +plSize = size(plane); +plSize(2) = 3; +[origins, normals] = deal(zeros(plSize)); +origins(:) = plane(:,1:3,:); +normals(:) = crossProduct3d(plane(:,4:6,:), plane(:, 7:9,:)); + +% difference between origins of plane and point +dp = bsxfun(@minus, origins, point); + +% relative position of point on normal's line +t = bsxfun(@rdivide, sum(bsxfun(@times,normals,dp),2), sum(normals.^2,2)); + +% add relative difference to project point back to plane +point = bsxfun(@plus, point, bsxfun(@times, t, normals)); + + +function points = intersectLineSphere(line, sphere, varargin) +%INTERSECTLINESPHERE Return intersection points between a line and a sphere + +% check if user-defined tolerance is given +tol = 1e-14; +if ~isempty(varargin) + tol = varargin{1}; +end + +% difference between centers +dc = bsxfun(@minus, line(:, 1:3), sphere(:, 1:3)); + +% equation coefficients +a = sum(line(:, 4:6) .* line(:, 4:6), 2); +b = 2 * sum(bsxfun(@times, dc, line(:, 4:6)), 2); +c = sum(dc.*dc, 2) - sphere(:,4).*sphere(:,4); + +% solve equation +delta = b.*b - 4*a.*c; + +% initialize empty results +points = NaN * ones(2 * size(delta, 1), 3); + +% process couples with two intersection points +inds = find(delta > tol); +if ~isempty(inds) + % delta positive: find two roots of second order equation + u1 = (-b(inds) -sqrt(delta(inds))) / 2 ./ a(inds); + u2 = (-b(inds) +sqrt(delta(inds))) / 2 ./ a(inds); + + % convert into 3D coordinate + points(inds, :) = line(inds, 1:3) + bsxfun(@times, u1, line(inds, 4:6)); + points(inds+length(delta),:) = line(inds, 1:3) + bsxfun(@times, u2, line(inds, 4:6)); +end + + +function theta = angle3Points(p1, p2, p3) +% Compute oriented angle made by 3 points +theta = lineAngle(createLine(p2, p1), createLine(p2, p3)); + + +function line = createLine(p1, p2) +% Create a line from two points on the line. +line = [p1(:,1), p1(:,2), p2(:,1)-p1(:,1), p2(:,2)-p1(:,2)]; + + +function theta = lineAngle(varargin) +% Computes angle between two straight lines + +if nargin == 1 + % angle of one line with horizontal + line = varargin{1}; + theta = mod(atan2(line(:,4), line(:,3)) + 2*pi, 2*pi); + +elseif nargin == 2 + % angle between two lines + theta1 = lineAngle(varargin{1}); + theta2 = lineAngle(varargin{2}); + theta = mod(bsxfun(@minus, theta2, theta1)+2*pi, 2*pi); +end + + +function c = crossProduct3d(a,b) +% Vector cross product faster than inbuilt MATLAB cross. + +% size of inputs +sizeA = size(a); +sizeB = size(b); + +% Initialise c to the size of a or b, whichever has more dimensions. If +% they have the same dimensions, initialise to the larger of the two +switch sign(numel(sizeA) - numel(sizeB)) + case 1 + c = zeros(sizeA); + case -1 + c = zeros(sizeB); + otherwise + c = zeros(max(sizeA, sizeB)); +end + +c(:) = bsxfun(@times, a(:,[2 3 1],:), b(:,[3 1 2],:)) - ... + bsxfun(@times, b(:,[2 3 1],:), a(:,[3 1 2],:)); + + +function vn = normalizeVector3d(v) +% Normalize a 3D vector to have norm equal to 1 +vn = bsxfun(@rdivide, v, sqrt(sum(v.^2, 2))); + +function n = vectorNorm3d(v) +% Norm of a 3D vector or of set of 3D vectors +n = sqrt(sum(v.*v, 2)); diff --git a/src/@Image/distanceMap.m b/src/@Image/distanceMap.m index 8a8c577..25cb0b6 100644 --- a/src/@Image/distanceMap.m +++ b/src/@Image/distanceMap.m @@ -3,22 +3,28 @@ % % MAP = distanceMap(BIN) % The distance transform is an operator applied to binary images, that -% results in a graylevel image that contains, for each foregournd pixel, +% results in an intensity image that contains, for each foreground pixel, % the distance to the closest background pixel. % +% This function requires binary image as input. For label images that +% contain adjacent regions, the function 'chamferDistanceMap' could be +% more adapted. +% % Example % img = Image.read('circles.png'); % map = distanceMap(img); % show(map) % % See also -% skeleton, geodesicDistanceMap +% skeleton, geodesicDistanceMap, chamferDistanceMap, +% regionInscribedCircle % ------ % Author: David Legland % e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) % Created: 2011-03-27, using Matlab 7.9.0.529 (R2009b) -% Copyright 2011 INRA - Cepia Software Platform. +% Copyright 2021 INRAE. % check type if ~strcmp(obj.Type, 'binary') diff --git a/src/@Image/extendedMaxima.m b/src/@Image/extendedMaxima.m index 4e757d9..8805f78 100644 --- a/src/@Image/extendedMaxima.m +++ b/src/@Image/extendedMaxima.m @@ -29,22 +29,12 @@ error('Requires a Grayscale or intensity image to work'); end -% default value for connectivity -conn = 4; -if obj.Dimension == 3 - conn = 6; -end - -% process input arguments -while ~isempty(varargin) - var = varargin{1}; +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); - if isnumeric(var) && isscalar(var) - % extract connectivity - conn = var; - varargin(1) = []; - continue; - end +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; end data = imextendedmax(obj.Data, dyn, conn); diff --git a/src/@Image/extendedMinima.m b/src/@Image/extendedMinima.m index 30a644a..d8eddef 100644 --- a/src/@Image/extendedMinima.m +++ b/src/@Image/extendedMinima.m @@ -21,23 +21,12 @@ error('Requires a Grayscale or intensity image to work'); end -% default values -conn = 4; +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); -if obj.Dimension == 3 - conn = 6; -end - -% process input arguments -while ~isempty(varargin) - var = varargin{1}; - - if isnumeric(var) && isscalar(var) - % extract connectivity - conn = var; - varargin(1) = []; - continue; - end +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; end data = imextendedmin(obj.Data, dyn, conn); diff --git a/src/@Image/fillHoles.m b/src/@Image/fillHoles.m index 26dc2dd..0f5070b 100644 --- a/src/@Image/fillHoles.m +++ b/src/@Image/fillHoles.m @@ -17,11 +17,8 @@ % Created: 2011-09-11, using Matlab 7.9.0.529 (R2009b) % Copyright 2011 INRA - Cepia Software Platform. -% default connectivity -conn = 4; -if ndims(obj) == 3 - conn = 6; -end +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); % parse input connectivity if ~isempty(varargin) diff --git a/src/@Image/floodFill.m b/src/@Image/floodFill.m index 22b2b4a..5baf523 100644 --- a/src/@Image/floodFill.m +++ b/src/@Image/floodFill.m @@ -5,6 +5,11 @@ % Determines the region of connected pixels with the same value and % containing the position POS, and replaces their value by V. % +% IMG2 = floodFill(IMG, POS, V, CONN) +% Also specifies the connectivity to use. Default connectivity is 4 for +% planar images, and 6 for 3D images. +% +% % Example % % remove the region corresponding to the label at a given position % img = Image.read('coins.png'); @@ -13,7 +18,7 @@ % figure; show(lbl2); colormap jet; % % See also -% reconstruction +% reconstruction, fillHoles, killBorders % % ------ @@ -29,6 +34,14 @@ 'position array size must match image dimension'); end +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); + +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; +end + % create binary image of mask pos = num2cell(pos); mask = obj == obj.Data(pos{:}); @@ -38,7 +51,7 @@ marker.Data(pos{:}) = true; % compute the region composed of connected pixels with same value -rec = reconstruction(marker, mask); +rec = reconstruction(marker, mask, conn); % replace values in result image name = createNewName(obj, '%s-floodFill'); diff --git a/src/@Image/geodesicDistanceMap.m b/src/@Image/geodesicDistanceMap.m index 6e6f779..50ed59c 100644 --- a/src/@Image/geodesicDistanceMap.m +++ b/src/@Image/geodesicDistanceMap.m @@ -65,7 +65,8 @@ % stability is reached. % % See also -% distanceMap, reconstruction +% distanceMap, reconstruction, chamferDistanceMap, +% regionGeodesicDiameter % % ------ @@ -176,7 +177,7 @@ outputType = class(w1); % allocate memory for result -dist = ones(size(mask), outputType); +dist = ones(size(mask.Data), outputType); % init result: either max value, or 0 for marker pixels if isinteger(w1) @@ -184,10 +185,10 @@ else dist(:) = inf; end -dist(marker) = 0; +dist(marker.Data) = 0; % size of image -[D1, D2] = size(mask); +[D1, D2] = size(mask.Data); %% Iterations until no more changes diff --git a/src/@Image/imposeMaxima.m b/src/@Image/imposeMaxima.m index a13a8f4..c3645a9 100644 --- a/src/@Image/imposeMaxima.m +++ b/src/@Image/imposeMaxima.m @@ -21,24 +21,14 @@ error('Requires a Grayscale or intensity image to work'); end -% default values -conn = 4; +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); -if obj.Dimension == 3 - conn = 6; +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; end -% process input arguments -while ~isempty(varargin) - var = varargin{1}; - - if isnumeric(var) && isscalar(var) - % extract connectivity - conn = var; - varargin(1) = []; - continue; - end -end if isa(marker, 'Image') marker = marker.Data; diff --git a/src/@Image/imposeMinima.m b/src/@Image/imposeMinima.m index 530e678..7c577c2 100644 --- a/src/@Image/imposeMinima.m +++ b/src/@Image/imposeMinima.m @@ -22,24 +22,14 @@ error('Requires a Grayscale or intensity image to work'); end -% default values -conn = 4; +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); -if obj.Dimension == 3 - conn = 6; +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; end -% process input arguments -while ~isempty(varargin) - var = varargin{1}; - - if isnumeric(var) && isscalar(var) - % extract connectivity - conn = var; - varargin(1) = []; - continue; - end -end if isa(marker, 'Image') marker = marker.Data; diff --git a/src/@Image/interp.m b/src/@Image/interp.m index ec87fb1..37743a5 100644 --- a/src/@Image/interp.m +++ b/src/@Image/interp.m @@ -3,8 +3,20 @@ % % V = interp(IMG, X, Y) % V = interp(IMG, X, Y, Z) +% Evaluates the value(s) within the image at the specified position(s), +% in pixel coordinates. +% The positions X, Y (and Z for 3D images) must be specified with numeric +% arrays the same size. +% For scalar images, the result V has the same size as the input arrays. +% % V = interp(IMG, POS) +% Speifies the positions as a N-by-2 or N-by-3 array of coordinates. The +% result V is a N-by-1 numeric array. +% % V = interp(..., 'method') +% Specifies the interpolation method to use. Valid options are the ones +% available for the interp2 and interp3 functions. +% Default is 'linear'. % % Example % interp @@ -15,7 +27,7 @@ % ------ % Author: David Legland -% e-mail: david.legland@inra.fr +% e-mail: david.legland@inrae.fr % Created: 2011-12-14, using Matlab 7.9.0.529 (R2009b) % Copyright 2011 INRA - Cepia Software Platform. @@ -54,7 +66,7 @@ y = yData(obj); if nc == 1 - val = interp2(y, x, double(obj.Data), ... + val(:) = interp2(y, x, double(obj.Data), ... point(:, 2), point(:, 1), method, fillValue); else for i = 1:nc @@ -69,7 +81,7 @@ y = yData(obj); z = zData(obj); if nc == 1 - val = interp3(y, x, z, double(obj.Data), ... + val(:) = interp3(y, x, z, double(obj.Data), ... point(:, 2), point(:, 1), point(:, 3), method, fillValue); else for i = 1:nc diff --git a/src/@Image/killBorders.m b/src/@Image/killBorders.m index 31f12b7..56fa144 100644 --- a/src/@Image/killBorders.m +++ b/src/@Image/killBorders.m @@ -23,10 +23,7 @@ % Copyright 2011 INRA - Cepia Software Platform. % choose default connectivity depending on dimension -conn = 4; -if ndims(obj) == 3 - conn = 6; -end +conn = defaultConnectivity(obj); % case of connectivity specified by user if ~isempty(varargin) diff --git a/src/@Image/largestRegion.m b/src/@Image/largestRegion.m index 5604bd0..828ce37 100644 --- a/src/@Image/largestRegion.m +++ b/src/@Image/largestRegion.m @@ -6,14 +6,14 @@ % image corresponding to obj label. Can be used to select automatically % the most proeminent region in a segmentation or labelling result. % -% [REG IND] = largestRegion(LBL) +% [REG, IND] = largestRegion(LBL) % Also returns the index of the largest region. % % REG = largestRegion(BIN) % REG = largestRegion(BIN, CONN) -% Finds the largest connected region in the binary image IMG. A labelling -% of the image is performed prior to the identification of the largest -% label. The connectivity can be specified. +% Finds the largest connected region in the binary image IMG. A connected +% component labelling of the image is performed prior to the +% identification of the largest label. The connectivity can be specified. % % Example % % Find the binary image corresponding to the largest label @@ -31,7 +31,7 @@ % 0 0 0 0 0 0 % 0 0 0 0 0 0 % -% % Keep largest region in a binary image +% % Keep the largest region in a binary image % BW = Image.read('text.png'); % BW2 = largestRegion(BW, 4); % figure; show(overlay(BW, BW2)); @@ -43,12 +43,13 @@ % show(overlay(img, bin2)); % % See also -% regionprops, killBorders, areaOpening +% regionprops, killBorders, areaOpening, componentLabeling, +% regionElementCounts % % ------ % Author: David Legland -% e-mail: david.legland@inra.fr +% e-mail: david.legland@inrae.fr % Created: 2012-07-27, using Matlab 7.9.0.529 (R2009b) % Copyright 2012 INRA - Cepia Software Platform. @@ -58,11 +59,10 @@ elseif isBinaryImage(obj) % if image is binary compute labeling - % first determines connectivity to use - conn = 4; - if obj.Dimension == 3 - conn = 6; - end + % choose default connectivity depending on dimension + conn = defaultConnectivity(obj); + + % case of connectivity specified by user if ~isempty(varargin) conn = varargin{1}; end diff --git a/src/@Image/medianFilter.m b/src/@Image/medianFilter.m index fc21f67..9868906 100644 --- a/src/@Image/medianFilter.m +++ b/src/@Image/medianFilter.m @@ -1,14 +1,18 @@ function res = medianFilter(obj, se, varargin) % Compute median value in the neighboorhood of each pixel. % +% RES = medianFilter(IMG2D, [M N]) +% RES = medianFilter(IMG3D, [M N P]) +% Applies median filtering to the input image, by computing the median +% value in the square or cubic neighborhood of each image element. +% % RES = medianFilter(IMG, SE) -% Compute the mean filter of image IMG, using structuring element SE. -% The goal of obj function is to provide the same interface as for -% other image filters (imopen, imerode ...), and to allow the use of -% mean filter with user-defined structuring element. +% Compute the median filter of image IMG, using structuring element SE. +% The goal of this function is to provide the same interface as for +% other image filters (opening, erosion...), and to allow the use of +% median filter with user-defined structuring element. % This function can be used for directional filtering. % -% % RES = medianFilter(IMG, SE, PADOPT) % also specify padding option. PADOPT can be one of: % 'zeros' @@ -17,21 +21,28 @@ % see ordfilt2 for details. Default is 'symmetric' (contrary to the % default for ordfilt2). % +% Implementation notes +% When neighborhood is given as a 1-by-2 or 1-by-3 array, the methods is +% a wrapper for the medfilt2 or medfilt3 function. Otherwise, the +% ordfilt2 function ise used. +% +% Example +% % apply median filtering on rice image +% img = Image.read('rice.png'); +% imgf = medianFilter(img, [3 3]); +% figure; show(imgf); +% % See also: % meanFilter, ordfilt2, median % % ------ % Author: David Legland -% e-mail: david.legland@inra.fr +% e-mail: david.legland@inrae.fr % Created: 2011-08-05, using Matlab 7.9.0.529 (R2009b) % Copyright 2011 INRA - Cepia Software Platform. -if obj.Dimension > 2 - error('Median filter implemented only for planar images'); -end - % transform STREL object into single array if isa(se, 'strel') se = getnhood(se); @@ -43,13 +54,26 @@ padopt = varargin{1}; end -% rotate structuring element -se = permute(se, [2 1 3:5]); - -% perform filtering -order = ceil(sum(se(:)) / 2); -data = ordfilt2(obj.Data, order, se, padopt); +if isnumeric(se) && all(size(se) == [1 2]) && obj.Dimension == 2 + % if input corresponds to filter size, use medfilt2 + data = medfilt2(obj.Data, se([2 1])); + +elseif isnumeric(se) && all(size(se) == [1 3]) && obj.Dimension == 3 + % process the 3D case, only for cubic neighborhoods + data = medfilt3(obj.Data, se([2 1 3])); + +else + % otherwise, use the ordfilt2 function by choosing the order according + % to SE size. + + % rotate structuring element + se = permute(se, [2 1 3:5]); + + % perform filtering + order = ceil(sum(se(:)) / 2); + data = ordfilt2(obj.Data, order, se, padopt); +end % create result image name = createNewName(obj, '%s-medianFilt'); -res = Image('Data', data, 'Parent', obj, 'Name', name); \ No newline at end of file +res = Image('Data', data, 'Parent', obj, 'Name', name); diff --git a/src/@Image/read.m b/src/@Image/read.m index f668dcc..e504f28 100644 --- a/src/@Image/read.m +++ b/src/@Image/read.m @@ -39,8 +39,30 @@ % Created: 2010-07-13, using Matlab 7.9.0.529 (R2009b) % Copyright 2010 INRAE - Cepia Software Platform. -% extract filename's extension -[path, name, ext] = fileparts(fileName); %#ok +% parse fileName components +[filePath, baseName, ext] = fileparts(fileName); + +% if file does not exist, try to add the path to the list of sample files +if exist(fileName, 'file') == 0 && isempty(filePath) + % retrieve the path to sample files + [filePath, ~] = fileparts(mfilename('fullpath')); + filePath = fullfile(filePath, 'sampleFiles'); + + % create newfile path + fileName = fullfile(filePath, [baseName ext]); + + % if file still does not exist, try to add default known extension + if exist(fileName, 'file') == 0 && isempty(ext) + knownExtensions = {'.tif', '.png'}; + for iExt = 1:length(knownExtensions) + ext = knownExtensions{iExt}; + fileName = fullfile(filePath, [baseName ext]); + if exist(fileName, 'file') > 0 + break; + end + end + end +end % try to deduce format from extension. % First use reader provided by Matlab then use readers in private directory. @@ -108,7 +130,7 @@ end % populate additional meta-data -img.Name = name; +img.Name = baseName; img.FilePath = fileName; end @@ -118,8 +140,13 @@ % % Can manage 3D images as well. +% disable some warnings specific to TIFF format +warning('off', 'imageio:tifftagsread:zeroComponentCount'); % check if the file contains a 3D image infoList = imfinfo(fileName); +% re-enable warnings +warning('on', 'imageio:tifftagsread:zeroComponentCount'); + info1 = infoList(1); read3d = false; if length(infoList) > 1 @@ -177,14 +204,15 @@ % default values nImages = size(img, 3); -nSlices = size(img, 3); -nChannels = size(img, 4); -nFrames = size(img, 5); +nSlices = 1; +nChannels = 1; +nFrames = 1; hyperstack = 'false'; %#ok spacing = img.Spacing; origin = img.Origin; unitName = img.UnitName; timeStep = img.TimeStep; +timeUnit = img.TimeUnit; % parse tokens in the "ImageDescription' Tag. tokens = regexp(desc, '\n', 'split'); @@ -214,6 +242,9 @@ origin = [0 0 0]; case 'finterval' timeStep = str2double(value); + case 'fps' + timeStep = 1 / str2double(value); + timeUnit = 's'; case {'min', 'max'} % nothing to do. otherwise @@ -239,6 +270,7 @@ img.Origin = origin; img.UnitName = unitName; img.TimeStep = timeStep; +img.TimeUnit = timeUnit; end diff --git a/src/@Image/reconstruction.m b/src/@Image/reconstruction.m index aefc6b2..922bd08 100644 --- a/src/@Image/reconstruction.m +++ b/src/@Image/reconstruction.m @@ -25,15 +25,28 @@ % ------ % Author: David Legland -% e-mail: david.legland@inra.fr +% e-mail: david.legland@inrae.fr % Created: 2011-08-01, using Matlab 7.9.0.529 (R2009b) % Copyright 2011 INRA - Cepia Software Platform. % HISTORY + +% Parse input arguments [marker, mask, parent] = parseInputCouple(marker, mask); -data = imreconstruct(marker, mask, varargin{:}); + +% choose default connectivity depending on dimension +conn = defaultConnectivity(parent); + +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; +end + + +% process reconstruction +data = imreconstruct(marker, mask, conn); % create result image -name = createNewName(parent, '%s-minima'); +name = createNewName(parent, '%s-rec'); res = Image('Data', data, 'Parent', parent, 'Type', parent.Type, 'Name', name); diff --git a/src/@Image/regionArea.m b/src/@Image/regionArea.m new file mode 100644 index 0000000..a63382d --- /dev/null +++ b/src/@Image/regionArea.m @@ -0,0 +1,56 @@ +function [area, labels] = regionArea(obj, varargin) +% Area of regions within a 2D binary or label image. +% +% A = regionArea(IMG); +% Compute the area of the regions in the image IMG. IMG can be either a +% binary image, or a label image. If IMG is binary, a single area is +% returned. In the case of a label image, the area of each region is +% returned in a column vector with as many elements as the number of +% regions. +% +% A = regionArea(..., LABELS); +% In the case of a label image, specifies the labels for which the area +% need to be computed. +% +% Example +% img = Image.read('rice.png'); +% img2 = img - opening(img, ones(30, 30)); +% lbl = componentLabeling(img2 > 50, 4); +% areas = regionArea(lbl); +% +% See Also +% regionprops, regionPerimeter, regionEulerNumber, regionElementCount +% regionVolume + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check dimensionality +nd = ndims(obj); +if nd ~= 2 + error('Requires a 2-dimensional image'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end + +% count the number of elements, and multiply by pixel volume +pixelCounts = regionElementCount(obj, labels); +area = pixelCounts * prod(obj.Spacing(1:2)); diff --git a/src/@Image/regionBoundingBox.m b/src/@Image/regionBoundingBox.m new file mode 100644 index 0000000..ff22aab --- /dev/null +++ b/src/@Image/regionBoundingBox.m @@ -0,0 +1,103 @@ +function [boxes, labels] = regionBoundingBox(obj, varargin) +% Bounding box of regions within a 2D or 3D binary or label image. +% +% BOX = regionBoundingBox(IMG) +% Compute the bounding boxes of the regions within the label image IMG. +% If the image is binary, a single box corresponding to the foreground +% (i.e. the pixels with value 1) is computed. +% +% The result is a N-by-4 array BOX = [XMIN XMAX YMIN YMAX], containing +% coordinates of the box extent. +% +% The same result could be obtained with the regionprops function. The +% advantage of using regionBoxes is that equivalent boxes can be +% obtained in one call. +% +% BOX = regionBoundingBox(IMG3D) +% If input image is a 3D array, the result is a N-by-6 array, containing +% the maximal coordinates in the X, Y and Z directions: +% BOX = [XMIN XMAX YMIN YMAX ZMIN ZMAX]. +% +% [BOX, LABELS] = regionBoundingBox(...) +% Also returns the labels of the regions for which a bounding box was +% computed. LABELS is a N-by-1 array with as many rows as BOX. +% +% [...] = regionBoxes(IMG, LABELS) +% Specifies the labels of the regions whose bounding box need to be +% computed. +% +% +% Example +% % Compute bounding box of coins regions +% img = Image.read('coins.png'); % read image +% bin = opening(img > 80, ones([3 3])); % binarize +% lbl = componentLabeling(bin); % compute labels +% figure; show(img); % display image +% boxes = regionBoundingBox(lbl); % compute bounding boxes +% hold on; drawBox(boxes, 'b'); % display boxes +% +% See also +% drawBox, regionCentroid +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-02-02, using Matlab 9.8.0.1323502 (R2020a) +% Copyright 2021 INRAE. + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && isnumeric(varargin{1}) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end + +% switch processing depending on dimension +nd = ndims(obj); +if nd == 2 + %% Process planar case + props = regionprops(obj.Data, 'BoundingBox'); + props = props(labels); + bb = reshape([props.BoundingBox], [4 length(props)])'; + + % convert to (x,y) indexing convention + boxes = [bb(:, 2) bb(:, 2)+bb(:, 4) bb(:, 1) bb(:, 1)+bb(:, 3)]; + + % spatial calibration + if isCalibrated(obj) + spacing = obj.Spacing([1 1 2 2]); + origin = obj.Origin([1 1 2 2]); + boxes = bsxfun(@plus, bsxfun(@times, (boxes - 1), spacing), origin); + end + +elseif nd == 3 + %% Process 3D case + props = regionprops3(obj.Data, 'BoundingBox'); + props = props(labels); + bb = reshape([props.BoundingBox], [6 size(props, 1)])'; + bb = bb(labels, :); + + % convert to (x,y,z) indexing convention + boxes = [bb(:, 2) bb(:, 2)+bb(:, 5) bb(:, 1) bb(:, 1)+bb(:, 4) bb(:, 3) bb(:, 3)+bb(:, 6)]; + + % spatial calibration + if isCalibrated(obj) + spacing = obj.Spacing([1 1 2 2 3 3]); + origin = obj.Origin([1 1 2 2 3 3]); + boxes = bsxfun(@plus, bsxfun(@times, (boxes - 1), spacing), origin); + end + +else + error('Image dimension must be 2 or 3'); +end diff --git a/src/@Image/regionBoxes.m b/src/@Image/regionBoxes.m index 2f6900a..5849a9b 100644 --- a/src/@Image/regionBoxes.m +++ b/src/@Image/regionBoxes.m @@ -1,6 +1,8 @@ function [boxes, labels] = regionBoxes(obj, varargin) % Bounding box of regions within a 2D or 3D binary or label image. % +% Note: deprecated, use 'regionBoundingBox' instead +% % BOX = regionBoxes(IMG) % Compute the bounding boxes of the regions within the label image IMG. % If the image is binary, a single box corresponding to the foreground @@ -47,6 +49,8 @@ % Created: 2021-02-02, using Matlab 9.8.0.1323502 (R2020a) % Copyright 2021 INRAE. +warning('Function "regionBoxes" is deprecated, use "regionBoundingBox" instead'); + % check image type if ~(isLabelImage(obj) || isBinaryImage(obj)) error('Requires a label of binary image'); diff --git a/src/@Image/regionCentroid.m b/src/@Image/regionCentroid.m new file mode 100644 index 0000000..1280eef --- /dev/null +++ b/src/@Image/regionCentroid.m @@ -0,0 +1,91 @@ +function [points, labels] = regionCentroid(obj, varargin) +% Centroid of region(s) in a binary or label image. +% +% C = regionCentroid(I) +% Returns the centroid C of the binary image I. C is a 1-by-2 or 1-by-3 +% row vector, depending on the dimension of the image. +% +% C = regionCentroid(LBL) +% If LBL is a label D-dimensional image, returns an array of N-by-D +% values, corresponding to the centroids of the N regions within the +% image. +% +% [C, LABELS] = regionCentroid(LBL) +% Also returns the labels of the regions that were measured. +% +% Example +% % Compute centroids of coins particles +% img = Image.read('coins.png'); % read image +% bin = opening(img > 80, ones([3 3])); % binarize +% lbl = componentLabeling(bin); % compute labels +% figure; show(img); % display image +% pts = regionCentroid(lbl); % compute centroids +% hold on; plot(pts(:,1), pts(:,2), 'b+'); % display centroids +% +% See also +% analyzeRegions, findRegionLabels, componentLabeling, regionprops +% regionEquivalentEllipse, regionBoundingBox + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2018-07-03, using Matlab 9.4.0.813654 (R2018a) +% Copyright 2018 INRA - Cepia Software Platform. + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + +% allocate memory for result +nd = ndims(obj); +points = zeros(nLabels, nd); + +% switch processing depending on image dimensionality +if nd == 2 + for i = 1:nLabels + % extract points of the current region + [x, y] = find(obj.Data == labels(i)); + + % coordinates of particle regionCentroid + xc = mean(x); + yc = mean(y); + + points(i, :) = [xc yc]; + end + +elseif nd == 3 + dim = size(obj.Data); + for i = 1:nLabels + % extract points of the current region + inds = find(obj.Data == labels(i)); + [x, y, z] = ind2sub(dim, inds); + + % coordinates of particle regionCentroid + xc = mean(x); + yc = mean(y); + zc = mean(z); + + points(i, :) = [xc yc zc]; + end + +else + error('Requires an image of dimension 2 or 3'); +end + +% calibrate result +if isCalibrated(obj) + points = bsxfun(@plus, bsxfun(@times, points-1, obj.Spacing), obj.Origin); +end diff --git a/src/@Image/regionCentroids.m b/src/@Image/regionCentroids.m index a940903..8b05146 100644 --- a/src/@Image/regionCentroids.m +++ b/src/@Image/regionCentroids.m @@ -1,6 +1,8 @@ function [points, labels] = regionCentroids(obj, varargin) % Centroid of region(s) in a binary or label image. % +% Note: deprecated, use 'regionCentroid' instead +% % C = regionCentroids(I) % Returns the centroid C of the binary image I. C is a 1-by-2 or 1-by-3 % row vector, depending on the dimension of the image. @@ -32,6 +34,8 @@ % Created: 2018-07-03, using Matlab 9.4.0.813654 (R2018a) % Copyright 2018 INRA - Cepia Software Platform. +warning('Function "regionCentroids" is deprecated, use "regionCentroid" instead'); + % check image type if ~(isLabelImage(obj) || isBinaryImage(obj)) error('Requires a label of binary image'); diff --git a/src/@Image/regionElementCount.m b/src/@Image/regionElementCount.m new file mode 100644 index 0000000..96a00c0 --- /dev/null +++ b/src/@Image/regionElementCount.m @@ -0,0 +1,54 @@ +function [counts, labels] = regionElementCount(obj, varargin) +% Count the number of pixels/voxels within each region of a label image. +% +% CNT = regionElementCount(LBL) +% For each region on the label image LBL, count the number of elements +% (pixels or voxels) that constitute this region. Return a column vector +% with as many elements as the number of regions. +% +% [CNT, LABELS] = regionElementCount(LBL) +% Also returns the labels of the regions. +% +% Example +% img = Image.read('coins.png'); +% bin = fillHoles(img > 100); +% lbl = componentLabeling(bin); +% regionElementCount(lbl)' +% ans = +% 2563 1899 2598 1840 2693 1906 2648 2725 1935 2796 +% +% See also +% regionCentroids, findRegionLabels, largestRegion +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2020-12-02, using Matlab 9.8.0.1323502 (R2020a) +% Copyright 2020 INRAE. + +% check input type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label image as input'); +end + +% determine labels +if isempty(varargin) + labels = unique(obj.Data(:)); + labels(labels==0) = []; +else + labels = varargin{1}; +end + +% rely on regionprops for speed +if size(obj.Data, 3) == 1 + props = regionprops(obj.Data, 'Area'); + counts = [props.Area]'; + counts = counts(labels); +else + props = regionprops3(obj.Data, 'Volume'); + counts = [props.Volume]; + counts = counts(labels); +end + \ No newline at end of file diff --git a/src/@Image/regionElementCounts.m b/src/@Image/regionElementCounts.m index 79683f4..0d99c0b 100644 --- a/src/@Image/regionElementCounts.m +++ b/src/@Image/regionElementCounts.m @@ -1,6 +1,8 @@ function [counts, labels] = regionElementCounts(obj, varargin) % Count the number of pixels/voxels within each region of a label image. % +% Note: deprecated, use 'regionElementCount' instead +% % CNT = regionElementCounts(LBL) % For each region on the label image LBL, count the number of elements % (pixels or voxels) that constitute this region. Return a column vector @@ -18,7 +20,7 @@ % 2563 1899 2598 1840 2693 1906 2648 2725 1935 2796 % % See also -% regionCentroids, findRegionLabels +% regionCentroids, findRegionLabels, largestRegion % % ------ @@ -28,6 +30,8 @@ % Created: 2020-12-02, using Matlab 9.8.0.1323502 (R2020a) % Copyright 2020 INRAE. +warning('Function "regionElementCounts" is deprecated, use "regionElementCount" instead'); + % check input type if ~isLabelImage(obj) error('Requires a label image as input'); diff --git a/src/@Image/regionEquivalentEllipse.m b/src/@Image/regionEquivalentEllipse.m new file mode 100644 index 0000000..8255f1e --- /dev/null +++ b/src/@Image/regionEquivalentEllipse.m @@ -0,0 +1,124 @@ +function [ellipse, labels] = regionEquivalentEllipse(obj, varargin) +% Equivalent ellipse of region(s) in a binary or label image. +% +% ELLI = regionEquivalentEllipse(IMG) +% Computes the ellipse with same second order moments for each region in +% label image IMG. If the case of a binary image, a single ellipse +% corresponding to the foreground (i.e. to the region with pixel value 1) +% will be computed. +% +% The result is a N-by-5 array ELLI = [XC YC A B THETA], containing the +% coordinates of ellipse center, the lengths of major and minor +% semi-axes, and the orientation of the largest axis (in degrees, +% counter-clockwise). +% +% ELLI = regionEquivalentEllipse(..., LABELS) +% Specifies the labels for which the equivalent ellipse needs to be +% computed. The result is a N-by-5 array with as many rows as the number +% of labels. +% +% +% Example +% % Draw a complex particle together with its equivalent ellipse +% img = Image.read('circles.png'); +% show(img); hold on; +% elli = regionEquivalentEllipse(img); +% drawEllipse(elli); % requires the MatGeom toolbox +% +% % Compute and display the equivalent ellipses of several regions +% img = Image.read('rice.png'); +% img2 = img - opening(img, ones(30, 30)); +% lbl = componentLabeling(img2 > 50, 4); +% ellipses = regionEquivalentEllipse(lbl); +% show(img); hold on; +% drawEllipse(ellipses, 'linewidth', 2, 'color', 'g'); +% +% See also +% regionCentroid, regionBoundingBox, regionEquivalentEllipsoid, +% drawEllipse, regionInscribedCircle +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-02-02, using Matlab 9.8.0.1323502 (R2020a) +% Copyright 2021 INRAE. + +%% check image type and dimension +if ~(isBinaryImage(obj) || isLabelImage(obj)) + error('Requires a label of binary image'); +end +if ndims(obj) ~= 2 %#ok + error('Requires a 2D image'); +end + + +%% Retrieve spatial calibration + +% extract calibration +spacing = obj.Spacing; +origin = obj.Origin; +calib = isCalibrated(obj); + + +%% Initialisations + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + +% allocate memory for result +ellipse = zeros(nLabels, 5); + + +%% Extract ellipse corresponding to each label + +for i = 1:nLabels + % extract points of the current region + [x, y] = find(obj.Data==labels(i)); + + % transform to physical space if needed + if calib + x = (x-1) * spacing(1) + origin(1); + y = (y-1) * spacing(2) + origin(2); + end + + % compute centroid, used as center of equivalent ellipse + xc = mean(x); + yc = mean(y); + + % recenter points (should be better for numerical accuracy) + x = x - xc; + y = y - yc; + + % number of points + n = length(x); + + % compute second order parameters. 1/12 is the contribution of a single + % pixel, then for regions with only one pixel the resulting ellipse has + % positive radii. + Ixx = sum(x.^2) / n + spacing(1)^2/12; + Iyy = sum(y.^2) / n + spacing(2)^2/12; + Ixy = sum(x.*y) / n; + + % compute semi-axis lengths of ellipse + common = sqrt( (Ixx - Iyy)^2 + 4 * Ixy^2); + ra = sqrt(2) * sqrt(Ixx + Iyy + common); + rb = sqrt(2) * sqrt(Ixx + Iyy - common); + + % compute ellipse angle and convert into degrees + theta = atan2(2 * Ixy, Ixx - Iyy) / 2; + theta = theta * 180 / pi; + + % create the resulting equivalent ellipse + ellipse(i,:) = [xc yc ra rb theta]; +end diff --git a/src/@Image/regionEquivalentEllipses.m b/src/@Image/regionEquivalentEllipses.m index 8240f53..27655e5 100644 --- a/src/@Image/regionEquivalentEllipses.m +++ b/src/@Image/regionEquivalentEllipses.m @@ -1,6 +1,8 @@ function [ellipse, labels] = regionEquivalentEllipses(obj, varargin) % Equivalent ellipse of region(s) in a binary or label image. % +% Note: deprecated, use 'regionEquivalentEllipse' instead +% % ELLI = regionEquivalentEllipses(IMG) % Computes the ellipse with same second order moments for each region in % label image IMG. If the case of a binary image, a single ellipse @@ -34,7 +36,7 @@ % drawEllipse(ellipses, 'linewidth', 2, 'color', 'g'); % % See also -% regionCentroids, regionBoxes +% regionCentroids, regionBoxes, regionOrientedBox % % ------ @@ -44,6 +46,7 @@ % Created: 2021-02-02, using Matlab 9.8.0.1323502 (R2020a) % Copyright 2021 INRAE. +warning('Function "regionEquivalentEllipses" is deprecated, use "regionEquivalentEllipse" instead'); %% extract spatial calibration diff --git a/src/@Image/regionEquivalentEllipsoid.m b/src/@Image/regionEquivalentEllipsoid.m new file mode 100644 index 0000000..31e7933 --- /dev/null +++ b/src/@Image/regionEquivalentEllipsoid.m @@ -0,0 +1,190 @@ +function [ellipsoid, labels] = regionEquivalentEllipsoid(obj, varargin) +% Equivalent ellipsoid of region(s) in a 3D binary or label image. +% +% ELLI = regionEquivalentEllipsoid(IMG) +% Where IMG is a binary image of a single region. +% ELLI = [XC YC ZC A B C PHI THETA PSI] is an ellipsoid defined by its +% center [XC YC ZC], 3 radiusses A, B anc C, and a 3D orientation angle +% given by (PHI, THETA, PSI). +% +% ELLI = regionEquivalentEllipsoid(LBL) +% Compute the ellipsoid with same second order moments for each region in +% label image LBL. If the case of a binary image, a single ellipsoid +% corresponding to the foreground (i.e. to the region with voxel value 1) +% will be computed. +% The result ELLI is NL-by-9 array, with NL being the number of unique +% labels in the input image. +% +% ELLI = regionEquivalentEllipsoid(..., LABELS) +% Specify the labels for which the ellipsoid needs to be computed. The +% result is a N-by-9 array with as many rows as the number of labels. +% +% Example +% % Generate an ellipsoid image and computes the equivalent ellipsoid +% % (one expects to obtain nearly same results) +% elli = [50 50 50 50 30 10 40 30 20]; +% img = Image(discreteEllipsoid(1:100, 1:100, 1:100, elli)); +% elli2 = regionEquivalentEllipsoid(img) +% elli2 = +% 50.00 50.00 50.00 50.0072 30.0032 10.0072 40.0375 29.9994 20.0182 +% +% % Draw equivalent ellipsoid of human head image +% % (requires image processing toolbox, and slicer program for display) +% img = Image.read('brainMRI.hdr'); +% img.Spacing = [1 1 2.5]; % fix spacing for display +% bin = closing(img > 0, ones([3 3 3])); +% figure; showOrthoSlices(img, [60 80 13]); +% axis equal; view(3); +% elli = regionEquivalentEllipsoid(bin); +% drawEllipsoid(elli, 'FaceAlpha', 0.5) % requires MatGeom library +% +% See also +% regionEquivalentEllipse, regionBoundingBox, regionCentroid +% drawEllipsoid, regionprops3 +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-03, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% check image type and dimension +if ~(isBinaryImage(obj) || isLabelImage(obj)) + error('Requires a label of binary image'); +end +if ndims(obj) ~= 3 + error('Requires a 3D image'); +end + + +%% Retrieve spatial calibration + +% extract calibration +spacing = obj.Spacing; +origin = obj.Origin; +dim = size(obj); + + +%% Initialisations + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end + +% allocate memory for result +nLabels = length(labels); +ellipsoid = zeros(nLabels, 9); + +for i = 1:nLabels + % extract position of voxels for the current region + inds = find(obj.Data == labels(i)); + if isempty(inds) + continue; + end + [x, y, z] = ind2sub(dim, inds); + + % compute approximate location of ellipsoid center + xc = mean(x); + yc = mean(y); + zc = mean(z); + + % compute center (in pixel coordinates) + center = [xc yc zc]; + + % recenter points (should be better for numerical accuracy) + x = (x - xc) * spacing(1); + y = (y - yc) * spacing(2); + z = (z - zc) * spacing(3); + + points = [x y z]; + + % compute the covariance matrix + covPts = cov(points, 1) + diag(spacing.^2 / 12); + + % perform a principal component analysis with 3 variables, + % to extract equivalent axes + [U, S] = svd(covPts); + + % extract length of each semi axis + radii = sqrt(5) * sqrt(diag(S))'; + + % sort axes from greater to lower + [radii, ind] = sort(radii, 'descend'); + + % format U to ensure first axis points to positive x direction + U = U(ind, :); + if U(1,1) < 0 + U = -U; + % keep matrix determinant positive + U(:,3) = -U(:,3); + end + + % convert axes rotation matrix to Euler angles + angles = rotation3dToEulerAngles(U); + + % concatenate result to form an ellipsoid object + center = (center - 1) .* spacing + origin; + ellipsoid(i, :) = [center radii angles]; +end + + +function varargout = rotation3dToEulerAngles(mat) +% Extract Euler angles from a rotation matrix. +% +% [PHI, THETA, PSI] = rotation3dToEulerAngles(MAT) +% Computes Euler angles PHI, THETA and PSI (in degrees) from a 3D 4-by-4 +% or 3-by-3 rotation matrix. +% +% ANGLES = rotation3dToEulerAngles(MAT) +% Concatenates results in a single 1-by-3 row vector. This format is used +% for representing some 3D shapes like ellipsoids. +% +% Example +% rotation3dToEulerAngles +% +% References +% Code from Graphics Gems IV on euler angles +% http://tog.acm.org/resources/GraphicsGems/gemsiv/euler_angle/EulerAngles.c +% Modified using explanations in: +% http://www.gregslabaugh.name/publications/euler.pdf +% +% See also +% MatGeom library + +% conversion from radians to degrees +k = 180 / pi; + +% extract |cos(theta)| +cy = hypot(mat(1, 1), mat(2, 1)); + +% avoid dividing by 0 +if cy > 16*eps + % normal case: theta <> 0 + psi = k * atan2( mat(3, 2), mat(3, 3)); + theta = k * atan2(-mat(3, 1), cy); + phi = k * atan2( mat(2, 1), mat(1, 1)); +else + % theta close to 0 + psi = k * atan2(-mat(2, 3), mat(2, 2)); + theta = k * atan2(-mat(3, 1), cy); + phi = 0; +end + +% format output arguments +if nargout <= 1 + % one array + varargout{1} = [phi theta psi]; +else + % three separate arrays + varargout = {phi, theta, psi}; +end diff --git a/src/@Image/regionEulerNumber.m b/src/@Image/regionEulerNumber.m new file mode 100644 index 0000000..308b4d6 --- /dev/null +++ b/src/@Image/regionEulerNumber.m @@ -0,0 +1,351 @@ +function [chi, labels] = regionEulerNumber(obj, varargin) +% Euler number of regions within a binary or label image. +% +% The function computes the Euler number, or Euler-Poincare +% characteristic, of a binary 2D image. The result corresponds to the +% number of connected components, minus the number of holes in the image. +% +% CHI = regionEulerNumber(IMG); +% Return the Euler-Poincaré Characteristic of the binary structure within +% the image IMG. +% +% CHI = regionEulerNumber(IMG, CONN); +% Specifies the connectivity used. Currently 4 and 8 connectivities are +% supported. +% +% Example +% img = Image.read('coins.png'); +% bin = closing(img>80, ones(3,3)); +% regionEulerNumber(bin) +% ans = +% 10 +% +% See Also: +% regionArea, regionPerimeter, regionSurfaceArea, regionprops +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Parse input arguments + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check image dimension +nd = ndims(obj); +if nd == 2 + conn = 4; +elseif nd == 3 + conn = 6; +else + error('First argument should be a 2D or a 3D image'); +end + +% default options +labels = []; + + +% parse input arguments +while ~isempty(varargin) + var1 = varargin{1}; + varargin(1) = []; + + if isscalar(var1) + % connectivity + conn = var1; + elseif size(var1, 2) == 1 + % the labels to compute + labels = var1; + else + error('Unable to interpret input argument'); + end +end + + +%% Process label image + +if isBinaryImage(obj) + % in case of binary image, compute only one label + if nd == 2 + chi = eulerNumberBinary2d(obj.Data, conn); + else + chi = eulerNumberBinary3d(obj.Data, conn); + end + labels = 1; + +elseif isLabelImage(obj) + % in case of a label image, return a vector with a set of results. + + % extract labels if necessary (considers 0 as background) + if isempty(labels) + labels = findRegionLabels(obj); + end + + % allocate result array + nLabels = length(labels); + chi = zeros(nLabels, 1); + + % compute bounding box of each region + bounds = regionMinMaxIndices(obj, labels); + + % compute Euler number of each label considered as binary image + if nd == 2 + for i = 1:nLabels + label = labels(i); + + % convert bounding box to image extent, in x and y directions + bx = bounds(i, [1 2]); + by = bounds(i, [3 4]); + + bin = obj.Data(bx(1):bx(2), by(1):by(2)) == label; + + chi(i) = eulerNumberBinary2d(bin, conn); + end + else + for i = 1:nLabels + label = labels(i); + + % convert bounding box to image extent, in x and y directions + bx = bounds(i, [1 2]); + by = bounds(i, [3 4]); + bz = bounds(i, [5 6]); + + bin = obj.Data(bx(1):bx(2), by(1):by(2), bz(1):bz(2)) == label; + chi(i) = eulerNumberBinary3d(bin, conn); + end + end +else + error('Wrong type of image'); +end + + +%% Process 2D binary image +function chi = eulerNumberBinary2d(img, conn) +% Compute euler number on a 2D binary image. +% +% Axis order of array follow physical order: X, Y. +% + +% size of image in each direction +N1 = size(img, 1); +N2 = size(img, 2); + +% compute number of nodes, number of edges (H and V) and number of faces. +% principle is erosion with simple structural elements (line, square) +% but it is replaced here by simple boolean operation, which is faster + +% count vertices +n = sum(img(:)); + +% count horizontal and vertical edges +n1 = sum(sum(img(1:N1-1,:) & img(2:N1,:))); +n2 = sum(sum(img(:,1:N2-1) & img(:,2:N2))); + +% count square faces +n1234 = sum(sum(... + img(1:N1-1,1:N2-1) & img(1:N1-1,2:N2) & ... + img(2:N1,1:N2-1) & img(2:N1,2:N2) )); + +if conn == 4 + % compute euler characteristics from graph counts + chi = n - n1 - n2 + n1234; + return; + +elseif conn == 8 + % For 8-connectivity, need also to count diagonal edges + n3 = sum(sum(img(1:N1-1,1:N2-1) & img(2:N1,2:N2))); + n4 = sum(sum(img(1:N1-1,2:N2) & img(2:N1,1:N2-1))); + + % and triangular faces + n123 = sum(sum(img(1:N1-1,1:N2-1) & img(1:N1-1,2:N2) & img(2:N1,1:N2-1) )); + n124 = sum(sum(img(1:N1-1,1:N2-1) & img(1:N1-1,2:N2) & img(2:N1,2:N2) )); + n134 = sum(sum(img(1:N1-1,1:N2-1) & img(2:N1,1:N2-1) & img(2:N1,2:N2) )); + n234 = sum(sum(img(1:N1-1,2:N2) & img(2:N1,1:N2-1) & img(2:N1,2:N2) )); + + % compute euler characteristics from graph counts + % chi = Nvertices - Nedges + Ntriangles + Nsquares + chi = n - (n1+n2+n3+n4) + (n123+n124+n134+n234) - n1234; + +else + error('regionEulerNumber: uknown connectivity option'); +end + +%% Process 3D binary image +function chi = eulerNumberBinary3d(img, conn) +% Compute euler number on a 3D binary image. + +% size of image in each direction +dim = size(img); +N1 = dim(1); +N2 = dim(2); +N3 = dim(3); + +% compute number of nodes, number of edges in each direction, number of +% faces in each plane, and number of cells. +% principle is erosion with simple structural elements (line, square) +% but it is replaced here by simple boolean operation, which is faster + +% count vertices +v = sum(img(:)); + +% count edges in each direction +e1 = sum(sum(sum(img(1:N1-1,:,:) & img(2:N1,:,:)))); +e2 = sum(sum(sum(img(:,1:N2-1,:) & img(:,2:N2,:)))); +e3 = sum(sum(sum(img(:,:,1:N3-1,:) & img(:,:,2:N3)))); + +% count square faces orthogonal to each main directions +f1 = sum(sum(sum(... + img(:, 1:N2-1, 1:N3-1) & img(:, 1:N2-1, 2:N3) & ... + img(:, 2:N2, 1:N3-1) & img(:, 2:N2, 2:N3) ))); +f2 = sum(sum(sum(... + img(1:N1-1, :, 1:N3-1) & img(1:N1-1, :, 2:N3) & ... + img(2:N1, :, 1:N3-1) & img(2:N1, :, 2:N3) ))); +f3 = sum(sum(sum(... + img(1:N1-1, 1:N2-1, :) & img(1:N1-1, 2:N2, :) & ... + img(2:N1, 1:N2-1, :) & img(2:N1, 2:N2, :) ))); + +% compute number of cubes +s = sum(sum(sum(... + img(1:N1-1, 1:N2-1, 1:N3-1) & img(1:N1-1, 2:N2, 1:N3-1) & ... + img(2:N1, 1:N2-1, 1:N3-1) & img(2:N1, 2:N2, 1:N3-1) & ... + img(1:N1-1, 1:N2-1, 2:N3) & img(1:N1-1, 2:N2, 2:N3) & ... + img(2:N1, 1:N2-1, 2:N3) & img(2:N1, 2:N2, 2:N3) ))); + +if conn == 6 + % compute euler characteristics using graph formula + chi = v - (e1+e2+e3) + (f1+f2+f3) - s; + return; + +elseif conn == 26 + + % compute EPC inside image, with correction on edges + % Compute the map of 2-by-2-by-2 configurations within image + configMap = img(1:N1-1, 1:N2-1, 1:N3-1) + ... + 2*img(2:N1, 1:N2-1, 1:N3-1) + ... + 4*img(1:N1-1, 2:N2, 1:N3-1) + ... + 8*img(2:N1, 2:N2, 1:N3-1) + ... + 16*img(1:N1-1, 1:N2-1, 2:N3) + ... + 32*img(2:N1, 1:N2-1, 2:N3) + ... + 64*img(1:N1-1, 2:N2, 2:N3) + ... + 128*img(2:N1, 2:N2, 2:N3); + % Compute histogram of configurations + histo = histcounts(configMap(:), 'BinLimits', [0 255], 'BinMethod', 'integers'); + % mutliplies by pre-computed contribution of each configuration + epcc = sum(histo .* eulerLutC26()); + + % Compute edge correction, in order to compensate it and obtain the + % Euler-Poincare characteristic computed for whole image. + + % compute epc on faces % 4 + f10 = imEuler2dC8(squeeze(img(1,:,:))); + f11 = imEuler2dC8(squeeze(img(N1,:,:))); + f20 = imEuler2dC8(squeeze(img(:,1,:))); + f21 = imEuler2dC8(squeeze(img(:,N2,:))); + f30 = imEuler2dC8(img(:,:,1)); + f31 = imEuler2dC8(img(:,:,N3)); + epcf = f10 + f11 + f20 + f21 + f30 + f31; + + % compute epc on edges % 24 + e11 = imEuler1d(img(:,1,1)); + e12 = imEuler1d(img(:,1,N3)); + e13 = imEuler1d(img(:,N2,1)); + e14 = imEuler1d(img(:,N2,N3)); + + e21 = imEuler1d(img(1,:,1)); + e22 = imEuler1d(img(1,:,N3)); + e23 = imEuler1d(img(N1,:,1)); + e24 = imEuler1d(img(N1,:,N3)); + + e31 = imEuler1d(img(1,1,:)); + e32 = imEuler1d(img(1,N2,:)); + e33 = imEuler1d(img(N1,1,:)); + e34 = imEuler1d(img(N1,N2,:)); + + epce = e11 + e12 + e13 + e14 + e21 + e22 + e23 + e24 + ... + e31 + e32 + e33 + e34; + + % compute epc on vertices + epcn = img(1,1,1) + img(1,1,N3) + img(1,N2,1) + img(1,N2,N3) + ... + img(N1,1,1) + img(N1,1,N3) + img(N1,N2,1) + img(N1,N2,N3); + + % compute epc from measurements made on interior of window, and + % facets of lower dimension + chi = epcc + ( epcf/2 - epce/4 + epcn/8); + +else + error('imEuler3d: uknown connectivity option'); +end + +function tab = eulerLutC26 +% Return the pre-computed array of Euler number contribution of each +% 2-by-2-by-2 configuration +tab = [... + 0 1 1 0 1 0 -2 -1 1 -2 0 -1 0 -1 -1 0 ... + 1 0 -2 -1 -2 -1 -1 -2 -6 -3 -3 -2 -3 -2 0 -1 ... + 1 -2 0 -1 -6 -3 -3 -2 -2 -1 -1 -2 -3 0 -2 -1 ... + 0 -1 -1 0 -3 -2 0 -1 -3 0 -2 -1 0 1 1 0 ... + 1 -2 -6 -3 0 -1 -3 -2 -2 -1 -3 0 -1 -2 -2 -1 ... + 0 -1 -3 -2 -1 0 0 -1 -3 0 0 1 -2 -1 1 0 ... + -2 -1 -3 0 -3 0 0 1 -1 4 0 3 0 3 1 2 ... + -1 -2 -2 -1 -2 -1 1 0 0 3 1 2 1 2 2 1 ... + 1 -6 -2 -3 -2 -3 -1 0 0 -3 -1 -2 -1 -2 -2 -1 ... + -2 -3 -1 0 -1 0 4 3 -3 0 0 1 0 1 3 2 ... + 0 -3 -1 -2 -3 0 0 1 -1 0 0 -1 -2 1 -1 0 ... + -1 -2 -2 -1 0 1 3 2 -2 1 -1 0 1 2 2 1 ... + 0 -3 -3 0 -1 -2 0 1 -1 0 -2 1 0 -1 -1 0 ... + -1 -2 0 1 -2 -1 3 2 -2 1 1 2 -1 0 2 1 ... + -1 0 -2 1 -2 1 1 2 -2 3 -1 2 -1 2 0 1 ... + 0 -1 -1 0 -1 0 2 1 -1 2 0 1 0 1 1 0 ... +] / 8; + + +function chi = imEuler2dC8(img) + +% size of image in each direction +dim = size(img); +N1 = dim(1); +N2 = dim(2); + +% compute number of nodes, number of edges (H and V) and number of faces. +% principle is erosion with simple structural elements (line, square) +% but it is replaced here by simple boolean operation, which is faster + +% count vertices +n = sum(img(:)); + +% count horizontal and vertical edges +n1 = sum(sum(img(1:N1-1,:) & img(2:N1,:))); +n2 = sum(sum(img(:,1:N2-1) & img(:,2:N2))); + +% count square faces +n1234 = sum(sum(... + img(1:N1-1,1:N2-1) & img(1:N1-1,2:N2) & ... + img(2:N1,1:N2-1) & img(2:N1,2:N2) )); + +% For 8-connectivity, need also to count diagonal edges +n3 = sum(sum(img(1:N1-1,1:N2-1) & img(2:N1,2:N2))); +n4 = sum(sum(img(1:N1-1,2:N2) & img(2:N1,1:N2-1))); + +% and triangular faces +n123 = sum(sum(img(1:N1-1,1:N2-1) & img(1:N1-1,2:N2) & img(2:N1,1:N2-1) )); +n124 = sum(sum(img(1:N1-1,1:N2-1) & img(1:N1-1,2:N2) & img(2:N1,2:N2) )); +n134 = sum(sum(img(1:N1-1,1:N2-1) & img(2:N1,1:N2-1) & img(2:N1,2:N2) )); +n234 = sum(sum(img(1:N1-1,2:N2) & img(2:N1,1:N2-1) & img(2:N1,2:N2) )); + +% compute Eeuler characteristics from graph counts +% chi = Nvertices - Nedges + Ntriangles + Nsquares +chi = n - (n1+n2+n3+n4) + (n123+n124+n134+n234) - n1234; + + +function chi = imEuler1d(img) +% Compute Euler number of a binary 1D image. +chi = sum(img(:)) - sum(img(1:end-1) & img(2:end)); + diff --git a/src/@Image/regionFeretDiameter.m b/src/@Image/regionFeretDiameter.m new file mode 100644 index 0000000..9187ece --- /dev/null +++ b/src/@Image/regionFeretDiameter.m @@ -0,0 +1,154 @@ +function [fd, labels] = regionFeretDiameter(obj, varargin) +% Feret diameter of region(s) for a given direction. +% +% FD = imFeretDiameter(IMG, THETA); +% Compute the Feret diameter for particles in image IMG (binary or +% label), for the direction THETA, given in degrees. +% The result is a N-by-1 column vector, containing the Feret diameter of +% each particle in IMG. +% +% THETA can be a set of directions. In this case, the result has as many +% columns as the number of directions, and as many rows as the number of +% particles. +% +% FD = imFeretDiameter(IMG); +% Uses a default set of directions (180) for computing Feret diameters. +% +% FD = imFeretDiameter(..., LABELS); +% Specifies the labels for which the Feret diameter should be computed. +% LABELS is a N-by-1 column vector. This can be used to save computation +% time when only few particles / regions are of interset within the +% entire image. +% +% [FD, LABELS] = imFeretDiameter(...); +% Also returns the set of labels that were considered for measure. +% +% The maximum Feret diameter can be obtained using a max() function, or +% by calling the "regionMaxFeretDiameter" function. +% +% Example: +% % compute Feret diameter for a discrete square +% data = zeros(100, 100, 'uint8'); +% data(21:80, 21:80) = 1; +% img = Image(data, 'type', 'binary'); +% theta = linspace(0, 180, 201); +% fd = regionFeretDiameter(img, theta); +% figure(1); clf; set(gca, 'fontsize', 14); +% plot(theta, fd); xlim([0 180]); +% xlabel('Angle (in degrees)'); +% ylabel('Diameter (in pixels)'); +% title('Feret diameter of discrete square'); +% +% % max Feret diameter: +% diam = max(fd, [], 2) +% ans = +% 84.4386 +% +% See also +% regionMaxFeretDiameter, regionOrientedBox +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-10-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Process input arguments + +if ndims(obj) ~= 2 %#ok + error('Requires 2D image as input'); +end + +% Extract number of orientations +theta = 180; +if ~isempty(varargin) + var1 = varargin{1}; + if isscalar(var1) + % Number of directions given as scalar + theta = var1; + varargin(1) = []; + + elseif ndims(var1) == 2 && sum(size(var1) ~= [1 2]) ~= 0 %#ok + % direction set given as vector + theta = var1; + varargin(1) = []; + end +end + + +%% Extract spatial calibration + +% extract calibration +spacing = obj.Spacing; +origin = obj.Origin; +calib = isCalibrated(obj); + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + + +%% Initialisations + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + +% allocate memory for result +nTheta = length(theta); +fd = zeros(nLabels, nTheta); + +% iterate over labels +for i = 1:nLabels + % extract pixel centroids + [x, y] = find(obj.Data == labels(i)); + if isempty(x) + continue; + end + + % transform to physical space if needed + if calib + x = (x-1) * spacing(1) + origin(1); + y = (y-1) * spacing(2) + origin(2); + end + + % keep only points of the convex hull + try + inds = convhull(x, y); + x = x(inds); + y = y(inds); + catch ME %#ok + % an exception can occur if points are colinear. + % in this case we transform all points + end + + % recenter points (should be better for numerical accuracy) + x = x - mean(x); + y = y - mean(y); + + % iterate over orientations + for t = 1:nTheta + % convert angle to radians, and change sign (to make transformed + % points aligned along x-axis) + theta2 = -theta(t) * pi / 180; + + % compute only transformed x-coordinate + x2 = x * cos(theta2) - y * sin(theta2); + + % compute diameter for extreme coordinates + xmin = min(x2); + xmax = max(x2); + + % store result (add 1 pixel to consider pixel width) + dl = spacing(1) * abs(cos(theta2)) + spacing(2) * abs(sin(theta2)); + fd(i, t) = xmax - xmin + dl; + end +end + diff --git a/src/@Image/regionGeodesicDiameter.m b/src/@Image/regionGeodesicDiameter.m new file mode 100644 index 0000000..5bc2d98 --- /dev/null +++ b/src/@Image/regionGeodesicDiameter.m @@ -0,0 +1,233 @@ +function [gd, labels] = regionGeodesicDiameter(obj, varargin) +% Compute geodesic diameter of regions within a label imag. +% +% GD = regionGeodesicDiameter(IMG) +% where IMG is a label image, returns the geodesic diameter of each +% particle in the image. If IMG is a binary image, a connected-components +% labelling is performed first. +% GD is a column vector containing the geodesic diameter of each particle. +% +% GD = regionGeodesicDiameter(IMG, WS) +% Specifies the weights associated to neighbor pixels. WS(1) is the +% distance to orthogonal pixels, and WS(2) is the distance to diagonal +% pixels. An optional WS(3) weight may be specified, corresponding to +% chess-knight moves. Default is [5 7 11], recommended for 5-by-5 masks. +% The final length is normalized by weight for orthogonal pixels. For +% thin structures (skeletonization result), or for very close particles, +% the [3 4] weights recommended by Borgeors may be more appropriate. +% +% GD = regionGeodesicDiameter(..., 'verbose', true); +% Display some informations about the computation procedure, that may +% take some time for large and/or complicated images. +% +% [GD, LABELS] = regionGeodesicDiameter(...); +% Also returns the list of labels for which the geodesic diameter was +% computed. +% +% +% These algorithm uses 3 steps: +% * first propagate distance from region boundary to find a pixel +% approximately in the center of the particle(s) +% * propagate distances from the center, and keep the furthest pixel, +% which is assumed to be a geodesic extremity +% * propagate distances from the geodesic extremity, and keep the maximal +% distance. +% This algorithm is less time-consuming than the direct approach that +% consists in computing geodesic propagation and keeping the max value. +% However, for some cases (e.g. particles with holes) in can happen that +% the two methods give different results. +% +% +% Notes: +% * only planar images are currently supported. +% * the particles are assumed to be 8 connected. If two or more particles +% touch by a corner, the result will not be valid. +% +% +% Example +% % segment and labelize image of grains, and compute their geodesic +% % diameter +% img = Image.read('rice.png'); +% img2 = whiteTopHat(img, ones(30, 30)); +% bin = opening(img2 > 50, ones(3, 3)); +% lbl = componentLabeling(bin); +% gd = regionGeodesicDiameter(lbl); +% plot(gd, '+'); +% +% References +% * Lantuejoul, C. and Beucher, S. (1981): "On the use of geodesic metric +% in image analysis", J. Microsc., 121(1), pp. 39-49. +% http://dx.doi.org/10.1111/j.1365-2818.1981.tb01197.x +% * Coster & Chermant: "Precis d'analyse d'images", Ed. CNRS 1989. +% +% See also +% regionMaxFeretDiameter, geodesicDistanceMap, chamferDistanceMap +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Default values + +% weights for propagating geodesic distances +ws = [5 7 11]; + +% no verbosity by default +verbose = 0; + +labels = []; + + +%% Process input arguments + +% extract weights if present +if ~isempty(varargin) + if isnumeric(varargin{1}) + ws = varargin{1}; + varargin(1) = []; + end +end + +% Extract options +while ~isempty(varargin) + paramName = varargin{1}; + if strcmpi(paramName, 'verbose') + verbose = varargin{2}; + elseif strcmpi(paramName, 'labels') + labels = varargin{2}; + else + error(['Unkown option in regionGeodesicDiameter: ' paramName]); + end + varargin(1:2) = []; +end + +% make input image a label image if this is not the case +if isBinaryImage(obj) + labels = 1; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + + +%% Detection of center point (furthest point from boundary) + +if verbose + disp(sprintf('Computing geodesic diameters of %d region(s).', nLabels)); %#ok<*DSPS> +end + +if verbose + disp('Computing initial centers...'); +end + +% computation of distance map from empirical markers +dist = chamferDistanceMap(obj, ws, 'normalize', false, 'verbose', verbose); + + +%% Second pass: find a geodesic extremity + +if verbose + disp('Create marker image of initial centers'); +end + +% Create arrays to find the pixel with largest distance in each label +maxVals = -ones(nLabels, 1); +maxValInds = zeros(nLabels, 1); + +% iterate over pixels, and compare current distance with max distance +% stored for corresponding label +for i = 1:numel(obj.Data) + label = obj.Data(i); + + if label > 0 + ind = find(labels == label); + if dist.Data(i) > maxVals(ind) + maxVals(ind) = dist.Data(i); + maxValInds(ind) = i; + end + end +end + +% compute new seed point in each label, and use it as new marker +markers = Image.false(size(obj)); +markers.Data(maxValInds) = 1; + +if verbose + disp('Propagate distance from initial centers'); +end + +% recomputes geodesic distance from new markers +dist = geodesicDistanceMap(markers, obj, ws, 'normalize', false, 'verbose', verbose); + + +%% third pass: find second geodesic extremity + +if verbose + disp('Create marker image of first geodesic extremity'); +end + +% reset arrays to find the pixel with largest distance in each label +maxVals = -ones(nLabels, 1); +maxValInds = zeros(nLabels, 1); + +% iterate over pixels to identify second geodesic extremities +for i = 1:numel(obj.Data) + label = obj.Data(i); + if label > 0 + ind = find(labels == label); + if dist.Data(i) > maxVals(ind) + maxVals(ind) = dist.Data(i); + maxValInds(ind) = i; + end + end +end + +% compute new seed point in each label, and use it as new marker +markers = Image.false(size(obj)); +markers.Data(maxValInds) = 1; + +if verbose + disp('Propagate distance from first geodesic extremity'); +end + +% recomputes geodesic distance from new markers +dist = geodesicDistanceMap(markers, obj, ws, 'normalize', false, 'verbose', verbose); + + +%% Final computation of geodesic distances + +if verbose + disp('Compute geodesic diameters'); +end + +% keep max geodesic distance inside each label +if isinteger(ws) + gd = zeros(nLabels, 1, class(ws)); +else + gd = -ones(nLabels, 1, class(ws)); +end + +for i = 1:numel(obj.Data) + label = obj.Data(i); + if label > 0 + ind = find(labels == label); + if dist.Data(i) > gd(ind) + gd(ind) = dist.Data(i); + end + end +end + +% normalize by first weight, and add 1 for taking into account pixel +% thickness +gd = gd / ws(1) + 1; + +% finally, normalize with spatial calibration of image +gd = gd * obj.Spacing(1); diff --git a/src/@Image/regionInscribedBall.m b/src/@Image/regionInscribedBall.m new file mode 100644 index 0000000..c52219a --- /dev/null +++ b/src/@Image/regionInscribedBall.m @@ -0,0 +1,71 @@ +function [ball, labels] = regionInscribedBall(obj, varargin) +% Largest ball inscribed within a region. +% +% BALL = regionInscribedBall(IMG) +% Computes the maximal ball inscribed in a given region of a 3D binary +% image, or within each region of 3D label image. +% +% BALL = regionInscribedBall(..., LABELS) +% Specify the labels for which the inscribed balls needs to be computed. +% The result is a N-by-3 array with as many rows as the number of labels. +% +% Example +% img = Image.false([12 12 12]); +% img(2:10, 2:10, 2:10) = 1; +% ball = regionInscribedBall(img) +% ball = +% 6 6 6 5 +% +% See also +% regionEquivalentEllipsoid, drawSphere, distanceMap, +% regionInscribedCircle +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Process input arguments + +if ndims(obj.Data) ~= 3 + error('Requires a 3D input image'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + + +%% Main processing + +% allocate memory for result (3 coords + 1 radius) +ball = zeros(nLabels, 4); + +for iLabel = 1:nLabels + % compute distance map + distMap = distanceMap(obj == labels(iLabel)); + + % find value and position of the maximum + [maxi, inds] = max(distMap.Data(:)); + [yb, xb, zb] = ind2sub(size(distMap), inds); + + ball(iLabel,:) = [xb yb zb maxi]; +end + +% apply spatial calibration +if isCalibrated(obj) + ball(:,1:3) = bsxfun(@plus, bsxfun(@times, ball(:,1:3) - 1, obj.Spacing), obj.Origin); + ball(:,4) = ball(:,4) * obj.Spacing(1); +end diff --git a/src/@Image/regionInscribedCircle.m b/src/@Image/regionInscribedCircle.m new file mode 100644 index 0000000..c05862e --- /dev/null +++ b/src/@Image/regionInscribedCircle.m @@ -0,0 +1,78 @@ +function [circle, labels] = regionInscribedCircle(obj, varargin) +% Largest circle inscribed within a region. +% +% CIRC = regionInscribedCircle(IMG) +% Computes the maximal circle inscribed in a given region of a binary +% image, or within each region of label image. +% +% CIRC = regionInscribedCircle(..., LABELS) +% Specify the labels for which the inscribed circle needs to be computed. +% The result is a N-by-3 array with as many rows as the number of labels. +% +% +% Example +% % Draw a commplex particle together with its enclosing circle +% img = fillHoles(Image.read('circles.png')); +% figure; show(img); hold on; +% circ = regionInscribedCircle(img); +% drawCircle(circ, 'LineWidth', 2) +% +% % Compute and display the equivalent ellipses of several particles +% img = Image.read('rice.png'); +% img2 = whiteTopHat(img, ones(30, 30)); +% lbl = componentLabeling(img2 > 50, 4); +% circles = regionInscribedCircle(lbl); +% figure; show(img); hold on; +% drawCircle(circles, 'LineWidth', 2, 'Color', 'g'); +% +% See also +% regionEquivalentEllipse, drawCircle, distanceMap +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + +%% Process input arguments + +if ~ismatrix(obj.Data) + error('Requires a 2D input image'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + + +%% Main processing + +% allocate memory for result +circle = zeros(nLabels, 3); + +for iLabel = 1:nLabels + % compute distance map + distMap = distanceMap(obj == labels(iLabel)); + + % find value and position of the maximum + maxi = max(distMap(:)); + [xc, yc] = find(distMap==maxi, 1, 'first'); + + circle(iLabel,:) = [xc yc maxi]; +end + +% apply spatial calibration +if isCalibrated(obj) + circle(:,1:2) = bsxfun(@plus, bsxfun(@times, circle(:,1:2) - 1, obj.Spacing), obj.Origin); + circle(:,3) = circle(:,3) * obj.Spacing(1); +end diff --git a/src/@Image/regionIsosurface.m b/src/@Image/regionIsosurface.m new file mode 100644 index 0000000..e14652c --- /dev/null +++ b/src/@Image/regionIsosurface.m @@ -0,0 +1,129 @@ +function [res, labels] = regionIsosurface(obj, varargin) +% Generate isosurface of each region within a label image. +% +% MESHES = regionIsosurface(LBL) +% Computes isosurfaces of each region within the label image LBL. +% +% +% Example +% markers = Image.true([50 50 50]); +% n = 100; +% rng(10); +% seeds = randi(50, [n 3]); +% for i = 1:n +% markers(seeds(i,1), seeds(i,2), seeds(i,3)) = false; +% end +% wat = watershed(distanceMap(markers), 6); +% wat2 = killBorders(wat); +% figure; hold on; axis equal; +% regionIsosurface(wat2, 'smoothRadius', 2, 'LineStyle', 'none'); +% view(3), light; +% +% See also +% isosurface, gt +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-02-22, using Matlab 9.8.0.1323502 (R2020a) +% Copyright 2021 INRAE. + + +%% Input arguiment processing + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; + varargin(1) = []; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + +% parse other options +smoothRadius = 0; +siz = 0; +if ~isempty(varargin) && strcmpi(varargin{1}, 'smoothRadius') + smoothRadius = varargin{2}; + if isscalar(smoothRadius) + smoothRadius = smoothRadius([1 1 1]); + end + siz = floor(smoothRadius * 2) + 1; + varargin(1:2) = []; +end + + +%% Preprocessing + +% retrieve voxel coordinates in physical space +% (common to all labels) +x = getX(obj); +y = getY(obj); +z = getZ(obj); + +% permute data to comply with Matlab orientation +v = permute(obj.Data(:,:,:,1,1), [2 1 3]); + +% allocate memory for labels +meshes = cell(1, nLabels); + + +%% Isosurface computation + +% iterate over regions to generate isosurfaces +for iLabel = 1:nLabels + label = labels(iLabel); + + vol = double(v == label); + % optional smoothing + if smoothRadius > 0 + vol = filterData(vol, siz, smoothRadius); + end + + meshes{iLabel} = isosurface(x, y, z, vol, 0.5); +end + + +%% Finalization + +if nargout == 0 + % display isosurfaces + for i = 1:nLabels + patch(meshes{i}, varargin{:}); + end + +else + % return computed mesh list + res = meshes; +end + + +%% Utility function +function data = filterData(data, kernelSize, sigma) +% process each direction +for i = 1:3 + % compute spatial reference + refSize = (kernelSize(i) - 1) / 2; + s0 = floor(refSize); + s1 = ceil(refSize); + lx = -s0:s1; + + % compute normalized kernel + sigma2 = 2*sigma(i).^2; + kernel = exp(-(lx.^2 / sigma2)); + kernel = kernel / sum(kernel); + + % reshape the kernel such as it is elongated in the i-th direction + newDim = [ones(1, i-1) kernelSize(i) ones(1, 3-i)]; + kernel = reshape(kernel, newDim); + + % apply filtering along one direction + data = imfilter(data, kernel, 'replicate'); +end + diff --git a/src/@Image/regionIsosurfaces.m b/src/@Image/regionIsosurfaces.m index 7ab4312..bbf6177 100644 --- a/src/@Image/regionIsosurfaces.m +++ b/src/@Image/regionIsosurfaces.m @@ -1,6 +1,8 @@ function [res, labels] = regionIsosurfaces(obj, varargin) % Generate isosurface of each region within a label image. % +% Deprecated: replaced by regionIsosurface +% % MESHES = regionIsosurfaces(LBL) % Computes isosurfaces of each region within the label image LBL. % @@ -30,6 +32,8 @@ % Created: 2021-02-22, using Matlab 9.8.0.1323502 (R2020a) % Copyright 2021 INRAE. +warning('deprecated: replaced by "regionIsosurface" method'); + %% Input arguiment processing diff --git a/src/@Image/regionMaxFeretDiameter.m b/src/@Image/regionMaxFeretDiameter.m new file mode 100644 index 0000000..42469ed --- /dev/null +++ b/src/@Image/regionMaxFeretDiameter.m @@ -0,0 +1,87 @@ +function [diam, thetaMax] = regionMaxFeretDiameter(obj, varargin) +% Maximum Feret diameter of regions within a binary or label image. +% +% FD = regionMaxFeretDiameter(IMG) +% Computes the maximum Feret diameter of particles in label image IMG. +% The result is a N-by-1 column vector, containing the Feret diameter of +% each particle in IMG. +% +% [FD, THETAMAX] = regionMaxFeretDiameter(IMG) +% Also returns the direction for which the diameter is maximal. THETAMAX +% is given in degrees, between 0 and 180. +% +% FD = regionMaxFeretDiameter(IMG, LABELS) +% Specify the labels for which the Feret diameter needs to be computed. +% The result is a N-by-1 array with as many rows as the number of labels. +% +% +% Example +% img = Image.read('circles.png'); +% diam = regionMaxFeretDiameter(img) +% diam = +% 272.7144 +% +% See also +% regionFeretDiameter, regionOrientedBox +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-10-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Process input arguments + +if ndims(obj) ~= 2 %#ok + error('Requires 2D image as input'); +end + +% extract orientations +thetas = 180; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + thetas = varargin{1}; + varargin(1) = []; +end + +if isscalar(thetas) + % assume this is the number of directions to use + thetas = linspace(0, 180, thetas+1); + thetas = thetas(1:end-1); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + + +%% Initialisations + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + +% allocate memory for result +diam = zeros(nLabels, 1); +thetaMax = zeros(nLabels, 1); + + +%% Main processing + +% for each region, compute set of diameters, and keep the max +for i = 1:nLabels + % compute Feret Diameters of current region + diams = regionFeretDiameter(obj == labels(i), thetas); + + % find max diameter, with indices + [diam(i), ind] = max(diams, [], 2); + + % keep max orientation + thetaMax(i) = thetas(ind); +end diff --git a/src/@Image/regionMeanBreadth.m b/src/@Image/regionMeanBreadth.m new file mode 100644 index 0000000..f7fa811 --- /dev/null +++ b/src/@Image/regionMeanBreadth.m @@ -0,0 +1,285 @@ +function [breadth, labels] = regionMeanBreadth(obj, varargin) +% Mean breadth of regions within a 3D binary or label image. +% +% B = regionMeanBreadth(IMG) +% Computes the mean breadth of the binary structure in IMG, or of each +% particle in the label image IMG. +% +% B = regionMeanBreadth(IMG, NDIRS) +% Specifies the number of directions used for estimating the mean breadth +% from the Crofton formula. Can be either 3 (the default) or 13. +% +% B = regionMeanBreadth(..., SPACING) +% Specifies the spatial calibration of the image. SPACING is a 1-by-3 row +% vector containing the voxel size in the X, Y and Z directions, in that +% orders. +% +% [B, LABELS]= regionMeanBreadth(LBL, ...) +% Also returns the set of labels for which the mean breadth was computed. +% +% Example +% % define a ball from its center and a radius (use a slight shift to +% % avoid discretisation artefacts) +% xc = 50.12; yc = 50.23; zc = 50.34; radius = 40.0; +% % Create a discretized image of the ball +% [x y z] = meshgrid(1:100, 1:100, 1:100); +% img = Image(sqrt( (x - xc).^2 + (y - yc).^2 + (z - zc).^2) < radius); +% % compute mean breadth of the ball +% % (expected: the diameter of the ball) +% b = regionMeanBreadth(img) +% b = +% 80 +% +% +% % compute mean breadth of several regions in a label image +% img = Image(zeros([10 10 10], 'uint8'), 'Type', 'Label'); +% img(2:3, 2:3, 2:3) = 1; +% img(5:8, 2:3, 2:3) = 2; +% img(5:8, 5:8, 2:3) = 4; +% img(2:3, 5:8, 2:3) = 3; +% img(5:8, 5:8, 5:8) = 8; +% [breadths, labels] = regionMeanBreadth(img) +% breadths = +% 2.1410 +% 2.0676 +% 2.0676 +% 3.9942 +% 4.9208 +% labels = +% 1 +% 2 +% 3 +% 4 +% 8 +% +% See also +% regionVolume, regionsSurfaceArea, regionsEulerNumber, regionPerimeter +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Parse input arguments + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check dimensionality +nd = ndims(obj); +if nd ~= 3 + error('Requires a 3-dimensional image'); +end + +% default values of parameters +nDirs = 13; +labels = []; + +% Process user input arguments +while ~isempty(varargin) + var1 = varargin{1}; + + if isnumeric(var1) + % option is either connectivity or resolution + if isscalar(var1) + nDirs = var1; + elseif size(var1, 2) == 1 + labels = var1; + end + varargin(1) = []; + + else + error('option should be numeric'); + end + +end + + +%% Process label images + +if isBinaryImage(obj) + % in case of binary image, compute only one label + breadth = meanBreadthBinaryData(obj.Data, nDirs, obj.Spacing); + labels = 1; + +else + % in case of a label image, return a vector with a set of results + + % extract labels if necessary (considers 0 as background) + if isempty(labels) + labels = findRegionLabels(obj); + end + + % allocate result array + nLabels = length(labels); + breadth = zeros(nLabels, 1); + + % compute bounding box of each region + bounds = regionMinMaxIndices(obj, labels); + + % compute perimeter of each region considered as binary image + for i = 1:nLabels + label = labels(i); + + % convert bounding box to image extent, in x and y directions + bx = bounds(i, [1 2]); + by = bounds(i, [3 4]); + bz = bounds(i, [5 6]); + + bin = obj.Data(bx(1):bx(2), by(1):by(2), bz(1):bz(2)) == label; + breadth(i) = meanBreadthBinaryData(bin, nDirs, obj.Spacing); + end +end + + +%% Process Binary data +function breadth = meanBreadthBinaryData(img, nDirs, spacing) + +%% Process binary images + +% pre-compute distances between a pixel and its neighbours. +d1 = spacing(1); +d2 = spacing(2); +d3 = spacing(3); +vol = d1 * d2 * d3; + +%% Main processing for 3 directions + +% number of voxels +nv = sum(img(:)); + +% number of connected components along the 3 main directions +ne1 = sum(sum(sum(img(1:end-1,:,:) & img(2:end,:,:)))); +ne2 = sum(sum(sum(img(:,1:end-1,:) & img(:,2:end,:)))); +ne3 = sum(sum(sum(img(:,:,1:end-1) & img(:,:,2:end)))); + +% number of square faces on plane with normal directions 1 to 3 +nf1 = sum(sum(sum(... + img(:,1:end-1,1:end-1) & img(:,2:end,1:end-1) & ... + img(:,1:end-1,2:end) & img(:,2:end,2:end) ))); +nf2 = sum(sum(sum(... + img(1:end-1,:,1:end-1) & img(2:end,:,1:end-1) & ... + img(1:end-1,:,2:end) & img(2:end,:,2:end) ))); +nf3 = sum(sum(sum(... + img(1:end-1,1:end-1,:) & img(2:end,1:end-1,:) & ... + img(1:end-1,2:end,:) & img(2:end,2:end,:) ))); + +% mean breadth in 3 main directions +b1 = nv - (ne2 + ne3) + nf1; +b2 = nv - (ne1 + ne3) + nf2; +b3 = nv - (ne1 + ne2) + nf3; + +% inverse of planar density (in m = m^3/m^2) in each direction +a1 = vol / (d2 * d3); +a2 = vol / (d1 * d3); +a3 = vol / (d1 * d2); + +if nDirs == 3 + breadth = (b1 * a1 + b2 * a2 + b3 * a3) / 3; + return; +end + + +% number of connected components along the 6 planar diagonal +ne4 = sum(sum(sum(img(1:end-1,1:end-1,:) & img(2:end,2:end,:)))); +ne5 = sum(sum(sum(img(1:end-1,2:end,:) & img(2:end,1:end-1,:)))); +ne6 = sum(sum(sum(img(1:end-1,:,1:end-1) & img(2:end,:,2:end)))); +ne7 = sum(sum(sum(img(1:end-1,:,2:end) & img(2:end,:,1:end-1)))); +ne8 = sum(sum(sum(img(:,1:end-1,1:end-1,:) & img(:,2:end,2:end)))); +ne9 = sum(sum(sum(img(:,1:end-1,2:end,:) & img(:,2:end,1:end-1)))); + +% number of square faces on plane with normal directions 4 to 9 +nf4 = sum(sum(sum(... + img(2:end,1:end-1,1:end-1) & img(1:end-1,2:end,1:end-1) & ... + img(2:end,1:end-1,2:end) & img(1:end-1,2:end,2:end) ))); +nf5 = sum(sum(sum(... + img(1:end-1,1:end-1,1:end-1) & img(2:end,2:end,1:end-1) & ... + img(1:end-1,1:end-1,2:end) & img(2:end,2:end,2:end) ))); + +nf6 = sum(sum(sum(... + img(2:end,1:end-1,1:end-1) & img(2:end,2:end,1:end-1) & ... + img(1:end-1,1:end-1,2:end) & img(1:end-1,2:end,2:end) ))); +nf7 = sum(sum(sum(... + img(1:end-1,1:end-1,1:end-1) & img(1:end-1,2:end,1:end-1) & ... + img(2:end,1:end-1,2:end) & img(2:end,2:end,2:end) ))); + +nf8 = sum(sum(sum(... + img(1:end-1,2:end,1:end-1) & img(2:end,2:end,1:end-1) & ... + img(1:end-1,1:end-1,2:end) & img(2:end,1:end-1,2:end) ))); +nf9 = sum(sum(sum(... + img(1:end-1,1:end-1,1:end-1) & img(2:end,1:end-1,1:end-1) & ... + img(1:end-1,2:end,2:end) & img(2:end,2:end,2:end) ))); + +b4 = nv - (ne5 + ne3) + nf4; +b5 = nv - (ne4 + ne3) + nf5; +b6 = nv - (ne7 + ne2) + nf6; +b7 = nv - (ne6 + ne2) + nf7; +b8 = nv - (ne9 + ne1) + nf8; +b9 = nv - (ne8 + ne1) + nf9; + +% number of triangular faces on plane with normal directions 10 to 13 +nf10 = sum(sum(sum(... + img(2:end,1:end-1,1:end-1) & img(1:end-1,2:end,1:end-1) & ... + img(1:end-1,1:end-1,2:end) ))) ... + + sum(sum(sum(... + img(2:end,2:end,1:end-1) & img(1:end-1,2:end,2:end) & ... + img(2:end,1:end-1,2:end) ))) ; + +nf11 = sum(sum(sum(... + img(1:end-1,1:end-1,1:end-1) & img(2:end,2:end,1:end-1) & ... + img(2:end,1:end-1,2:end) ))) ... + + sum(sum(sum(... + img(1:end-1,2:end,1:end-1) & img(1:end-1,1:end-1,2:end) & ... + img(2:end,2:end,2:end) ))) ; + +nf12 = sum(sum(sum(... + img(1:end-1,1:end-1,1:end-1) & img(2:end,2:end,1:end-1) & ... + img(1:end-1,2:end,2:end) ))) ... + + sum(sum(sum(... + img(2:end,1:end-1,1:end-1) & img(1:end-1,1:end-1,2:end) & ... + img(2:end,2:end,2:end) ))) ; + +nf13 = sum(sum(sum(... + img(2:end,1:end-1,1:end-1) & img(1:end-1,2:end,1:end-1) & ... + img(2:end,2:end,2:end) ))) ... + + sum(sum(sum(... + img(1:end-1,1:end-1,1:end-1) & img(2:end,1:end-1,2:end) & ... + img(1:end-1,2:end,2:end) ))) ; + +% length of diagonals +d12 = hypot(d1, d2); +d13 = hypot(d1, d3); +d23 = hypot(d2, d3); + +% inverse of planar density (in m = m^3/m^2) in directions 4 to 13 +a4 = vol / (d3 * d12); +a6 = vol / (d2 * d13); +a8 = vol / (d1 * d23); + +% compute area of diagonal triangle via Heron's formula +s = (d12 + d13 + d23) / 2; +a10 = vol / (2 * sqrt( s * (s-d12) * (s-d13) * (s-d23) )); + + +b10 = nv - (ne5 + ne7 + ne9) + nf10; +b11 = nv - (ne4 + ne6 + ne9) + nf11; +b12 = nv - (ne4 + ne7 + ne8) + nf12; +b13 = nv - (ne5 + ne6 + ne8) + nf13; + +if nDirs ~= 13 + error('Unknown number of directions'); +end + +c = Image.directionWeights3d13(spacing); + +% weighted average over directions +breadth = ... + (b1*c(1)*a1 + b2*c(2)*a2 + b3*c(3)*a3) + ... + ((b4+b5)*c(4)*a4 + (b6+b7)*c(6)*a6 + (b8+b9)*c(8)*a8) + ... + ((b10 + b11 + b12 + b13)*c(10)*a10) ; diff --git a/src/@Image/regionMinMaxIndices.m b/src/@Image/regionMinMaxIndices.m new file mode 100644 index 0000000..9bd361b --- /dev/null +++ b/src/@Image/regionMinMaxIndices.m @@ -0,0 +1,80 @@ +function [boxes, labels] = regionMinMaxIndices(obj, varargin) +% Bounding indices of regions within a label image, in pixel coordinates. +% +% BI = regionMinMaxIndices(lbl); +% Similar to the regionBoundingBox function, but do not take into account +% spatial calibration of images. +% Returns a set of start and end indices for each dimension and each +% region. +% BI = [INDXMIN INDXMAX INDYMIN INDYMAX] +% +% The region within the imag can be cropped using: +% img2 = img(BI(1,1):BI(1,2), BI(2,1):BI(2,2)); +% +% Example +% % crop an image using the result of regionMinMaxIndices +% img = Image.read('circles.png'); +% bi = regionMinMaxIndices(img); +% img2 = img{bi(1):bi(2), bi(3):bi(4)}; +% figure; +% show(img2) +% +% See also +% drawBox, regionBoundingBox +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-10-19, using Matlab 9.8.0.1323502 (R2020a) +% Copyright 2021 INRAE. + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && isnumeric(varargin{1}) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end + +% switch processing depending on dimension +nd = ndims(obj); +if nd == 2 + %% Process planar case + % compute bounds using regionprops for speed. Result is a struct array. + props = regionprops(obj.Data, 'BoundingBox'); + props = props(labels); + bb = reshape([props.BoundingBox], [4 length(props)])'; + + % round start index + bb(:,[1 2]) = ceil(bb(:,[1 2])); + bb(:,[3 4]) = bb(:,[3 4]) - 1; + + % convert to (x,y) indexing convention + boxes = [bb(:, 2) bb(:, 2)+bb(:, 4) bb(:, 1) bb(:, 1)+bb(:, 3)]; + +elseif nd == 3 + %% Process 3D case + % compute bounds using regionprops3 for speed. Result is a table. + props = regionprops3(obj.Data, 'BoundingBox'); + bb = props.BoundingBox(labels, :); + + % round start index + bb(:,[1 2 3]) = ceil(bb(:,[1 2 3])); + bb(:,[4 5 6]) = bb(:,[4 5 6]) - 1; + + % convert to (x,y,z) indexing convention + boxes = [bb(:, 2) bb(:, 2)+bb(:, 5) bb(:, 1) bb(:, 1)+bb(:, 4) bb(:, 3) bb(:, 3)+bb(:, 6)]; + +else + error('Image dimension must be 2 or 3'); +end diff --git a/src/@Image/regionOrientedBox.m b/src/@Image/regionOrientedBox.m new file mode 100644 index 0000000..af2741e --- /dev/null +++ b/src/@Image/regionOrientedBox.m @@ -0,0 +1,252 @@ +function rect = regionOrientedBox(obj, varargin) +% Minimum-width oriented bounding box of region(s) within image. +% +% OBB = regionOrientedBox(IMG); +% Computes the minimum width oriented bounding box of the region(s) in +% image IMG. IMG is either a binary or a label image. +% The result OBB is a N-by-5 array, containing the center, the length, +% the width, and the orientation of the bounding box of each particle in +% image. The orientation is given in degrees, in the direction of the +% largest box axis. +% +% [OBB, LABELS] = regionOrientedBox(...); +% Also returns the list of region labels for which the bounding box was +% computed. +% +% Example +% % Compute and display the oriented box of several rice grains +% img = Image.read('rice.png'); +% img2 = img - opening(img, ones(30, 30)); +% lbl = componentLabeling(img2 > 50, 4); +% boxes = regionOrientedBox(lbl); +% show(img); hold on; +% drawOrientedBox(boxes, 'linewidth', 2, 'color', 'g'); +% +% See also +% regionFeretDiameter, regionEquivalentEllipse, regionMaxFeretDiameter + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-10-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Process input arguments + +if ndims(obj) ~= 2 %#ok + error('Requires 2D image as input'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + + +%% Initialisations + +%% Extract spatial calibration + +% extract calibration +spacing = obj.Spacing; +origin = obj.Origin; +calib = isCalibrated(obj); + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end +nLabels = length(labels); + +% allocate memory for result +rect = zeros(nLabels, 5); + + +%% Iterate over labels + +for i = 1:nLabels + % extract points of the current region + [x, y] = find(obj.Data == labels(i)); + if isempty(x) + continue; + end + + % transform to physical space if needed + if calib + x = (x-1) * spacing(1) + origin(1); + y = (y-1) * spacing(2) + origin(2); + end + + % special case of regions composed of only one pixel + if length(x) == 1 + rect(i,:) = [x y 1 1 0]; + continue; + end + + % compute bounding box of region pixel centers + try + obox = orientedBox([x y]); + catch ME %#ok + % if points are aligned, convex hull computation fails. + % Perform manual computation of box. + xc = mean(x); + yc = mean(y); + x = x - xc; + y = y - yc; + + theta = mean(mod(atan2(y, x), pi)); + [x2, y2] = transformPoint(x, y, createRotation(-theta)); %#ok + dmin = min(x2); + dmax = max(x2); + center = [(dmin + dmax)/2 0]; + center = transformPoint(center, createRotation(theta)) + [xc yc]; + obox = [center (dmax-dmin) 0 rad2deg(theta)]; + end + + % pre-compute trigonometric functions + thetaMax = obox(5); + cot = cosd(thetaMax); + sit = sind(thetaMax); + + % add a thickness of one pixel in both directions + dsx = spacing(1) * abs(cot) + spacing(2) * abs(sit); + dsy = spacing(1) * abs(sit) + spacing(2) * abs(cot); + obox(3:4) = obox(3:4) + [dsx dsy]; + + % concatenate rectangle data + rect(i,:) = obox; +end + + +function varargout = transformPoint(varargin) +% Apply an affine transform to a point or a point set. +% +% PT2 = transformPoint(PT1, TRANSFO); +% Returns the result of the transformation TRANSFO applied to the point +% PT1. PT1 has the form [xp yp], and TRANSFO is either a 2-by-2, a +% 2-by-3, or a 3-by-3 matrix, +% +% Format of TRANSFO can be one of : +% [a b] , [a b c] , or [a b c] +% [d e] [d e f] [d e f] +% [0 0 1] +% +% PT2 = transformPoint(PT1, TRANSFO); +% Also works when PTA is a N-by-2 array representing point coordinates. +% In this case, the result PT2 has the same size as PT1. +% +% [X2, Y2] = transformPoint(X1, Y1, TRANS); +% Also works when PX1 and PY1 are two arrays the same size. The function +% transforms each pair (PX1, PY1), and returns the result in (X2, Y2), +% which has the same size as (PX1 PY1). +% +% +% See also: +% points2d, transforms2d, translation, rotation +% + +% parse input arguments +if length(varargin) == 2 + var = varargin{1}; + px = var(:,1); + py = var(:,2); + trans = varargin{2}; +elseif length(varargin) == 3 + px = varargin{1}; + py = varargin{2}; + trans = varargin{3}; +else + error('wrong number of arguments in "transformPoint"'); +end + + +% apply linear part of the transform +px2 = px * trans(1,1) + py * trans(1,2); +py2 = px * trans(2,1) + py * trans(2,2); + +% add translation vector, if exist +if size(trans, 2) > 2 + px2 = px2 + trans(1,3); + py2 = py2 + trans(2,3); +end + +% format output arguments +if nargout < 2 + varargout{1} = [px2 py2]; +elseif nargout + varargout{1} = px2; + varargout{2} = py2; +end + + +function trans = createRotation(varargin) +%CREATEROTATION Create the 3*3 matrix of a rotation. +% +% TRANS = createRotation(THETA); +% Returns the rotation corresponding to angle THETA (in radians) +% The returned matrix has the form : +% [cos(theta) -sin(theta) 0] +% [sin(theta) cos(theta) 0] +% [0 0 1] +% +% TRANS = createRotation(POINT, THETA); +% TRANS = createRotation(X0, Y0, THETA); +% Also specifies origin of rotation. The result is similar as performing +% translation(-X0, -Y0), rotation(THETA), and translation(X0, Y0). +% +% Example +% % apply a rotation on a polygon +% poly = [0 0; 30 0;30 10;10 10;10 20;0 20]; +% trans = createRotation([10 20], pi/6); +% polyT = transformPoint(poly, trans); +% % display the original and the rotated polygons +% figure; hold on; axis equal; axis([-10 40 -10 40]); +% drawPolygon(poly, 'k'); +% drawPolygon(polyT, 'b'); +% +% See also: +% transforms2d, transformPoint, createRotation90, createTranslation +% + +% --------- +% author : David Legland +% INRA - TPV URPOI - BIA IMASTE +% created the 06/04/2004. +% + +% HISTORY +% 22/04/2009: rename as createRotation + +% default values +cx = 0; +cy = 0; +theta = 0; + +% get input values +if length(varargin)==1 + % only angle + theta = varargin{1}; +elseif length(varargin)==2 + % origin point (as array) and angle + var = varargin{1}; + cx = var(1); + cy = var(2); + theta = varargin{2}; +elseif length(varargin)==3 + % origin (x and y) and angle + cx = varargin{1}; + cy = varargin{2}; + theta = varargin{3}; +end + +% compute coefs +cot = cos(theta); +sit = sin(theta); +tx = cy*sit - cx*cot + cx; +ty = -cy*cot - cx*sit + cy; + +% create transformation matrix +trans = [cot -sit tx; sit cot ty; 0 0 1]; diff --git a/src/@Image/regionPerimeter.m b/src/@Image/regionPerimeter.m new file mode 100644 index 0000000..90a77be --- /dev/null +++ b/src/@Image/regionPerimeter.m @@ -0,0 +1,225 @@ +function [perim, labels] = regionPerimeter(obj, varargin) +% Perimeter of regions within a 2D binary or label image. +% +% P = imPerimeter(IMG); +% Return an estimate of the perimeter of the image, computed by +% counting intersections with 2D lines, and using discretized version of +% the Crofton formula. +% +% P = imPerimeter(IMG, NDIRS); +% Specify number of directions to use. Use either 2 or 4 (the default). +% +% [P, LABELS] = imPerimeter(LBL, ...) +% Process a label image, and return also the labels for which a value was +% computed. +% +% Example +% % compute the perimeter of a binary disk of radius 40 +% lx = 1:100; ly = 1:100; +% [x, y] = meshgrid(lx, ly); +% img = Image(hypot(x - 50.12, y - 50.23) < 40); +% regionPerimeter(img) +% ans = +% 251.1751 +% % to be compared to (2 * pi * 40), approximately 251.3274 +% +% See also +% regionArea, regionEulerNumber, regionSurfaceArea, regionprops +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Parse input arguments + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check dimensionality +nd = ndims(obj); +if nd ~= 2 + error('Requires a 2-dimensional image'); +end + +% default values of parameters +nDirs = 4; +%delta = [1 1]; +labels = []; + +% parse parameter name-value pairs +while ~isempty(varargin) + var1 = varargin{1}; + + if isnumeric(var1) + % option can be number of directions, or list of labels + if isscalar(var1) + nDirs = var1; + elseif size(var1, 2) == 1 + labels = var1; + end + varargin(1) = []; + + elseif ischar(var1) + if length(varargin) < 2 + error('Parameter name must be followed by parameter value'); + end + + if strcmpi(var1, 'ndirs') + nDirs = varargin{2}; + elseif strcmpi(var1, 'Labels') + labels = var1; + else + error(['Unknown parameter name: ' var1]); + end + + varargin(1:2) = []; + end +end + + +%% Process label images + +if isBinaryImage(obj) + % in case of binary image, compute only one label + perim = perimeterBinaryData(obj.Data, nDirs, obj.Spacing); + labels = 1; + +else + % in case of a label image, return a vector with a set of results + + % extract labels if necessary (considers 0 as background) + if isempty(labels) + labels = findRegionLabels(obj); + end + + % allocate result array + nLabels = length(labels); + perim = zeros(nLabels, 1); + + % compute bounding box of each region + bounds = regionMinMaxIndices(obj, labels); + + % compute perimeter of each region considered as binary image + for i = 1:nLabels + label = labels(i); + + % convert bounding box to image extent, in x and y directions + bx = bounds(i, [1 2]); + by = bounds(i, [3 4]); + + bin = obj.Data(bx(1):bx(2), by(1):by(2)) == label; + perim(i) = perimeterBinaryData(bin, nDirs, obj.Spacing); + end +end + + +%% Process 2D binary image +function perim = perimeterBinaryData(img, nDirs, spacing) + +%% Initialisations + +% distances between a pixel and its neighbours (orthogonal, and diagonal) +% (d1 is dx, d2 is dy) +d1 = spacing(1); +d2 = spacing(2); +d12 = hypot(d1, d2); + +% area of a pixel (used for computing line densities) +vol = d1 * d2; + +% size of image +D1 = size(img, 1); +D2 = size(img, 2); + + +%% Processing for 2 or 4 main directions + +% compute number of pixels, equal to the total number of vertices in graph +% reconstructions +nv = sum(img(:)); + +% compute number of connected components along orthogonal lines +% (Use Graph-based formula: chi = nVertices - nEdges) +n1 = nv - sum(sum(img(1:D1-1, :) & img(2:D1, :))); +n2 = nv - sum(sum(img(:, 1:D2-1) & img(:, 2:D2))); + +% Compute perimeter using 2 directions +% equivalent to: +% perim = mean([n1/(d1/a) n2/(d2/a)]) * pi/2; +% with a = d1*d2 being the area of the unit tile +if nDirs == 2 + perim = pi * mean([n1*d2 n2*d1]); + return; +end + + +%% Processing specific to 4 directions + +% Number of connected components along diagonal lines +n3 = nv - sum(sum(img(1:D1-1, 1:D2-1) & img(2:D1, 2:D2))); +n4 = nv - sum(sum(img(1:D1-1, 2:D2 ) & img(2:D1, 1:D2-1))); + +% compute direction weights (necessary for anisotropic case) +if any(d1 ~= d2) + c = computeDirectionWeights2d4([d1 d2])'; +else + c = [1 1 1 1] * 0.25; +end + +% compute weighted average over directions +perim = pi * sum( [n1/d1 n2/d2 n3/d12 n4/d12] * vol .* c ); + + +%% Compute direction weights for 4 directions +function c = computeDirectionWeights2d4(delta) +%COMPUTEDIRECTIONWEIGHTS2D4 Direction weights for 4 directions in 2D +% +% C = computeDirectionWeights2d4 +% Returns an array of 4-by-1 values, corresponding to directions: +% [+1 0] +% [ 0 +1] +% [+1 +1] +% [-1 +1] +% +% C = computeDirectionWeights2d4(DELTA) +% With DELTA = [DX DY]. +% +% Example +% computeDirectionWeights2d4 +% +% See also +% +% +% ------ +% Author: David Legland +% e-mail: david.legland@grignon.inra.fr +% Created: 2010-10-18, using Matlab 7.9.0.529 (R2009b) +% Copyright 2010 INRA - Cepia Software Platform. + +% check case of empty argument +if nargin == 0 + delta = [1 1]; +end + +% angle of the diagonal +theta = atan2(delta(2), delta(1)); + +% angular sector for direction 1 ([1 0]) +alpha1 = theta; + +% angular sector for direction 2 ([0 1]) +alpha2 = (pi/2 - theta); + +% angular sector for directions 3 and 4 ([1 1] and [-1 1]) +alpha34 = pi/4; + +% concatenate the different weights +c = [alpha1 alpha2 alpha34 alpha34]' / pi; + diff --git a/src/@Image/regionSurfaceArea.m b/src/@Image/regionSurfaceArea.m new file mode 100644 index 0000000..5f95432 --- /dev/null +++ b/src/@Image/regionSurfaceArea.m @@ -0,0 +1,245 @@ +function [surf, labels] = regionSurfaceArea(obj, varargin) +% Surface area of the regions within a 3D binary or label image. +% +% S = regionSurfaceArea(IMG) +% Estimates the surface area of the 3D binary structure represented by +% IMG. +% +% S = regionSurfaceArea(IMG, NDIRS) +% Specifies the number of directions used for estimating surface area. +% NDIRS can be either 3 or 13, default is 13. +% +% S = regionSurfaceArea(..., SPACING) +% Specifies the spatial calibration of the image. SPACING is a 1-by-3 row +% vector containing the voxel size in the X, Y and Z directions, in that +% orders. +% +% S = regionSurfaceArea(LBL) +% [S, L] = imSurface(LBL) +% When LBL is a label image, returns the surface area of each label in +% the 3D array, and eventually returns the indices of processed labels. +% +% S = regionSurfaceArea(..., LABELS) +% In the case of a label image, specifies the labels of the region to +% analyse. +% +% +% Example +% % Create a binary image of a ball +% [x y z] = meshgrid(1:100, 1:100, 1:100); +% img = Image(sqrt( (x-50.12).^2 + (y-50.23).^2 + (z-50.34).^2) < 40); +% % compute surface area of the ball +% S = regionSurfaceArea(img) +% S = +% 2.0103e+04 +% % compare with theoretical value +% Sth = 4*pi*40^2; +% 100 * (S - Sth) / Sth +% ans = +% -0.0167 +% +% % compute surface area of several regions in a label image +% img = Image(zeros([10 10 10]), 'Type', 'Label'); +% img(2:3, 2:3, 2:3) = 1; +% img(5:8, 2:3, 2:3) = 2; +% img(5:8, 5:8, 2:3) = 4; +% img(2:3, 5:8, 2:3) = 3; +% img(5:8, 5:8, 5:8) = 8; +% [surfs, labels] = regionSurfaceArea(img) +% surfs = +% 16.4774 +% 29.1661 +% 29.1661 +% 49.2678 +% 76.7824 +% labels = +% 1 +% 2 +% 3 +% 4 +% 8 +% +% +% See also +% regionVolume, regionMeanBreadth, regionEulerNumber, regionPerimeter +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + + +%% Parse input arguments + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check dimensionality +nd = ndims(obj); +if nd ~= 3 + error('Requires a 3-dimensional image'); +end + +% default values of parameters +nDirs = 13; +labels = []; +% methods to compute direction weights. Can be {'Voronoi'}, 'isotropic'. +directionWeights = 'voronoi'; + +% Process user input arguments +while ~isempty(varargin) + var1 = varargin{1}; + + if isnumeric(var1) + % option is either connectivity or resolution + if isscalar(var1) + nDirs = var1; + elseif size(var1, 2) == 1 + labels = var1; + end + varargin(1) = []; + + elseif ischar(var1) + if length(varargin) < 2 + error('optional named argument require a second argument as value'); + end + if strcmpi(var1, 'directionweights') + directionWeights = varargin{2}; + end + varargin(1:2) = []; + + else + error('option should be numeric'); + end + +end + + +%% Process label images + +if isBinaryImage(obj) + % in case of binary image, compute only one label + surf = surfaceAreaBinaryData(obj.Data, nDirs, obj.Spacing, directionWeights); + labels = 1; + +else + % in case of a label image, return a vector with a set of results + + % extract labels if necessary (considers 0 as background) + if isempty(labels) + labels = findRegionLabels(obj); + end + + % allocate result array + nLabels = length(labels); + surf = zeros(nLabels, 1); + + % compute bounding box of each region + bounds = regionMinMaxIndices(obj, labels); + + % compute perimeter of each region considered as binary image + for i = 1:nLabels + label = labels(i); + + % convert bounding box to image extent, in x and y directions + bx = bounds(i, [1 2]); + by = bounds(i, [3 4]); + bz = bounds(i, [5 6]); + + bin = obj.Data(bx(1):bx(2), by(1):by(2), bz(1):bz(2)) == label; + surf(i) = surfaceAreaBinaryData(bin, nDirs, obj.Spacing, directionWeights); + end +end + + +%% Process Binary data +function surf = surfaceAreaBinaryData(img, nDirs, spacing, directionWeights) + +% distances between a pixel and its neighbours. +d1 = spacing(1); % x +d2 = spacing(2); % y +d3 = spacing(3); % z + +% volume of a voxel (used for computing line densities) +vol = d1 * d2 * d3; + + +%% Main processing for 3 directions + +% number of voxels +nv = sum(img(:)); + +% number of connected components along the 3 main directions +% (Use Graph-based formula: chi = nVertices - nEdges) +n1 = nv - sum(sum(sum(img(1:end-1,:,:) & img(2:end,:,:)))); % x +n2 = nv - sum(sum(sum(img(:,1:end-1,:) & img(:,2:end,:)))); % y +n3 = nv - sum(sum(sum(img(:,:,1:end-1) & img(:,:,2:end)))); % z + +if nDirs == 3 + % compute surface area by averaging over the 3 main directions + surf = 4/3 * (n1/d1 + n2/d2 + n3/d3) * vol; + return; +end + + +%% Additional processing for 13 directions + +% Number of connected components along diagonals contained in the three +% main planes +% XY planes +n4 = nv - sum(sum(sum(img(2:end,1:end-1,:) & img(1:end-1,2:end,:)))); +n5 = nv - sum(sum(sum(img(1:end-1,1:end-1,:) & img(2:end,2:end,:)))); +% XZ planes +n6 = nv - sum(sum(sum(img(2:end,:,1:end-1) & img(1:end-1,:,2:end)))); +n7 = nv - sum(sum(sum(img(1:end-1,:,1:end-1) & img(2:end,:,2:end)))); +% YZ planes +n8 = nv - sum(sum(sum(img(:,2:end,1:end-1) & img(:,1:end-1,2:end)))); +n9 = nv - sum(sum(sum(img(:,1:end-1,1:end-1) & img(:,2:end,2:end)))); +% % XZ planes +% n6 = nv - sum(sum(sum(img(:,2:end,1:end-1) & img(:,1:end-1,2:end)))); +% n7 = nv - sum(sum(sum(img(:,1:end-1,1:end-1) & img(:,2:end,2:end)))); +% % YZ planes +% n8 = nv - sum(sum(sum(img(2:end,:,1:end-1) & img(1:end-1,:,2:end)))); +% n9 = nv - sum(sum(sum(img(1:end-1,:,1:end-1) & img(2:end,:,2:end)))); + +% Number of connected components along lines corresponding to diagonals of +% the unit cube +n10 = nv - sum(sum(sum(img(1:end-1,1:end-1,1:end-1) & img(2:end,2:end,2:end)))); +n11 = nv - sum(sum(sum(img(2:end,1:end-1,1:end-1) & img(1:end-1,2:end,2:end)))); +n12 = nv - sum(sum(sum(img(1:end-1,2:end,1:end-1) & img(2:end,1:end-1,2:end)))); +n13 = nv - sum(sum(sum(img(2:end,2:end,1:end-1) & img(1:end-1,1:end-1,2:end)))); + +% space between 2 voxels in each direction +d12 = hypot(d1, d2); +d13 = hypot(d1, d3); +d23 = hypot(d2, d3); +d123 = sqrt(d1^2 + d2^2 + d3^2); + +% Compute weights corresponding to surface fraction of spherical caps +if strcmp(directionWeights, 'isotropic') + c = zeros(13,1); + c(1) = 0.04577789120476 * 2; % Ox + c(2) = 0.04577789120476 * 2; % Oy + c(3) = 0.04577789120476 * 2; % Oz + c(4:5) = 0.03698062787608 * 2; % Oxy + c(6:7) = 0.03698062787608 * 2; % Oxz + c(8:9) = 0.03698062787608 * 2; % Oyz + c(10:13) = 0.03519563978232 * 2; % Oxyz + +else + c = Image.directionWeights3d13(spacing); +end + +% compute the weighted sum of each direction +% intersection count * direction weight / line density +surf = 4 * vol * (... + n1*c(1)/d1 + n2*c(2)/d2 + n3*c(3)/d3 + ... + (n4+n5)*c(4)/d12 + (n6+n7)*c(6)/d13 + (n8+n9)*c(8)/d23 + ... + (n10 + n11 + n12 + n13)*c(10)/d123 ); + + diff --git a/src/@Image/regionVolume.m b/src/@Image/regionVolume.m new file mode 100644 index 0000000..1e487ef --- /dev/null +++ b/src/@Image/regionVolume.m @@ -0,0 +1,56 @@ +function [vol, labels] = regionVolume(obj, varargin) +% Volume of regions within a 3D binary or label image. +% +% V = regionVolume(IMG); +% Computes the volume of the region(s) within the image. IMG is either a +% binary image, or a label image. In the case of a label image, the area +% of each region is returned in a column vector with as many elements as +% the number of labels. +% +% +% Example +% % compute the volume of a binary ball of radius 10 +% lx = 1:30; ly = 1:30; lz = 1:30; +% [x, y, z] = meshgrid(lx, ly, lz); +% img = Image(hypot(hypot(x - 15.12, y - 15.23), z - 15.34) < 40); +% v = regionVolume(img) +% v = +% 4187 +% % to be compared to (4 * pi * 10 ^ 3 / 3), approximately 4188.79 +% +% See also +% regionSurfaceArea, regionEulerNumber, regionArea, regionElementCount +% + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% INRAE - BIA Research Unit - BIBS Platform (Nantes) +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE. + +% check image type +if ~(isLabelImage(obj) || isBinaryImage(obj)) + error('Requires a label of binary image'); +end + +% check dimensionality +nd = ndims(obj); +if nd ~= 3 + error('Requires a 3-dimensional image'); +end + +% check if labels are specified +labels = []; +if ~isempty(varargin) && size(varargin{1}, 2) == 1 + labels = varargin{1}; +end + +% extract the set of labels, without the background +if isempty(labels) + labels = findRegionLabels(obj); +end + +% count the number of elements, and multiply by voxel volume +pixelCounts = regionElementCount(obj, labels); +vol = pixelCounts * prod(obj.Spacing(1:3)); diff --git a/src/@Image/regionalMaxima.m b/src/@Image/regionalMaxima.m index af7dd14..73316b1 100644 --- a/src/@Image/regionalMaxima.m +++ b/src/@Image/regionalMaxima.m @@ -21,24 +21,14 @@ error('Requires a Grayscale or intensity image to work'); end -% default values -conn = 4; +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); -if obj.Dimension == 3 - conn = 6; +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; end -% process input arguments -while ~isempty(varargin) - var = varargin{1}; - - if isnumeric(var) && isscalar(var) - % extract connectivity - conn = var; - varargin(1) = []; - continue; - end -end data = imregionalmax(obj.Data, conn); diff --git a/src/@Image/regionalMinima.m b/src/@Image/regionalMinima.m index 63f7676..5436835 100644 --- a/src/@Image/regionalMinima.m +++ b/src/@Image/regionalMinima.m @@ -21,24 +21,14 @@ error('Requires a Grayscale or intensity image to work'); end -% default values -conn = 4; +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); -if obj.Dimension == 3 - conn = 6; +% case of connectivity specified by user +if ~isempty(varargin) + conn = varargin{1}; end -% process input arguments -while ~isempty(varargin) - var = varargin{1}; - - if isnumeric(var) && isscalar(var) - % extract connectivity - conn = var; - varargin(1) = []; - continue; - end -end data = imregionalmin(obj.Data, conn); diff --git a/src/@Image/sampleFiles/wheatGrainSlice.tif b/src/@Image/sampleFiles/wheatGrainSlice.tif new file mode 100644 index 0000000..16f8acd Binary files /dev/null and b/src/@Image/sampleFiles/wheatGrainSlice.tif differ diff --git a/src/@Image/show.m b/src/@Image/show.m index ec76447..30fe6c0 100644 --- a/src/@Image/show.m +++ b/src/@Image/show.m @@ -14,7 +14,10 @@ % parse options options = {}; while length(varargin) > 1 - if strcmp(varargin{1}, 'showAxisNames') + if strcmpi(varargin{1}, 'showAxisNames') + showAxisNames = varargin{2}; + elseif strcmpi(varargin{1}, 'showTitle') + showTitle = varargin{2}; else options = [options, varargin(1:2)]; %#ok end @@ -49,6 +52,10 @@ xdata = xData(obj); ydata = yData(obj); +% get axis bounds before image display +xl = xlim; +yl = ylim; + %% Display data @@ -57,9 +64,7 @@ % check extent of image extent = physicalExtent(obj); -xl = xlim; xl = [min(xl(1), extent(1)) max(xl(2), extent(2))]; -yl = ylim; yl = [min(yl(1), extent(3)) max(yl(2), extent(4))]; xlim(xl); ylim(yl); diff --git a/src/@Image/watershed.m b/src/@Image/watershed.m index 9a78bb6..a941a14 100644 --- a/src/@Image/watershed.m +++ b/src/@Image/watershed.m @@ -42,10 +42,10 @@ % default values dyn = []; marker = []; -conn = 4; -if obj.Dimension == 3 - conn = 6; -end + +% choose default connectivity depending on dimension +conn = defaultConnectivity(obj); + % process input arguments while ~isempty(varargin) diff --git a/tests/image/files/ellipsoid_Center30x27x25_Size20x12x8_Orient40x30x20.tif b/tests/image/files/ellipsoid_Center30x27x25_Size20x12x8_Orient40x30x20.tif new file mode 100644 index 0000000..eaf8d32 Binary files /dev/null and b/tests/image/files/ellipsoid_Center30x27x25_Size20x12x8_Orient40x30x20.tif differ diff --git a/tests/image/test_catChannels.m b/tests/image/test_catChannels.m index 14f1e78..341cc93 100644 --- a/tests/image/test_catChannels.m +++ b/tests/image/test_catChannels.m @@ -24,5 +24,5 @@ function test_Simple(testCase) %#ok<*DEFNU> res = catChannels(img, img, invert(img)); -assertEqual(3, channelCount(res)); +assertEqual(testCase, 3, channelCount(res)); assertEqual(testCase, size(img), size(res)); diff --git a/tests/image/test_catFrames.m b/tests/image/test_catFrames.m index b5e4018..b9a442c 100644 --- a/tests/image/test_catFrames.m +++ b/tests/image/test_catFrames.m @@ -24,5 +24,5 @@ function test_Simple(testCase) %#ok<*DEFNU> res = catFrames(img, img, invert(img), invert(img), img); -assertEqual(5, frameCount(res)); +assertEqual(testCase, 5, frameCount(res)); assertEqual(testCase, size(img), size(res, 1:2)); diff --git a/tests/image/test_chamferDistanceMap.m b/tests/image/test_chamferDistanceMap.m new file mode 100644 index 0000000..1b724f9 --- /dev/null +++ b/tests/image/test_chamferDistanceMap.m @@ -0,0 +1,77 @@ +function tests = test_chamferDistanceMap +% Test suite for the file chamferDistanceMap. +% +% Test suite for the file chamferDistanceMap +% +% Example +% test_chamferDistanceMap +% +% See also +% chamferDistanceMap + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + +function test_SimpleBinary(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +% generate an image of a 10x10 square, with one pixel border +img = Image.false([12, 12]); +img(2:11, 2:11) = 1; + +% compute distance map +distMap = chamferDistanceMap(img); + +assertEqual(testCase, size(img), size(distMap)); +% distance equal to 1 on the borders of the square +assertEqual(testCase, 1, distMap(2, 5)); +assertEqual(testCase, 1, distMap(11, 5)); +assertEqual(testCase, 1, distMap(5, 2)); +assertEqual(testCase, 1, distMap(5, 11)); +% distance equal to 5 in the middle of the square +assertEqual(testCase, 5, distMap(6, 6)); + + +function test_TouchingLabels(testCase) +% Aim is to compute distance map within each label, even if some of them +% touch each other. +% Uses an image with a completely landlocked label region. + +data = [... + 0 0 0 0 0 0 0 0 0 0 0; ... + 0 1 1 1 2 2 2 3 3 3 0; ... + 0 1 1 1 2 2 2 3 3 3 0; ... + 0 1 1 1 2 2 2 3 3 3 0; ... + 0 1 1 1 4 4 4 3 3 3 0; ... + 0 1 1 1 4 4 4 3 3 3 0; ... + 0 1 1 1 4 4 4 3 3 3 0; ... + 0 1 1 1 5 5 5 3 3 3 0; ... + 0 1 1 1 5 5 5 3 3 3 0; ... + 0 1 1 1 5 5 5 3 3 3 0; ... + 0 0 0 0 0 0 0 0 0 0 0; ... +]; +img = Image(data, 'type', 'label'); + + +distMap = chamferDistanceMap(img); + +exp = Image([... + 0 0 0 0 0 0 0 0 0 0 0; ... + 0 1 1 1 1 1 1 1 1 1 0; ... + 0 1 2 1 1 2 1 1 2 1 0; ... + 0 1 2 1 1 1 1 1 2 1 0; ... + 0 1 2 1 1 1 1 1 2 1 0; ... + 0 1 2 1 1 2 1 1 2 1 0; ... + 0 1 2 1 1 1 1 1 2 1 0; ... + 0 1 2 1 1 1 1 1 2 1 0; ... + 0 1 2 1 1 2 1 1 2 1 0; ... + 0 1 1 1 1 1 1 1 1 1 0; ... + 0 0 0 0 0 0 0 0 0 0 0; ... +]); +assertEqual(testCase, size(img), size(distMap)); +assertEqual(testCase, distMap.Data, exp.Data); diff --git a/tests/image/test_geodesicDistanceMap.m b/tests/image/test_geodesicDistanceMap.m index 770e7e8..c1fba97 100644 --- a/tests/image/test_geodesicDistanceMap.m +++ b/tests/image/test_geodesicDistanceMap.m @@ -29,7 +29,7 @@ function test_MarkerAtUpperLeftCorner_10x12(testCase) %#ok<*DEFNU> dist = geodesicDistanceMap(marker, mask, [1 0]); maxDist = max(dist(mask)); -assertTrue(isfinite(maxDist)); +assertTrue(testCase, isfinite(maxDist)); expDist = 10+12-2; assertEqual(testCase, expDist, maxDist); @@ -44,7 +44,7 @@ function test_MarkerAtBottomRightCorner_10x12(testCase) %#ok<*DEFNU> dist = geodesicDistanceMap(marker, mask, [1 0]); maxDist = max(dist(mask)); -assertTrue(isfinite(maxDist)); +assertTrue(testCase, isfinite(maxDist)); expDist = 10+12-2; assertEqual(testCase, expDist, maxDist); @@ -60,7 +60,7 @@ function test_MarkerAtUpperLeftCorner_10x30(testCase) %#ok<*DEFNU> dist = geodesicDistanceMap(marker, mask, [1 1]); maxDist = max(dist(mask)); -assertTrue(isfinite(maxDist)); +assertTrue(testCase, isfinite(maxDist)); expDist = 29; assertEqual(testCase, expDist, maxDist); @@ -76,7 +76,7 @@ function test_MarkerAtBottomRightCorner_10x30(testCase) %#ok<*DEFNU> dist = geodesicDistanceMap(marker, mask, [1 1]); maxDist = max(dist(mask)); -assertTrue(isfinite(maxDist)); +assertTrue(testCase, isfinite(maxDist)); expDist = 29; assertEqual(testCase, expDist, maxDist); @@ -102,4 +102,4 @@ function test_FiniteDistWithMarkerOutside(testCase) dist = imGeodesicDistanceMap(marker, mask); maxDist = max(dist(isfinite(dist))); -assertTrue(isfinite(maxDist)); +assertTrue(testCase, isfinite(maxDist)); diff --git a/tests/image/test_max.m b/tests/image/test_max.m index 4fedf1a..a2bea2a 100644 --- a/tests/image/test_max.m +++ b/tests/image/test_max.m @@ -43,7 +43,7 @@ function test_2d_color_2(testCase) img = Image.read('peppers.png'); res = max(img, 50); -assertTrue(isa(res, 'Image')); +assertTrue(testCase, isa(res, 'Image')); assertEqual(testCase, [50 50 50], min(res)); diff --git a/tests/image/test_medianFilter.m b/tests/image/test_medianFilter.m new file mode 100644 index 0000000..6470cba --- /dev/null +++ b/tests/image/test_medianFilter.m @@ -0,0 +1,49 @@ +function tests = test_medianFilter +% Test suite for the file medianFilter. +% +% Test suite for the file medianFilter +% +% Example +% test_medianFilter +% +% See also +% medianFilter + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-19, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + +function test_2d_Size(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +img = Image.read('rice.png'); + +res = medianFilter(img, [3 3]); + +assertEqual(testCase, size(res), size(img)); + + +function test_2d_array(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +img = Image.read('rice.png'); + +se = [0 1 0;1 1 1;0 1 0]; +res = medianFilter(img, se); + +assertEqual(testCase, size(res), size(img)); + + +function test_3d_Size(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +img = Image(ones([7 7 7])); + +res = medianFilter(img, [3 3 3]); + +assertEqual(testCase, size(res), size(img)); + diff --git a/tests/image/test_min.m b/tests/image/test_min.m index ab6a28b..b757f91 100644 --- a/tests/image/test_min.m +++ b/tests/image/test_min.m @@ -43,7 +43,7 @@ function test_2d_color_2(testCase) img = Image.read('peppers.png'); res = min(img, 50); -assertTrue(isa(res, 'Image')); +assertTrue(testCase, isa(res, 'Image')); assertEqual(testCase, [50 50 50], max(res)); diff --git a/tests/image/test_plus.m b/tests/image/test_plus.m index 31dee10..be2bdd0 100644 --- a/tests/image/test_plus.m +++ b/tests/image/test_plus.m @@ -33,7 +33,7 @@ function test_AddConstant(testCase) exp = Image.create([3 4 5 6; 7 8 9 10; 11 12 13 14]); res = img1 + 2; -assertElementsAlmostEqual(exp.Data, res.Data); +assertEqual(testCase, exp.Data, res.Data); res = 2 + img1; assertEqual(testCase, exp.Data, res.Data); diff --git a/tests/image/test_read.m b/tests/image/test_read.m index 459cee7..74413d3 100644 --- a/tests/image/test_read.m +++ b/tests/image/test_read.m @@ -30,3 +30,18 @@ function test_read_color2d(testCase) assertEqual(testCase, 2, ndims(img)); + +function test_read_sampleFile(testCase) %#ok<*DEFNU> + +img = Image.read('wheatGrainSlice.tif'); + +assertEqual(testCase, [340 340], size(img)); + + + +function test_read_sampleFile_noExtension(testCase) %#ok<*DEFNU> + +img = Image.read('wheatGrainSlice'); + +assertEqual(testCase, [340 340], size(img)); + diff --git a/tests/image/test_regionArea.m b/tests/image/test_regionArea.m new file mode 100644 index 0000000..35d0ba3 --- /dev/null +++ b/tests/image/test_regionArea.m @@ -0,0 +1,74 @@ +function tests = test_regionArea +% Test suite for the file regionArea. +% +% Test suite for the file regionArea +% +% Example +% test_regionArea +% +% See also +% regionArea + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + + +function testSquare(testCase) + +img = Image.false(10, 10); +img(3:3+4, 4:4+4) = true; + +a = regionArea(img); + +assertEqual(testCase, 25, a); + + +function testDelta(testCase) +% Test with a non uniform resolution + +img = Image.false(10, 10); +img(3:3+2, 4:4+3) = true; +delta = [3 5]; +img.Spacing = delta; + +a = regionArea(img, delta); + +expectedArea = 3*delta(1) * 4*delta(2); +assertEqual(testCase, expectedArea, a); + + +function testLabel(testCase) + +% create image with 5 different regions +data = [... + 1 1 1 1 2 2 2 2 ; ... + 1 1 1 1 2 2 2 2 ; ... + 1 1 3 3 3 3 2 2 ; ... + 1 1 3 3 3 3 2 2 ; ... + 4 4 3 3 3 3 5 5 ; ... + 4 4 3 3 3 3 5 5 ; ... + 4 4 4 4 5 5 5 5 ; ... + 4 4 4 4 5 5 5 5 ]; +img = Image(data, 'Type', 'label'); + +a = regionArea(img); + +assertEqual(testCase, length(a), 5); +assertEqual(testCase, numel(data), sum(a)); + + +function testLabelImage(testCase) + +img = Image.read('coins.png'); +lbl = componentLabeling(img > 100); + +a = regionArea(lbl); + +assertEqual(testCase, 10, length(a)); +assertTrue(min(a) > 1500); +assertTrue(max(a) < 3000); diff --git a/tests/image/test_regionCentroids.m b/tests/image/test_regionCentroid.m similarity index 75% rename from tests/image/test_regionCentroids.m rename to tests/image/test_regionCentroid.m index c7da221..1c08dfa 100644 --- a/tests/image/test_regionCentroids.m +++ b/tests/image/test_regionCentroid.m @@ -1,13 +1,13 @@ -function tests = test_regionCentroids -% Test suite for the file regionCentroids. +function tests = test_regionCentroid +% Test suite for the file regionCentroid. % -% Test suite for the file regionCentroids +% Test suite for the file regionCentroid % % Example -% test_regionCentroids +% test_regionCentroid % % See also -% regionCentroids +% regionCentroid % ------ % Author: David Legland @@ -27,7 +27,7 @@ function test_Simple(testCase) %#ok<*DEFNU> data(6:10, 6:10) = 8; img = Image('Data', data, 'Type', 'Label'); -[centroids, labels] = regionCentroids(img); +[centroids, labels] = regionCentroid(img); assertEqual(testCase, size(centroids), [4 2]); assertEqual(testCase, centroids, [3 3;8 3;3 8;8 8]); diff --git a/tests/image/test_regionElementCounts.m b/tests/image/test_regionElementCount.m similarity index 79% rename from tests/image/test_regionElementCounts.m rename to tests/image/test_regionElementCount.m index 64d6006..a0d7226 100644 --- a/tests/image/test_regionElementCounts.m +++ b/tests/image/test_regionElementCount.m @@ -1,13 +1,13 @@ -function tests = test_regionElementCounts -% Test suite for the file regionElementCounts. +function tests = test_regionElementCount +% Test suite for the file regionElementCount. % -% Test suite for the file regionElementCounts +% Test suite for the file regionElementCount % % Example -% test_regionElementCounts +% test_regionElementCount % % See also -% regionElementCounts +% regionElementCount % ------ % Author: David Legland @@ -27,7 +27,7 @@ function test_2d(testCase) %#ok<*DEFNU> data(4:8, 4:8) = 9; img = Image('Data', data, 'Type', 'label'); -counts = regionElementCounts(img); +counts = regionElementCount(img); assertEqual(testCase, length(counts), 4); assertEqual(testCase, counts, [1 5 5 25]'); @@ -47,7 +47,7 @@ function test_3d(testCase) %#ok<*DEFNU> data(4:8, 4:8, 4:8) = 19; img = Image('Data', data, 'Type', 'label'); -counts = regionElementCounts(img); +counts = regionElementCount(img); assertEqual(testCase, length(counts), 8); assertEqual(testCase, counts, [1 5 5 5 25 25 25 125]'); diff --git a/tests/image/test_regionEquivalentEllipses.m b/tests/image/test_regionEquivalentEllipse.m similarity index 71% rename from tests/image/test_regionEquivalentEllipses.m rename to tests/image/test_regionEquivalentEllipse.m index 9798a48..2fe6bd4 100644 --- a/tests/image/test_regionEquivalentEllipses.m +++ b/tests/image/test_regionEquivalentEllipse.m @@ -1,13 +1,13 @@ -function tests = test_regionEquivalentEllipses -% Test suite for the file regionEquivalentEllipses. +function tests = test_regionEquivalentEllipse +% Test suite for the file regionEquivalentEllipse. % -% Test suite for the file regionEquivalentEllipses +% Test suite for the file regionEquivalentEllipse % % Example -% test_regionEquivalentEllipses +% test_regionEquivalentEllipse % % See also -% regionEquivalentEllipses +% regionEquivalentEllipse % ------ % Author: David Legland @@ -22,7 +22,7 @@ function test_Simple(testCase) %#ok<*DEFNU> img = Image.read('circles.png'); -elli = regionEquivalentEllipses(img); +elli = regionEquivalentEllipse(img > 0); assertEqual(testCase, size(elli), [1 5]); @@ -37,7 +37,7 @@ function test_severalLabels(testCase) %#ok<*DEFNU> data(6:10, 6:10) = 8; img = Image('Data', data, 'Type', 'Label'); -[elli, labels] = regionEquivalentEllipses(img); +[elli, labels] = regionEquivalentEllipse(img); assertEqual(testCase, size(elli), [4 5]); assertEqual(testCase, size(labels), [4 1]); diff --git a/tests/image/test_regionEquivalentEllipsoid.m b/tests/image/test_regionEquivalentEllipsoid.m new file mode 100644 index 0000000..c5c56c6 --- /dev/null +++ b/tests/image/test_regionEquivalentEllipsoid.m @@ -0,0 +1,49 @@ +function tests = test_regionEquivalentEllipsoid +% Test suite for the file regionEquivalentEllipsoid. +% +% Test suite for the file regionEquivalentEllipsoid +% +% Example +% test_regionEquivalentEllipsoid +% +% See also +% regionEquivalentEllipsoid + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-03, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + +function test_Simple(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +fileName = 'ellipsoid_Center30x27x25_Size20x12x8_Orient40x30x20.tif'; +img = Image.read(fullfile('files', fileName)); + +elli = regionEquivalentEllipsoid(img > 0); + +assertEqual(testCase, size(elli), [1 9]); +assertEqual(testCase, [30 27 25], elli(1:3), 'AbsTol', 0.5); +assertEqual(testCase, [20 12 8], elli(4:6), 'AbsTol', 0.5); +assertEqual(testCase, [40 30 20], elli(7:9), 'AbsTol', 0.5); + + +function test_Calibrated(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +fileName = 'ellipsoid_Center30x27x25_Size20x12x8_Orient40x30x20.tif'; +img = Image.read(fullfile('files', fileName)); +img.Spacing = [0.5 0.5 0.5]; +img.Origin = [0.5 0.5 0.5]; + +elli = regionEquivalentEllipsoid(img > 0); + +assertEqual(testCase, size(elli), [1 9]); +assertEqual(testCase, [15 13.5 12.5], elli(1:3), 'AbsTol', 0.5); +assertEqual(testCase, [10 6 4], elli(4:6), 'AbsTol', 0.5); +assertEqual(testCase, [40 30 20], elli(7:9), 'AbsTol', 0.5); + + diff --git a/tests/image/test_regionEulerNumber.m b/tests/image/test_regionEulerNumber.m new file mode 100644 index 0000000..b8ea1b1 --- /dev/null +++ b/tests/image/test_regionEulerNumber.m @@ -0,0 +1,192 @@ +function tests = test_regionEulerNumber +% Test suite for the file regionEulerNumber. +% +% Test suite for the file regionEulerNumber +% +% Example +% test_regionEulerNumber +% +% See also +% regionEulerNumber + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + + +function test_2d_SimplePoints(testCase) +% Four points in a black image. + +img = Image.false([10 10]); +img(3, 4) = true; +img(7, 8) = true; +img(1, 2) = true; +img(10, 2) = true; + +epc = regionEulerNumber(img); + +assertEqual(testCase, 4, epc); + + +function test_2d_BorderPoints(testCase) +% Five points in a black image, on the boundary. + +img = Image.false([10 10]); +img(1, 2) = true; +img(10, 2) = true; +img(6, 10) = true; +img(4, 10) = true; +img(10, 10) = true; + +epc = regionEulerNumber(img); + +assertEqual(testCase, 5, epc); + + +function test_2d_Conn8(testCase) +% test with 3 points touching by corner. + +img = Image.false([10 10]); +img(3, 4) = true; +img(4, 5) = true; +img(5, 4) = true; + +epc4 = regionEulerNumber(img); +epc8 = regionEulerNumber(img, 8); + +assertEqual(testCase, 3, epc4); +assertEqual(testCase, 1, epc8); + + +function test_2d_Labels(testCase) +% Label images with several regions return vector of results. + +% create a label image with 3 labels, one of them with a hole +img = Image(zeros([10 10]), 'Type', 'Label'); +img(2:3, 2:3) = 3; +img(6:8, 2:3) = 5; +img(3:5, 5:8) = 9; +img(4, 6) = 0; + +[chi, labels] = regionEulerNumber(img); + +assertEqual(testCase, chi, [1 1 0]'); +assertEqual(testCase, labels, [3 5 9]'); + + + +function test_3d_ball_C6(testCase) + +% create a simple ball +img = Image.false([5 5 5]); +img(2:4, 2:4, 2:4) = true; + +% check EPC=1 +epcTh = 1; +assertEqual(testCase, epcTh, regionEulerNumber(img, 6)); + +% add a hole in the ball -> EPC=2 +img(3, 3, 3) = false; + +% check EPC=2 +epcTh = 2; +assertEqual(testCase, epcTh, regionEulerNumber(img, 6)); + + +function test_3d_ball_C26(testCase) + +% create a simple ball +img = Image.false([5 5 5]); +img(2:4, 2:4, 2:4) = true; + +% check EPC=1 +epcTh = 1; +assertEqual(testCase, epcTh, regionEulerNumber(img, 26)); + +% add a hole in the ball -> EPC=2 +img(3, 3, 3) = false; + +% check EPC=2 +epcTh = 2; +assertEqual(testCase, epcTh, regionEulerNumber(img, 26)); + + + +function test_3d_torus_C6(testCase) + +% create a simple ball +img = Image.false([5 5 5]); +img(2:4, 2:4, 2:4) = true; +img(3, 3, 2:4) = false; + +% check EPC=0 +epcTh = 0; +assertEqual(testCase, epcTh, regionEulerNumber(img, 6)); + + +function test_3d_torus_C26(testCase) + +% create a simple ball +img = Image.false([5 5 5]); +img(2:4, 2:4, 2:4) = true; +img(3, 3, 2:4) = false; + +% check EPC=0 +epcTh = 0; +assertEqual(testCase, epcTh, regionEulerNumber(img, 26)); + + +function test_3D_cubeDiagonals_C6(testCase) + +% create a small 3D image with points along cube diagonal +img = Image.false([5 5 5]); +for i = 1:5 + img(i, i, i) = true; + img(6-i, i, i) = true; + img(i, 6-i, i) = true; + img(6-i, 6-i, i) = true; +end + +epc6 = regionEulerNumber(img, 6); + +epc6Th = 17; +assertEqual(testCase, epc6, epc6Th); + + +function test_3D_cubeDiagonals_C26(testCase) + +% create a small 3D image with points along cube diagonal +img = Image.false([7 7 7]); +for i = 2:6 + img(i, i, i) = true; + img(8-i, i, i) = true; + img(i, 8-i, i) = true; + img(8-i, 8-i, i) = true; +end + +epc26 = regionEulerNumber(img, 26); + +epc26Th = 1; +assertEqual(testCase, epc26, epc26Th); + + +function test_3D_cubeDiagonals_touchingBorder_C26(testCase) + +% create a small 3D image with points along cube diagonal +img = Image.false([5 5 5]); +for i = 1:5 + img(i, i, i) = true; + img(6-i, i, i) = true; + img(i, 6-i, i) = true; + img(6-i, 6-i, i) = true; +end + +epc26 = regionEulerNumber(img, 26); + +epc26Th = 1; +assertEqual(testCase, epc26, epc26Th); + diff --git a/tests/image/test_regionGeodesicDiameter.m b/tests/image/test_regionGeodesicDiameter.m new file mode 100644 index 0000000..3601790 --- /dev/null +++ b/tests/image/test_regionGeodesicDiameter.m @@ -0,0 +1,194 @@ +function tests = test_regionGeodesicDiameter +% Test suite for the file regionGeodesicDiameter. +% +% Test suite for the file regionGeodesicDiameter +% +% Example +% test_regionGeodesicDiameter +% +% See also +% regionGeodesicDiameter + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-18, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + + +% function test_Simple(testCase) %#ok<*DEFNU> +% % Test call of function without argument. +% regionGeodesicDiameter(); +% value = 10; +% assertEqual(testCase, value, 10); + +function test_Square5x5(testCase) %#ok<*DEFNU> + +img = Image.false(8, 8); +img(2:6, 3:7) = 1; + +assertEqual(testCase, (2*11+2*5+1)/5, regionGeodesicDiameter(img)); +assertEqual(testCase, 5, regionGeodesicDiameter(img, [1 1])); +assertEqual(testCase, 9, regionGeodesicDiameter(img, [1 2])); +assertEqual(testCase, 19/3, regionGeodesicDiameter(img, [3 4])); + + +function test_SmallSpiral(testCase) %#ok<*DEFNU> + +data = [... + 0 0 0 0 0 0 0 0 0 0; ... + 0 0 0 0 0 0 0 0 0 0; ... + 0 1 1 1 1 1 1 1 1 0; ... + 0 0 0 0 0 0 0 1 1 0; ... + 0 0 1 1 1 1 0 0 1 0; ... + 0 1 0 0 0 1 0 0 1 0; ... + 0 1 1 0 0 0 0 1 1 0; ... + 0 0 1 1 1 1 1 1 1 0; ... + 0 0 0 0 1 1 0 0 0 0; ... + 0 0 0 0 0 0 0 0 0 0]; + +% number of orthogonal and diagonal move between extremities +no = 5 + 1 + 3 + 2; +nd = 2 + 2 + 3 + 1; + +img = Image(data, 'type', 'binary'); + +exp11 = no + nd + 1; +assertEqual(testCase, exp11, regionGeodesicDiameter(img, [1 1])); +exp12 = no + nd*2 + 1; +assertEqual(testCase, exp12, regionGeodesicDiameter(img, [1 2])); +exp34 = (no*3 + nd*4)/3 + 1; +assertEqual(testCase, exp34, regionGeodesicDiameter(img, [3 4])); + + + +function test_VerticalLozenge(testCase) +% vertical lozenge that did not pass test with first version of algo + +img = Image([... + 0 0 0 0 0 0 0 ; ... + 0 0 0 1 0 0 0 ; ... + 0 0 1 1 1 0 0 ; ... + 0 0 1 1 1 0 0 ; ... + 0 1 1 1 1 1 0 ; ... + 0 0 1 1 1 0 0 ; ... + 0 0 1 1 1 0 0 ; ... + 0 0 0 1 0 0 0 ; ... + 0 0 0 0 0 0 0 ; ... + ], 'type', 'binary'); +exp = 7; + +assertEqual(testCase, exp, regionGeodesicDiameter(img)); +assertEqual(testCase, exp, regionGeodesicDiameter(img, [1 1])); +assertEqual(testCase, exp, regionGeodesicDiameter(img, [1 2])); +assertEqual(testCase, exp, regionGeodesicDiameter(img, [3 4])); +assertEqual(testCase, uint16(exp), regionGeodesicDiameter(img, uint16([3 4]))); + + +function test_SeveralParticles(testCase) + +img = Image(zeros([10 10]), 'type', 'label'); +img(2:4, 2:4) = 1; +img(6:9, 2:4) = 2; +img(2:4, 6:9) = 3; +img(6:9, 6:9) = 4; + +exp11 = [2 3 3 3]' + 1; +exp12 = [4 5 5 6]' + 1; +exp34 = [8/3 11/3 11/3 12/3]' + 1; + +% test on label image +assertEqual(testCase, exp11, regionGeodesicDiameter(img, [1 1])); +assertEqual(testCase, exp12, regionGeodesicDiameter(img, [1 2])); +assertEqual(testCase, exp34, regionGeodesicDiameter(img, [3 4])); + + +function test_SeveralParticles_UInt16(testCase) + +img = Image(zeros([10 10]), 'type', 'label'); +img(2:4, 2:4) = 1; +img(6:9, 2:4) = 2; +img(2:4, 6:9) = 3; +img(6:9, 6:9) = 4; + +exp11 = uint16([2 3 3 3]' + 1); +exp12 = uint16([4 5 5 6]' + 1); +exp34 = uint16([8/3 11/3 11/3 12/3]' + 1); + +% test on label image +assertEqual(testCase, exp11, regionGeodesicDiameter(img, uint16([1 1]))); +assertEqual(testCase, exp12, regionGeodesicDiameter(img, uint16([1 2]))); +assertEqual(testCase, exp34, regionGeodesicDiameter(img, uint16([3 4]))); + + +function test_TouchingRegions(testCase) + +img = Image([... + 0 0 0 0 0 0 0 0; ... + 0 1 1 2 2 3 3 0; ... + 0 1 1 2 2 3 3 0; ... + 0 1 1 4 4 3 3 0; ... + 0 1 1 4 4 3 3 0; ... + 0 1 1 5 5 3 3 0; ... + 0 1 1 5 5 3 3 0; ... + 0 0 0 0 0 0 0 0; ... +], 'type', 'label'); + +exp11 = [5 1 5 1 1]' + 1; +exp12 = [6 2 6 2 2]' + 1; +exp34 = [16 4 16 4 4]'/3 + 1; + +assertEqual(testCase, exp11, regionGeodesicDiameter(img, [1 1])); +assertEqual(testCase, exp12, regionGeodesicDiameter(img, [1 2])); +assertEqual(testCase, exp34, regionGeodesicDiameter(img, [3 4])); + + +function test_MissingLabels(testCase) + +img = Image([... + 0 0 0 0 0 0 0 0 0 0; ... + 0 1 1 0 2 2 0 3 3 0; ... + 0 1 1 0 2 2 0 3 3 0; ... + 0 1 1 0 0 0 0 3 3 0; ... + 0 1 1 0 7 7 0 3 3 0; ... + 0 1 1 0 7 7 0 3 3 0; ... + 0 1 1 0 0 0 0 3 3 0; ... + 0 1 1 0 9 9 0 3 3 0; ... + 0 1 1 0 9 9 0 3 3 0; ... + 0 0 0 0 0 0 0 0 0 0; ... +], 'Type', 'Label'); + +exp11 = [7 1 7 1 1]' + 1; +exp12 = [8 2 8 2 2]' + 1; +exp34 = [22 4 22 4 4]'/3 + 1; + +% test on label image +assertEqual(testCase, exp11, regionGeodesicDiameter(img, [1 1])); +assertEqual(testCase, exp12, regionGeodesicDiameter(img, [1 2])); +assertEqual(testCase, exp34, regionGeodesicDiameter(img, [3 4])); + + +function test_OutputLabels(testCase) + +img = Image([... + 0 0 0 0 0 0 0 0 0 0; ... + 0 1 1 0 2 2 0 3 3 0; ... + 0 1 1 0 2 2 0 3 3 0; ... + 0 1 1 0 0 0 0 3 3 0; ... + 0 1 1 0 7 7 0 3 3 0; ... + 0 1 1 0 7 7 0 3 3 0; ... + 0 1 1 0 0 0 0 3 3 0; ... + 0 1 1 0 9 9 0 3 3 0; ... + 0 1 1 0 9 9 0 3 3 0; ... + 0 0 0 0 0 0 0 0 0 0; ... +], 'type', 'label'); + +exp1 = [22 4 22 4 4]'/3 + 1; +exp2 = [1 2 3 7 9]'; + +% test on label image +[res, labels] = regionGeodesicDiameter(img, [3 4]); +assertEqual(testCase, exp1, res); +assertEqual(testCase, exp2, labels); diff --git a/tests/image/test_regionInscribedCircle.m b/tests/image/test_regionInscribedCircle.m new file mode 100644 index 0000000..7aa0dc6 --- /dev/null +++ b/tests/image/test_regionInscribedCircle.m @@ -0,0 +1,83 @@ +function tests = test_regionInscribedCircle +% Test suite for the file regionInscribedCircle. +% +% Test suite for the file regionInscribedCircle +% +% Example +% test_regionInscribedCircle +% +% See also +% regionInscribedCircle + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-19, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + +function test_Simple(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +img = Image.false(100, 100); +img(30:60, 20:80) = 1; + +circle = regionInscribedCircle(img); + +assertEqual(testCase, size(circle), [1 3]); +assertEqual(testCase, circle(3), 16, 'AbsTol', 0.01); + + +function test_MultiLabels(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +img = Image([ ... + 0 0 0 0 0 0 0 0 0 0 0; ... + 0 1 1 1 0 4 4 4 4 4 0; ... + 0 1 1 1 0 4 4 4 4 4 0; ... + 0 1 1 1 0 4 4 4 4 4 0; ... + 0 0 0 0 0 0 0 0 0 0 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 6 6 6 6 6 6 6 6 6 0; ... + 0 0 0 0 0 0 0 0 0 0 0], 'Type', 'Label'); + +[circles, labels] = regionInscribedCircle(img); + +assertEqual(testCase, size(circles), [3 3]); +assertEqual(testCase, length(labels), 3); + +expRadius = [2 2 5]'; +assertEqual(testCase, circles(:,3), expRadius, 'AbsTol', 0.01); + + +function test_LabelsTouchingImageBorder(testCase) %#ok<*DEFNU> +% Test call of function without argument. + +img = Image([ ... + 1 1 1 0 4 4 4 4 4; ... + 1 1 1 0 4 4 4 4 4; ... + 1 1 1 0 4 4 4 4 4; ... + 0 0 0 0 0 0 0 0 0; ... + 6 6 6 6 6 6 6 6 6; ... + 6 6 6 6 6 6 6 6 6; ... + 6 6 6 6 6 6 6 6 6; ... + 6 6 6 6 6 6 6 6 6; ... + 6 6 6 6 6 6 6 6 6], 'Type', 'Label'); + +[circles, labels] = regionInscribedCircle(img); + +assertEqual(testCase, size(circles), [3 3]); +assertEqual(testCase, length(labels), 3); + +expRadius = [3 3 5]'; +assertEqual(testCase, circles(:,3), expRadius, 'AbsTol', 0.01); + diff --git a/tests/image/test_regionMeanBreadth.m b/tests/image/test_regionMeanBreadth.m new file mode 100644 index 0000000..eb8b13f --- /dev/null +++ b/tests/image/test_regionMeanBreadth.m @@ -0,0 +1,125 @@ +function tests = test_regionMeanBreadth +% Test suite for the file regionMeanBreadth. +% +% Test suite for the file regionMeanBreadth +% +% Example +% test_regionMeanBreadth +% +% See also +% regionMeanBreadth + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + +function test_ball_D3(testCase) + +[x, y, z] = meshgrid(1:30, 1:30, 1:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); + +mb = regionMeanBreadth(img, 3); + +mbth = 2 * 10; +assertTrue(testCase, abs(mb - mbth) < mbth * 0.05); + + +function test_ball_D13(testCase) + +[x, y, z] = meshgrid(1:30, 1:30, 1:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); + +mb = regionMeanBreadth(img, 13); + +mbth = 2 * 10; +assertTrue(testCase, abs(mb - mbth) < mbth * 0.05); + + +function test_ball_D3_aniso(testCase) + +[x, y, z] = meshgrid(1:30, 1:2:30, 1:3:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); +img.Spacing = [1 2 3]; + +mb = regionMeanBreadth(img, 3); + +mbth = 2 * 10; +assertTrue(testCase, abs(mb - mbth) < mbth * 0.05); + + +function test_ball_D13_aniso(testCase) + +[x, y, z] = meshgrid(1:30, 1:2:30, 1:3:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); +img.Spacing = [1 2 3]; + +mb = regionMeanBreadth(img, 13); + +mbth = 2 * 10; +assertTrue(testCase, abs(mb - mbth) < mbth * 0.05); + + +function test_TouchingBorder_D3(testCase) + +img1 = Image.false([7 7 7]); +img1(2:6, 2:6, 2:6) = true; +img2 = Image.true([5 5 5]); + +mb1 = regionMeanBreadth(img1, 3); +mb2 = regionMeanBreadth(img2, 3); + +assertEqual(testCase, mb1, mb2); + + +function test_TouchingBorder_D13(testCase) + +img1 = Image.false([7 7 7]); +img1(2:6, 2:6, 2:6) = true; +img2 = Image.true([5 5 5]); + +mb1 = regionMeanBreadth(img1, 13); +mb2 = regionMeanBreadth(img2, 13); + +assertEqual(testCase, mb1, mb2); + + + +function test_Labels(testCase) + +img = Image(zeros([6 6 6]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 2; +img(4:6, 1:3, 1:3) = 3; +img(1:3, 4:6, 1:3) = 5; +img(4:6, 4:6, 1:3) = 7; +img(1:3, 1:3, 4:6) = 11; +img(4:6, 1:3, 4:6) = 13; +img(1:3, 4:6, 4:6) = 17; +img(4:6, 4:6, 4:6) = 19; + +[mb, labels] = regionMeanBreadth(img); +assertEqual(testCase, size(mb), [8 1], .01); +assertEqual(testCase, labels, [2 3 5 7 11 13 17 19]'); + + +function test_LabelSelection(testCase) + +img = Image(zeros([6 6 6]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 2; +img(4:6, 1:3, 1:3) = 3; +img(1:3, 4:6, 1:3) = 5; +img(4:6, 4:6, 1:3) = 7; +img(1:3, 1:3, 4:6) = 11; +img(4:6, 1:3, 4:6) = 13; +img(1:3, 4:6, 4:6) = 17; +img(4:6, 4:6, 4:6) = 19; +labels = [2 3 5 7 11 13 17 19]'; + +mb = regionMeanBreadth(img, labels); +assertEqual(testCase, size(mb), [8 1]); + +mb2 = regionMeanBreadth(img, labels(1:2:end)); +assertEqual(testCase, size(mb2), [4 1]); diff --git a/tests/image/test_regionPerimeter.m b/tests/image/test_regionPerimeter.m new file mode 100644 index 0000000..b2d9e0b --- /dev/null +++ b/tests/image/test_regionPerimeter.m @@ -0,0 +1,121 @@ +function tests = test_regionPerimeter +% Test suite for the file regionPerimeter. +% +% Test suite for the file regionPerimeter +% +% Example +% test_regionPerimeter +% +% See also +% regionPerimeter + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + +function test_diskR10_D2(testCase) + +lx = 1:30; +[x, y] = meshgrid(lx, lx); +img = Image(hypot(x - 15.12, y - 15.23) < 10); + +p = regionPerimeter(img, 2); + +% assert perimeter estimate is within 5% of actual value +pTh = 2 * pi * 10; +assertTrue(testCase, abs(p - pTh) < 3); + + +function test_diskR10_D4(testCase) + +lx = 1:30; +[x, y] = meshgrid(lx, lx); +img = Image(hypot(x - 15.12, y - 15.23) < 10); + +p = regionPerimeter(img, 4); + +% assert perimeter estimate is within 5% of actual value +pTh = 2 * pi * 10; +assertTrue(testCase, abs(p - pTh) < 3); + + +function test_touchingBorder_D2(testCase) + +img1 = Image.false([7, 7]); +img1(2:6, 2:6) = 1; +img2 = Image.true([5, 5]); + +nd = 2; +p = regionPerimeter(img1, nd); +pb = regionPerimeter(img2, nd); + +assertEqual(testCase, p, pb); + + +function test_touchingBorder_D4(testCase) + +img1 = Image.false([7, 7]); +img1(2:6, 2:6) = 1; +img2 = Image.true([5, 5]); + +nd = 4; +p = regionPerimeter(img1, nd); +pb = regionPerimeter(img2, nd); + +assertEqual(testCase, p, pb); + + +function test_diskR10_D2_Aniso(testCase) + +[x, y] = meshgrid(1:2:50, 1:3:50); +img = Image(hypot(x - 25.12, y - 25.23) < 10); +img.Spacing = [2 3]; + +p = regionPerimeter(img, 2); + +% assert perimeter estimate is within 5% of actual value +pTh = 2 * pi * 10; +assertTrue(testCase, abs(p - pTh) < 3); + + +function test_diskR10_D4_Aniso(testCase) + +[x, y] = meshgrid(1:2:50, 1:3:50); +img = Image(hypot(x - 25.12, y - 25.23) < 10); +img.Spacing = [2 3]; + +p = regionPerimeter(img, 4); + +% assert perimeter estimate is within 5% of actual value +pTh = 2 * pi * 10; +assertTrue(testCase, abs(p - pTh) < 3); + + +function test_touchingBorder_D4_Aniso(testCase) + +img1 = Image.false([7, 7]); +img1(2:6, 2:6) = 1; +img2 = Image.true([5, 5]); + +nd = 4; +p = regionPerimeter(img1, nd, [2 3]); +pb = regionPerimeter(img2, nd, [2 3]); + +assertEqual(testCase, p, pb); + + +function testLabelImage(testCase) + +img = Image.read('coins.png'); +lbl = componentLabeling(img > 100); + +p = regionPerimeter(lbl); + +assertEqual(testCase, 10, length(p)); +assertTrue(min(p) > 150); +assertTrue(max(p) < 300); + diff --git a/tests/image/test_regionSurfaceArea.m b/tests/image/test_regionSurfaceArea.m new file mode 100644 index 0000000..1f1e1db --- /dev/null +++ b/tests/image/test_regionSurfaceArea.m @@ -0,0 +1,157 @@ +function tests = test_regionSurfaceArea +% Test suite for the file regionSurfaceArea. +% +% Test suite for the file regionSurfaceArea +% +% Example +% test_regionSurfaceArea +% +% See also +% regionSurfaceArea + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + +function test_ball_D3(testCase) + +[x, y, z] = meshgrid(1:30, 1:30, 1:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); + +s = regionSurfaceArea(img, 3); + +sth = 4 * pi * 10^2; +assertTrue(testCase, abs(s - sth) < sth * 0.05); + + +function test_ball_D13(testCase) + +[x, y, z] = meshgrid(1:30, 1:30, 1:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); + +s = regionSurfaceArea(img, 13); + +sth = 4 * pi * 10^2; +assertTrue(testCase, abs(s - sth) < sth * 0.05); + + +function test_ball_D3_aniso(testCase) + +[x, y, z] = meshgrid(1:30, 1:2:30, 1:3:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); +img.Spacing = [1 2 3]; + +s = regionSurfaceArea(img, 3); + +sth = 4 * pi * 10^2; +assertTrue(testCase, abs(s - sth) < sth * 0.05); + + +function test_ball_D13_aniso(testCase) + +[x, y, z] = meshgrid(1:30, 1:2:30, 1:3:30); +img = Image(sqrt((x-15.12).^2 + (y-15.23).^2 + (z-15.34).^2) < 10); +img.Spacing = [1 2 3]; + +s = regionSurfaceArea(img, 13); + +sth = 4 * pi * 10^2; +assertTrue(testCase, abs(s - sth) < sth * 0.05); + + +function test_TouchingBorder_D3(testCase) + +img1 = Image.false([7 7 7]); +img1(2:6, 2:6, 2:6) = true; +img2 = Image.true([5 5 5]); + +s1 = regionSurfaceArea(img1, 3); +s2 = regionSurfaceArea(img2, 3); + +assertEqual(testCase, s1, s2); + + +function test_TouchingBorder_D13(testCase) + +img1 = Image.false([7 7 7]); +img1(2:6, 2:6, 2:6) = true; +img2 = Image.true([5 5 5]); + +s1 = regionSurfaceArea(img1, 13); +s2 = regionSurfaceArea(img2, 13); + +assertEqual(testCase, s1, s2); + + +function test_Labels_D3(testCase) + +img = Image(zeros([5 5 5]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 1; +img(4:5, 1:3, 1:3) = 2; +img(1:3, 4:5, 1:3) = 3; +img(4:5, 4:5, 1:3) = 4; +img(1:3, 1:3, 4:5) = 5; +img(4:5, 1:3, 4:5) = 6; +img(1:3, 4:5, 4:5) = 7; +img(4:5, 4:5, 4:5) = 8; + +s3 = regionSurfaceArea(img, 3); +assertEqual(testCase, 8, length(s3)); + + +function test_Labels_D13(testCase) + +img = Image(zeros([5 5 5]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 1; +img(4:5, 1:3, 1:3) = 2; +img(1:3, 4:5, 1:3) = 3; +img(4:5, 4:5, 1:3) = 4; +img(1:3, 1:3, 4:5) = 5; +img(4:5, 1:3, 4:5) = 6; +img(1:3, 4:5, 4:5) = 7; +img(4:5, 4:5, 4:5) = 8; + +s13 = regionSurfaceArea(img, 13); +assertEqual(testCase, 8, length(s13)); + + +function test_LabelSelection_D3(testCase) + +img = Image(zeros([5 5 5]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 2; +img(4:5, 1:3, 1:3) = 3; +img(1:3, 4:5, 1:3) = 5; +img(4:5, 4:5, 1:3) = 7; +img(1:3, 1:3, 4:5) = 11; +img(4:5, 1:3, 4:5) = 13; +img(1:3, 4:5, 4:5) = 17; +img(4:5, 4:5, 4:5) = 19; + +[b3, labels3] = regionSurfaceArea(img, 3); + +assertEqual(testCase, 8, length(b3)); +assertEqual(testCase, 8, length(labels3)); +assertEqual(testCase, labels3, [2 3 5 7 11 13 17 19]'); + + +function test_LabelSelection_D13(testCase) + +img = Image(zeros([5 5 5]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 2; +img(4:5, 1:3, 1:3) = 3; +img(1:3, 4:5, 1:3) = 5; +img(4:5, 4:5, 1:3) = 7; +img(1:3, 1:3, 4:5) = 11; +img(4:5, 1:3, 4:5) = 13; +img(1:3, 4:5, 4:5) = 17; +img(4:5, 4:5, 4:5) = 19; + +[s13, labels13] = regionSurfaceArea(img, 13); + +assertEqual(testCase, 8, length(s13)); +assertEqual(testCase, 8, length(labels13)); +assertEqual(testCase, labels13, [2 3 5 7 11 13 17 19]'); diff --git a/tests/image/test_regionVolume.m b/tests/image/test_regionVolume.m new file mode 100644 index 0000000..77a06fe --- /dev/null +++ b/tests/image/test_regionVolume.m @@ -0,0 +1,98 @@ +function tests = test_regionVolume +% Test suite for the file regionVolume. +% +% Test suite for the file regionVolume +% +% Example +% test_regionVolume +% +% See also +% regionVolume + +% ------ +% Author: David Legland +% e-mail: david.legland@inrae.fr +% Created: 2021-11-02, using Matlab 9.10.0.1684407 (R2021a) Update 3 +% Copyright 2021 INRAE - BIA-BIBS. + +tests = functiontests(localfunctions); + + +function testBasic(testCase) + +img = Image.false([4 5 6]); +img(2:end-1, 2:end-1, 2:end-1) = true; + +v = regionVolume(img); + +exp = prod([2 3 4]); +assertEqual(testCase, exp, v); + + +function testAddBorder(testCase) + +img1 = Image.false([7 7 7]); +img1(2:6, 2:6, 2:6) = true; +img2 = Image.true([5 5 5]); + +v1 = regionVolume(img1); +v2 = regionVolume(img2); + +assertEqual(testCase, v1, v2); + + +function test_Labels(testCase) + +img = Image(zeros([6 6 6]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 2; +img(4:6, 1:3, 1:3) = 3; +img(1:3, 4:6, 1:3) = 5; +img(4:6, 4:6, 1:3) = 7; +img(1:3, 1:3, 4:6) = 11; +img(4:6, 1:3, 4:6) = 13; +img(1:3, 4:6, 4:6) = 17; +img(4:6, 4:6, 4:6) = 19; + +[vols, labels] = regionVolume(img); +assertEqual(testCase, vols, repmat(27, 8, 1), .01); +assertEqual(testCase, labels, [2 3 5 7 11 13 17 19]'); + + +function test_SelectedLabels(testCase) +% Compute volume on a selection of regions within a label image. + +img = Image(zeros([6 6 6]), 'type', 'label'); +img(1:3, 1:3, 1:3) = 2; +img(4:6, 1:3, 1:3) = 3; +img(1:3, 4:6, 1:3) = 5; +img(4:6, 4:6, 1:3) = 7; +img(1:3, 1:3, 4:6) = 11; +img(4:6, 1:3, 4:6) = 13; +img(1:3, 4:6, 4:6) = 17; +img(4:6, 4:6, 4:6) = 19; +labels = [2 3 5 7 11 13 17 19]'; + +vols = regionVolume(img, labels); +assertEqual(testCase, size(vols), [8 1]); +assertEqual(testCase, vols, repmat(27, 8, 1), .01); + +vols2 = regionVolume(img, labels(1:2:end)); +assertEqual(testCase, size(vols2), [4 1]); +assertEqual(testCase, vols2, repmat(27, 4, 1), .01); + + +function test_Anisotropic(testCase) + +img1 = Image.false([7 7 7]); +img1(2:6, 2:6, 2:6) = true; +img2 = Image.true([5 5 5]); +img1.Spacing = [1 2 3]; +img2.Spacing = [1 2 3]; + +v1 = regionVolume(img1); +v2 = regionVolume(img2); + +expectedVolume = (5*5*5) * (1*2*3); % number of elements times voxel volume +assertEqual(testCase, v1, expectedVolume); +assertEqual(testCase, v2, expectedVolume); + diff --git a/tests/image/test_std.m b/tests/image/test_std.m index f62545b..5863c43 100644 --- a/tests/image/test_std.m +++ b/tests/image/test_std.m @@ -24,7 +24,7 @@ function test_2d(testCase) %#ok<*DEFNU> exp = std(double(dat(:))); res = std(img); -assertEqual([1 1], size(res)); +assertEqual(testCase, [1 1], size(res)); assertEqual(testCase, exp, res, 'AbsTol', 1e-10); function test_3d(testCase) @@ -34,6 +34,6 @@ function test_3d(testCase) exp = std(double(dat(:))); res = std(img); -assertEqual([1 1], size(res)); +assertEqual(testCase, [1 1], size(res)); assertEqual(testCase, exp, res, 'AbsTol', 1e-10); diff --git a/tests/image/test_subsasgn.m b/tests/image/test_subsasgn.m index 013fa49..a9feaa6 100644 --- a/tests/image/test_subsasgn.m +++ b/tests/image/test_subsasgn.m @@ -27,11 +27,11 @@ function test_subsasgn_2d(testCase) %#ok<*DEFNU> % set one element img(1) = 99; -assertEqual(99, img(1)); +assertEqual(testCase, 99, img(1)); img(2) = 99; -assertEqual(99, img(2)); +assertEqual(testCase, 99, img(2)); img(end) = 99; -assertEqual(99, img(end)); +assertEqual(testCase, 99, img(end)); % set one row (y = cte) new = 8:2:18; diff --git a/tests/image/test_var.m b/tests/image/test_var.m index e087984..4f656e3 100644 --- a/tests/image/test_var.m +++ b/tests/image/test_var.m @@ -24,7 +24,7 @@ function test_2d(testCase) %#ok<*DEFNU> exp = var(double(dat(:))); res = var(img); -assertEqual([1 1], size(res)); +assertEqual(testCase, [1 1], size(res)); assertEqual(testCase, exp, res, 'AbsTol', 1e-10); function test_3d(testCase) @@ -34,7 +34,7 @@ function test_3d(testCase) exp = var(double(dat(:))); res = var(img); -assertEqual([1 1], size(res)); +assertEqual(testCase, [1 1], size(res)); assertEqual(testCase, exp, res, 'AbsTol', 1e-10);