Skip to content

Commit

Permalink
feat: Bidirectional Arrow and EllipticalROI adapters for CS3D (#264)
Browse files Browse the repository at this point in the history
* 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
sedghi committed May 6, 2022
1 parent 2c3b99f commit 1fc7932
Show file tree
Hide file tree
Showing 8 changed files with 541 additions and 5 deletions.
113 changes: 113 additions & 0 deletions packages/adapters/src/adapters/Cornerstone3D/ArrowAnnotate.js
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 packages/adapters/src/adapters/Cornerstone3D/Bidirectional.js
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 packages/adapters/src/adapters/Cornerstone3D/CodingScheme.js
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;
Loading

0 comments on commit 1fc7932

Please sign in to comment.