import React from 'react'; import { vec3 } from 'gl-matrix'; import { Component } from 'react'; import { View3D } from 'react-vtkjs-viewport'; import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData'; import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; import { api } from 'dicomweb-client'; import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction'; import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; import presets from './presets.js'; import buildMetadata from '../../OtherFunctions/buildMetadata_mine'; import sortDatasetsByImagePosition from '../../OtherFunctions/sortDatasetByImagePosition'; import loadImageData from '../../OtherFunctions/loadImageData_mine'; //import { withTranslation } from 'react-i18next'; import './Render3D.css'; window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader; const url = window.config.servers.dicomWeb[0].wadoRoot; function createActorMapper(imageData) { const mapper = vtkVolumeMapper.newInstance(); mapper.setInputData(imageData); const actor = vtkVolume.newInstance(); actor.setMapper(mapper); return { actor, mapper, }; } function getShiftRange(colorTransferArray) { // Credit to paraview-glance // https://github.com/Kitware/paraview-glance/blob/3fec8eeff31e9c19ad5b6bff8e7159bd745e2ba9/src/components/controls/ColorBy/script.js#L133 // shift range is original rgb/opacity range centered around 0 let min = Infinity; let max = -Infinity; for (let i = 0; i < colorTransferArray.length; i += 4) { min = Math.min(min, colorTransferArray[i]); max = Math.max(max, colorTransferArray[i]); } const center = (max - min) / 2; return { shiftRange: [-center, center], min, max, }; } function applyPointsToPiecewiseFunction(points, range, pwf) { const width = range[1] - range[0]; const rescaled = points.map(([x, y]) => [x * width + range[0], y]); pwf.removeAllPoints(); rescaled.forEach(([x, y]) => pwf.addPoint(x, y)); return rescaled; } function applyPointsToRGBFunction(points, range, cfun) { const width = range[1] - range[0]; const rescaled = points.map(([x, r, g, b]) => [ x * width + range[0], r, g, b, ]); cfun.removeAllPoints(); rescaled.forEach(([x, r, g, b]) => cfun.addRGBPoint(x, r, g, b)); return rescaled; } function applyPreset(actor, preset) { // Create color transfer function const colorTransferArray = preset.colorTransfer .split(' ') .splice(1) .map(parseFloat); const { shiftRange } = getShiftRange(colorTransferArray); let min = shiftRange[0]; const width = shiftRange[1] - shiftRange[0]; const cfun = vtkColorTransferFunction.newInstance(); const normColorTransferValuePoints = []; for (let i = 0; i < colorTransferArray.length; i += 4) { let value = colorTransferArray[i]; const r = colorTransferArray[i + 1]; const g = colorTransferArray[i + 2]; const b = colorTransferArray[i + 3]; value = (value - min) / width; normColorTransferValuePoints.push([value, r, g, b]); } applyPointsToRGBFunction(normColorTransferValuePoints, shiftRange, cfun); actor.getProperty().setRGBTransferFunction(0, cfun); // Create scalar opacity function const scalarOpacityArray = preset.scalarOpacity .split(' ') .splice(1) .map(parseFloat); const ofun = vtkPiecewiseFunction.newInstance(); const normPoints = []; for (let i = 0; i < scalarOpacityArray.length; i += 2) { let value = scalarOpacityArray[i]; const opacity = scalarOpacityArray[i + 1]; value = (value - min) / width; normPoints.push([value, opacity]); } applyPointsToPiecewiseFunction(normPoints, shiftRange, ofun); actor.getProperty().setScalarOpacity(0, ofun); const [ gradientMinValue, gradientMinOpacity, gradientMaxValue, gradientMaxOpacity, ] = preset.gradientOpacity .split(' ') .splice(1) .map(parseFloat); actor.getProperty().setUseGradientOpacity(0, true); actor.getProperty().setGradientOpacityMinimumValue(0, gradientMinValue); actor.getProperty().setGradientOpacityMinimumOpacity(0, gradientMinOpacity); actor.getProperty().setGradientOpacityMaximumValue(0, gradientMaxValue); actor.getProperty().setGradientOpacityMaximumOpacity(0, gradientMaxOpacity); if (preset.interpolation === '1') { actor.getProperty().setInterpolationTypeToFastLinear(); //actor.getProperty().setInterpolationTypeToLinear() } const ambient = parseFloat(preset.ambient); //const shade = preset.shade === '1' const diffuse = parseFloat(preset.diffuse); const specular = parseFloat(preset.specular); const specularPower = parseFloat(preset.specularPower); //actor.getProperty().setShade(shade) actor.getProperty().setAmbient(ambient); actor.getProperty().setDiffuse(diffuse); actor.getProperty().setSpecular(specular); actor.getProperty().setSpecularPower(specularPower); } function createCT3dPipeline(imageData, ctTransferFunctionPresetId) { const { actor, mapper } = createActorMapper(imageData); const sampleDistance = 1.2 * Math.sqrt( imageData .getSpacing() .map(v => v * v) .reduce((a, b) => a + b, 0) ); const range = imageData .getPointData() .getScalars() .getRange(); actor .getProperty() .getRGBTransferFunction(0) .setRange(range[0], range[1]); mapper.setSampleDistance(sampleDistance); const preset = presets.find( preset => preset.id === ctTransferFunctionPresetId ); applyPreset(actor, preset); actor.getProperty().setScalarOpacityUnitDistance(0, 2.5); return actor; } function createStudyImageIds(baseUrl, studySearchOptions) { const SOP_INSTANCE_UID = '00080018'; const SERIES_INSTANCE_UID = '0020000E'; const client = new api.DICOMwebClient({ url }); return new Promise((resolve, reject) => { console.log(studySearchOptions); client.retrieveStudyMetadata(studySearchOptions).then(instances => { const imageIds = instances.map(metaData => { const imageId = `wadors:` + baseUrl + '/studies/' + studySearchOptions.studyInstanceUID + '/series/' + metaData[SERIES_INSTANCE_UID].Value[0] + '/instances/' + metaData[SOP_INSTANCE_UID].Value[0] + '/frames/1'; cornerstoneWADOImageLoader.wadors.metaDataManager.add( imageId, metaData ); return imageId; }); resolve(imageIds); }, reject); }); } class VTKFusionExample extends Component { state = { volumeRenderingVolumes: null, ctTransferFunctionPresetId: 'vtkMRMLVolumePropertyNode4', petColorMapId: 'hsv', }; async componentDidMount() { const lsAntycipa = JSON.parse(localStorage.getItem('antycipa')); const studyInstanceUID = lsAntycipa['studyData']['StudyInstanceUID']; const ctSeriesInstanceUID = lsAntycipa['studyData']['SeriesInstanceUID']; const Modality = lsAntycipa['studyData']['Modality']; console.log(Modality) const imgOptions = { studyInstanceUID, ctSeriesInstanceUID, }; console.log(url); const imageIdPromise = createStudyImageIds(url, imgOptions); this.apis = []; const imageIds = await imageIdPromise; console.log(await imageIdPromise) const ctImageIds = imageIds.filter(imageId => imageId.includes(ctSeriesInstanceUID) ); const ctImageDataObject = this.loadDataset(ctImageIds); const ctImageData = ctImageDataObject.vtkImageData; if (Modality === 'PT') { this.setState({ ctTransferFunctionPresetId: 'vtkMRMLVolumePropertyNode26', }); } if (Modality === 'MR') { this.setState({ ctTransferFunctionPresetId: 'vtkMRMLVolumePropertyNode23', }); } const ctVolVR = createCT3dPipeline( ctImageData, this.state.ctTransferFunctionPresetId ); this.setState({ volumeRenderingVolumes: [ctVolVR], percentComplete: 0, }); } saveApiReference = api => { this.apis = [api]; }; handleChangeCTTransferFunction = value => { const ctTransferFunctionPresetId = value; const preset = presets.find( preset => preset.id === ctTransferFunctionPresetId ); const actor = this.state.volumeRenderingVolumes[0]; applyPreset(actor, preset); this.rerenderAll(); this.setState({ ctTransferFunctionPresetId, }); }; rerenderAll = () => { // Update all render windows, since the automatic re-render might not // happen if the viewport is not currently using the painting widget Object.keys(this.apis).forEach(viewportIndex => { const renderWindow = this.apis[ viewportIndex ].genericRenderWindow.getRenderWindow(); renderWindow.render(); }); }; buildImageData(imageIds) { const { metaData0, metaDataMap, imageMetaData0 } = buildMetadata(imageIds); const { rowCosines, columnCosines } = metaData0; const rowCosineVec = vec3.fromValues(...rowCosines); const colCosineVec = vec3.fromValues(...columnCosines); const scanAxisNormal = vec3.cross([], rowCosineVec, colCosineVec); const { spacing, origin, sortedDatasets } = sortDatasetsByImagePosition( scanAxisNormal, metaDataMap ); const xSpacing = metaData0.columnPixelSpacing; const ySpacing = metaData0.rowPixelSpacing; const zSpacing = spacing; const xVoxels = metaData0.columns; const yVoxels = metaData0.rows; const zVoxels = metaDataMap.size; const signed = imageMetaData0.pixelRepresentation === 1; const multiComponent = metaData0.numberOfComponents > 1; if (multiComponent) { throw new Error('Multi component image not supported by this plugin.'); } let pixelArray; switch (imageMetaData0.bitsAllocated) { case 8: if (signed) { throw new Error( '8 Bit signed images are not yet supported by this plugin.' ); } else { throw new Error( '8 Bit unsigned images are not yet supported by this plugin.' ); } case 16: pixelArray = new Float32Array(xVoxels * yVoxels * zVoxels); break; } const scalarArray = vtkDataArray.newInstance({ name: 'Pixels', numberOfComponents: 1, values: pixelArray, }); const imageData = vtkImageData.newInstance(); const direction = [...rowCosineVec, ...colCosineVec, ...scanAxisNormal]; imageData.setDimensions(xVoxels, yVoxels, zVoxels); imageData.setSpacing(xSpacing, ySpacing, zSpacing); imageData.setDirection(direction); imageData.setOrigin(...origin); imageData.getPointData().setScalars(scalarArray); const _publishPixelDataInserted = count => { imageDataObject_new.subscriptions.onPixelDataInserted.forEach( callback => { callback(count); } ); }; const _publishPixelDataInsertedError = error => { imageDataObject_new.subscriptions.onPixelDataInsertedError.forEach( callback => { callback(error); } ); }; const _publishAllPixelDataInserted = () => { imageDataObject_new.subscriptions.onAllPixelDataInserted.forEach( callback => { callback(); } ); imageDataObject_new.isLoading = false; imageDataObject_new.loaded = true; imageDataObject_new.vtkImageData.modified(); // Remove all subscriptions on completion. imageDataObject_new.subscriptions = { onPixelDataInserted: [], onPixelDataInsertedError: [], onAllPixelDataInserted: [], }; }; const imageDataObject_new = { imageIds, metaData0, imageMetaData0, dimensions: [xVoxels, yVoxels, zVoxels], spacing: [xSpacing, ySpacing, zSpacing], origin, direction, vtkImageData: imageData, metaDataMap, sortedDatasets, loaded: false, subscriptions: { onPixelDataInserted: [], onPixelDataInsertedError: [], onAllPixelDataInserted: [], }, onPixelDataInserted: callback => { imageDataObject_new.subscriptions.onPixelDataInserted.push(callback); }, onPixelDataInsertedError: callback => { imageDataObject_new.subscriptions.onPixelDataInsertedError.push( callback ); }, onAllPixelDataInserted: callback => { imageDataObject_new.subscriptions.onAllPixelDataInserted.push(callback); }, _publishPixelDataInserted, _publishAllPixelDataInserted, _publishPixelDataInsertedError, }; return imageDataObject_new; } loadDataset(imageIds) { const imageDataObject = this.buildImageData(imageIds); loadImageData(imageDataObject); const numberOfFrames = imageIds.length; const onPixelDataInsertedCallback = numberProcessed => { const percentComplete = Math.floor( (numberProcessed * 100) / numberOfFrames ); if (this.state.percentComplete !== percentComplete) { this.setState({ percentComplete }); } if (percentComplete % 20 === 0) { this.rerenderAll(); } }; const onAllPixelDataInsertedCallback = () => { this.rerenderAll(); }; imageDataObject.onPixelDataInserted(onPixelDataInsertedCallback); imageDataObject.onAllPixelDataInserted(onAllPixelDataInsertedCallback); return imageDataObject; } render() { var loadStr; var progStr; if ( localStorage.i18nextLng === 'it' || localStorage.i18nextLng === 'it-IT' ) { loadStr = 'Caricamento...'; progStr = 'Progresso'; } else { loadStr = 'Loading...'; progStr = 'Progress'; } if (!this.state.volumeRenderingVolumes) { return