diff --git a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts index 42cb9a2f84..b86fb92ee1 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts @@ -1,9 +1,12 @@ import { utilities } from 'dcmjs'; import MeasurementReport from './MeasurementReport'; +import { utilities as csUtilities } from '@cornerstonejs/core'; import { scoordToWorld, toScoord, toArray } from '../helpers'; + import BaseAdapter3D from './BaseAdapter3D'; const { Bidirectional: TID300Bidirectional } = utilities.TID300; +const { toFiniteNumber } = csUtilities; const LONG_AXIS = 'Long Axis'; const SHORT_AXIS = 'Short Axis'; @@ -137,8 +140,8 @@ class Bidirectional extends BaseAdapter3D { point1: shortAxisStartImage, point2: shortAxisEndImage, }, - longAxisLength: length, - shortAxisLength: width, + longAxisLength: toFiniteNumber(length), + shortAxisLength: toFiniteNumber(width), unit, trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding: finding, diff --git a/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts b/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts index 422af6b36b..1138f0bfaf 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts @@ -1,10 +1,13 @@ import { utilities } from 'dcmjs'; +import { utilities as csUtilities } from '@cornerstonejs/core'; import MeasurementReport from './MeasurementReport'; import BaseAdapter3D from './BaseAdapter3D'; import { toScoord } from '../helpers'; + import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler'; const { Circle: TID300Circle } = utilities.TID300; +const { toFiniteNumber } = csUtilities; class CircleROI extends BaseAdapter3D { static { @@ -93,16 +96,16 @@ class CircleROI extends BaseAdapter3D { const perimeter = 2 * Math.PI * radius; return { - area, + area: toFiniteNumber(area), areaUnit, - perimeter, + perimeter: toFiniteNumber(perimeter), modalityUnit, radiusUnit, - radius, - max, - min, - stdDev, - mean, + radius: toFiniteNumber(radius), + max: toFiniteNumber(max), + min: toFiniteNumber(min), + stdDev: toFiniteNumber(stdDev), + mean: toFiniteNumber(mean), points: [center, end], trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, diff --git a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts index c31df8e57a..36eb6dd6d7 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts @@ -3,10 +3,13 @@ import { utilities } from 'dcmjs'; import MeasurementReport from './MeasurementReport'; import BaseAdapter3D from './BaseAdapter3D'; +import { utilities as csUtilities } from '@cornerstonejs/core'; import { toScoord } from '../helpers'; + import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler'; const { Ellipse: TID300Ellipse } = utilities.TID300; +const { toFiniteNumber } = csUtilities; const EPSILON = 1e-4; @@ -109,12 +112,12 @@ class EllipticalROI extends BaseAdapter3D { const convertedPoints = points.map((point) => toScoord(scoordProps, point)); return { - area, + area: toFiniteNumber(area), + max: toFiniteNumber(max), + min: toFiniteNumber(min), + mean: toFiniteNumber(mean), + stdDev: toFiniteNumber(stdDev), areaUnit, - max, - min, - mean, - stdDev, modalityUnit, points: convertedPoints, trackingIdentifierTextValue: this.trackingIdentifierTextValue, diff --git a/packages/adapters/src/adapters/Cornerstone3D/Length.ts b/packages/adapters/src/adapters/Cornerstone3D/Length.ts index e176a2ec23..3a5d1b1fb8 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/Length.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/Length.ts @@ -1,9 +1,11 @@ import { utilities } from 'dcmjs'; import MeasurementReport from './MeasurementReport'; import BaseAdapter3D from './BaseAdapter3D'; +import { utilities as csUtilities } from '@cornerstonejs/core'; import { toScoord } from '../helpers'; const { Length: TID300Length } = utilities.TID300; +const { toFiniteNumber } = csUtilities; const LENGTH = 'Length'; @@ -76,7 +78,7 @@ export default class Length extends BaseAdapter3D { return { point1, point2, - distance, + distance: toFiniteNumber(distance), trackingIdentifierTextValue: this.trackingIdentifierTextValue, finding, findingSites: findingSites || [], diff --git a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts index 592815cd6a..f5df3accfd 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/PlanarFreehandROI.ts @@ -1,5 +1,6 @@ import MeasurementReport from './MeasurementReport'; import { utilities } from 'dcmjs'; +import { utilities as csUtilities } from '@cornerstonejs/core'; import { vec3 } from 'gl-matrix'; import BaseAdapter3D from './BaseAdapter3D'; import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler'; @@ -7,6 +8,8 @@ import { toScoords, toArray } from '../helpers'; import ControlPointPolyline from './ControlPointPolyline'; import { SPLINE_TYPE_CODE } from './constants'; +const { toFiniteNumber } = csUtilities; + /** Contour/polyline SR logic is shared by LivewireContour, registered as a subtype. */ class PlanarFreehandROI extends BaseAdapter3D { public static closedContourThreshold = 1e-5; @@ -144,13 +147,13 @@ class PlanarFreehandROI extends BaseAdapter3D { /** From cachedStats */ points, controlPoints, - area, + area: toFiniteNumber(area), areaUnit, - perimeter: perimeter ?? length, + perimeter: toFiniteNumber(perimeter ?? length), modalityUnit, - mean, - max, - stdDev, + mean: toFiniteNumber(mean), + max: toFiniteNumber(max), + stdDev: toFiniteNumber(stdDev), /** Other */ splineType: data.spline?.type, trackingIdentifierTextValue: this.trackingIdentifierTextValue, diff --git a/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts b/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts index 88b2413975..0a5544df13 100644 --- a/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts +++ b/packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts @@ -1,12 +1,15 @@ import { utilities } from 'dcmjs'; +import { utilities as csUtilities } from '@cornerstonejs/core'; import { toScoords } from '../helpers'; + import MeasurementReport from './MeasurementReport'; import BaseAdapter3D from './BaseAdapter3D'; import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler'; import { mapUnitFromUCUM } from './unitMapper'; const { Polyline: TID300Polyline } = utilities.TID300; +const { toFiniteNumber } = csUtilities; export class RectangleROI extends BaseAdapter3D { static { @@ -102,11 +105,11 @@ export class RectangleROI extends BaseAdapter3D { return { points: [corners[0], corners[1], corners[3], corners[2], corners[0]], - area, - perimeter, - max, - mean, - stdDev, + area: toFiniteNumber(area), + perimeter: toFiniteNumber(perimeter), + max: toFiniteNumber(max), + mean: toFiniteNumber(mean), + stdDev: toFiniteNumber(stdDev), areaUnit, modalityUnit, trackingIdentifierTextValue: this.trackingIdentifierTextValue, diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index f4cb1e1600..57f2bb14ab 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -107,6 +107,7 @@ export * from './getPixelSpacingInformation'; export * from './getPlaneCubeIntersectionDimensions'; export * from './rotateToViewCoordinates'; import { asArray } from './asArray'; +import { toFiniteNumber } from './toNumber'; import { getNormalizedAspectRatio } from './getNormalizedAspectRatio'; export { updatePlaneRestriction } from './updatePlaneRestriction'; @@ -215,5 +216,6 @@ export { buildMetadata, calculateNeighborhoodStats, asArray, + toFiniteNumber, getNormalizedAspectRatio, }; diff --git a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts index 2f3848c416..2df8e708c6 100644 --- a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts +++ b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts @@ -960,16 +960,44 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { let intersections = []; let intersectionCounter = 0; + const { viewPlaneNormal } = viewport.getCamera(); + + /** + * Detect if the current viewport orientation is oblique. + * For oblique planes, the worldToCanvas transformation can produce + * slightly different floating-point Y values for points that belong + * to the same scanline due to floating-point precision and projection. + * + * This causes the scanline intersection logic to reset frequently, + * resulting in missing voxels and incorrect min/max/density statistics. + * + * To stabilize scanline detection, we introduce a tolerance (rowDelta) + * when comparing the current canvas Y coordinate with the previous row. + * + * - For orthogonal views, no tolerance is required (rowDelta = 0). + * - For oblique views, we allow a small half-pixel tolerance (0.5) + * so that points with very small floating-point differences are + * treated as belonging to the same scanline. + */ + const TOLERANCE = 0.5; + + const isOblique = + viewPlaneNormal.filter((c) => Math.abs(c) > EPSILON).length > 1; + + const rowDelta = isOblique ? TOLERANCE : 0; + let pointsInShape; + if (voxelManager) { pointsInShape = voxelManager.forEach( this.configuration.statsCalculator.statsCallback, { imageData, isInObject: (pointLPS, _pointIJK) => { - let result = true; const point = viewport.worldToCanvas(pointLPS); - if (point[1] != curRow) { + // Use tolerance-based comparison to avoid scanline resets caused + // by floating-point precision differences in oblique projections. + if (Math.abs(point[1] - curRow) > rowDelta) { intersectionCounter = 0; curRow = point[1]; intersections = getLineSegmentIntersectionsCoordinates( @@ -993,10 +1021,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { intersections.shift(); intersectionCounter++; } - if (intersectionCounter % 2 === 0) { - result = false; - } - return result; + return intersectionCounter % 2 === 1; }, boundsIJK, returnPoints: this.configuration.storePointData,