Viewer components: Toolbar is set in extensions/xnat/src/toolbarModule.js
. Here, the items on the toolbar are connected to the actual software implementations. Here I am exposing two tools, a 3D brush tool and a 3D HU gated brush tool which appear as Manual and Smart CT in the menu.
const definitions = [
[...]
{
id: 'brushTools',
label: 'Mask',
icon: 'xnat-mask',
buttons: [
{
id: 'Brush',
label: 'Manual',
[...]
commandOptions: {
return true;
}
As far as I can tell, the algorithm is the following - as mouseDown is about to happen (some controversy there already), an initialization is performed (_startPainting). A watchdog is started that will issue an _endPainting (not explicitly shown) through _startListeningForMouseUp. A tool handler _paint is invoked for the current point. The processing of the first point completes.
Intermediate points get directed towards _paint through mouseDragEvent but not in mouseMoveEvent, toolName: PEPPERMINT_TOOL_NAMES.BRUSH_3D_TOOL },
},
{
id: 'Brush3DHUGatedTool',
label: 'Smart CT',
[...]
commandOptions: { toolName: PEPPERMINT_TOOL_NAMES.BRUSH_3D_HU_GATED_TOOL },
},
{
id: 'SphericalBrush',
label: 'Spherical',
commandOptions: { toolName: PEPPERMINT_TOOL_NAMES.XNAT_SPHERICAL_BRUSH_TOOL },
},
[...]
]
[...]
}
]
```
Smart CT: I am using Smart CT is a role model for a hypothetical Smart PET, where a predefined threshold (SUV) is set on PET and a brush is used to "paint" all lesions in the area.
In toolbarModule.js
the Smart CT is set up to be a PEPPERMINT_TOOL_NAMES.BRUSH_3D_HU_GATED_TOOL
, with mappings to classes given in extensions/xnat/src/peppermint-tools/toolNames.js
:
const TOOL_NAMES = {
[...]
BRUSH_3D_TOOL: 'Brush3DTool',
BRUSH_3D_HU_GATED_TOOL: 'Brush3DHUGatedTool',
XNAT_SPHERICAL_BRUSH_TOOL: 'XNATSphericalBrushTool',
[...]
}
Brush3DHUGatedTool: extensions/xnat/src/peppermint-tools/tools/segmentation/Brush3DHUGatedTool.js
, descends from Brush3DTool
:
export default class Brush3DHUGatedTool extends Brush3DTool {
Brush3DTool: Lineage: BaseTool->BaseBrushTool->BrushTool->Brush3DTool
. Everything down to BrushTool is part of Cornerstone3D, one of 1848 external node dependencies of ohifxnatviewer. Cornerstone tools are part of the ohif-xnat-plugin git repository under node_modules/cornerstone-tools
:
import cornerstoneTools from 'cornerstone-tools';
[...]
const { BrushTool } = cornerstoneTools;
[...]
export default class Brush3DTool extends BrushTool {
brush has a radius, or possibly, depth, settable through configuration object
const radius = configuration.radius;
brush works in 2D only, although 3D is in its name. TBD why.
Spherical
, which is an XNATSphericalBrushTool
, does 3D markup. Smart PET should therefore descend from it.
XNATSphericalBrushTool
is a cornerstone-tools Sphericradius: 10, minRadius: 1, maxRadius: 50, fillAlpha: 0.2,alBrushTool
import csTools from 'cornerstone-tools';
[...]
const { SphericalBrushTool } = csTools;
[...]
export default class XNATSphericalBrushTool extends SphericalBrushTool {
constructor(props = {}) {
...
}
preMouseDownCallback(evt) {
...
super.preMouseDownCallback(evt);
}
}
all additional magic is happening in preMouseDownCallback
.
basic behaviour from BaseBrushTool from cornerstone, lineage is BaseTool->BaseBrushTool->BrushTool->SphericalBrushTool
.
in BaseTool
the tool interaction model is exposed - the tool consists of declared only callbacks to standard interface events, such as:
in BaseBrushTool
the callbacks get elaborated and explicit:
preMouseDownCallback(evt) {
const eventData = evt.detail;
const { element, currentPoints } = eventData;
this._startPainting(evt);
this._lastImageCoords = currentPoints.image;
this._drawing = true;
this._startListeningForMouseUp(element);
this._paint(evt);
return true;
}
As far as I can tell, the algorithm is the following - as mouseDown is about to happen (some controversy there already), an initialization is performed (_startPainting
). A watchdog is started that will issue an _endPainting
(not explicitly shown) through _startListeningForMouseUp
. A tool handler _paint
is invoked for the current point. The processing of the first point completes.
Intermediate points get directed towards _paint
through mouseDragEvent
but not in mouseMoveEvent
, so there must be a difference between the two, which is not yet apparent.
In _endPainting
the process is reversed - the last point is shipped to _paint
and the configuration is dissolved.
_paint
is abstract and conscientiously such - even an error is thrown - BaseBrushTool
must be extended.
I started a class XNATSphericalPETThresholdBrushTool
. I based it off XNATSphericalBrushTool
as it already does the 3D handling. To do the thresholding, I copied _paint
from SphericalBrushTool
and made sure the new class imported all relevant sources. Particularly:
import csCore from 'cornerstone-core';
const { getCircle, drawBrushPixels } = csTools.importInternal('util/segmentationUtils');
const segmentationModule = csTools.getModule('segmentation');
Then, the viewer must be told we added the new class, and that is spread over multiple classes:
ohifviewerxnat/extensions/xnat/src/peppermint-tools/index.js
ohifviewerxnat/extensions/xnat/src/peppermint-tools/tools/index.js
ohifviewerxnat/extensions/xnat/src/peppermint-tools/toolNames.js
ohifviewerxnat/extensions/xnat/src/toolbarModule.js
ohifviewerxnat/extensions/xnat/src/init.js
To set the threshold, we need first a place for the setting to live, Standard settings (brush size, etc.) are under segmentationModule
, which is accessed through csTools
. This is from Brush3DHUGatedTool
:
const radius = segmentationModule.configuration.radius;
segmentationModule is part of cornerstoneTools, but luckily enough the module gets extended in ohifviewerxnat/extensions/xnat/src/init.js
:
const config = Object.assign({}, getDefaultConfiguration(), configuration);
extendSegmentationModule(segmentationModule, config);
with extendSegmentationModule
defined in ohifviewerxnat/extensions/xnat/src/peppermint-tools/modules/extendSegmentationModule.js
. The function getDefaultConfiguration()
in init.js
already attaches some HU gating related variables to the native cornerstone configuration, and I see it as a place to attach
our petThreshold
as well:
const getDefaultConfiguration = () => {
...
const defaultConfig = {
...
petThreshold: 3,
...
}
...
return defaultConfig;
};
In extendedSegmentationModule.js
we then copy the threshold to the segmentationModule configuration:
configuration.petThreshold=config.petThreshold;
The UI for segmentation settings is ohifviewerxnat/extensions/xnat/src/components/XNATSegmentationSettings/XNATSegmentationSettings.js
, where we generate another custom slider for PET threshold. The step and max should still be configured - on true PET images they should be in units of SUV, for test image (uncorrected) I set a broader range.
const PETThreshold = (
<div className="settings-group" style={{ marginBottom: 15 }}>
<div className="custom-check">
<label>PET Threshold</label>
</div>
<CustomRange
label="PET Threshold"
step={100}
min={0}
max={10000}
value={configuration.petThreshold}
onChange={event => save('petThreshold', parseInt(event.target.value))}
showValue
/>
</div>
);
return (<div className="dcmseg-segmentation-settings">
<div className="settings-title">
<h3>Mask ROI Settings</h3>
<button className="return-button" onClick={onBack}>
Back
</button>
</div>
{BrushSize}
{SegmentFill}
{SegmentOutline}
{PETThreshold}
</div>
);