Skip to content
This repository was archived by the owner on Jul 26, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e199e4c
wip: add ellipse as a new feature
EscapedGibbon Apr 12, 2023
66df20a
chore: get ellipse to scale to match ROI's surface
EscapedGibbon Apr 19, 2023
454c820
refactor: change surface name to lower case
EscapedGibbon Apr 19, 2023
04aa982
test: fix testing case result
EscapedGibbon Apr 19, 2023
089c39e
refactor: change names of the variables and return types
EscapedGibbon Apr 20, 2023
4ac0855
chore: remove unnecessary packages
EscapedGibbon Apr 20, 2023
695109e
refactor: change the way of notation of power
EscapedGibbon Apr 20, 2023
c67a52c
test: fix the testing case to match previous commits
EscapedGibbon Apr 20, 2023
ea56c1f
chore: redo the whole implementation of an ellipse property due to a bug
EscapedGibbon Apr 22, 2023
c058ff3
test: change test case results due to refactoring
EscapedGibbon Apr 22, 2023
8b80239
Merge branch 'main' into 246-new-roi-property-ellipse
EscapedGibbon Apr 22, 2023
4979f2c
chore: remove commented import
EscapedGibbon Apr 22, 2023
dcfb002
Merge branch '246-new-roi-property-ellipse' of https://github.com/ima…
EscapedGibbon Apr 22, 2023
c0d99f7
fix: add a missing bracket to border interface
EscapedGibbon Apr 22, 2023
7eae27c
fix: remove ts-expect errors and fix object property names
EscapedGibbon Apr 22, 2023
e6e8522
chore: remove unused dependency
EscapedGibbon Apr 22, 2023
9a7e5be
test: add testing cases for better code coverage
EscapedGibbon Apr 22, 2023
43cc291
chore: convert angle in ellipse from rad to degrees
EscapedGibbon Apr 23, 2023
7e48ea3
test: convert angles to degrees in testing cases
EscapedGibbon Apr 23, 2023
3cdcaac
chore: remove unused ml-array-variance
EscapedGibbon Apr 24, 2023
e4d5ff9
Merge branch 'main' into 246-new-roi-property-ellipse
EscapedGibbon Apr 24, 2023
dad528b
Merge branch '246-new-roi-property-ellipse' of https://github.com/ima…
EscapedGibbon Apr 24, 2023
5062aa2
chore: move function getEllipse to a separate folder
EscapedGibbon Apr 24, 2023
2319485
refactor: refactor conditions for eigenvalues
EscapedGibbon Apr 25, 2023
0af6d0a
docs: add some comments about function and its parameters
EscapedGibbon Apr 25, 2023
8bb7bc0
chore: scale the ellipse internally instead of calculating the ellips…
EscapedGibbon Apr 26, 2023
39b0480
test: fix minorAxis expecting result
EscapedGibbon Apr 26, 2023
beb23d9
chore: remove surface as parameter and nbSD since no longer used
EscapedGibbon Apr 26, 2023
3fdca16
test: add testing case for 1 1 1 mask
EscapedGibbon Apr 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/roi/Roi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Point } from '../utils/geometry/points';
import { RoiMap } from './RoiMapManager';
import { getBorderPoints } from './getBorderPoints';
import { getMask, GetMaskOptions } from './getMask';
import { Ellipse, getEllipse } from './properties/getEllipse';

interface Border {
connectedID: number; // refers to the roiID of the contiguous ROI
Expand All @@ -36,6 +37,7 @@ interface Computed {
fillRatio: number;
internalIDs: number[];
feret: Feret;
ellipse: Ellipse;
centroid: Point;
}
export class Roi {
Expand Down Expand Up @@ -392,6 +394,13 @@ export class Roi {
});
}

get ellipse(): Ellipse {
return this.#getComputed('ellipse', () => {
const ellipse = getEllipse(this);
return ellipse;
});
}

/**
* Number of holes in the ROI and their total surface.
* Used to calculate fillRatio.
Expand Down Expand Up @@ -608,7 +617,7 @@ export class Roi {
* @param x
*/
computeIndex(y: number, x: number): number {
const roiMap = this.getMap();
const roiMap = this.map;
return (y + this.origin.row) * roiMap.width + x + this.origin.column;
}
}
160 changes: 160 additions & 0 deletions src/roi/__tests__/ellipse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { fromMask } from '..';

