Notes on XNAT OHIF

  • 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:

    • BaseTool~preMouseDownCallback
    • BaseTool~postMouseDownCallback
  • 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.

XNATSphericalPETThresholdBrush

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
    In all cases I used XNATSphericalBrushTool as a guideline and added PET tool to the same list.

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>
        );


Discussion