-
Notifications
You must be signed in to change notification settings - Fork 285
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Bidirectional Arrow and EllipticalROI adapters for CS3D (#264)
* feat: Add Bidirectional and EllipticalROI adapters for cs3D * feat: Add ArrowAnnotate adapter for cs3d * apply review comments * fix scheme naming * fix ellipse points calculations for oblique plane * apply review comments * apply review comments
- Loading branch information
Showing
8 changed files
with
541 additions
and
5 deletions.
There are no files selected for viewing
113 changes: 113 additions & 0 deletions
113
packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import MeasurementReport from "./MeasurementReport.js"; | ||
import TID300Point from "../../utilities/TID300/Point.js"; | ||
import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; | ||
import CodingScheme from "./CodingScheme"; | ||
|
||
const ARROW_ANNOTATE = "ArrowAnnotate"; | ||
const trackingIdentifierTextValue = "Cornerstone3DTools@^0.1.0:ArrowAnnotate"; | ||
|
||
const { codeValues, CodingSchemeDesignator } = CodingScheme; | ||
|
||
class ArrowAnnotate { | ||
constructor() {} | ||
|
||
static getMeasurementData(MeasurementGroup, imageId, imageToWorldCoords) { | ||
const { | ||
defaultState, | ||
SCOORDGroup, | ||
findingGroup | ||
} = MeasurementReport.getSetupMeasurementData(MeasurementGroup); | ||
|
||
const text = findingGroup.ConceptCodeSequence.CodeMeaning; | ||
|
||
const { GraphicData } = SCOORDGroup; | ||
|
||
const worldCoords = []; | ||
for (let i = 0; i < GraphicData.length; i += 2) { | ||
const point = imageToWorldCoords(imageId, [ | ||
GraphicData[i], | ||
GraphicData[i + 1] | ||
]); | ||
worldCoords.push(point); | ||
} | ||
|
||
const state = { | ||
...defaultState, | ||
toolType: ArrowAnnotate.toolType, | ||
data: { | ||
text, | ||
handles: { | ||
points: [worldCoords[0], worldCoords[1]], | ||
activeHandleIndex: 0, | ||
textBox: { | ||
hasMoved: false | ||
} | ||
} | ||
} | ||
}; | ||
|
||
return state; | ||
} | ||
|
||
static getTID300RepresentationArguments(tool, worldToImageCoords) { | ||
const { data, metadata } = tool; | ||
let { finding, findingSites } = tool; | ||
const { referencedImageId } = metadata; | ||
|
||
if (!referencedImageId) { | ||
throw new Error( | ||
"ArrowAnnotate.getTID300RepresentationArguments: referencedImageId is not defined" | ||
); | ||
} | ||
|
||
const { points } = data.handles; | ||
|
||
const pointsImage = points.map(point => { | ||
const pointImage = worldToImageCoords(referencedImageId, point); | ||
return { | ||
x: pointImage[0], | ||
y: pointImage[1] | ||
}; | ||
}); | ||
|
||
const TID300RepresentationArguments = { | ||
points: pointsImage, | ||
trackingIdentifierTextValue, | ||
findingSites: findingSites || [] | ||
}; | ||
|
||
// If freetext finding isn't present, add it from the tool text. | ||
if (!finding || finding.CodeValue !== codeValues.CORNERSTONEFREETEXT) { | ||
finding = { | ||
CodeValue: codeValues.CORNERSTONEFREETEXT, | ||
CodingSchemeDesignator, | ||
CodeMeaning: data.text | ||
}; | ||
} | ||
|
||
TID300RepresentationArguments.finding = finding; | ||
|
||
return TID300RepresentationArguments; | ||
} | ||
} | ||
|
||
ArrowAnnotate.toolType = ARROW_ANNOTATE; | ||
ArrowAnnotate.utilityToolType = ARROW_ANNOTATE; | ||
ArrowAnnotate.TID300Representation = TID300Point; | ||
ArrowAnnotate.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { | ||
if (!TrackingIdentifier.includes(":")) { | ||
return false; | ||
} | ||
|
||
const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); | ||
|
||
if (cornerstone4Tag !== CORNERSTONE_3D_TAG) { | ||
return false; | ||
} | ||
|
||
return toolType === ARROW_ANNOTATE; | ||
}; | ||
|
||
MeasurementReport.registerTool(ArrowAnnotate); | ||
|
||
export default ArrowAnnotate; |
203 changes: 203 additions & 0 deletions
203
packages/adapters/src/adapters/Cornerstone3D/Bidirectional.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import MeasurementReport from "./MeasurementReport"; | ||
import TID300Bidirectional from "../../utilities/TID300/Bidirectional"; | ||
import CORNERSTONE_3D_TAG from "./cornerstone3DTag"; | ||
|
||
import { toArray } from "../helpers.js"; | ||
|
||
const BIDIRECTIONAL = "Bidirectional"; | ||
const LONG_AXIS = "Long Axis"; | ||
const SHORT_AXIS = "Short Axis"; | ||
const FINDING = "121071"; | ||
const FINDING_SITE = "G-C0E3"; | ||
const trackingIdentifierTextValue = "Cornerstone3DTools@^0.1.0:Bidirectional"; | ||
|
||
class Bidirectional { | ||
constructor() {} | ||
|
||
static getMeasurementData(MeasurementGroup, imageId, imageToWorldCoords) { | ||
const { defaultState } = MeasurementReport.getSetupMeasurementData( | ||
MeasurementGroup | ||
); | ||
const { ContentSequence } = MeasurementGroup; | ||
|
||
const findingGroup = toArray(ContentSequence).find( | ||
group => group.ConceptNameCodeSequence.CodeValue === FINDING | ||
); | ||
|
||
const findingSiteGroups = toArray(ContentSequence).filter( | ||
group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE | ||
); | ||
|
||
const longAxisNUMGroup = toArray(ContentSequence).find( | ||
group => group.ConceptNameCodeSequence.CodeMeaning === LONG_AXIS | ||
); | ||
|
||
const longAxisSCOORDGroup = toArray( | ||
longAxisNUMGroup.ContentSequence | ||
).find(group => group.ValueType === "SCOORD"); | ||
|
||
const shortAxisNUMGroup = toArray(ContentSequence).find( | ||
group => group.ConceptNameCodeSequence.CodeMeaning === SHORT_AXIS | ||
); | ||
|
||
const shortAxisSCOORDGroup = toArray( | ||
shortAxisNUMGroup.ContentSequence | ||
).find(group => group.ValueType === "SCOORD"); | ||
|
||
const worldCoords = []; | ||
|
||
[longAxisSCOORDGroup, shortAxisSCOORDGroup].forEach(group => { | ||
const { GraphicData } = group; | ||
for (let i = 0; i < GraphicData.length; i += 2) { | ||
const point = imageToWorldCoords(imageId, [ | ||
GraphicData[i], | ||
GraphicData[i + 1] | ||
]); | ||
worldCoords.push(point); | ||
} | ||
}); | ||
|
||
const state = { | ||
...defaultState, | ||
finding: findingGroup | ||
? findingGroup.ConceptCodeSequence | ||
: undefined, | ||
findingSites: findingSiteGroups.map(fsg => { | ||
return { ...fsg.ConceptCodeSequence }; | ||
}), | ||
toolType: Bidirectional.toolType, | ||
data: { | ||
handles: { | ||
points: [ | ||
worldCoords[0], | ||
worldCoords[1], | ||
worldCoords[2], | ||
worldCoords[3] | ||
], | ||
activeHandleIndex: 0, | ||
textBox: { | ||
hasMoved: false | ||
} | ||
}, | ||
cachedStats: { | ||
[`imageId:${imageId}`]: { | ||
length: | ||
longAxisNUMGroup.MeasuredValueSequence.NumericValue, | ||
width: | ||
shortAxisNUMGroup.MeasuredValueSequence.NumericValue | ||
} | ||
} | ||
} | ||
}; | ||
|
||
return state; | ||
} | ||
|
||
static getTID300RepresentationArguments(tool, worldToImageCoords) { | ||
const { data, finding, findingSites, metadata } = tool; | ||
const { cachedStats, handles } = data; | ||
|
||
const { referencedImageId } = metadata; | ||
|
||
if (!referencedImageId) { | ||
throw new Error( | ||
"Bidirectional.getTID300RepresentationArguments: referencedImageId is not defined" | ||
); | ||
} | ||
|
||
const { length, width } = cachedStats[`imageId:${referencedImageId}`]; | ||
const { points } = handles; | ||
|
||
// Find the length and width point pairs by comparing the distances of the points at 0,1 to points at 2,3 | ||
let firstPointPairs = [points[0], points[1]]; | ||
let secondPointPairs = [points[2], points[3]]; | ||
|
||
let firstPointPairsDistance = Math.sqrt( | ||
Math.pow(firstPointPairs[0][0] - firstPointPairs[1][0], 2) + | ||
Math.pow(firstPointPairs[0][1] - firstPointPairs[1][1], 2) + | ||
Math.pow(firstPointPairs[0][2] - firstPointPairs[1][2], 2) | ||
); | ||
|
||
let secondPointPairsDistance = Math.sqrt( | ||
Math.pow(secondPointPairs[0][0] - secondPointPairs[1][0], 2) + | ||
Math.pow(secondPointPairs[0][1] - secondPointPairs[1][1], 2) + | ||
Math.pow(secondPointPairs[0][2] - secondPointPairs[1][2], 2) | ||
); | ||
|
||
let shortAxisPoints; | ||
let longAxisPoints; | ||
if (firstPointPairsDistance > secondPointPairsDistance) { | ||
shortAxisPoints = firstPointPairs; | ||
longAxisPoints = secondPointPairs; | ||
} else { | ||
shortAxisPoints = secondPointPairs; | ||
longAxisPoints = firstPointPairs; | ||
} | ||
|
||
const longAxisStartImage = worldToImageCoords( | ||
referencedImageId, | ||
shortAxisPoints[0] | ||
); | ||
const longAxisEndImage = worldToImageCoords( | ||
referencedImageId, | ||
shortAxisPoints[1] | ||
); | ||
const shortAxisStartImage = worldToImageCoords( | ||
referencedImageId, | ||
longAxisPoints[0] | ||
); | ||
const shortAxisEndImage = worldToImageCoords( | ||
referencedImageId, | ||
longAxisPoints[1] | ||
); | ||
|
||
return { | ||
longAxis: { | ||
point1: { | ||
x: longAxisStartImage[0], | ||
y: longAxisStartImage[1] | ||
}, | ||
point2: { | ||
x: longAxisEndImage[0], | ||
y: longAxisEndImage[1] | ||
} | ||
}, | ||
shortAxis: { | ||
point1: { | ||
x: shortAxisStartImage[0], | ||
y: shortAxisStartImage[1] | ||
}, | ||
point2: { | ||
x: shortAxisEndImage[0], | ||
y: shortAxisEndImage[1] | ||
} | ||
}, | ||
longAxisLength: length, | ||
shortAxisLength: width, | ||
trackingIdentifierTextValue, | ||
finding: finding, | ||
findingSites: findingSites || [] | ||
}; | ||
} | ||
} | ||
|
||
Bidirectional.toolType = BIDIRECTIONAL; | ||
Bidirectional.utilityToolType = BIDIRECTIONAL; | ||
Bidirectional.TID300Representation = TID300Bidirectional; | ||
Bidirectional.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => { | ||
if (!TrackingIdentifier.includes(":")) { | ||
return false; | ||
} | ||
|
||
const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":"); | ||
|
||
if (cornerstone4Tag !== CORNERSTONE_3D_TAG) { | ||
return false; | ||
} | ||
|
||
return toolType === BIDIRECTIONAL; | ||
}; | ||
|
||
MeasurementReport.registerTool(Bidirectional); | ||
|
||
export default Bidirectional; |
16 changes: 16 additions & 0 deletions
16
packages/adapters/src/adapters/Cornerstone3D/CodingScheme.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// This is a custom coding scheme defined to store some annotations from Cornerstone. | ||
// Note: CodeMeaning is VR type LO, which means we only actually support 64 characters | ||
// here this is fine for most labels, but may be problematic at some point. | ||
const CORNERSTONEFREETEXT = "CORNERSTONEFREETEXT"; | ||
|
||
// Cornerstone specified coding scheme for storing findings | ||
const CodingSchemeDesignator = "CORNERSTONEJS"; | ||
|
||
const CodingScheme = { | ||
CodingSchemeDesignator, | ||
codeValues: { | ||
CORNERSTONEFREETEXT | ||
} | ||
}; | ||
|
||
export default CodingScheme; |
Oops, something went wrong.