test('ellipse on a small figure 3x3', () => {
const mask = testUtils.createMask([
[0, 1, 0],
[0, 1, 0],
[0, 1, 0],
]);
const roiMapManager = fromMask(mask);
const rois = roiMapManager.getRois();
const result = rois[0].ellipse;

expect(result).toBeDeepCloseTo({
center: { column: 1, row: 1 },
majorAxis: {
points: [
{ column: NaN, row: Infinity },
{ column: NaN, row: -Infinity },
],
length: Infinity,
angle: NaN,
},
minorAxis: {
points: [
{ column: NaN, row: NaN },
{ column: NaN, row: NaN },
],
length: NaN,
angle: NaN,
},
surface: NaN,
});
});
test('ellipse on a small figure 3x3', () => {
const mask = testUtils.createMask([
[1, 1, 0],
[0, 1, 0],
[0, 0, 0],
]);
const roiMapManager = fromMask(mask);

const rois = roiMapManager.getRois();
const result = rois[0].ellipse;

expect(result).toBeDeepCloseTo({
center: { column: 0.6666666666666666, row: 0.3333333333333333 },
majorAxis: {
points: [
{ column: 1.4978340587735344, row: 1.1645007254402011 },
{ column: -0.1645007254402011, row: -0.4978340587735344 },
],
length: 2.3508963970396173,
angle: -135,
},
minorAxis: {
points: [
{ column: 1.146541384241206, row: -0.14654138424120605 },
{ column: 0.18679194909212726, row: 0.8132080509078727 },
],
length: 1.6247924149339357,
angle: 135,
},
surface: 3,
});
});

test('ellipse on 3x3 cross', () => {
const mask = testUtils.createMask([
[0, 1, 0],
[1, 1, 1],
[0, 1, 0],
]);
const roiMapManager = fromMask(mask);

const rois = roiMapManager.getRois();
const result = rois[0].ellipse;
expect(result).toBeDeepCloseTo({
center: { column: 1, row: 1 },
majorAxis: {
points: [
{ column: 1, row: 2.7841241161527712 },
{ column: 1, row: -0.7841241161527714 },
],
length: 3.5682482323055424,
angle: -90,
},
minorAxis: {
points: [
{ column: 2.7841241161527712, row: 1 },
{ column: -0.7841241161527714, row: 1 },
],
length: 1.7841241161527712,
angle: 180,
},
surface: 5,
});
});
test('ellipse on slightly changed 3x3 cross', () => {
const mask = testUtils.createMask([
[1, 1, 0],
[1, 1, 1],
[0, 1, 0],
]);
const roiMapManager = fromMask(mask);

const rois = roiMapManager.getRois();
const result = rois[0].ellipse;
expect(result).toBeDeepCloseTo({
center: { column: 0.8333333333333334, row: 0.8333333333333334 },
majorAxis: {
points: [
{ column: 2.175183801009782, row: 2.175183801009782 },
{ column: -0.5085171343431153, row: -0.5085171343431155 },
],
length: 3.7953262601294284,
angle: -135,
},
minorAxis: {
points: [
{ column: -0.15768891509232064, row: 1.8243555817589874 },
{ column: 1.8243555817589874, row: -0.15768891509232064 },
],
length: 2.01285390103734,
angle: -45,
},
surface: 6.000000000000002,
});
});
test('ellipse on 4x4 ROI', () => {
const mask = testUtils.createMask([
[0, 0, 1, 1],
[0, 0, 1, 0],
[0, 1, 1, 1],
[1, 1, 1, 0],
]);
const roiMapManager = fromMask(mask);

const rois = roiMapManager.getRois();
const result = rois[0].ellipse;
expect(result).toBeDeepCloseTo({
center: { column: 1.7777777777777777, row: 1.7777777777777777 },
majorAxis: {
points: [
{ column: 0.488918397751106, row: 3.6243064249674615 },
{ column: 3.0666371578044496, row: -0.06875086941190611 },
],
length: 4.503699166851579,
angle: -55.08532670592521,
},
minorAxis: {
points: [
{ column: 0.8646151602330147, row: 1.1403989822180598 },
{ column: 2.690940395322541, row: 2.4151565733374953 },
],
length: 2.544387508597132,
angle: 34.91467329407479,
},
surface: 9.000000000000002,
});
});
135 changes: 135 additions & 0 deletions src/roi/properties/getEllipse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { EigenvalueDecomposition } from 'ml-matrix';
import { xVariance, xyCovariance } from 'ml-spectra-processing';

