Skip to content
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

feat(tools): Add new CircleStartEndThresholdTool and pointsInVolume statistics for 3D annotations #972

Merged
merged 16 commits into from
Feb 20, 2024

Conversation

Celian-abd
Copy link
Contributor

Context

We created a new tool based on the RectangleROIStartEndThresholdTool which is the CircleROIStartEndThresholdTool.
We added to both of this tools a new statistic called pointsInVolume which is litteraly the points inside the 3D annotation.

Changes & Results

  • Add new tool CircleROIStartEndThresholdTool
  • Add new statistic for RectangleROIStartEndThresholdTool and CircleROIStartEndThresholdTool called pointsInVolume

Testing

There is a new example in tools/examples/CircleROIStartEndThreshold

Checklist

PR

  • [] My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • [] "OS:
  • [] "Node version:
  • [] "Browser:

Copy link

netlify bot commented Dec 22, 2023

Deploy Preview for cornerstone-3d-docs canceled.

Name Link
🔨 Latest commit 14c96bb
🔍 Latest deploy log https://app.netlify.com/sites/cornerstone-3d-docs/deploys/65d4d720886bce00086ec61f

},
data: {
label: '',
startSlice: newStartIndex,
Copy link
Member

Choose a reason for hiding this comment

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

why in the RectangleROIStartEndThreshold, the startSlice is startIndex which is imageIdIndex, but here is different?

Copy link
Member

Choose a reason for hiding this comment

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

I believe I grasp the reason why, but just to confirm, this tool differs from the RectangleROITool in that it projects both before and after, whereas the rectangle only projects to after. Is this the intended functionality, or is it purely coincidental?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes it is intentional, when the physician is going to draw a 3D ROI he is likely to use the center slice to draw the ROI which should propagate upper and lower of the drawn ROI.

It's hard to imagine that the physician is going to to draw a ROI on a upperslice (in which the tumor is not visible if you don't want to miss slices) and expect the ROI to propagate only in lowerslice

Copy link
Member

@sedghi sedghi left a comment

Choose a reason for hiding this comment

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

see my comments, thanks

Comment on lines 354 to 356
if (this.configuration.calculatePointsInsideVolume) {
this._computePointsInsideVolume(annotation, imageVolume, enabledElement);
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of the calculatePoints function calling computePoints. How about we rename both functions to compute?

@Celian-abd Celian-abd requested a review from sedghi January 16, 2024 07:20
Comment on lines +271 to +341
_computePointsInsideVolume(annotation, imageVolume, enabledElement) {
const { data } = annotation;
const projectionPoints = data.cachedStats.projectionPoints;

const pointsInsideVolume: Types.Point3[][] = [[]];

for (let i = 0; i < projectionPoints.length; i++) {
// If image does not exists for the targetId, skip. This can be due
// to various reasons such as if the target was a volumeViewport, and
// the volumeViewport has been decached in the meantime.
if (!imageVolume) {
continue;
}

const projectionPoint = projectionPoints[i][0];

const worldPos1 = data.handles.points[0];
const worldPos2 = data.handles.points[3];

const { dimensions, imageData } = imageVolume;

const worldPos1Index = transformWorldToIndex(imageData, worldPos1);
//We only need to change the Z of our bounds so we are getting the Z from the current projection point
const worldProjectionPointIndex = transformWorldToIndex(
imageData,
projectionPoint
);

worldPos1Index[0] = Math.floor(worldPos1Index[0]);
worldPos1Index[1] = Math.floor(worldPos1Index[1]);
worldPos1Index[2] = Math.floor(worldProjectionPointIndex[2]);

const worldPos2Index = transformWorldToIndex(imageData, worldPos2);

worldPos2Index[0] = Math.floor(worldPos2Index[0]);
worldPos2Index[1] = Math.floor(worldPos2Index[1]);
worldPos2Index[2] = Math.floor(worldProjectionPointIndex[2]);

// Check if one of the indexes are inside the volume, this then gives us
// Some area to do stats over.

if (this._isInsideVolume(worldPos1Index, worldPos2Index, dimensions)) {
this.isHandleOutsideImage = false;
const iMin = Math.min(worldPos1Index[0], worldPos2Index[0]);
const iMax = Math.max(worldPos1Index[0], worldPos2Index[0]);

const jMin = Math.min(worldPos1Index[1], worldPos2Index[1]);
const jMax = Math.max(worldPos1Index[1], worldPos2Index[1]);

const kMin = Math.min(worldPos1Index[2], worldPos2Index[2]);
const kMax = Math.max(worldPos1Index[2], worldPos2Index[2]);

const boundsIJK = [
[iMin, iMax],
[jMin, jMax],
[kMin, kMax],
] as [Types.Point2, Types.Point2, Types.Point2];

const pointsInShape = pointInShapeCallback(
imageData,
() => true,
null,
boundsIJK
);

//@ts-ignore
pointsInsideVolume.push(pointsInShape);
}
}
data.cachedStats.pointsInVolume = pointsInsideVolume;
}
Copy link
Member

Choose a reason for hiding this comment

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

So this will run at every frame (typicall 60fps) when the tool is modified, why do you need to re-calculate that everytime? I think mouse up is a better place for it. I also think maybe even this is a post processing step and you should have it in the app not here in cornerstone3D

Why do you need points in volume?

Copy link
Contributor

Choose a reason for hiding this comment

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

The point in Volume is add like we add previously points for 2D ROI, these are usefull to implement in the app, histogramm based calculation, if you have all points value you can calculate histogram based quantification such as quantiles, entropy ...
In all anotations, Cornerstone handle the basic calculations and there is even the problem that cornerstone trie to guess what it need to display (some SUV normalization logic is in cornerstone, which don't think it is a good partern).

Globally I agree that the rendering library tsuch as cornerstone should not handle calculation logic, I think it would be better to have a quantification class in which we would have a volume ID and an Annotation object, and this class will produce the results and will be called by the app.

However the implementation of annotation is coupled for now as it include the quantification and rendering of results, that's something we improved in this PR by abstracting the calculator and making the rendering modifiable by the app : #723

For this PR, yes the calculation should be done on mouse up, celian will change it.

But then if you want to uncouple the annotation from the point calculation step it will need to redesgin all anotations to keep a common structure ... As the number of annotation tools is increasing it sounds to be a critical choice, if this change is not done now it will be more difficult to make this change later.

Globally i think the actual design is not perfect but suitable. (and hopfully i'm happy to not be the maintainer of such complicated project that is going to carry all medical imaging ecosystem ^^)

Copy link
Member

@sedghi sedghi left a comment

Choose a reason for hiding this comment

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

see my comments, I"m back to open source yay

@Celian-abd
Copy link
Contributor Author

Celian-abd commented Feb 20, 2024

I made the change to apply _computePointsInsideVolume only on mouseUp but I had to override the _endCallback from both CircleROITool and RectangleROITool. Should we keep it like this or should we change the callbacks to protected. It would means that we need to change it for every tool as well.

@Celian-abd Celian-abd requested a review from sedghi February 20, 2024 16:54
Copy link
Member

@sedghi sedghi left a comment

Choose a reason for hiding this comment

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

Looks great, thanks

@sedghi sedghi changed the title feat(new-tool): Add new CircleStartEndThresholdTool and pointsInVolume statistics for 3D annotations feat(tools): Add new CircleStartEndThresholdTool and pointsInVolume statistics for 3D annotations Feb 20, 2024
@sedghi sedghi merged commit 69350f4 into cornerstonejs:main Feb 20, 2024
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants