Skip to content

Commit

Permalink
feat(cornerstone): Feature add cornerstone adapters (#225)
Browse files Browse the repository at this point in the history
* feature(dcmjs): Adding cobb-angle support

* feat(dcmjs): Add adapter for CobbAngle, and add description and location mapping

* fix(dcmjs): Fix the arrow, cobb and length to all get the description/group in the same way

* feat(dcmjs):Add adapters for FreehandRoi, RectangleRoi and Angle

* fix(dcmjs): Fixed the angle and rectangle cornerstone adapters

* fix(dcmjs): Store an optional second point for the arrow annotation
The point TID300 normally takes 1 point, but seems to be ok if an extra
one is added for the source of the indicator.

* fix(dcmjs):Use the SCT codes rather than SRT as requested
  • Loading branch information
wayfarer3130 authored Oct 26, 2021
1 parent 70b2433 commit c65b930
Show file tree
Hide file tree
Showing 15 changed files with 565 additions and 178 deletions.
96 changes: 96 additions & 0 deletions src/adapters/Cornerstone/Angle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import MeasurementReport from "./MeasurementReport.js";
import TID300CobbAngle from "../../utilities/TID300/CobbAngle.js";
import CORNERSTONE_4_TAG from "./cornerstone4Tag";

const ANGLE = "Angle";

class Angle {
constructor() {}

/**
* Generate TID300 measurement data for a plane angle measurement - use a CobbAngle, but label it as Angle
* @param MeasurementGroup
* @returns
*/
static getMeasurementData(MeasurementGroup) {
const {
defaultState,
NUMGroup,
SCOORDGroup
} = MeasurementReport.getSetupMeasurementData(MeasurementGroup);

const state = {
...defaultState,
rAngle: NUMGroup.MeasuredValueSequence.NumericValue,
toolType: Angle.toolType,
handles: {
start: {},
middle: {},
end: {},
textBox: {
hasMoved: false,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true
}
}
};

[
state.handles.start.x,
state.handles.start.y,
state.handles.middle.x,
state.handles.middle.y,
state.handles.middle.x,
state.handles.middle.y,
state.handles.end.x,
state.handles.end.y
] = SCOORDGroup.GraphicData;

return state;
}

static getTID300RepresentationArguments(tool) {
const { handles, finding, findingSites } = tool;
const point1 = handles.start;
const point2 = handles.middle;
const point3 = handles.middle;
const point4 = handles.end;
const rAngle = tool.rAngle;

const trackingIdentifierTextValue = "cornerstoneTools@^4.0.0:Angle";

return {
point1,
point2,
point3,
point4,
rAngle,
trackingIdentifierTextValue,
finding,
findingSites: findingSites || []
};
}
}

Angle.toolType = ANGLE;
Angle.utilityToolType = ANGLE;
Angle.TID300Representation = TID300CobbAngle;
Angle.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => {
if (!TrackingIdentifier.includes(":")) {
return false;
}

const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":");

if (cornerstone4Tag !== CORNERSTONE_4_TAG) {
return false;
}

return toolType === ANGLE;
};

MeasurementReport.registerTool(Angle);

export default Angle;
58 changes: 18 additions & 40 deletions src/adapters/Cornerstone/ArrowAnnotate.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,26 @@
import MeasurementReport from "./MeasurementReport.js";
import TID300Point from "../../utilities/TID300/Point.js";
import CORNERSTONE_4_TAG from "./cornerstone4Tag";
import { toArray } from "../helpers.js";

const ARROW_ANNOTATE = "ArrowAnnotate";
const FINDING = "121071";
const FINDING_SITE = "G-C0E3";
const CORNERSTONEFREETEXT = "CORNERSTONEFREETEXT";

class ArrowAnnotate {
constructor() {}

// TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport.
static getMeasurementData(MeasurementGroup) {
const { ContentSequence } = MeasurementGroup;

const NUMGroup = toArray(ContentSequence).find(
group => group.ValueType === "NUM"
);

const SCOORDGroup = toArray(NUMGroup.ContentSequence).find(
group => group.ValueType === "SCOORD"
);

const findingGroup = toArray(ContentSequence).find(
group => group.ConceptNameCodeSequence.CodeValue === FINDING
);

const findingSiteGroups = toArray(ContentSequence).filter(
group => group.ConceptNameCodeSequence.CodeValue === FINDING_SITE
);
const {
defaultState,
SCOORDGroup,
findingGroup
} = MeasurementReport.getSetupMeasurementData(MeasurementGroup);

const text = findingGroup.ConceptCodeSequence.CodeMeaning;

const { GraphicData } = SCOORDGroup;

const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence;
const {
ReferencedSOPInstanceUID,
ReferencedFrameNumber
} = ReferencedSOPSequence;
const state = {
sopInstanceUid: ReferencedSOPInstanceUID,
frameIndex: ReferencedFrameNumber || 0,
...defaultState,
toolType: ArrowAnnotate.toolType,
active: false,
handles: {
Expand All @@ -52,11 +30,17 @@ class ArrowAnnotate {
highlight: true,
active: false
},
// TODO: How do we choose where the end goes?
// Just put it pointing from the bottom right for now?
// Use a generic offset if the stored data doesn't have the endpoint, otherwise
// use the actual endpoint.
end: {
x: GraphicData[0] + 20,
y: GraphicData[1] + 20,
x:
GraphicData.length == 4
? GraphicData[2]
: GraphicData[0] + 20,
y:
GraphicData.length == 4
? GraphicData[3]
: GraphicData[1] + 20,
highlight: true,
active: false
},
Expand All @@ -70,20 +54,14 @@ class ArrowAnnotate {
},
invalidated: true,
text,
visible: true,
finding: findingGroup
? findingGroup.ConceptCodeSequence
: undefined,
findingSites: findingSiteGroups.map(fsg => {
return { ...fsg.ConceptCodeSequence };
})
visible: true
};

return state;
}

static getTID300RepresentationArguments(tool) {
const points = [tool.handles.start];
const points = [tool.handles.start, tool.handles.end];

let { finding, findingSites } = tool;

Expand Down
99 changes: 99 additions & 0 deletions src/adapters/Cornerstone/CobbAngle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import MeasurementReport from "./MeasurementReport.js";
import TID300CobbAngle from "../../utilities/TID300/CobbAngle.js";
import CORNERSTONE_4_TAG from "./cornerstone4Tag";

const COBB_ANGLE = "CobbAngle";

class CobbAngle {
constructor() {}

// TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport.
static getMeasurementData(MeasurementGroup) {
const {
defaultState,
NUMGroup,
SCOORDGroup
} = MeasurementReport.getSetupMeasurementData(MeasurementGroup);

const state = {
...defaultState,
rAngle: NUMGroup.MeasuredValueSequence.NumericValue,
toolType: CobbAngle.toolType,
handles: {
start: {},
end: {},
start2: {
highlight: true,
drawnIndependently: true
},
end2: {
highlight: true,
drawnIndependently: true
},
textBox: {
hasMoved: false,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true
}
}
};

[
state.handles.start.x,
state.handles.start.y,
state.handles.end.x,
state.handles.end.y,
state.handles.start2.x,
state.handles.start2.y,
state.handles.end2.x,
state.handles.end2.y
] = SCOORDGroup.GraphicData;

return state;
}

static getTID300RepresentationArguments(tool) {
const { handles, finding, findingSites } = tool;
const point1 = handles.start;
const point2 = handles.end;
const point3 = handles.start2;
const point4 = handles.end2;
const rAngle = tool.rAngle;

const trackingIdentifierTextValue = "cornerstoneTools@^4.0.0:CobbAngle";

return {
point1,
point2,
point3,
point4,
rAngle,
trackingIdentifierTextValue,
finding,
findingSites: findingSites || []
};
}
}

CobbAngle.toolType = COBB_ANGLE;
CobbAngle.utilityToolType = COBB_ANGLE;
CobbAngle.TID300Representation = TID300CobbAngle;
CobbAngle.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => {
if (!TrackingIdentifier.includes(":")) {
return false;
}

const [cornerstone4Tag, toolType] = TrackingIdentifier.split(":");

if (cornerstone4Tag !== CORNERSTONE_4_TAG) {
return false;
}

return toolType === COBB_ANGLE;
};

MeasurementReport.registerTool(CobbAngle);

export default CobbAngle;
38 changes: 7 additions & 31 deletions src/adapters/Cornerstone/EllipticalRoi.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,11 @@ class EllipticalRoi {

// TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport.
static getMeasurementData(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 NUMGroup = toArray(ContentSequence).find(
group => group.ValueType === "NUM"
);

const SCOORDGroup = toArray(NUMGroup.ContentSequence).find(
group => group.ValueType === "SCOORD"
);
const {
defaultState,
NUMGroup,
SCOORDGroup
} = MeasurementReport.getSetupMeasurementData(MeasurementGroup);

const { GraphicData } = SCOORDGroup;

Expand Down Expand Up @@ -66,14 +54,8 @@ class EllipticalRoi {
x: majorAxis[1].x - minorAxisDirection.x * halfMinorAxisLength,
y: majorAxis[1].y - minorAxisDirection.y * halfMinorAxisLength
};
const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence;
const {
ReferencedSOPInstanceUID,
ReferencedFrameNumber
} = ReferencedSOPSequence;
const state = {
sopInstanceUid: ReferencedSOPInstanceUID,
frameIndex: ReferencedFrameNumber || 0,
...defaultState,
toolType: EllipticalRoi.toolType,
active: false,
cachedStats: {
Expand Down Expand Up @@ -102,13 +84,7 @@ class EllipticalRoi {
}
},
invalidated: true,
visible: true,
finding: findingGroup
? findingGroup.ConceptCodeSequence
: undefined,
findingSites: findingSiteGroups.map(fsg => {
return { ...fsg.ConceptCodeSequence };
})
visible: true
};

return state;
Expand Down
Loading

0 comments on commit c65b930

Please sign in to comment.