Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 8 additions & 3 deletions packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { utilities } from 'dcmjs';
import MeasurementReport from './MeasurementReport';
import { scoordToWorld, toScoord, toArray } from '../helpers';
import {
scoordToWorld,
toScoord,
toArray,
validateNumericValue,
} from '../helpers';
import BaseAdapter3D from './BaseAdapter3D';

const { Bidirectional: TID300Bidirectional } = utilities.TID300;
Expand Down Expand Up @@ -137,8 +142,8 @@ class Bidirectional extends BaseAdapter3D {
point1: shortAxisStartImage,
point2: shortAxisEndImage,
},
longAxisLength: length,
shortAxisLength: width,
longAxisLength: validateNumericValue(length),
shortAxisLength: validateNumericValue(width),
unit,
trackingIdentifierTextValue: this.trackingIdentifierTextValue,
finding: finding,
Expand Down
16 changes: 8 additions & 8 deletions packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { utilities } from 'dcmjs';
import MeasurementReport from './MeasurementReport';
import BaseAdapter3D from './BaseAdapter3D';
import { toScoord } from '../helpers';
import { toScoord, validateNumericValue } from '../helpers';
import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler';

const { Circle: TID300Circle } = utilities.TID300;
Expand Down Expand Up @@ -93,16 +93,16 @@ class CircleROI extends BaseAdapter3D {
const perimeter = 2 * Math.PI * radius;

return {
area,
area: validateNumericValue(area),
areaUnit,
perimeter,
perimeter: validateNumericValue(perimeter),
modalityUnit,
radiusUnit,
radius,
max,
min,
stdDev,
mean,
radius: validateNumericValue(radius),
max: validateNumericValue(max),
min: validateNumericValue(min),
stdDev: validateNumericValue(stdDev),
mean: validateNumericValue(mean),
points: [center, end],
trackingIdentifierTextValue: this.trackingIdentifierTextValue,
finding,
Expand Down
12 changes: 6 additions & 6 deletions packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { utilities } from 'dcmjs';

import MeasurementReport from './MeasurementReport';
import BaseAdapter3D from './BaseAdapter3D';
import { toScoord } from '../helpers';
import { toScoord, validateNumericValue } from '../helpers';
import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler';

const { Ellipse: TID300Ellipse } = utilities.TID300;
Expand Down Expand Up @@ -109,12 +109,12 @@ class EllipticalROI extends BaseAdapter3D {
const convertedPoints = points.map((point) => toScoord(scoordProps, point));

return {
area,
area: validateNumericValue(area),
areaUnit,
max,
min,
mean,
stdDev,
max: validateNumericValue(max),
min: validateNumericValue(min),
mean: validateNumericValue(mean),
stdDev: validateNumericValue(stdDev),
modalityUnit,
points: convertedPoints,
trackingIdentifierTextValue: this.trackingIdentifierTextValue,
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/src/adapters/Cornerstone3D/Length.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { utilities } from 'dcmjs';
import MeasurementReport from './MeasurementReport';
import BaseAdapter3D from './BaseAdapter3D';
import { toScoord } from '../helpers';
import { toScoord, validateNumericValue } from '../helpers';

const { Length: TID300Length } = utilities.TID300;

Expand Down Expand Up @@ -76,7 +76,7 @@ export default class Length extends BaseAdapter3D {
return {
point1,
point2,
distance,
distance: validateNumericValue(distance),
trackingIdentifierTextValue: this.trackingIdentifierTextValue,
finding,
findingSites: findingSites || [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { utilities } from 'dcmjs';
import { vec3 } from 'gl-matrix';
import BaseAdapter3D from './BaseAdapter3D';
import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler';
import { toScoords, toArray } from '../helpers';
import { toScoords, toArray, validateNumericValue } from '../helpers';
import ControlPointPolyline from './ControlPointPolyline';
import { SPLINE_TYPE_CODE } from './constants';

Expand Down Expand Up @@ -142,13 +142,13 @@ class PlanarFreehandROI extends BaseAdapter3D {
/** From cachedStats */
points,
controlPoints,
area,
area: validateNumericValue(area),
areaUnit,
perimeter: perimeter ?? length,
perimeter: validateNumericValue(perimeter ?? length),
modalityUnit,
mean,
max,
stdDev,
mean: validateNumericValue(mean),
max: validateNumericValue(max),
stdDev: validateNumericValue(stdDev),
/** Other */
splineType: data.spline?.type,
trackingIdentifierTextValue: this.trackingIdentifierTextValue,
Expand Down
12 changes: 6 additions & 6 deletions packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { utilities } from 'dcmjs';

import { toScoords } from '../helpers';
import { toScoords, validateNumericValue } from '../helpers';
import MeasurementReport from './MeasurementReport';
import BaseAdapter3D from './BaseAdapter3D';
import { extractAllNUMGroups, restoreAdditionalMetrics } from './metricHandler';
Expand Down Expand Up @@ -95,11 +95,11 @@ export class RectangleROI extends BaseAdapter3D {

return {
points: [corners[0], corners[1], corners[3], corners[2], corners[0]],
area,
perimeter,
max,
mean,
stdDev,
area: validateNumericValue(area),
perimeter: validateNumericValue(perimeter),
max: validateNumericValue(max),
mean: validateNumericValue(mean),
stdDev: validateNumericValue(stdDev),
areaUnit,
modalityUnit,
trackingIdentifierTextValue: this.trackingIdentifierTextValue,
Expand Down
10 changes: 9 additions & 1 deletion packages/adapters/src/adapters/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ import { toArray } from './toArray';
import { codeMeaningEquals } from './codeMeaningEquals';
import { graphicTypeEquals } from './graphicTypeEquals';
import { downloadDICOMData } from './downloadDICOMData';
import { validateNumericValue } from './validateNumericValue';

export { copyStudyTags } from './copyStudyTags';
export { copySeriesTags } from './copySeriesTags';

export * from './toScoordType';
export * from './scoordToWorld';
export * from './toPoint3';

export { toArray, codeMeaningEquals, graphicTypeEquals, downloadDICOMData };
export {
toArray,
codeMeaningEquals,
graphicTypeEquals,
downloadDICOMData,
validateNumericValue,
};
14 changes: 14 additions & 0 deletions packages/adapters/src/adapters/helpers/validateNumericValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Validates a numeric value for DICOM SR NumericValue field (VR: DS).
* DS (Decimal String) does not support Infinity, -Infinity, or NaN.
*
* @param value - The numeric value to validate
* @returns The value if valid, undefined if invalid (Infinity, -Infinity, NaN, null, or undefined)
*/
export function validateNumericValue(
value: number | null | undefined
): number | undefined {
return typeof value === 'number' && Number.isFinite(value)
? value
: undefined;
}
37 changes: 31 additions & 6 deletions packages/tools/src/tools/annotation/PlanarFreehandROITool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,16 +961,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(
Expand All @@ -994,10 +1022,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,
Expand Down
Loading