-
Notifications
You must be signed in to change notification settings - Fork 327
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
Conversation
✅ Deploy Preview for cornerstone-3d-docs canceled.
|
packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts
Show resolved
Hide resolved
packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts
Outdated
Show resolved
Hide resolved
}, | ||
data: { | ||
label: '', | ||
startSlice: newStartIndex, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts
Outdated
Show resolved
Hide resolved
packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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
packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts
Show resolved
Hide resolved
if (this.configuration.calculatePointsInsideVolume) { | ||
this._computePointsInsideVolume(annotation, imageVolume, enabledElement); | ||
} |
There was a problem hiding this comment.
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?
…into CircleThresholdTool
_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; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ^^)
There was a problem hiding this 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
…into CircleThresholdTool
…into CircleThresholdTool
I made the change to apply |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, thanks
Context
We created a new tool based on the
RectangleROIStartEndThresholdTool
which is theCircleROIStartEndThresholdTool
.We added to both of this tools a new statistic called
pointsInVolume
which is litteraly the points inside the 3D annotation.Changes & Results
CircleROIStartEndThresholdTool
RectangleROIStartEndThresholdTool
andCircleROIStartEndThresholdTool
calledpointsInVolume
Testing
There is a new example in
tools/examples/CircleROIStartEndThreshold
Checklist
PR
semantic-release format and guidelines.
Code
[] My code has been well-documented (function documentation, inline comments,
etc.)
[] I have run the
yarn build:update-api
to update the API documentation, and havecommitted the changes to this PR. (Read more here https://www.cornerstonejs.org/docs/contribute/update-api)
Public Documentation Updates
additions or removals.
Tested Environment