import React, { Component } from "react"; import { getImageData, loadImageData } from "react-vtkjs-viewport"; import ConnectedVTKViewport from "./ConnectedVTKViewport"; import LoadingIndicator from "./LoadingIndicator.js"; import OHIF from "@ohif/core"; import PropTypes from "prop-types"; import cornerstone from "cornerstone-core"; import handleSegmentationStorage from "./handleSegmentationStorage.js"; import vtkDataArray from "vtk.js/Sources/Common/Core/DataArray"; import vtkImageData from "vtk.js/Sources/Common/DataModel/ImageData"; import vtkVolume from "vtk.js/Sources/Rendering/Core/Volume"; import vtkVolumeMapper from "vtk.js/Sources/Rendering/Core/VolumeMapper"; const { StackManager } = OHIF.utils; // Metadata configuration const metadataProvider = new OHIF.cornerstone.MetadataProvider(); cornerstone.metaData.addProvider( metadataProvider.provider.bind(metadataProvider) ); StackManager.setMetadataProvider(metadataProvider); const SOP_CLASSES = { SEGMENTATION_STORAGE: "1.2.840.10008.5.1.4.1.1.66.4" }; const specialCaseHandlers = {}; specialCaseHandlers[ SOP_CLASSES.SEGMENTATION_STORAGE ] = handleSegmentationStorage; // TODO: Figure out where we plan to put this long term const volumeCache = {}; /** * Create a labelmap image with the same dimensions as our background volume. * * @param backgroundImageData vtkImageData */ function createLabelMapImageData(backgroundImageData) { const labelMapData = vtkImageData.newInstance( backgroundImageData.get("spacing", "origin", "direction") ); labelMapData.setDimensions(backgroundImageData.getDimensions()); labelMapData.computeTransforms(); const values = new Uint8Array(backgroundImageData.getNumberOfPoints()); const dataArray = vtkDataArray.newInstance({ numberOfComponents: 1, // labelmap with single component values }); labelMapData.getPointData().setScalars(dataArray); return labelMapData; } class OHIFVTKViewport extends Component { state = { volumes: null, paintFilterLabelMapImageData: null, paintFilterBackgroundImageData: null }; static propTypes = { studies: PropTypes.object, displaySet: PropTypes.object, viewportIndex: PropTypes.number, children: PropTypes.node }; static id = "OHIFVTKViewport"; static init() { console.log("OHIFVTKViewport init()"); } static destroy() { console.log("OHIFVTKViewport destroy()"); StackManager.clearStacks(); } static getCornerstoneStack( studies, studyInstanceUid, displaySetInstanceUid, sopInstanceUid, frameIndex ) { // Create shortcut to displaySet const study = studies.find( study => study.studyInstanceUid === studyInstanceUid ); const displaySet = study.displaySets.find(set => { return set.displaySetInstanceUid === displaySetInstanceUid; }); // Get stack from Stack Manager const storedStack = StackManager.findOrCreateStack(study, displaySet); // Clone the stack here so we don't mutate it const stack = Object.assign({}, storedStack); if (frameIndex !== undefined) { stack.currentImageIdIndex = frameIndex; } else if (sopInstanceUid) { const index = stack.imageIds.findIndex(imageId => { const sopCommonModule = cornerstone.metaData.get( "sopCommonModule", imageId ); if (!sopCommonModule) { return; } return sopCommonModule.sopInstanceUID === sopInstanceUid; }); if (index > -1) { stack.currentImageIdIndex = index; } } else { stack.currentImageIdIndex = 0; } return stack; } getViewportData = ( studies, studyInstanceUid, displaySetInstanceUid, sopClassUid, sopInstanceUid, frameIndex ) => { const stack = OHIFVTKViewport.getCornerstoneStack( studies, studyInstanceUid, displaySetInstanceUid, sopClassUid, sopInstanceUid, frameIndex ); let imageDataObject; let labelmapDataObject; switch (sopClassUid) { case SOP_CLASSES.SEGMENTATION_STORAGE: throw new Error("Not yet implemented"); const data = handleSegmentationStorage( stack.imageIds, displaySetInstanceUid ); imageDataObject = data.referenceDataObject; labelmapDataObject = data.labelmapDataObject; return loadImageData(imageDataObject).then(() => { return { data: imageDataObject.vtkImageData, labelmap: labelmapDataObject }; }); default: imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid); return loadImageData(imageDataObject).then(() => { return { data: imageDataObject.vtkImageData }; }); } }; getOrCreateVolume(data, displaySetInstanceUid) { if (volumeCache[displaySetInstanceUid]) { return volumeCache[displaySetInstanceUid]; } const volumeActor = vtkVolume.newInstance(); const volumeMapper = vtkVolumeMapper.newInstance(); volumeActor.setMapper(volumeMapper); volumeMapper.setInputData(data); const range = data.getPointData().getScalars().getRange(); // TODO: For PET we might want to just set this to 0-5 SUV volumeActor.getProperty().getRGBTransferFunction(0).setRange(range[0], range[1]); // TODO: Should look into implementing autoAdjustSampleDistance in vtk const sampleDistance = 1.2 * Math.sqrt( data .getSpacing() .map((v) => v * v) .reduce((a, b) => a + b, 0) ); volumeMapper.setSampleDistance(sampleDistance); volumeCache[displaySetInstanceUid] = volumeActor; return volumeActor; } async setStateFromProps() { const { studies, displaySet } = this.props.viewportData; const { studyInstanceUid, displaySetInstanceUid, sopClassUids, sopInstanceUid, frameIndex } = displaySet; if (sopClassUids.length > 1) { console.warn( "More than one SOPClassUid in the same series is not yet supported." ); } const sopClassUid = sopClassUids[0]; let { data, labelmap } = await this.getViewportData( studies, studyInstanceUid, displaySetInstanceUid, sopClassUid, sopInstanceUid, frameIndex ); // TODO: Temporarily disabling this since it is not yet // being used and hurts performance significantly. /*if (!labelmap) { labelmap = createLabelMapImageData(data); }*/ const volumeActor = this.getOrCreateVolume(data, displaySetInstanceUid); this.setState({ volumes: [volumeActor], paintFilterBackgroundImageData: data, paintFilterLabelMapImageData: labelmap }); } componentDidMount() { this.setStateFromProps(); } componentDidUpdate(prevProps) { const { studies, displaySet } = this.props.viewportData; const prevDisplaySet = prevProps.viewportData.displaySet; if ( displaySet.displaySetInstanceUid !== prevDisplaySet.displaySetInstanceUid || displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid || displaySet.frameIndex !== prevDisplaySet.frameIndex ) { this.setStateFromProps(); } } render() { let childrenWithProps = null; // TODO: Does it make more sense to use Context? if (this.props.children && this.props.children.length) { childrenWithProps = this.props.children.map((child, index) => { return React.cloneElement(child, { viewportIndex: this.props.viewportIndex, key: index }); }); } const style = { width: "100%", height: "100%", position: "relative" }; return ( <> {this.state.volumes ? ( ) : (
)} {childrenWithProps} ); } } export default OHIFVTKViewport;