Skip to content

fix CircleROI tool for wsi #1940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b9c9f50
check viewport type for voxel stats
mbellehumeur Mar 26, 2025
ca3fdb8
check imageData.getScalar() instead of viewport type
mbellehumeur Mar 27, 2025
3c6e74a
This will be fixed in wsiviewport worldToCanvas func
mbellehumeur Mar 27, 2025
0356835
Fix for isInsideModule?
mbellehumeur Mar 27, 2025
c6db595
revert
mbellehumeur Mar 27, 2025
f8b94a0
Fix wsiviewport deviceScaling
mbellehumeur Apr 2, 2025
64a7b95
Refactor ROI tools to check for scalar data before processing points …
mbellehumeur Apr 16, 2025
7f07ac4
Refactor WSIViewport to return typed scalar data
mbellehumeur Apr 16, 2025
bebdbd6
Remove unnecessary initialization of scalarData in getScalarData method
mbellehumeur Apr 16, 2025
9f286a5
Refactor scalar data handling in ROI tools to use CanvasScalarData ty…
mbellehumeur Apr 17, 2025
57c88c5
Refactor PlanarFreehandROITool to use getScalarValueFromWorld for sca…
mbellehumeur Apr 17, 2025
63336f6
Remove commented-out code in WSIViewport for canvas position transfor…
mbellehumeur Apr 17, 2025
5ded038
Fixed closing bracket not to exclude stats calculation
mbellehumeur Apr 17, 2025
99f2d2b
Refactor WSIViewport to replace getScalarData with _getScalarData
mbellehumeur Apr 20, 2025
048f9dd
Refactor ROI tools to check voxelManager length instead of image.scal…
mbellehumeur Apr 21, 2025
00bf564
Refactor ROI tools to use imageData.getScalarValueFromWorld for point…
mbellehumeur Apr 21, 2025
65d8149
Refactor ROI tools to use voxelManager for pointsInShape calculation
mbellehumeur Apr 21, 2025
b8fb6b2
Refactor WSIViewport to replace getScalarData with _getScalarData method
mbellehumeur Apr 21, 2025
73a6d4b
Refactor WSI Annotation Tools example to include Circle, Rectangle, a…
mbellehumeur Apr 21, 2025
6d21c80
fix: Display/hide area for color images correctly
wayfarer3130 Apr 21, 2025
88b5bfd
Merge branch 'fix-circleroi-wsi' of https://github.com/mbellehumeur/c…
wayfarer3130 Apr 21, 2025
613279f
fix: Update number validation in ROI tools to use AnnotationTool.isNu…
mbellehumeur May 28, 2025
5fb2331
fix: Update canvasToIndex method to return Point3 instead of Point2
mbellehumeur May 28, 2025
1fb803a
fix: Initialize WSIUtilFunctions and update indexToCanvas transformation
mbellehumeur May 28, 2025
474fb89
fix: Remove unused CanvasScalarData type and WSIUtilFunctions declara…
mbellehumeur May 28, 2025
cd18632
fix: Define CanvasScalarData type and initialize with default values …
mbellehumeur May 28, 2025
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
30 changes: 22 additions & 8 deletions packages/core/src/RenderingEngine/WSIViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {
CPUIImageData,
ViewportInput,
BoundsIJK,
CPUImageData,
} from '../types';
import uuidv4 from '../utilities/uuidv4';
import * as metaData from '../metaData';
Expand Down Expand Up @@ -228,8 +227,16 @@ class WSIViewport extends Viewport {
this.setProperties({});
}