import { FeretDiameter } from '../../maskAnalysis';
import { getAngle } from '../../maskAnalysis/utils/getAngle';
import { assert } from '../../utils/assert';
import { toDegrees } from '../../utils/geometry/angles';
import { Roi } from '../Roi';

export interface Ellipse {
center: {
column: number;
row: number;
};
majorAxis: FeretDiameter;
minorAxis: FeretDiameter;
surface: number;
}
/**
*Calculates ellipse on around ROI
*
* @param roi - region of interest
* @param surface - the surface of ROI that ellipse should match
* @returns Ellipse
*/
export function getEllipse(roi: Roi): Ellipse {
let xCenter = roi.centroid.column;
let yCenter = roi.centroid.row;

let xCentered = roi.points.map((point: number[]) => point[0] - xCenter);
let yCentered = roi.points.map((point: number[]) => point[1] - yCenter);

let centeredXVariance = xVariance(xCentered, { unbiased: false });
let centeredYVariance = xVariance(yCentered, { unbiased: false });

let centeredCovariance = xyCovariance(
{
x: xCentered,
y: yCentered,
},
{ unbiased: false },
);

//spectral decomposition of the sample covariance matrix
let sampleCovarianceMatrix = [
[centeredXVariance, centeredCovariance],
[centeredCovariance, centeredYVariance],
];
let e = new EigenvalueDecomposition(sampleCovarianceMatrix);
let eigenvalues = e.realEigenvalues;
let vectors = e.eigenvectorMatrix;

let radiusMajor: number;
let radiusMinor: number;
let vectorMajor: number[];
let vectorMinor: number[];

assert(eigenvalues[0] <= eigenvalues[1]);
radiusMajor = Math.sqrt(eigenvalues[1]);
radiusMinor = Math.sqrt(eigenvalues[0]);
vectorMajor = vectors.getColumn(1);
vectorMinor = vectors.getColumn(0);

let majorAxisPoint1 = {
column: xCenter + radiusMajor * vectorMajor[0],
row: yCenter + radiusMajor * vectorMajor[1],
};
let majorAxisPoint2 = {
column: xCenter - radiusMajor * vectorMajor[0],
row: yCenter - radiusMajor * vectorMajor[1],
};
let minorAxisPoint1 = {
column: xCenter + radiusMinor * vectorMinor[0],
row: yCenter + radiusMinor * vectorMinor[1],
};
let minorAxisPoint2 = {
column: xCenter - radiusMinor * vectorMinor[0],
row: yCenter - radiusMinor * vectorMinor[1],
};

let majorLength = Math.sqrt(
(majorAxisPoint1.column - majorAxisPoint2.column) ** 2 +
(majorAxisPoint1.row - majorAxisPoint2.row) ** 2,
);
let minorLength = Math.sqrt(
(minorAxisPoint1.column - majorAxisPoint2.column) ** 2 +
(minorAxisPoint1.row - minorAxisPoint2.row) ** 2,
);

let ellipseSurface = (((minorLength / 2) * majorLength) / 2) * Math.PI;
if (ellipseSurface !== roi.surface) {
const scaleFactor = Math.sqrt(roi.surface / ellipseSurface);
radiusMajor *= scaleFactor;
radiusMinor *= scaleFactor;
majorAxisPoint1 = {
column: xCenter + radiusMajor * vectorMajor[0],
row: yCenter + radiusMajor * vectorMajor[1],
};
majorAxisPoint2 = {
column: xCenter - radiusMajor * vectorMajor[0],
row: yCenter - radiusMajor * vectorMajor[1],
};
minorAxisPoint1 = {
column: xCenter + radiusMinor * vectorMinor[0],
row: yCenter + radiusMinor * vectorMinor[1],
};
minorAxisPoint2 = {
column: xCenter - radiusMinor * vectorMinor[0],
row: yCenter - radiusMinor * vectorMinor[1],
};

majorLength *= scaleFactor;

minorLength *= scaleFactor;
ellipseSurface *= scaleFactor ** 2;
}

return {
center: {
column: xCenter,
row: yCenter,
},
majorAxis: {
points: [majorAxisPoint1, majorAxisPoint2],
length: majorLength,
angle: toDegrees(getAngle(majorAxisPoint1, majorAxisPoint2)),
},
minorAxis: {
points: [minorAxisPoint1, minorAxisPoint2],
length: minorLength,
angle: toDegrees(getAngle(minorAxisPoint1, minorAxisPoint2)),
},
surface: ellipseSurface,
};
}