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
7 changes: 5 additions & 2 deletions packages/adapters/src/adapters/Cornerstone3D/Bidirectional.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 10 additions & 7 deletions packages/adapters/src/adapters/Cornerstone3D/CircleROI.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 8 additions & 5 deletions packages/adapters/src/adapters/Cornerstone3D/EllipticalROI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/adapters/src/adapters/Cornerstone3D/Length.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -76,7 +78,7 @@ export default class Length extends BaseAdapter3D {
return {
point1,
point2,
distance,
distance: toFiniteNumber(distance),
trackingIdentifierTextValue: this.trackingIdentifierTextValue,
finding,
findingSites: findingSites || [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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';
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;
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 8 additions & 5 deletions packages/adapters/src/adapters/Cornerstone3D/RectangleROI.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -215,5 +216,6 @@ export {
buildMetadata,
calculateNeighborhoodStats,
asArray,
toFiniteNumber,
getNormalizedAspectRatio,
};
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 @@ -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(
Expand All @@ -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,
Expand Down
Loading