From e199e4c47858bea6c5a3378738e46efb6409d721 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 12 Apr 2023 09:56:39 +0200 Subject: [PATCH 01/25] wip: add ellipse as a new feature --- package.json | 3 + src/roi/Roi.ts | 110 ++++++++++++++++++++++++++++++ src/roi/__tests__/ellipse.test.ts | 26 +++++++ 3 files changed, 139 insertions(+) create mode 100644 src/roi/__tests__/ellipse.test.ts diff --git a/package.json b/package.json index 3eff8a438..a47c2a2f3 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,10 @@ "image-type": "^4.1.0", "jpeg-js": "^0.4.4", "median-quickselect": "^1.0.1", + "ml-array-variance": "^1.1.8", + "ml-array-xy-covariance": "^0.2.1", "ml-convolution": "^2.0.0", + "ml-directional-distribution": "github:mljs/directional-distribution", "ml-matrix": "^6.10.4", "ml-regression-multivariate-linear": "^2.0.4", "ml-spectra-processing": "^12.0.0", diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 6e8a14062..57379970e 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -1,3 +1,8 @@ +import variance from 'ml-array-variance'; +//@ts-expect-error just to check if algorithm works +import covariance from 'ml-array-xy-covariance'; +import { EigenvalueDecomposition } from 'ml-matrix'; + import { Mask } from '../Mask'; import { GetBorderPointsOptions, @@ -13,6 +18,22 @@ import { RoiMap } from './RoiMapManager'; import { getBorderPoints } from './getBorderPoints'; import { getMask, GetMaskOptions } from './getMask'; +interface Ellipse { + rMajor: number; + rMinor: number; + position: { + x: number; + y: number; + }; + majorAxis: { + point1: { x: number; y: number }; + point2: { x: number; y: number }; + }; + minorAxis: { + point1: { x: number; y: number }; + point2: { x: number; y: number }; + }; +} interface Computed { perimeter: number; borderIDs: number[]; @@ -33,6 +54,7 @@ interface Computed { fillRatio: number; internalIDs: number[]; feret: Feret; + ellipse: Ellipse; centroid: Point; } export class Roi { @@ -221,6 +243,94 @@ export class Roi { return 2 * Math.sqrt(this.surface / Math.PI); }); } + get ellipse(): Ellipse { + return this.#getComputed('ellipse', (): Ellipse => { + const nbSD = 2; + let xCenter = this.centroid.column; + let yCenter = this.centroid.row; + let newPoints: number[][] = this.points; + + let xCentered = newPoints.map((item: number[]) => item[0] - xCenter); + let yCentered = newPoints.map((item: number[]) => item[1] - yCenter); + + let centeredXVariance = variance(xCentered, { unbiased: false }); + let centeredYVariance = variance(yCentered, { unbiased: false }); + + let centeredCovariance = covariance( + { + 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 rMajor: number; + let rMinor: number; + let vectorMajor: number[]; + let vectorMinor: number[]; + + if (eigenvalues[0] > eigenvalues[1]) { + rMajor = Math.sqrt(eigenvalues[0] * nbSD); + rMinor = Math.sqrt(eigenvalues[1] * nbSD); + vectorMajor = vectors.getColumn(0); + vectorMinor = vectors.getColumn(1); + } else if (eigenvalues[0] < eigenvalues[1]) { + rMajor = Math.sqrt(eigenvalues[1] * nbSD); + rMinor = Math.sqrt(eigenvalues[0] * nbSD); + vectorMajor = vectors.getColumn(1); + vectorMinor = vectors.getColumn(0); + } else { + // order here does not matter + rMajor = Math.sqrt(eigenvalues[1] * nbSD); + rMinor = Math.sqrt(eigenvalues[0] * nbSD); + vectorMajor = vectors.getColumn(1); + vectorMinor = vectors.getColumn(0); + } + + let majorAxisPoint1 = { + x: xCenter + rMajor * vectorMajor[0], + y: yCenter + rMajor * vectorMajor[1], + }; + let majorAxisPoint2 = { + x: xCenter - rMajor * vectorMajor[0], + y: yCenter - rMajor * vectorMajor[1], + }; + let minorAxisPoint1 = { + x: xCenter + rMinor * vectorMinor[0], + y: yCenter + rMinor * vectorMinor[1], + }; + let minorAxisPoint2 = { + x: xCenter - rMinor * vectorMinor[0], + y: yCenter - rMinor * vectorMinor[1], + }; + + return { + rMajor, + rMinor, + position: { + x: xCenter, + y: yCenter, + }, + majorAxis: { + point1: majorAxisPoint1, + point2: majorAxisPoint2, + }, + minorAxis: { + point1: minorAxisPoint1, + point2: minorAxisPoint2, + }, + }; + }); + } get holesInfo() { return this.#getComputed('holesInfo', () => { diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts new file mode 100644 index 000000000..cb2f3e809 --- /dev/null +++ b/src/roi/__tests__/ellipse.test.ts @@ -0,0 +1,26 @@ +import { fromMask } from '..'; + +test('calculates surface from eqpc', () => { + 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({ + rMajor: 1.1055415967851332, + rMinor: 0.8164965809277259, + position: { x: 0.8333333333333334, y: 0.8333333333333334 }, + majorAxis: { + point1: { x: 1.6150692933039048, y: 1.6150692933039048 }, + point2: { x: 0.051597373362761934, y: 0.05159737336276182 }, + }, + minorAxis: { + point1: { x: 0.25598306414370764, y: 1.410683602522959 }, + point2: { x: 1.4106836025229592, y: 0.25598306414370775 }, + }, + }); +}); From 66df20a6a383eff046efa16c981a7935d0ce374b Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 19 Apr 2023 06:14:55 +0200 Subject: [PATCH 02/25] chore: get ellipse to scale to match ROI's surface --- src/roi/Roi.ts | 193 ++++++++++++++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 84 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 57379970e..db50c5571 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -33,6 +33,7 @@ interface Ellipse { point1: { x: number; y: number }; point2: { x: number; y: number }; }; + Surface: number; } interface Computed { perimeter: number; @@ -244,91 +245,13 @@ export class Roi { }); } get ellipse(): Ellipse { - return this.#getComputed('ellipse', (): Ellipse => { - const nbSD = 2; - let xCenter = this.centroid.column; - let yCenter = this.centroid.row; - let newPoints: number[][] = this.points; - - let xCentered = newPoints.map((item: number[]) => item[0] - xCenter); - let yCentered = newPoints.map((item: number[]) => item[1] - yCenter); - - let centeredXVariance = variance(xCentered, { unbiased: false }); - let centeredYVariance = variance(yCentered, { unbiased: false }); - - let centeredCovariance = covariance( - { - 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 rMajor: number; - let rMinor: number; - let vectorMajor: number[]; - let vectorMinor: number[]; - - if (eigenvalues[0] > eigenvalues[1]) { - rMajor = Math.sqrt(eigenvalues[0] * nbSD); - rMinor = Math.sqrt(eigenvalues[1] * nbSD); - vectorMajor = vectors.getColumn(0); - vectorMinor = vectors.getColumn(1); - } else if (eigenvalues[0] < eigenvalues[1]) { - rMajor = Math.sqrt(eigenvalues[1] * nbSD); - rMinor = Math.sqrt(eigenvalues[0] * nbSD); - vectorMajor = vectors.getColumn(1); - vectorMinor = vectors.getColumn(0); - } else { - // order here does not matter - rMajor = Math.sqrt(eigenvalues[1] * nbSD); - rMinor = Math.sqrt(eigenvalues[0] * nbSD); - vectorMajor = vectors.getColumn(1); - vectorMinor = vectors.getColumn(0); + return this.#getComputed('ellipse', () => { + let ellipse = getEllipse(this, { nbSD: 2, scale: 1 }); + if (ellipse.Surface !== this.surface) { + const scaleFactor = Math.sqrt(this.surface / ellipse.Surface); + ellipse = getEllipse(this, { nbSD: 2, scale: scaleFactor }); } - - let majorAxisPoint1 = { - x: xCenter + rMajor * vectorMajor[0], - y: yCenter + rMajor * vectorMajor[1], - }; - let majorAxisPoint2 = { - x: xCenter - rMajor * vectorMajor[0], - y: yCenter - rMajor * vectorMajor[1], - }; - let minorAxisPoint1 = { - x: xCenter + rMinor * vectorMinor[0], - y: yCenter + rMinor * vectorMinor[1], - }; - let minorAxisPoint2 = { - x: xCenter - rMinor * vectorMinor[0], - y: yCenter - rMinor * vectorMinor[1], - }; - - return { - rMajor, - rMinor, - position: { - x: xCenter, - y: yCenter, - }, - majorAxis: { - point1: majorAxisPoint1, - point2: majorAxisPoint2, - }, - minorAxis: { - point1: minorAxisPoint1, - point2: minorAxisPoint2, - }, - }; + return ellipse; }); } @@ -718,3 +641,105 @@ function getBorders(roi: Roi): { ids: number[]; lengths: number[] } { lengths: borderLengths, }; } + +function getEllipse( + roi: Roi, + options: { nbSD: number; scale: number }, +): Ellipse { + let xCenter = roi.centroid.column; + let yCenter = roi.centroid.row; + let newPoints: number[][] = roi.points; + + let xCentered = newPoints.map((item: number[]) => item[0] - xCenter); + let yCentered = newPoints.map((item: number[]) => item[1] - yCenter); + + let centeredXVariance = variance(xCentered, { unbiased: false }); + let centeredYVariance = variance(yCentered, { unbiased: false }); + + let centeredCovariance = covariance( + { + 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 rMajor: number; + let rMinor: number; + let vectorMajor: number[]; + let vectorMinor: number[]; + let surface: number; + if (eigenvalues[0] > eigenvalues[1]) { + rMajor = Math.sqrt(eigenvalues[0] * options.nbSD); + rMinor = Math.sqrt(eigenvalues[1] * options.nbSD); + vectorMajor = vectors.getColumn(0); + vectorMinor = vectors.getColumn(1); + } else if (eigenvalues[0] < eigenvalues[1]) { + rMajor = Math.sqrt(eigenvalues[1] * options.nbSD); + rMinor = Math.sqrt(eigenvalues[0] * options.nbSD); + vectorMajor = vectors.getColumn(1); + vectorMinor = vectors.getColumn(0); + } else { + // order here does not matter + rMajor = Math.sqrt(eigenvalues[1] * options.nbSD); + rMinor = Math.sqrt(eigenvalues[0] * options.nbSD); + vectorMajor = vectors.getColumn(1); + vectorMinor = vectors.getColumn(0); + } + rMajor *= options.scale; + rMinor *= options.scale; + + let majorAxisPoint1 = { + x: xCenter + rMajor * vectorMajor[0], + y: yCenter + rMajor * vectorMajor[1], + }; + let majorAxisPoint2 = { + x: xCenter - rMajor * vectorMajor[0], + y: yCenter - rMajor * vectorMajor[1], + }; + let minorAxisPoint1 = { + x: xCenter + rMinor * vectorMinor[0], + y: yCenter + rMinor * vectorMinor[1], + }; + let minorAxisPoint2 = { + x: xCenter - rMinor * vectorMinor[0], + y: yCenter - rMinor * vectorMinor[1], + }; + + surface = + Math.sqrt( + Math.pow(majorAxisPoint1.x - xCenter, 2) + + Math.pow(majorAxisPoint1.y - yCenter, 2), + ) * + Math.sqrt( + Math.pow(minorAxisPoint1.x - xCenter, 2) + + Math.pow(minorAxisPoint1.y - yCenter, 2), + ) * + Math.PI; + return { + rMajor, + rMinor, + position: { + x: xCenter, + y: yCenter, + }, + majorAxis: { + point1: majorAxisPoint1, + point2: majorAxisPoint2, + }, + minorAxis: { + point1: minorAxisPoint1, + point2: minorAxisPoint2, + }, + Surface: surface, + }; +} From 454c82098083b5c03eeec88837d5b531328aadbe Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 19 Apr 2023 13:02:04 +0200 Subject: [PATCH 03/25] refactor: change surface name to lower case --- src/roi/Roi.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index db50c5571..8deb8e78a 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -33,7 +33,7 @@ interface Ellipse { point1: { x: number; y: number }; point2: { x: number; y: number }; }; - Surface: number; + surface: number; } interface Computed { perimeter: number; @@ -247,8 +247,8 @@ export class Roi { get ellipse(): Ellipse { return this.#getComputed('ellipse', () => { let ellipse = getEllipse(this, { nbSD: 2, scale: 1 }); - if (ellipse.Surface !== this.surface) { - const scaleFactor = Math.sqrt(this.surface / ellipse.Surface); + if (ellipse.surface !== this.surface) { + const scaleFactor = Math.sqrt(this.surface / ellipse.surface); ellipse = getEllipse(this, { nbSD: 2, scale: scaleFactor }); } return ellipse; @@ -677,7 +677,7 @@ function getEllipse( let rMinor: number; let vectorMajor: number[]; let vectorMinor: number[]; - let surface: number; + let ellipseSurface: number; if (eigenvalues[0] > eigenvalues[1]) { rMajor = Math.sqrt(eigenvalues[0] * options.nbSD); rMinor = Math.sqrt(eigenvalues[1] * options.nbSD); @@ -715,7 +715,7 @@ function getEllipse( y: yCenter - rMinor * vectorMinor[1], }; - surface = + ellipseSurface = Math.sqrt( Math.pow(majorAxisPoint1.x - xCenter, 2) + Math.pow(majorAxisPoint1.y - yCenter, 2), @@ -740,6 +740,6 @@ function getEllipse( point1: minorAxisPoint1, point2: minorAxisPoint2, }, - Surface: surface, + surface: ellipseSurface, }; } From 04aa982ef75d7c56fb0aa7f00bedd8abec90094a Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 19 Apr 2023 13:06:28 +0200 Subject: [PATCH 04/25] test: fix testing case result --- src/roi/__tests__/ellipse.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts index cb2f3e809..a03d367b0 100644 --- a/src/roi/__tests__/ellipse.test.ts +++ b/src/roi/__tests__/ellipse.test.ts @@ -11,16 +11,17 @@ test('calculates surface from eqpc', () => { const rois = roiMapManager.getRois(); const result = rois[0].ellipse; expect(result).toBeDeepCloseTo({ - rMajor: 1.1055415967851332, - rMinor: 0.8164965809277259, + rMajor: 1.6080925781597333, + rMinor: 1.1876550784708841, position: { x: 0.8333333333333334, y: 0.8333333333333334 }, majorAxis: { - point1: { x: 1.6150692933039048, y: 1.6150692933039048 }, - point2: { x: 0.051597373362761934, y: 0.05159737336276182 }, + point1: { x: 1.9704265001258388, y: 1.9704265001258392 }, + point2: { x: -0.3037598334591721, y: -0.30375983345917235 }, }, minorAxis: { - point1: { x: 0.25598306414370764, y: 1.410683602522959 }, - point2: { x: 1.4106836025229592, y: 0.25598306414370775 }, + point1: { x: -0.0064656263640701095, y: 1.6731322930307369 }, + point2: { x: 1.6731322930307369, y: -0.0064656263640699985 }, }, + surface: 6.000000000000001, }); }); From 089c39ed5bd925b34377afd4f6bf7e3431ba74e1 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Thu, 20 Apr 2023 18:02:17 +0200 Subject: [PATCH 05/25] refactor: change names of the variables and return types --- src/roi/Roi.ts | 54 +++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 8deb8e78a..2ddb11fab 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -19,11 +19,9 @@ import { getBorderPoints } from './getBorderPoints'; import { getMask, GetMaskOptions } from './getMask'; interface Ellipse { - rMajor: number; - rMinor: number; - position: { - x: number; - y: number; + center: { + column: number; + row: number; }; majorAxis: { point1: { x: number; y: number }; @@ -673,51 +671,51 @@ function getEllipse( let eigenvalues = e.realEigenvalues; let vectors = e.eigenvectorMatrix; - let rMajor: number; - let rMinor: number; + let radiusMajor: number; + let radiusMinor: number; let vectorMajor: number[]; let vectorMinor: number[]; let ellipseSurface: number; if (eigenvalues[0] > eigenvalues[1]) { - rMajor = Math.sqrt(eigenvalues[0] * options.nbSD); - rMinor = Math.sqrt(eigenvalues[1] * options.nbSD); + radiusMajor = Math.sqrt(eigenvalues[0] * options.nbSD); + radiusMinor = Math.sqrt(eigenvalues[1] * options.nbSD); vectorMajor = vectors.getColumn(0); vectorMinor = vectors.getColumn(1); } else if (eigenvalues[0] < eigenvalues[1]) { - rMajor = Math.sqrt(eigenvalues[1] * options.nbSD); - rMinor = Math.sqrt(eigenvalues[0] * options.nbSD); + radiusMajor = Math.sqrt(eigenvalues[1] * options.nbSD); + radiusMinor = Math.sqrt(eigenvalues[0] * options.nbSD); vectorMajor = vectors.getColumn(1); vectorMinor = vectors.getColumn(0); } else { // order here does not matter - rMajor = Math.sqrt(eigenvalues[1] * options.nbSD); - rMinor = Math.sqrt(eigenvalues[0] * options.nbSD); + radiusMajor = Math.sqrt(eigenvalues[1] * options.nbSD); + radiusMinor = Math.sqrt(eigenvalues[0] * options.nbSD); vectorMajor = vectors.getColumn(1); vectorMinor = vectors.getColumn(0); } - rMajor *= options.scale; - rMinor *= options.scale; + radiusMajor *= options.scale; + radiusMinor *= options.scale; let majorAxisPoint1 = { - x: xCenter + rMajor * vectorMajor[0], - y: yCenter + rMajor * vectorMajor[1], + x: xCenter + radiusMajor * vectorMajor[0], + y: yCenter + radiusMajor * vectorMajor[1], }; let majorAxisPoint2 = { - x: xCenter - rMajor * vectorMajor[0], - y: yCenter - rMajor * vectorMajor[1], + x: xCenter - radiusMajor * vectorMajor[0], + y: yCenter - radiusMajor * vectorMajor[1], }; let minorAxisPoint1 = { - x: xCenter + rMinor * vectorMinor[0], - y: yCenter + rMinor * vectorMinor[1], + x: xCenter + radiusMinor * vectorMinor[0], + y: yCenter + radiusMinor * vectorMinor[1], }; let minorAxisPoint2 = { - x: xCenter - rMinor * vectorMinor[0], - y: yCenter - rMinor * vectorMinor[1], + x: xCenter - radiusMinor * vectorMinor[0], + y: yCenter - radiusMinor * vectorMinor[1], }; ellipseSurface = Math.sqrt( - Math.pow(majorAxisPoint1.x - xCenter, 2) + + (majorAxisPoint1.x - xCenter) ** 2 + Math.pow(majorAxisPoint1.y - yCenter, 2), ) * Math.sqrt( @@ -726,11 +724,9 @@ function getEllipse( ) * Math.PI; return { - rMajor, - rMinor, - position: { - x: xCenter, - y: yCenter, + center: { + column: xCenter, + row: yCenter, }, majorAxis: { point1: majorAxisPoint1, From 4ac085595a1150374888fce01927aedebe4d08fa Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Thu, 20 Apr 2023 20:26:00 +0200 Subject: [PATCH 06/25] chore: remove unnecessary packages --- package.json | 1 - src/roi/Roi.ts | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a47c2a2f3..5bb83a29c 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "jpeg-js": "^0.4.4", "median-quickselect": "^1.0.1", "ml-array-variance": "^1.1.8", - "ml-array-xy-covariance": "^0.2.1", "ml-convolution": "^2.0.0", "ml-directional-distribution": "github:mljs/directional-distribution", "ml-matrix": "^6.10.4", diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 2ddb11fab..d02c56291 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -1,7 +1,5 @@ -import variance from 'ml-array-variance'; -//@ts-expect-error just to check if algorithm works -import covariance from 'ml-array-xy-covariance'; import { EigenvalueDecomposition } from 'ml-matrix'; +import { xVariance, xyCovariance } from 'ml-spectra-processing'; import { Mask } from '../Mask'; import { @@ -651,10 +649,10 @@ function getEllipse( let xCentered = newPoints.map((item: number[]) => item[0] - xCenter); let yCentered = newPoints.map((item: number[]) => item[1] - yCenter); - let centeredXVariance = variance(xCentered, { unbiased: false }); - let centeredYVariance = variance(yCentered, { unbiased: false }); + let centeredXVariance = xVariance(xCentered, { unbiased: false }); + let centeredYVariance = xVariance(yCentered, { unbiased: false }); - let centeredCovariance = covariance( + let centeredCovariance = xyCovariance( { x: xCentered, y: yCentered, From 695109e89aa8f7fc1aa859ffd4bcceadbf0f13d7 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Thu, 20 Apr 2023 20:59:02 +0200 Subject: [PATCH 07/25] refactor: change the way of notation of power --- src/roi/Roi.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index d02c56291..c7f763735 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -713,12 +713,10 @@ function getEllipse( ellipseSurface = Math.sqrt( - (majorAxisPoint1.x - xCenter) ** 2 + - Math.pow(majorAxisPoint1.y - yCenter, 2), + (majorAxisPoint1.x - xCenter) ** 2 + (majorAxisPoint1.y - yCenter) ** 2, ) * Math.sqrt( - Math.pow(minorAxisPoint1.x - xCenter, 2) + - Math.pow(minorAxisPoint1.y - yCenter, 2), + (minorAxisPoint1.x - xCenter) ** 2 + (minorAxisPoint1.y - yCenter) ** 2, ) * Math.PI; return { From c67a52c6ae5b39bd828759856b310dc519abb84b Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Thu, 20 Apr 2023 21:33:30 +0200 Subject: [PATCH 08/25] test: fix the testing case to match previous commits --- src/roi/__tests__/ellipse.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts index a03d367b0..e5bbade1a 100644 --- a/src/roi/__tests__/ellipse.test.ts +++ b/src/roi/__tests__/ellipse.test.ts @@ -11,9 +11,7 @@ test('calculates surface from eqpc', () => { const rois = roiMapManager.getRois(); const result = rois[0].ellipse; expect(result).toBeDeepCloseTo({ - rMajor: 1.6080925781597333, - rMinor: 1.1876550784708841, - position: { x: 0.8333333333333334, y: 0.8333333333333334 }, + center: { column: 0.8333333333333334, row: 0.8333333333333334 }, majorAxis: { point1: { x: 1.9704265001258388, y: 1.9704265001258392 }, point2: { x: -0.3037598334591721, y: -0.30375983345917235 }, From ea56c1ff18cd871266f553246c8439ab2c1e935b Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sat, 22 Apr 2023 02:22:05 +0200 Subject: [PATCH 09/25] chore: redo the whole implementation of an ellipse property due to a bug --- package.json | 2 +- src/roi/Roi.ts | 92 +++++++++++++++++++++++++------------------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 5bb83a29c..e24f323bd 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "ml-directional-distribution": "github:mljs/directional-distribution", "ml-matrix": "^6.10.4", "ml-regression-multivariate-linear": "^2.0.4", - "ml-spectra-processing": "^12.0.0", + "ml-spectra-processing": "^12.1.0", "robust-point-in-polygon": "^1.0.3", "ssim.js": "^3.5.0", "tiff": "^5.0.3" diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index c7f763735..0210aa842 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -1,3 +1,4 @@ +//import { covariance } from 'ml-array-xy-covariance'; import { EigenvalueDecomposition } from 'ml-matrix'; import { xVariance, xyCovariance } from 'ml-spectra-processing'; @@ -9,7 +10,9 @@ import { getConvexHull, getMbr, Mbr, + FeretDiameter, } from '../maskAnalysis'; +import { getAngle } from '../maskAnalysis/utils/getAngle'; import { Point } from '../utils/geometry/points'; import { RoiMap } from './RoiMapManager'; @@ -21,14 +24,8 @@ interface Ellipse { column: number; row: number; }; - majorAxis: { - point1: { x: number; y: number }; - point2: { x: number; y: number }; - }; - minorAxis: { - point1: { x: number; y: number }; - point2: { x: number; y: number }; - }; + majorAxis: FeretDiameter; + minorAxis: FeretDiameter; surface: number; } interface Computed { @@ -242,10 +239,10 @@ export class Roi { } get ellipse(): Ellipse { return this.#getComputed('ellipse', () => { - let ellipse = getEllipse(this, { nbSD: 2, scale: 1 }); + let ellipse = getEllipse(this, 1); if (ellipse.surface !== this.surface) { const scaleFactor = Math.sqrt(this.surface / ellipse.surface); - ellipse = getEllipse(this, { nbSD: 2, scale: scaleFactor }); + ellipse = getEllipse(this, scaleFactor); } return ellipse; }); @@ -638,22 +635,21 @@ function getBorders(roi: Roi): { ids: number[]; lengths: number[] } { }; } -function getEllipse( - roi: Roi, - options: { nbSD: number; scale: number }, -): Ellipse { +function getEllipse(roi: Roi, scale: number): Ellipse { + const nbSD = 2; + let xCenter = roi.centroid.column; let yCenter = roi.centroid.row; - let newPoints: number[][] = roi.points; - let xCentered = newPoints.map((item: number[]) => item[0] - xCenter); - let yCentered = newPoints.map((item: number[]) => item[1] - yCenter); + 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( { + //@ts-expect-error check x: xCentered, y: yCentered, }, @@ -673,64 +669,68 @@ function getEllipse( let radiusMinor: number; let vectorMajor: number[]; let vectorMinor: number[]; - let ellipseSurface: number; + if (eigenvalues[0] > eigenvalues[1]) { - radiusMajor = Math.sqrt(eigenvalues[0] * options.nbSD); - radiusMinor = Math.sqrt(eigenvalues[1] * options.nbSD); + radiusMajor = Math.sqrt(eigenvalues[0] * nbSD); + radiusMinor = Math.sqrt(eigenvalues[1] * nbSD); vectorMajor = vectors.getColumn(0); vectorMinor = vectors.getColumn(1); } else if (eigenvalues[0] < eigenvalues[1]) { - radiusMajor = Math.sqrt(eigenvalues[1] * options.nbSD); - radiusMinor = Math.sqrt(eigenvalues[0] * options.nbSD); + radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); + radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); vectorMajor = vectors.getColumn(1); vectorMinor = vectors.getColumn(0); } else { // order here does not matter - radiusMajor = Math.sqrt(eigenvalues[1] * options.nbSD); - radiusMinor = Math.sqrt(eigenvalues[0] * options.nbSD); + radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); + radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); vectorMajor = vectors.getColumn(1); vectorMinor = vectors.getColumn(0); } - radiusMajor *= options.scale; - radiusMinor *= options.scale; + radiusMajor *= scale; + radiusMinor *= scale; let majorAxisPoint1 = { - x: xCenter + radiusMajor * vectorMajor[0], - y: yCenter + radiusMajor * vectorMajor[1], + column: xCenter + radiusMajor * vectorMajor[0], + row: yCenter + radiusMajor * vectorMajor[1], }; let majorAxisPoint2 = { - x: xCenter - radiusMajor * vectorMajor[0], - y: yCenter - radiusMajor * vectorMajor[1], + column: xCenter - radiusMajor * vectorMajor[0], + row: yCenter - radiusMajor * vectorMajor[1], }; let minorAxisPoint1 = { - x: xCenter + radiusMinor * vectorMinor[0], - y: yCenter + radiusMinor * vectorMinor[1], + column: xCenter + radiusMinor * vectorMinor[0], + row: yCenter + radiusMinor * vectorMinor[1], }; let minorAxisPoint2 = { - x: xCenter - radiusMinor * vectorMinor[0], - y: yCenter - radiusMinor * vectorMinor[1], + column: xCenter - radiusMinor * vectorMinor[0], + row: yCenter - radiusMinor * vectorMinor[1], }; - ellipseSurface = - Math.sqrt( - (majorAxisPoint1.x - xCenter) ** 2 + (majorAxisPoint1.y - yCenter) ** 2, - ) * - Math.sqrt( - (minorAxisPoint1.x - xCenter) ** 2 + (minorAxisPoint1.y - yCenter) ** 2, - ) * - Math.PI; + const majorLength = Math.sqrt( + (majorAxisPoint1.column - majorAxisPoint2.column) ** 2 + + (majorAxisPoint1.row - majorAxisPoint2.row) ** 2, + ); + const minorLength = Math.sqrt( + (minorAxisPoint1.column - majorAxisPoint2.column) ** 2 + + (minorAxisPoint1.row - minorAxisPoint2.row) ** 2, + ); + + let ellipseSurface = (((minorLength / 2) * majorLength) / 2) * Math.PI; return { center: { column: xCenter, row: yCenter, }, majorAxis: { - point1: majorAxisPoint1, - point2: majorAxisPoint2, + points: [majorAxisPoint1, majorAxisPoint2], + length: majorLength, + angle: getAngle(majorAxisPoint1, majorAxisPoint2), }, minorAxis: { - point1: minorAxisPoint1, - point2: minorAxisPoint2, + points: [minorAxisPoint1, minorAxisPoint1], + length: minorLength, + angle: getAngle(minorAxisPoint1, minorAxisPoint2), }, surface: ellipseSurface, }; From c058ff374b5ca929cbc6891aae0b3997b54b20aa Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sat, 22 Apr 2023 02:27:18 +0200 Subject: [PATCH 10/25] test: change test case results due to refactoring --- src/roi/__tests__/ellipse.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts index e5bbade1a..8d5f69606 100644 --- a/src/roi/__tests__/ellipse.test.ts +++ b/src/roi/__tests__/ellipse.test.ts @@ -13,13 +13,21 @@ test('calculates surface from eqpc', () => { expect(result).toBeDeepCloseTo({ center: { column: 0.8333333333333334, row: 0.8333333333333334 }, majorAxis: { - point1: { x: 1.9704265001258388, y: 1.9704265001258392 }, - point2: { x: -0.3037598334591721, y: -0.30375983345917235 }, + points: [ + { column: 2.175183801009782, row: 2.175183801009782 }, + { column: -0.5085171343431153, row: -0.5085171343431155 }, + ], + length: 3.7953262601294284, + angle: -2.356194490192345, }, minorAxis: { - point1: { x: -0.0064656263640701095, y: 1.6731322930307369 }, - point2: { x: 1.6731322930307369, y: -0.0064656263640699985 }, + points: [ + { column: -0.15768891509232064, row: 1.8243555817589874 }, + { column: -0.15768891509232064, row: 1.8243555817589874 }, + ], + length: 2.01285390103734, + angle: -0.7853981633974483, }, - surface: 6.000000000000001, + surface: 6.000000000000002, }); }); From 4979f2c16764639880cad06d41ac152ad4ce0166 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sat, 22 Apr 2023 02:37:17 +0200 Subject: [PATCH 11/25] chore: remove commented import --- src/roi/Roi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 0210aa842..6a4c97a8f 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -1,4 +1,3 @@ -//import { covariance } from 'ml-array-xy-covariance'; import { EigenvalueDecomposition } from 'ml-matrix'; import { xVariance, xyCovariance } from 'ml-spectra-processing'; From c0d99f7583552e3d7c074673ceaf31326476c14d Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sat, 22 Apr 2023 02:40:33 +0200 Subject: [PATCH 12/25] fix: add a missing bracket to border interface --- src/roi/Roi.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 5fc43ee95..1cf140500 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -18,7 +18,6 @@ import { RoiMap } from './RoiMapManager'; import { getBorderPoints } from './getBorderPoints'; import { getMask, GetMaskOptions } from './getMask'; - interface Ellipse { center: { column: number; @@ -27,11 +26,10 @@ interface Ellipse { majorAxis: FeretDiameter; minorAxis: FeretDiameter; surface: number; - +} interface Border { connectedID: number; // refers to the roiID of the contiguous ROI length: number; - } interface Computed { perimeter: number; From 7eae27c77f0edbc18e613a868561c32765f5c007 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sat, 22 Apr 2023 02:47:08 +0200 Subject: [PATCH 13/25] fix: remove ts-expect errors and fix object property names --- src/roi/Roi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 1cf140500..01cb6215f 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -668,7 +668,6 @@ function getEllipse(roi: Roi, scale: number): Ellipse { let centeredCovariance = xyCovariance( { - //@ts-expect-error check x: xCentered, y: yCentered, }, From e6e8522340f229130fda3ed5a0a63a5da9a4728c Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sat, 22 Apr 2023 12:34:53 +0200 Subject: [PATCH 14/25] chore: remove unused dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 2af8f8dec..a74517a1b 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "median-quickselect": "^1.0.1", "ml-array-variance": "^1.1.8", "ml-convolution": "^2.0.0", - "ml-directional-distribution": "github:mljs/directional-distribution", "ml-matrix": "^6.10.4", "ml-regression-multivariate-linear": "^2.0.4", "ml-spectra-processing": "^12.1.0", From 9a7e5bef2d78a09b0d79f300211a24461cffe381 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sat, 22 Apr 2023 15:02:39 +0200 Subject: [PATCH 15/25] test: add testing cases for better code coverage --- src/roi/__tests__/ellipse.test.ts | 98 ++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts index 8d5f69606..f6bdb291f 100644 --- a/src/roi/__tests__/ellipse.test.ts +++ b/src/roi/__tests__/ellipse.test.ts @@ -1,6 +1,69 @@ import { fromMask } from '..'; -test('calculates surface from eqpc', () => { +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: -2.356194490192345, + }, + minorAxis: { + points: [ + { column: 1.146541384241206, row: -0.14654138424120605 }, + { column: 1.146541384241206, row: -0.14654138424120605 }, + ], + length: 1.6247924149339357, + angle: 2.356194490192345, + }, + 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: -1.5707963267948966, + }, + minorAxis: { + points: [ + { column: 2.7841241161527712, row: 1 }, + { column: 2.7841241161527712, row: 1 }, + ], + length: 1.7841241161527712, + angle: 3.141592653589793, + }, + surface: 5, + }); +}); +test('ellipse on slightly changed 3x3 cross', () => { const mask = testUtils.createMask([ [1, 1, 0], [1, 1, 1], @@ -31,3 +94,36 @@ test('calculates surface from eqpc', () => { 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: -0.961420320555157, + }, + minorAxis: { + points: [ + { column: 0.8646151602330147, row: 1.1403989822180598 }, + { column: 0.8646151602330147, row: 1.1403989822180598 }, + ], + length: 2.544387508597132, + angle: 0.6093760062397394, + }, + surface: 9.000000000000002, + }); +}); From 43cc291ac4a9dff63c5fee007992e987552b4a0f Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sun, 23 Apr 2023 20:04:32 +0200 Subject: [PATCH 16/25] chore: convert angle in ellipse from rad to degrees --- src/roi/Roi.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 01cb6215f..fca2ddcdf 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -12,6 +12,7 @@ import { FeretDiameter, } from '../maskAnalysis'; import { getAngle } from '../maskAnalysis/utils/getAngle'; +import { toDegrees } from '../utils/geometry/angles'; import { Point } from '../utils/geometry/points'; import { RoiMap } from './RoiMapManager'; @@ -743,12 +744,12 @@ function getEllipse(roi: Roi, scale: number): Ellipse { majorAxis: { points: [majorAxisPoint1, majorAxisPoint2], length: majorLength, - angle: getAngle(majorAxisPoint1, majorAxisPoint2), + angle: toDegrees(getAngle(majorAxisPoint1, majorAxisPoint2)), }, minorAxis: { points: [minorAxisPoint1, minorAxisPoint1], length: minorLength, - angle: getAngle(minorAxisPoint1, minorAxisPoint2), + angle: toDegrees(getAngle(minorAxisPoint1, minorAxisPoint2)), }, surface: ellipseSurface, }; From 7e48ea3bf26789d8d3adfe5792b528a68827f098 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Sun, 23 Apr 2023 20:05:14 +0200 Subject: [PATCH 17/25] test: convert angles to degrees in testing cases --- src/roi/__tests__/ellipse.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts index f6bdb291f..11f4271b4 100644 --- a/src/roi/__tests__/ellipse.test.ts +++ b/src/roi/__tests__/ellipse.test.ts @@ -18,7 +18,7 @@ test('ellipse on a small figure 3x3', () => { { column: -0.1645007254402011, row: -0.4978340587735344 }, ], length: 2.3508963970396173, - angle: -2.356194490192345, + angle: -135, }, minorAxis: { points: [ @@ -26,7 +26,7 @@ test('ellipse on a small figure 3x3', () => { { column: 1.146541384241206, row: -0.14654138424120605 }, ], length: 1.6247924149339357, - angle: 2.356194490192345, + angle: 135, }, surface: 3, }); @@ -50,7 +50,7 @@ test('ellipse on 3x3 cross', () => { { column: 1, row: -0.7841241161527714 }, ], length: 3.5682482323055424, - angle: -1.5707963267948966, + angle: -90, }, minorAxis: { points: [ @@ -58,7 +58,7 @@ test('ellipse on 3x3 cross', () => { { column: 2.7841241161527712, row: 1 }, ], length: 1.7841241161527712, - angle: 3.141592653589793, + angle: 180, }, surface: 5, }); @@ -81,7 +81,7 @@ test('ellipse on slightly changed 3x3 cross', () => { { column: -0.5085171343431153, row: -0.5085171343431155 }, ], length: 3.7953262601294284, - angle: -2.356194490192345, + angle: -135, }, minorAxis: { points: [ @@ -89,7 +89,7 @@ test('ellipse on slightly changed 3x3 cross', () => { { column: -0.15768891509232064, row: 1.8243555817589874 }, ], length: 2.01285390103734, - angle: -0.7853981633974483, + angle: -45, }, surface: 6.000000000000002, }); @@ -114,7 +114,7 @@ test('ellipse on 4x4 ROI', () => { { column: 3.0666371578044496, row: -0.06875086941190611 }, ], length: 4.503699166851579, - angle: -0.961420320555157, + angle: -55.08532670592521, }, minorAxis: { points: [ @@ -122,7 +122,7 @@ test('ellipse on 4x4 ROI', () => { { column: 0.8646151602330147, row: 1.1403989822180598 }, ], length: 2.544387508597132, - angle: 0.6093760062397394, + angle: 34.91467329407479, }, surface: 9.000000000000002, }); From 3cdcaac9ba8b05c2ef81109048dba911f7bfbc3b Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Mon, 24 Apr 2023 08:34:05 +0200 Subject: [PATCH 18/25] chore: remove unused ml-array-variance --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index a74517a1b..bdf684da4 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "image-type": "^4.1.0", "jpeg-js": "^0.4.4", "median-quickselect": "^1.0.1", - "ml-array-variance": "^1.1.8", "ml-convolution": "^2.0.0", "ml-matrix": "^6.10.4", "ml-regression-multivariate-linear": "^2.0.4", From 5062aa23f16cd321dfdc29f299eba89d672ad681 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Mon, 24 Apr 2023 22:12:46 +0200 Subject: [PATCH 19/25] chore: move function getEllipse to a separate folder --- src/roi/Roi.ts | 363 +------------------------------ src/roi/properties/getEllipse.ts | 121 +++++++++++ 2 files changed, 123 insertions(+), 361 deletions(-) create mode 100644 src/roi/properties/getEllipse.ts diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index a1668b690..1839269ae 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -1,6 +1,3 @@ -import { EigenvalueDecomposition } from 'ml-matrix'; -import { xVariance, xyCovariance } from 'ml-spectra-processing'; - import { Mask } from '../Mask'; import { GetBorderPointsOptions, @@ -9,25 +6,14 @@ import { getConvexHull, getMbr, Mbr, - FeretDiameter, } from '../maskAnalysis'; -import { getAngle } from '../maskAnalysis/utils/getAngle'; -import { toDegrees } from '../utils/geometry/angles'; 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 Ellipse { - center: { - column: number; - row: number; - }; - majorAxis: FeretDiameter; - minorAxis: FeretDiameter; - surface: number; -} interface Border { connectedID: number; // refers to the roiID of the contiguous ROI length: number; @@ -635,352 +621,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; } } - - -/** - * - * @param roi -ROI - * @returns object which tells how many pixels are exposed externally to how many sides - */ -function getPerimeterInfo(roi: Roi) { - const roiMap = roi.getMap(); - const data = roiMap.data; - let one = 0; - let two = 0; - let three = 0; - let four = 0; - let externalIDs = roi.externalBorders.map((element) => element.connectedID); - for (let column = 0; column < roi.width; column++) { - for (let row = 0; row < roi.height; row++) { - let target = roi.computeIndex(row, column); - if (data[target] === roi.id) { - let nbAround = 0; - if (column === 0) { - nbAround++; - } else if (externalIDs.includes(data[target - 1])) { - nbAround++; - } - - if (column === roiMap.width - 1) { - nbAround++; - } else if (externalIDs.includes(data[target + 1])) { - nbAround++; - } - - if (row === 0) { - nbAround++; - } else if (externalIDs.includes(data[target - roiMap.width])) { - nbAround++; - } - - if (row === roiMap.height - 1) { - nbAround++; - } else if (externalIDs.includes(data[target + roiMap.width])) { - nbAround++; - } - switch (nbAround) { - case 1: - one++; - break; - case 2: - two++; - break; - case 3: - three++; - break; - case 4: - four++; - break; - default: - } - } - } - } - return { one, two, three, four }; -} - -/** - * - * @param roi - ROI - * @returns the surface of holes in ROI - */ -function getHolesInfo(roi: Roi) { - let surface = 0; - const data = roi.getMap().data; - for (let column = 1; column < roi.width - 1; column++) { - for (let row = 1; row < roi.height - 1; row++) { - let target = roi.computeIndex(row, column); - if (roi.internalIDs.includes(data[target]) && data[target] !== roi.id) { - surface++; - } - } - } - return { - number: roi.internalIDs.length - 1, - surface, - }; -} -/** - * Calculates internal IDs of the ROI - * - * @param roi - * @returns internalIDs - */ -function getInternalIDs(roi: Roi) { - let internal = [roi.id]; - let roiMap = roi.getMap(); - let data = roiMap.data; - - if (roi.height > 2) { - for (let column = 0; column < roi.width; column++) { - let target = roi.computeIndex(0, column); - if (internal.includes(data[target])) { - let id = data[target + roiMap.width]; - if (!internal.includes(id) && !roi.boxIDs.includes(id)) { - internal.push(id); - } - } - } - } - - let array = new Array(4); - for (let column = 1; column < roi.width - 1; column++) { - for (let row = 1; row < roi.height - 1; row++) { - let target = roi.computeIndex(row, column); - if (internal.includes(data[target])) { - // we check if one of the neighbour is not yet in - - array[0] = data[target - 1]; - array[1] = data[target + 1]; - array[2] = data[target - roiMap.width]; - array[3] = data[target + roiMap.width]; - - for (let i = 0; i < 4; i++) { - let id = array[i]; - if (!internal.includes(id) && !roi.boxIDs.includes(id)) { - internal.push(id); - } - } - } - } - } - - return internal; -} - -function getBoxIDs(roi: Roi): number[] { - let surroundingIDs = new Set(); // allows to get a unique list without indexOf - - const roiMap = roi.getMap(); - const data = roiMap.data; - - // we check the first line and the last line - for (let row of [0, roi.height - 1]) { - for (let column = 0; column < roi.width; column++) { - let target = roi.computeIndex(row, column); - if ( - column - roi.origin.column > 0 && - data[target] === roi.id && - data[target - 1] !== roi.id - ) { - let value = data[target - 1]; - surroundingIDs.add(value); - } - if ( - roiMap.width - column - roi.origin.column > 1 && - data[target] === roi.id && - data[target + 1] !== roi.id - ) { - let value = data[target + 1]; - surroundingIDs.add(value); - } - } - } - - // we check the first column and the last column - for (let column of [0, roi.width - 1]) { - for (let row = 0; row < roi.height; row++) { - let target = roi.computeIndex(row, column); - if ( - row - roi.origin.row > 0 && - data[target] === roi.id && - data[target - roiMap.width] !== roi.id - ) { - let value = data[target - roiMap.width]; - surroundingIDs.add(value); - } - if ( - roiMap.height - row - roi.origin.row > 1 && - data[target] === roi.id && - data[target + roiMap.width] !== roi.id - ) { - let value = data[target + roiMap.width]; - surroundingIDs.add(value); - } - } - } - - return Array.from(surroundingIDs); // the selection takes the whole rectangle -} - -/** - * - * @param roi - ROI - * @returns borders' length and their IDs - */ -function getBorders(roi: Roi): Border[] { - const roiMap = roi.getMap(); - const data = roiMap.data; - let surroudingIDs = new Set(); // allows to get a unique list without indexOf - let surroundingBorders = new Map(); - let visitedData = new Set(); - let dx = [+1, 0, -1, 0]; - let dy = [0, +1, 0, -1]; - - for ( - let column = roi.origin.column; - column <= roi.origin.column + roi.width; - column++ - ) { - for (let row = roi.origin.row; row <= roi.origin.row + roi.height; row++) { - let target = column + row * roiMap.width; - if (data[target] === roi.id) { - for (let dir = 0; dir < 4; dir++) { - let newX = column + dx[dir]; - let newY = row + dy[dir]; - if ( - newX >= 0 && - newY >= 0 && - newX < roiMap.width && - newY < roiMap.height - ) { - let neighbour = newX + newY * roiMap.width; - - if (data[neighbour] !== roi.id && !visitedData.has(neighbour)) { - visitedData.add(neighbour); - surroudingIDs.add(data[neighbour]); - let surroundingBorder = surroundingBorders.get(data[neighbour]); - if (!surroundingBorder) { - surroundingBorders.set(data[neighbour], 1); - } else { - surroundingBorders.set(data[neighbour], ++surroundingBorder); - } - } - } - } - } - } - } - let id: number[] = Array.from(surroudingIDs); - return id.map((id) => { - return { - connectedID: id, - length: surroundingBorders.get(id), - }; - }); -} - -function getEllipse(roi: Roi, scale: number): Ellipse { - const nbSD = 2; - - 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[]; - - if (eigenvalues[0] > eigenvalues[1]) { - radiusMajor = Math.sqrt(eigenvalues[0] * nbSD); - radiusMinor = Math.sqrt(eigenvalues[1] * nbSD); - vectorMajor = vectors.getColumn(0); - vectorMinor = vectors.getColumn(1); - } else if (eigenvalues[0] < eigenvalues[1]) { - radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); - radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); - vectorMajor = vectors.getColumn(1); - vectorMinor = vectors.getColumn(0); - } else { - // order here does not matter - radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); - radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); - vectorMajor = vectors.getColumn(1); - vectorMinor = vectors.getColumn(0); - } - - radiusMajor *= scale; - radiusMinor *= scale; - 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], - }; - - const majorLength = Math.sqrt( - (majorAxisPoint1.column - majorAxisPoint2.column) ** 2 + - (majorAxisPoint1.row - majorAxisPoint2.row) ** 2, - ); - const minorLength = Math.sqrt( - (minorAxisPoint1.column - majorAxisPoint2.column) ** 2 + - (minorAxisPoint1.row - minorAxisPoint2.row) ** 2, - ); - - let ellipseSurface = (((minorLength / 2) * majorLength) / 2) * Math.PI; - return { - center: { - column: xCenter, - row: yCenter, - }, - majorAxis: { - points: [majorAxisPoint1, majorAxisPoint2], - length: majorLength, - angle: toDegrees(getAngle(majorAxisPoint1, majorAxisPoint2)), - }, - minorAxis: { - points: [minorAxisPoint1, minorAxisPoint1], - length: minorLength, - angle: toDegrees(getAngle(minorAxisPoint1, minorAxisPoint2)), - }, - surface: ellipseSurface, - }; -} - diff --git a/src/roi/properties/getEllipse.ts b/src/roi/properties/getEllipse.ts new file mode 100644 index 000000000..1f892b505 --- /dev/null +++ b/src/roi/properties/getEllipse.ts @@ -0,0 +1,121 @@ +import { EigenvalueDecomposition } from 'ml-matrix'; +import { xVariance, xyCovariance } from 'ml-spectra-processing'; + +import { FeretDiameter } from '../../maskAnalysis'; +import { getAngle } from '../../maskAnalysis/utils/getAngle'; +import { toDegrees } from '../../utils/geometry/angles'; +import { Roi } from '../Roi'; + +export interface Ellipse { + center: { + column: number; + row: number; + }; + majorAxis: FeretDiameter; + minorAxis: FeretDiameter; + surface: number; +} +/** + * + * @param roi + * @param scale + */ +export function getEllipse(roi: Roi, scale: number): Ellipse { + const nbSD = 2; + + 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[]; + + if (eigenvalues[0] > eigenvalues[1]) { + radiusMajor = Math.sqrt(eigenvalues[0] * nbSD); + radiusMinor = Math.sqrt(eigenvalues[1] * nbSD); + vectorMajor = vectors.getColumn(0); + vectorMinor = vectors.getColumn(1); + } else if (eigenvalues[0] < eigenvalues[1]) { + radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); + radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); + vectorMajor = vectors.getColumn(1); + vectorMinor = vectors.getColumn(0); + } else { + // order here does not matter + radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); + radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); + vectorMajor = vectors.getColumn(1); + vectorMinor = vectors.getColumn(0); + } + + radiusMajor *= scale; + radiusMinor *= scale; + 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], + }; + + const majorLength = Math.sqrt( + (majorAxisPoint1.column - majorAxisPoint2.column) ** 2 + + (majorAxisPoint1.row - majorAxisPoint2.row) ** 2, + ); + const minorLength = Math.sqrt( + (minorAxisPoint1.column - majorAxisPoint2.column) ** 2 + + (minorAxisPoint1.row - minorAxisPoint2.row) ** 2, + ); + + let ellipseSurface = (((minorLength / 2) * majorLength) / 2) * Math.PI; + return { + center: { + column: xCenter, + row: yCenter, + }, + majorAxis: { + points: [majorAxisPoint1, majorAxisPoint2], + length: majorLength, + angle: toDegrees(getAngle(majorAxisPoint1, majorAxisPoint2)), + }, + minorAxis: { + points: [minorAxisPoint1, minorAxisPoint1], + length: minorLength, + angle: toDegrees(getAngle(minorAxisPoint1, minorAxisPoint2)), + }, + surface: ellipseSurface, + }; +} From 2319485487f106bb8fd711a8d1abbecae8009cc3 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Apr 2023 10:32:55 +0200 Subject: [PATCH 20/25] refactor: refactor conditions for eigenvalues --- src/roi/properties/getEllipse.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/roi/properties/getEllipse.ts b/src/roi/properties/getEllipse.ts index 1f892b505..63dc4455a 100644 --- a/src/roi/properties/getEllipse.ts +++ b/src/roi/properties/getEllipse.ts @@ -3,6 +3,7 @@ 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'; @@ -54,23 +55,11 @@ export function getEllipse(roi: Roi, scale: number): Ellipse { let vectorMajor: number[]; let vectorMinor: number[]; - if (eigenvalues[0] > eigenvalues[1]) { - radiusMajor = Math.sqrt(eigenvalues[0] * nbSD); - radiusMinor = Math.sqrt(eigenvalues[1] * nbSD); - vectorMajor = vectors.getColumn(0); - vectorMinor = vectors.getColumn(1); - } else if (eigenvalues[0] < eigenvalues[1]) { - radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); - radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); - vectorMajor = vectors.getColumn(1); - vectorMinor = vectors.getColumn(0); - } else { - // order here does not matter - radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); - radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); - vectorMajor = vectors.getColumn(1); - vectorMinor = vectors.getColumn(0); - } + assert(eigenvalues[0] <= eigenvalues[1]); + radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); + radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); + vectorMajor = vectors.getColumn(1); + vectorMinor = vectors.getColumn(0); radiusMajor *= scale; radiusMinor *= scale; From 0af6d0ae79cb349a8ad370fdc0aa73450a9632c7 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Tue, 25 Apr 2023 10:41:55 +0200 Subject: [PATCH 21/25] docs: add some comments about function and its parameters --- src/roi/properties/getEllipse.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/roi/properties/getEllipse.ts b/src/roi/properties/getEllipse.ts index 63dc4455a..d0e200b48 100644 --- a/src/roi/properties/getEllipse.ts +++ b/src/roi/properties/getEllipse.ts @@ -17,9 +17,11 @@ export interface Ellipse { surface: number; } /** + *Calculates ellipse on around ROI * - * @param roi - * @param scale + * @param roi - region of interest + * @param scale - the multiplier to match the surface of ellipse with the surface of ROI + * @returns Ellipse */ export function getEllipse(roi: Roi, scale: number): Ellipse { const nbSD = 2; From 8bb7bc0e95e9c1c00d1575b6c47a951c697d794b Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 26 Apr 2023 11:30:12 +0200 Subject: [PATCH 22/25] chore: scale the ellipse internally instead of calculating the ellipse, comparing it and scaling it --- src/roi/Roi.ts | 7 ++---- src/roi/properties/getEllipse.ts | 39 ++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index 1839269ae..e177a0e88 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -396,11 +396,8 @@ export class Roi { get ellipse(): Ellipse { return this.#getComputed('ellipse', () => { - let ellipse = getEllipse(this, 1); - if (ellipse.surface !== this.surface) { - const scaleFactor = Math.sqrt(this.surface / ellipse.surface); - ellipse = getEllipse(this, scaleFactor); - } + const ellipse = getEllipse(this, this.surface); + return ellipse; }); } diff --git a/src/roi/properties/getEllipse.ts b/src/roi/properties/getEllipse.ts index d0e200b48..8eedca425 100644 --- a/src/roi/properties/getEllipse.ts +++ b/src/roi/properties/getEllipse.ts @@ -20,10 +20,10 @@ export interface Ellipse { *Calculates ellipse on around ROI * * @param roi - region of interest - * @param scale - the multiplier to match the surface of ellipse with the surface of ROI + * @param surface - the surface of ROI that ellipse should match * @returns Ellipse */ -export function getEllipse(roi: Roi, scale: number): Ellipse { +export function getEllipse(roi: Roi, surface: number): Ellipse { const nbSD = 2; let xCenter = roi.centroid.column; @@ -63,8 +63,6 @@ export function getEllipse(roi: Roi, scale: number): Ellipse { vectorMajor = vectors.getColumn(1); vectorMinor = vectors.getColumn(0); - radiusMajor *= scale; - radiusMinor *= scale; let majorAxisPoint1 = { column: xCenter + radiusMajor * vectorMajor[0], row: yCenter + radiusMajor * vectorMajor[1], @@ -82,16 +80,43 @@ export function getEllipse(roi: Roi, scale: number): Ellipse { row: yCenter - radiusMinor * vectorMinor[1], }; - const majorLength = Math.sqrt( + let majorLength = Math.sqrt( (majorAxisPoint1.column - majorAxisPoint2.column) ** 2 + (majorAxisPoint1.row - majorAxisPoint2.row) ** 2, ); - const minorLength = Math.sqrt( + let minorLength = Math.sqrt( (minorAxisPoint1.column - majorAxisPoint2.column) ** 2 + (minorAxisPoint1.row - minorAxisPoint2.row) ** 2, ); let ellipseSurface = (((minorLength / 2) * majorLength) / 2) * Math.PI; + if (ellipseSurface !== surface) { + const scaleFactor = Math.sqrt(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, @@ -103,7 +128,7 @@ export function getEllipse(roi: Roi, scale: number): Ellipse { angle: toDegrees(getAngle(majorAxisPoint1, majorAxisPoint2)), }, minorAxis: { - points: [minorAxisPoint1, minorAxisPoint1], + points: [minorAxisPoint1, minorAxisPoint2], length: minorLength, angle: toDegrees(getAngle(minorAxisPoint1, minorAxisPoint2)), }, From 39b0480cf07c59242916b5f00f6ca3f170341d77 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 26 Apr 2023 11:30:45 +0200 Subject: [PATCH 23/25] test: fix minorAxis expecting result --- src/roi/__tests__/ellipse.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts index 11f4271b4..df510167d 100644 --- a/src/roi/__tests__/ellipse.test.ts +++ b/src/roi/__tests__/ellipse.test.ts @@ -10,6 +10,7 @@ test('ellipse on a small figure 3x3', () => { const rois = roiMapManager.getRois(); const result = rois[0].ellipse; + expect(result).toBeDeepCloseTo({ center: { column: 0.6666666666666666, row: 0.3333333333333333 }, majorAxis: { @@ -23,7 +24,7 @@ test('ellipse on a small figure 3x3', () => { minorAxis: { points: [ { column: 1.146541384241206, row: -0.14654138424120605 }, - { column: 1.146541384241206, row: -0.14654138424120605 }, + { column: 0.18679194909212726, row: 0.8132080509078727 }, ], length: 1.6247924149339357, angle: 135, @@ -55,7 +56,7 @@ test('ellipse on 3x3 cross', () => { minorAxis: { points: [ { column: 2.7841241161527712, row: 1 }, - { column: 2.7841241161527712, row: 1 }, + { column: -0.7841241161527714, row: 1 }, ], length: 1.7841241161527712, angle: 180, @@ -86,7 +87,7 @@ test('ellipse on slightly changed 3x3 cross', () => { minorAxis: { points: [ { column: -0.15768891509232064, row: 1.8243555817589874 }, - { column: -0.15768891509232064, row: 1.8243555817589874 }, + { column: 1.8243555817589874, row: -0.15768891509232064 }, ], length: 2.01285390103734, angle: -45, @@ -105,7 +106,6 @@ test('ellipse on 4x4 ROI', () => { const rois = roiMapManager.getRois(); const result = rois[0].ellipse; - expect(result).toBeDeepCloseTo({ center: { column: 1.7777777777777777, row: 1.7777777777777777 }, majorAxis: { @@ -119,7 +119,7 @@ test('ellipse on 4x4 ROI', () => { minorAxis: { points: [ { column: 0.8646151602330147, row: 1.1403989822180598 }, - { column: 0.8646151602330147, row: 1.1403989822180598 }, + { column: 2.690940395322541, row: 2.4151565733374953 }, ], length: 2.544387508597132, angle: 34.91467329407479, From beb23d9bd40fee1b99dc0d5ef07a13e7e98ec9e6 Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 26 Apr 2023 12:43:09 +0200 Subject: [PATCH 24/25] chore: remove surface as parameter and nbSD since no longer used --- src/roi/Roi.ts | 3 +-- src/roi/properties/getEllipse.ts | 12 +++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/roi/Roi.ts b/src/roi/Roi.ts index e177a0e88..8ec3ca6ee 100644 --- a/src/roi/Roi.ts +++ b/src/roi/Roi.ts @@ -396,8 +396,7 @@ export class Roi { get ellipse(): Ellipse { return this.#getComputed('ellipse', () => { - const ellipse = getEllipse(this, this.surface); - + const ellipse = getEllipse(this); return ellipse; }); } diff --git a/src/roi/properties/getEllipse.ts b/src/roi/properties/getEllipse.ts index 8eedca425..a3caaf964 100644 --- a/src/roi/properties/getEllipse.ts +++ b/src/roi/properties/getEllipse.ts @@ -23,9 +23,7 @@ export interface Ellipse { * @param surface - the surface of ROI that ellipse should match * @returns Ellipse */ -export function getEllipse(roi: Roi, surface: number): Ellipse { - const nbSD = 2; - +export function getEllipse(roi: Roi): Ellipse { let xCenter = roi.centroid.column; let yCenter = roi.centroid.row; @@ -58,8 +56,8 @@ export function getEllipse(roi: Roi, surface: number): Ellipse { let vectorMinor: number[]; assert(eigenvalues[0] <= eigenvalues[1]); - radiusMajor = Math.sqrt(eigenvalues[1] * nbSD); - radiusMinor = Math.sqrt(eigenvalues[0] * nbSD); + radiusMajor = Math.sqrt(eigenvalues[1]); + radiusMinor = Math.sqrt(eigenvalues[0]); vectorMajor = vectors.getColumn(1); vectorMinor = vectors.getColumn(0); @@ -90,8 +88,8 @@ export function getEllipse(roi: Roi, surface: number): Ellipse { ); let ellipseSurface = (((minorLength / 2) * majorLength) / 2) * Math.PI; - if (ellipseSurface !== surface) { - const scaleFactor = Math.sqrt(surface / ellipseSurface); + if (ellipseSurface !== roi.surface) { + const scaleFactor = Math.sqrt(roi.surface / ellipseSurface); radiusMajor *= scaleFactor; radiusMinor *= scaleFactor; majorAxisPoint1 = { From 3fdca166d9b2c486506793abe5e3fc69c5c38c1d Mon Sep 17 00:00:00 2001 From: EscapedGibbon Date: Wed, 26 Apr 2023 13:09:53 +0200 Subject: [PATCH 25/25] test: add testing case for 1 1 1 mask --- src/roi/__tests__/ellipse.test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/roi/__tests__/ellipse.test.ts b/src/roi/__tests__/ellipse.test.ts index df510167d..0ab0a142a 100644 --- a/src/roi/__tests__/ellipse.test.ts +++ b/src/roi/__tests__/ellipse.test.ts @@ -1,5 +1,36 @@ 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],