diff --git a/README.md b/README.md
index c80e3b0..408e07b 100644
--- a/README.md
+++ b/README.md
@@ -18,40 +18,42 @@ The `Image` class is at the basis of the development of the `ImageM` application
## Example 1
The following example performs a segmentation on a grayscale image. It uses computation of gradient, filtering, morphological processing, and management of label images.
-
- % read a grayscale image
- img = Image.read('coins.png');
- % compute gradient as a vector image.
- grad = gradient(img);
- % Compute the norm of the gradient, and smooth
- gradf = boxFilter(norm(grad), [5 5]);
- figure; show(gradf, []);
- % compute watershed after imposition of extended minima
- emin = extendedMinima(gradf, 20, 4);
- grad2 = imposeMinima(gradf, emin, 4);
- lbl = watershed(grad2, 4);
- % display binary overlay over grayscale image
- show(overlay(img, lbl==0, 'g'));
- % cleanup segmentation and convert to RGB image
- lbl2 = killBorders(lbl);
- show(label2rgb(lbl2, 'jet', 'w'));
+```matlab
+% read a grayscale image
+img = Image.read('coins.png');
+% compute gradient as a vector image.
+grad = gradient(img);
+% Compute the norm of the gradient, and smooth
+gradf = boxFilter(norm(grad), [5 5]);
+figure; show(gradf, []);
+% compute watershed after imposition of extended minima
+emin = extendedMinima(gradf, 20, 4);
+grad2 = imposeMinima(gradf, emin, 4);
+lbl = watershed(grad2, 4);
+% display binary overlay over grayscale image
+show(overlay(img, lbl==0, 'g'));
+% cleanup segmentation and convert to RGB image
+lbl2 = killBorders(lbl);
+show(label2rgb(lbl2, 'jet', 'w'));
+```

## Example 2
The following example presents various ways to explore and display the content of a 3D image.
-
- % read data, adjust contrast, and specify spatial calibration
- img = adjustDynamic(Image.read('brainMRI.hdr'));
- img.Spacing = [1 1 2.5];
- % show as three orthogonal planes
- figure; showOrthoPlanes(img, [60 80 13]); axis equal;
- % show as three orthogonal slices in 3D
- figure; showOrthoSlices(img, [60 80 13]); axis equal; view(3);
- axis(physicalExtent(img));
- % display as isosurface
- figure; isosurface(gaussianFilter(img, [5 5 5], 2), 50);
- axis equal; axis(physicalExtent(img)); view([145 25]); light;
+```matlab
+% read data, adjust contrast, and specify spatial calibration
+img = adjustDynamic(Image.read('brainMRI.hdr'));
+img.Spacing = [1 1 2.5];
+% show as three orthogonal planes
+figure; showOrthoPlanes(img, [60 80 13]); axis equal;
+% show as three orthogonal slices in 3D
+figure; showOrthoSlices(img, [60 80 13]); axis equal; view(3);
+axis(physicalExtent(img));
+% display as isosurface
+figure; isosurface(gaussianFilter(img, [5 5 5], 2), 50);
+axis equal; axis(physicalExtent(img)); view([145 25]); light;
+```

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 AppContents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Input data
+
+img = Image.read('wheatGrainSlice.tif');
+
+figure; show(img);
+
Boxes
+seg = img > 160;
+seg2 = areaOpening(killBorders(seg), 10);
+
+
+box = regionBoundingBox(seg2);
+obox = regionOrientedBox(seg2);
+
+
+
+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);