protected getScalarData() {
return null;
protected _getScalarData() {
// Return an empty CanvasScalarData object
type CanvasScalarData = Uint8ClampedArray & {
frameNumber?: number;
getRange?: () => [number, number];
};
const emptyData = new Uint8ClampedArray() as CanvasScalarData;
emptyData.getRange = () => [0, 255];
emptyData.frameNumber = -1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an emptyData.region that is a rectangle of the enclosing region, and a scale parameter? The region should be zero sized for now, and the scale can be 1, but again I'd like to start thinking about the future.

return emptyData;
}

public getImageData(): CPUIImageData {
Expand All @@ -244,7 +251,7 @@ class WSIViewport extends Viewport {
getDirection: () => metadata.direction,
getDimensions: () => metadata.dimensions,
getRange: () => [0, 255],
getScalarData: () => this.getScalarData(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a viewReference to the getter as an optional? I know we don't pass it in yet, but we need to start thinking about it as the getScalarData for whole slide imaging won't be able to return the entire scalar data.

getScalarData: () => this._getScalarData(),
getSpacing: () => metadata.spacing,
worldToIndex: (point: Point3) => {
const canvasPoint = this.worldToCanvas(point);
Expand Down Expand Up @@ -272,9 +279,10 @@ class WSIViewport extends Viewport {
preScale: {
scaled: false,
},
scalarData: this.getScalarData(),
scalarData: this._getScalarData(),
imageData,
// It is for the annotations to work, since all of them work on voxelManager and not on scalarData now
/*
voxelManager: {
forEach: (
callback: (args: {
Expand All @@ -298,6 +306,7 @@ class WSIViewport extends Viewport {
});
},
},
*/
};

// @ts-expect-error we need to fully migrate the voxelManager to the new system
Expand Down Expand Up @@ -612,15 +621,20 @@ class WSIViewport extends Viewport {

public getRotation = () => 0;

protected canvasToIndex = (canvasPos: Point2): Point2 => {
protected canvasToIndex = (canvasPos: Point2): Point3 => {
const transform = this.getTransform();
transform.invert();
return transform.transformPoint(canvasPos);
const indexPoint = transform.transformPoint(
canvasPos.map((it) => it * devicePixelRatio) as Point2
);
return [indexPoint[0], indexPoint[1], 0] as Point3;
};

protected indexToCanvas = (indexPos: Point2): Point2 => {
const transform = this.getTransform();
return transform.transformPoint(indexPos);
return transform
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will need to convert back to negative values, so if you on initial setup store an index of which values are negative and multiple by that vector you should get the right sign.

.transformPoint([indexPos[0], indexPos[1]])
.map((it) => it / devicePixelRatio) as Point2;
};

/** This can be implemented later when multi-slice WSI is supported */
Expand Down
8 changes: 4 additions & 4 deletions packages/tools/examples/wsiAnnotationTools/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Types } from '@cornerstonejs/core';
import { RenderingEngine, Enums, WSIViewport } from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools';
import dicomImageLoader from '@cornerstonejs/dicom-image-loader';
import dicomImageLoader from '../../../dicomImageLoader/src';
import { api } from 'dicomweb-client';

import {
Expand Down Expand Up @@ -53,9 +53,9 @@ setTitleAndDescription(
const toolsNames = [
LengthTool.toolName,
// ProbeTool.toolName,
// RectangleROITool.toolName,
// EllipticalROITool.toolName,
// CircleROITool.toolName,
RectangleROITool.toolName,
EllipticalROITool.toolName,
CircleROITool.toolName,
BidirectionalTool.toolName,
AngleTool.toolName,
CobbAngleTool.toolName,
Expand Down
38 changes: 24 additions & 14 deletions packages/tools/src/tools/annotation/CircleROITool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,19 +982,21 @@ class CircleROITool extends AnnotationTool {
pixelUnitsOptions
);

const pointsInShape = voxelManager.forEach(
this.configuration.statsCalculator.statsCallback,
{
isInObject: (pointLPS) =>
pointInEllipse(ellipseObj, pointLPS, { fast: true }),
boundsIJK,
imageData,
returnPoints: this.configuration.storePointData,
}
);
let pointsInShape;
if (voxelManager) {
pointsInShape = voxelManager.forEach(
this.configuration.statsCalculator.statsCallback,
{
isInObject: (pointLPS) =>
pointInEllipse(ellipseObj, pointLPS, { fast: true }),
boundsIJK,
imageData,
returnPoints: this.configuration.storePointData,
}
);
}

const stats = this.configuration.statsCalculator.getStatistics();

cachedStats[targetId] = {
Modality: metadata.Modality,
area,
Expand Down Expand Up @@ -1028,6 +1030,14 @@ class CircleROITool extends AnnotationTool {
};

_isInsideVolume = (index1, index2, dimensions) => {
// for wsiViewport
if (index1[1] < 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put this fix into the WSIViewport itself?

index1[1] = -index1[1];
}
if (index2[1] < 0) {
index2[1] = -index2[1];
Comment on lines +1033 to +1038
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look good. Could you please check if the viewport type is WSI before proceeding? I imagine this might disrupt other types of viewports.

}

return (
csUtils.indexWithinDimensions(index1, dimensions) &&
csUtils.indexWithinDimensions(index2, dimensions)
Expand Down Expand Up @@ -1131,15 +1141,15 @@ function defaultGetTextLines(data, targetId): string[] {
textLines.push(areaLine);
}

if (mean) {
if (AnnotationTool.isNumber(mean)) {
textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`);
}

if (max) {
if (AnnotationTool.isNumber(max)) {
textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`);
}

if (stdDev) {
if (AnnotationTool.isNumber(stdDev)) {
textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`);
}

Expand Down
26 changes: 14 additions & 12 deletions packages/tools/src/tools/annotation/EllipticalROITool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,17 +1144,19 @@ class EllipticalROITool extends AnnotationTool {
pixelUnitsOptions
);

const pointsInShape = voxelManager.forEach(
this.configuration.statsCalculator.statsCallback,
{
boundsIJK,
imageData,
isInObject: (pointLPS) =>
pointInEllipse(ellipseObj, pointLPS, { fast: true }),
returnPoints: this.configuration.storePointData,
}
);

let pointsInShape;
if (voxelManager) {
pointsInShape = voxelManager.forEach(
this.configuration.statsCalculator.statsCallback,
{
boundsIJK,
imageData,
isInObject: (pointLPS) =>
pointInEllipse(ellipseObj, pointLPS, { fast: true }),
returnPoints: this.configuration.storePointData,
}
);
}
const stats = this.configuration.statsCalculator.getStatistics();
cachedStats[targetId] = {
Modality: metadata.Modality,
Expand Down Expand Up @@ -1248,7 +1250,7 @@ function defaultGetTextLines(data, targetId): string[] {
textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`);
}

if (max) {
if (AnnotationTool.isNumber(max)) {
textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`);
}

Expand Down
88 changes: 45 additions & 43 deletions packages/tools/src/tools/annotation/PlanarFreehandROITool.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AnnotationTool } from '../base';
import {
CONSTANTS,
getEnabledElement,
Expand Down Expand Up @@ -914,49 +915,50 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool {
let curRow = 0;
let intersections = [];
let intersectionCounter = 0;
const pointsInShape = voxelManager.forEach(
this.configuration.statsCalculator.statsCallback,
{
imageData,
isInObject: (pointLPS, _pointIJK) => {
let result = true;
const point = viewport.worldToCanvas(pointLPS);
if (point[1] != curRow) {
intersectionCounter = 0;
curRow = point[1];
intersections = getLineSegmentIntersectionsCoordinates(
canvasCoordinates,
point,
[canvasPosEnd[0], point[1]]
);
intersections.sort(
(function (index) {
return function (a, b) {
return a[index] === b[index]
? 0
: a[index] < b[index]
? -1
: 1;
};
})(0)
);
}
if (intersections.length && point[0] > intersections[0][0]) {
intersections.shift();
intersectionCounter++;
}
if (intersectionCounter % 2 === 0) {
result = false;
}
return result;
},
boundsIJK,
returnPoints: this.configuration.storePointData,
}
);

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) {
intersectionCounter = 0;
curRow = point[1];
intersections = getLineSegmentIntersectionsCoordinates(
canvasCoordinates,
point,
[canvasPosEnd[0], point[1]]
);
intersections.sort(
(function (index) {
return function (a, b) {
return a[index] === b[index]
? 0
: a[index] < b[index]
? -1
: 1;
};
})(0)
);
}
if (intersections.length && point[0] > intersections[0][0]) {
intersections.shift();
intersectionCounter++;
}
if (intersectionCounter % 2 === 0) {
result = false;
}
return result;
},
boundsIJK,
returnPoints: this.configuration.storePointData,
}
);
}
const stats = this.configuration.statsCalculator.getStatistics();

cachedStats[targetId] = {
Modality: metadata.Modality,
area,
Expand Down Expand Up @@ -1085,7 +1087,7 @@ function defaultGetTextLines(data, targetId): string[] {
textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`);
}

if (Number.isFinite(max)) {
if (AnnotationTool.isNumber(max)) {
textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`);
}

Expand Down
44 changes: 28 additions & 16 deletions packages/tools/src/tools/annotation/RectangleROITool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,6 @@ class RectangleROITool extends AnnotationTool {

this.editData.hasMoved = true;

const enabledElement = getEnabledElement(element);

triggerAnnotationRenderForViewportIds(viewportIdsToRender);

if (annotation.invalidated) {
Expand Down Expand Up @@ -930,16 +928,18 @@ class RectangleROITool extends AnnotationTool {
pixelUnitsOptions
);

const pointsInShape = voxelManager.forEach(
this.configuration.statsCalculator.statsCallback,
{
boundsIJK,
imageData,
returnPoints: this.configuration.storePointData,
}
);
let pointsInShape;
if (voxelManager) {
pointsInShape = voxelManager.forEach(
this.configuration.statsCalculator.statsCallback,
{
boundsIJK,
imageData,
returnPoints: this.configuration.storePointData,
}
);
}
const stats = this.configuration.statsCalculator.getStatistics();

cachedStats[targetId] = {
Modality: metadata.Modality,
area,
Expand Down Expand Up @@ -968,6 +968,14 @@ class RectangleROITool extends AnnotationTool {
};

_isInsideVolume = (index1, index2, dimensions) => {
// for wsiViewport
if (index1[1] < 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look good. Could you please check if the viewport type is WSI before proceeding? I imagine this might disrupt other types of viewports.

index1[1] = -index1[1];
}
if (index2[1] < 0) {
index2[1] = -index2[1];
}

return (
csUtils.indexWithinDimensions(index1, dimensions) &&
csUtils.indexWithinDimensions(index2, dimensions)
Expand Down Expand Up @@ -1048,12 +1056,16 @@ function defaultGetTextLines(data, targetId: string): string[] {
}

const textLines: string[] = [];

textLines.push(`Area: ${csUtils.roundNumber(area)} ${areaUnit}`);
textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`);
textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`);
textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`);

if (AnnotationTool.isNumber(mean)) {
textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`);
}
if (AnnotationTool.isNumber(max)) {
textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`);
}
if (AnnotationTool.isNumber(stdDev)) {
textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`);
}
return textLines;
}

Expand Down
Loading
Loading