Skip to content

Commit f7495ab

Browse files
authored
IDC2345: add tollerance parameter to generateToolState (#200)
* fix: 🐛 IDC2345: add tollerance parameter to generateToolState and use 1.e-3 as default * fix: 🐛 implement better floating point rounding checks * fix: 🐛 move nearlyEqual into utils and add explicit copyrights
1 parent 00c2b39 commit f7495ab

File tree

3 files changed

+113
-54
lines changed

3 files changed

+113
-54
lines changed

src/adapters/Cornerstone/Segmentation_4X.js

Lines changed: 78 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
rotateDirectionCosinesInPlane,
1212
flipImageOrientationPatient as flipIOP,
1313
flipMatrix2D,
14-
rotateMatrix902D
14+
rotateMatrix902D,
15+
nearlyEqual
1516
} from "../../utilities/orientation/index.js";
1617
import {
1718
encode,
@@ -258,9 +259,10 @@ function _createSegFromImages(images, isMultiframe, options) {
258259
* generateToolState - Given a set of cornrstoneTools imageIds and a Segmentation buffer,
259260
* derive cornerstoneTools toolState and brush metadata.
260261
*
261-
* @param {string[]} imageIds An array of the imageIds.
262-
* @param {ArrayBuffer} arrayBuffer The SEG arrayBuffer.
263-
* @param {*} metadataProvider
262+
* @param {string[]} imageIds - An array of the imageIds.
263+
* @param {ArrayBuffer} arrayBuffer - The SEG arrayBuffer.
264+
* @param {*} metadataProvider.
265+
* @param {number} tolerance - checks tolerance, default value 1.e-3.
264266
*
265267
* @return {[]ArrayBuffer}a list of array buffer for each labelMap
266268
* @return {Object} an object from which the segment metadata can be derived
@@ -272,7 +274,8 @@ function generateToolState(
272274
imageIds,
273275
arrayBuffer,
274276
metadataProvider,
275-
skipOverlapping = false
277+
skipOverlapping = false,
278+
tolerance = 1e-3
276279
) {
277280
const dicomData = DicomMessage.readFile(arrayBuffer);
278281
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
@@ -340,11 +343,12 @@ function generateToolState(
340343
}
341344
}
342345

343-
const orientation = checkOrientation(multiframe, validOrientations, [
344-
imagePlaneModule.rows,
345-
imagePlaneModule.columns,
346-
imageIds.length
347-
]);
346+
const orientation = checkOrientation(
347+
multiframe,
348+
validOrientations,
349+
[imagePlaneModule.rows, imagePlaneModule.columns, imageIds.length],
350+
tolerance
351+
);
348352

349353
let overlapping = false;
350354
if (!skipOverlapping) {
@@ -353,7 +357,8 @@ function generateToolState(
353357
multiframe,
354358
imageIds,
355359
validOrientations,
356-
metadataProvider
360+
metadataProvider,
361+
tolerance
357362
);
358363
}
359364

@@ -401,7 +406,8 @@ function generateToolState(
401406
multiframe,
402407
imageIds,
403408
validOrientations,
404-
metadataProvider
409+
metadataProvider,
410+
tolerance
405411
);
406412

407413
return {
@@ -617,7 +623,8 @@ function checkSEGsOverlapping(
617623
multiframe,
618624
imageIds,
619625
validOrientations,
620-
metadataProvider
626+
metadataProvider,
627+
tolerance
621628
) {
622629
const {
623630
SharedFunctionalGroupsSequence,
@@ -678,7 +685,8 @@ function checkSEGsOverlapping(
678685
const alignedPixelDataI = alignPixelDataWithSourceData(
679686
pixelDataI2D,
680687
ImageOrientationPatientI,
681-
validOrientations
688+
validOrientations,
689+
tolerance
682690
);
683691

684692
if (!alignedPixelDataI) {
@@ -777,7 +785,8 @@ function insertOverlappingPixelDataPlanar(
777785
multiframe,
778786
imageIds,
779787
validOrientations,
780-
metadataProvider
788+
metadataProvider,
789+
tolerance
781790
) {
782791
const {
783792
SharedFunctionalGroupsSequence,
@@ -850,7 +859,8 @@ function insertOverlappingPixelDataPlanar(
850859
const alignedPixelDataI = alignPixelDataWithSourceData(
851860
pixelDataI2D,
852861
ImageOrientationPatientI,
853-
validOrientations
862+
validOrientations,
863+
tolerance
854864
);
855865

856866
if (!alignedPixelDataI) {
@@ -992,7 +1002,8 @@ function insertPixelDataPlanar(
9921002
multiframe,
9931003
imageIds,
9941004
validOrientations,
995-
metadataProvider
1005+
metadataProvider,
1006+
tolerance
9961007
) {
9971008
const {
9981009
SharedFunctionalGroupsSequence,
@@ -1027,7 +1038,8 @@ function insertPixelDataPlanar(
10271038
const alignedPixelDataI = alignPixelDataWithSourceData(
10281039
pixelDataI2D,
10291040
ImageOrientationPatientI,
1030-
validOrientations
1041+
validOrientations,
1042+
tolerance
10311043
);
10321044

10331045
if (!alignedPixelDataI) {
@@ -1119,7 +1131,12 @@ function insertPixelDataPlanar(
11191131
}
11201132
}
11211133

1122-
function checkOrientation(multiframe, validOrientations, sourceDataDimensions) {
1134+
function checkOrientation(
1135+
multiframe,
1136+
validOrientations,
1137+
sourceDataDimensions,
1138+
tolerance
1139+
) {
11231140
const {
11241141
SharedFunctionalGroupsSequence,
11251142
PerFrameFunctionalGroupsSequence
@@ -1139,15 +1156,15 @@ function checkOrientation(multiframe, validOrientations, sourceDataDimensions) {
11391156
.ImageOrientationPatient;
11401157

11411158
const inPlane = validOrientations.some(operation =>
1142-
compareIOP(iop, operation)
1159+
compareIOP(iop, operation, tolerance)
11431160
);
11441161

11451162
if (inPlane) {
11461163
return "Planar";
11471164
}
11481165

11491166
if (
1150-
checkIfPerpendicular(iop, validOrientations[0]) &&
1167+
checkIfPerpendicular(iop, validOrientations[0], tolerance) &&
11511168
sourceDataDimensions.includes(multiframe.Rows) &&
11521169
sourceDataDimensions.includes(multiframe.Rows)
11531170
) {
@@ -1159,14 +1176,15 @@ function checkOrientation(multiframe, validOrientations, sourceDataDimensions) {
11591176
}
11601177

11611178
/**
1162-
* compareIOP - Returns true if iop1 and iop2 are equal
1163-
* within a tollerance, dx.
1179+
* checkIfPerpendicular - Returns true if iop1 and iop2 are perpendicular
1180+
* within a tolerance.
11641181
*
11651182
* @param {Number[6]} iop1 An ImageOrientationPatient array.
11661183
* @param {Number[6]} iop2 An ImageOrientationPatient array.
1167-
* @return {Boolean} True if iop1 and iop2 are equal.
1184+
* @param {Number} tolerance.
1185+
* @return {Boolean} True if iop1 and iop2 are equal.
11681186
*/
1169-
function checkIfPerpendicular(iop1, iop2) {
1187+
function checkIfPerpendicular(iop1, iop2, tolerance) {
11701188
const absDotColumnCosines = Math.abs(
11711189
iop1[0] * iop2[0] + iop1[1] * iop2[1] + iop1[2] * iop2[2]
11721190
);
@@ -1175,8 +1193,10 @@ function checkIfPerpendicular(iop1, iop2) {
11751193
);
11761194

11771195
return (
1178-
(absDotColumnCosines < dx || Math.abs(absDotColumnCosines - 1) < dx) &&
1179-
(absDotRowCosines < dx || Math.abs(absDotRowCosines - 1) < dx)
1196+
(absDotColumnCosines < tolerance ||
1197+
Math.abs(absDotColumnCosines - 1) < tolerance) &&
1198+
(absDotRowCosines < tolerance ||
1199+
Math.abs(absDotRowCosines - 1) < tolerance)
11801200
);
11811201
}
11821202

@@ -1343,44 +1363,50 @@ function getValidOrientations(iop) {
13431363
/**
13441364
* alignPixelDataWithSourceData -
13451365
*
1346-
* @param {Ndarray} pixelData2D The data to align.
1347-
* @param {Number[6]} iop The orientation of the image slice.
1348-
* @param {Number[8][6]} orientations An array of valid imageOrientationPatient values.
1349-
* @return {Ndarray} The aligned pixelData.
1366+
* @param {Ndarray} pixelData2D - The data to align.
1367+
* @param {Number[6]} iop - The orientation of the image slice.
1368+
* @param {Number[8][6]} orientations - An array of valid imageOrientationPatient values.
1369+
* @param {Number} tolerance.
1370+
* @return {Ndarray} The aligned pixelData.
13501371
*/
1351-
function alignPixelDataWithSourceData(pixelData2D, iop, orientations) {
1352-
if (compareIOP(iop, orientations[0])) {
1372+
function alignPixelDataWithSourceData(
1373+
pixelData2D,
1374+
iop,
1375+
orientations,
1376+
tolerance
1377+
) {
1378+
if (compareIOP(iop, orientations[0], tolerance)) {
13531379
return pixelData2D;
1354-
} else if (compareIOP(iop, orientations[1])) {
1380+
} else if (compareIOP(iop, orientations[1], tolerance)) {
13551381
// Flipped vertically.
13561382

13571383
// Undo Flip
13581384
return flipMatrix2D.v(pixelData2D);
1359-
} else if (compareIOP(iop, orientations[2])) {
1385+
} else if (compareIOP(iop, orientations[2], tolerance)) {
13601386
// Flipped horizontally.
13611387

13621388
// Unfo flip
13631389
return flipMatrix2D.h(pixelData2D);
1364-
} else if (compareIOP(iop, orientations[3])) {
1390+
} else if (compareIOP(iop, orientations[3], tolerance)) {
13651391
//Rotated 90 degrees
13661392

13671393
// Rotate back
13681394
return rotateMatrix902D(pixelData2D);
1369-
} else if (compareIOP(iop, orientations[4])) {
1395+
} else if (compareIOP(iop, orientations[4], tolerance)) {
13701396
//Rotated 90 degrees and fliped horizontally.
13711397

13721398
// Undo flip and rotate back.
13731399
return rotateMatrix902D(flipMatrix2D.h(pixelData2D));
1374-
} else if (compareIOP(iop, orientations[5])) {
1400+
} else if (compareIOP(iop, orientations[5], tolerance)) {
13751401
// Rotated 90 degrees and fliped vertically
13761402

13771403
// Unfo flip and rotate back.
13781404
return rotateMatrix902D(flipMatrix2D.v(pixelData2D));
1379-
} else if (compareIOP(iop, orientations[6])) {
1405+
} else if (compareIOP(iop, orientations[6], tolerance)) {
13801406
// Rotated 180 degrees. // TODO -> Do this more effeciently, there is a 1:1 mapping like 90 degree rotation.
13811407

13821408
return rotateMatrix902D(rotateMatrix902D(pixelData2D));
1383-
} else if (compareIOP(iop, orientations[7])) {
1409+
} else if (compareIOP(iop, orientations[7], tolerance)) {
13841410
// Rotated 270 degrees
13851411

13861412
// Rotate back.
@@ -1390,24 +1416,23 @@ function alignPixelDataWithSourceData(pixelData2D, iop, orientations) {
13901416
}
13911417
}
13921418

1393-
const dx = 1e-5;
1394-
13951419
/**
13961420
* compareIOP - Returns true if iop1 and iop2 are equal
1397-
* within a tollerance, dx.
1421+
* within a tolerance.
13981422
*
1399-
* @param {Number[6]} iop1 An ImageOrientationPatient array.
1400-
* @param {Number[6]} iop2 An ImageOrientationPatient array.
1401-
* @return {Boolean} True if iop1 and iop2 are equal.
1423+
* @param {Number[6]} iop1 - An ImageOrientationPatient array.
1424+
* @param {Number[6]} iop2 - An ImageOrientationPatient array.
1425+
* @param {Number} tolerance.
1426+
* @return {Boolean} True if iop1 and iop2 are equal.
14021427
*/
1403-
function compareIOP(iop1, iop2) {
1428+
function compareIOP(iop1, iop2, tolerance) {
14041429
return (
1405-
Math.abs(iop1[0] - iop2[0]) < dx &&
1406-
Math.abs(iop1[1] - iop2[1]) < dx &&
1407-
Math.abs(iop1[2] - iop2[2]) < dx &&
1408-
Math.abs(iop1[3] - iop2[3]) < dx &&
1409-
Math.abs(iop1[4] - iop2[4]) < dx &&
1410-
Math.abs(iop1[5] - iop2[5]) < dx
1430+
nearlyEqual(iop1[0], iop2[0], tolerance) &&
1431+
nearlyEqual(iop1[1], iop2[1], tolerance) &&
1432+
nearlyEqual(iop1[2], iop2[2], tolerance) &&
1433+
nearlyEqual(iop1[3], iop2[3], tolerance) &&
1434+
nearlyEqual(iop1[4], iop2[4], tolerance) &&
1435+
nearlyEqual(iop1[5], iop2[5], tolerance)
14111436
);
14121437
}
14131438

src/utilities/orientation/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import rotateDirectionCosinesInPlane from "./rotateDirectionCosinesInPlane.js";
44
import rotateVectorAroundUnitVector from "./rotateVectorAroundUnitVector.js";
55
import { flipMatrix2D } from "./flipMatrix2D.js";
66
import rotateMatrix902D from "./rotateMatrix902D.js";
7+
import nearlyEqual from "./nearlyEqual.js";
78

89
export {
910
crossProduct3D,
1011
flipImageOrientationPatient,
1112
rotateDirectionCosinesInPlane,
1213
rotateVectorAroundUnitVector,
1314
flipMatrix2D,
14-
rotateMatrix902D
15+
rotateMatrix902D,
16+
nearlyEqual
1517
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* nearlyEqual - Returns true if a and b are nearly equal
3+
* within a tolerance.
4+
*
5+
* This function logic source comes from:
6+
* https://floating-point-gui.de/errors/comparison/
7+
*
8+
* https://floating-point-gui.de is published under
9+
* the Creative Commons Attribution License (BY):
10+
* http://creativecommons.org/licenses/by/3.0/
11+
*
12+
* @param {Number} a
13+
* @param {Number} b
14+
* @param {Number} tolerance.
15+
* @return {Boolean} True if a and b are nearly equal.
16+
*/
17+
export default function nearlyEqual(a, b, epsilon) {
18+
const absA = Math.abs(a);
19+
const absB = Math.abs(b);
20+
const diff = Math.abs(a - b);
21+
if (a == b) {
22+
// shortcut, handles infinities
23+
return true;
24+
} else if (a == 0 || b == 0 || absA + absB < Number.EPSILON) {
25+
// a or b is zero or both are extremely close to it
26+
// relative error is less meaningful here
27+
return diff < epsilon * Number.EPSILON;
28+
} else {
29+
// use relative error
30+
return diff / Math.min(absA + absB, Number.MAX_VALUE) < epsilon;
31+
}
32+
}

0 commit comments

Comments
 (0)