From d3775f585234ef3a5c885418b114fb09a2469990 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Mon, 6 Nov 2023 15:31:24 +0530 Subject: [PATCH 1/7] Implemented store functionality for DicomSeg. --- .../src/DicomJSONDataSource/index.js | 103 +++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js index 8a65bce..2f9a418 100644 --- a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js +++ b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js @@ -1,3 +1,5 @@ +import dcmjs from 'dcmjs'; +import pako from 'pako' import { DicomMetadataStore, IWebApiDataSource, @@ -10,6 +12,7 @@ import getImageId from '../DicomWebDataSource/utils/getImageId'; import _ from 'lodash'; const metadataProvider = classes.MetadataProvider; +const { datasetToBlob } = dcmjs.data; const mappings = { studyInstanceUid: 'StudyInstanceUID', @@ -206,6 +209,93 @@ const findStudies = (key, value) => { return studies; }; +const mapSegSeriesFromDataSet = (dataSet) => { + return { + Modality: dataSet.Modality, + SeriesInstanceUID: dataSet.SeriesInstanceUID, + SeriesDescription: dataSet.SeriesDescription, + SeriesNumber: Number(dataSet.SeriesNumber), + SeriesDate: dataSet.SeriesDate, + SliceThickness: + Number(dataSet.SharedFunctionalGroupsSequence.PixelMeasuresSequence + .SliceThickness), + StudyInstanceUID: dataSet.StudyInstanceUID, + instances: [ + { + metadata: { + SOPInstanceUID: dataSet.SOPInstanceUID, + SOPClassUID: dataSet.SOPClassUID, + ReferencedSeriesSequence: dataSet.ReferencedSeriesSequence, + SharedFunctionalGroupsSequence: dataSet.SharedFunctionalGroupsSequence, + }, + url: dataSet.url, + } + ], + }; +}; + +const storeDicomSeg = async (naturalizedReport, headers) => { + const { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SeriesDescription, + } = naturalizedReport; + + const params = new URLSearchParams(window.location.search); + const bucket = params.get('bucket') || 'gradient-health-search-viewer-links'; + const prefix = params.get('bucket-prefix') || 'dicomweb'; + + const fileName = `${prefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/intances/${SOPInstanceUID}/${encodeURIComponent( + SeriesDescription + )}.dcm`; + const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${fileName}`; + const blob = datasetToBlob(naturalizedReport); + + await fetch(segUploadUri, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/dicom', + }, + body: blob, + }) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + throw new Error( + `${data.error.code}: ${data.error.message}` || 'Failed to store DicomSeg file' + ); + } + + const segUri = `dicomweb:https://storage.googleapis.com/${bucket}/${data.name}`; + // We are storing the imageId so that when naturalizedReport is made to displayset we can get url to DicomSeg file. + naturalizedReport.url = segUri + const segSeries = mapSegSeriesFromDataSet(naturalizedReport); + const compressedFile = pako.gzip(JSON.stringify(segSeries)); + + return fetch( + `https://storage.googleapis.com/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${prefix}/${StudyInstanceUID}/${SeriesInstanceUID}/metadata.json.gz&contentEncoding=gzip`, + { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: compressedFile, + } + ) + .then((response) => response.json()) + .then((data) => { + if (data.error) { + throw new Error( + `${data.error.code}: ${data.error.message}` || 'Failed to store DicomSeg metadata' + ); + } + }) + }) +}; + let _dicomJsonConfig = null; function createDicomJSONApi(dicomJsonConfig, servicesManager) { @@ -394,8 +484,17 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) { }, }, store: { - dicom: () => { - console.debug(' DICOMJson store dicom'); + dicom: async (dataset) => { + if (dataset.Modality === 'SEGss') { + const headers = servicesManager.services.UserAuthenticationService.getAuthorizationHeader() + try { + await storeDicomSeg(dataset, headers) + } catch (error) { + throw error + } + } else { + console.debug(' DICOMJson store dicom'); + } }, }, getImageIdsForDisplaySet(displaySet) { From 0392e62a0c867e57ed061a529ee13bea09372d27 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 8 Nov 2023 16:04:03 +0530 Subject: [PATCH 2/7] Corrected typo. --- .../src/DicomJSONDataSource/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js index 2f9a418..80b3d68 100644 --- a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js +++ b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js @@ -485,7 +485,7 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) { }, store: { dicom: async (dataset) => { - if (dataset.Modality === 'SEGss') { + if (dataset.Modality === 'SEG') { const headers = servicesManager.services.UserAuthenticationService.getAuthorizationHeader() try { await storeDicomSeg(dataset, headers) From c013b100688c8e303f11a10527bb8b6627c11dd6 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Wed, 8 Nov 2023 17:29:46 +0530 Subject: [PATCH 3/7] Added FrameOfReferenceUID to the generated Segmentation json. --- .../src/DicomJSONDataSource/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js index 80b3d68..951ded3 100644 --- a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js +++ b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js @@ -223,6 +223,7 @@ const mapSegSeriesFromDataSet = (dataSet) => { instances: [ { metadata: { + FrameOfReferenceUID: dataSet.FrameOfReferenceUID, SOPInstanceUID: dataSet.SOPInstanceUID, SOPClassUID: dataSet.SOPClassUID, ReferencedSeriesSequence: dataSet.ReferencedSeriesSequence, From 48e7990d60029c6b28404e33b94ababb8240fbe7 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Thu, 9 Nov 2023 18:49:53 +0530 Subject: [PATCH 4/7] Resolved all the comments --- .../src/DicomJSONDataSource/index.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js index 951ded3..9988544 100644 --- a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js +++ b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js @@ -216,9 +216,6 @@ const mapSegSeriesFromDataSet = (dataSet) => { SeriesDescription: dataSet.SeriesDescription, SeriesNumber: Number(dataSet.SeriesNumber), SeriesDate: dataSet.SeriesDate, - SliceThickness: - Number(dataSet.SharedFunctionalGroupsSequence.PixelMeasuresSequence - .SliceThickness), StudyInstanceUID: dataSet.StudyInstanceUID, instances: [ { @@ -246,11 +243,13 @@ const storeDicomSeg = async (naturalizedReport, headers) => { const params = new URLSearchParams(window.location.search); const bucket = params.get('bucket') || 'gradient-health-search-viewer-links'; const prefix = params.get('bucket-prefix') || 'dicomweb'; + const segBucket = params.get('seg-bucket') || bucket + const segPrefix = params.get('seg-prefix') || prefix - const fileName = `${prefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/intances/${SOPInstanceUID}/${encodeURIComponent( + const fileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent( SeriesDescription )}.dcm`; - const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${fileName}`; + const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${fileName}`; const blob = datasetToBlob(naturalizedReport); await fetch(segUploadUri, { @@ -265,18 +264,18 @@ const storeDicomSeg = async (naturalizedReport, headers) => { .then((data) => { if (data.error) { throw new Error( - `${data.error.code}: ${data.error.message}` || 'Failed to store DicomSeg file' + `${data.error.code}: ${data.error.message}` ); } - const segUri = `dicomweb:https://storage.googleapis.com/${bucket}/${data.name}`; + const segUri = `dicomweb:https://storage.googleapis.com/${segBucket}/${data.name}`; // We are storing the imageId so that when naturalizedReport is made to displayset we can get url to DicomSeg file. naturalizedReport.url = segUri const segSeries = mapSegSeriesFromDataSet(naturalizedReport); const compressedFile = pako.gzip(JSON.stringify(segSeries)); return fetch( - `https://storage.googleapis.com/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${prefix}/${StudyInstanceUID}/${SeriesInstanceUID}/metadata.json.gz&contentEncoding=gzip`, + `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${segPrefix}/${StudyInstanceUID}/${SeriesInstanceUID}/metadata.json.gz&contentEncoding=gzip`, { method: 'POST', headers: { @@ -290,10 +289,16 @@ const storeDicomSeg = async (naturalizedReport, headers) => { .then((data) => { if (data.error) { throw new Error( - `${data.error.code}: ${data.error.message}` || 'Failed to store DicomSeg metadata' + `${data.error.code}: ${data.error.message}` ); } }) + .catch((error) => { + throw new Error(error.message || 'Failed to store DicomSeg metadata') + }) + }) + .catch((error) => { + throw new Error(error.message || 'Failed to store DicomSeg file') }) }; From a76415bd454bd31d2217cfc42ba8ad138fe9b1c2 Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 10 Nov 2023 10:53:51 +0530 Subject: [PATCH 5/7] Added GRADIENT_README.md --- .../GRADIENT_README.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 extensions/ohif-gradienthealth-extension/GRADIENT_README.md diff --git a/extensions/ohif-gradienthealth-extension/GRADIENT_README.md b/extensions/ohif-gradienthealth-extension/GRADIENT_README.md new file mode 100644 index 0000000..41b3813 --- /dev/null +++ b/extensions/ohif-gradienthealth-extension/GRADIENT_README.md @@ -0,0 +1,68 @@ +# @gradienthealth/ohif-gradienthealth-extension + +### Segmentation in DicomJson Series metadata Sample + +```json +{ + "Modality": "SEG", + "SeriesInstanceUID": "2.25.478875556728379505207796619011601982565", + "SeriesDescription": "Seg2", + "SeriesNumber": 99, + "SeriesDate": "20231109", + "StudyInstanceUID": "1.2.40.1.13.2.1464541835.1640.1480008284780.704.2.0", + "instances": [ + { + "metadata": { + "FrameOfReferenceUID": "1.3.46.670589.11.18776.5.0.6996.2016112418000306013", + "SOPInstanceUID": "2.25.381554502573666068693329632131787392307", + "SOPClassUID": "1.2.840.10008.5.1.4.1.1.66.4", + "ReferencedSeriesSequence": { + "SeriesInstanceUID": "1.3.46.670589.11.18776.5.0.6088.2016112418015260387", + "ReferencedInstanceSequence": [ + { + "ReferencedSOPClassUID": "1.2.840.10008.5.1.4.1.1.4", + "ReferencedSOPInstanceUID": "1.3.46.670589.11.18776.5.0.6088.2016112418034896398" + }, + { + "ReferencedSOPClassUID": "1.2.840.10008.5.1.4.1.1.4", + "ReferencedSOPInstanceUID": "1.3.46.670589.11.18776.5.0.6088.2016112418052378421" + }, + { + "ReferencedSOPClassUID": "1.2.840.10008.5.1.4.1.1.4", + "ReferencedSOPInstanceUID": "1.3.46.670589.11.18776.5.0.6088.2016112418034934404" + }, + { + "ReferencedSOPClassUID": "1.2.840.10008.5.1.4.1.1.4", + "ReferencedSOPInstanceUID": "1.3.46.670589.11.18776.5.0.6088.2016112418052326410" + } + ] + }, + "SharedFunctionalGroupsSequence": { + "PlaneOrientationSequence": { + "ImageOrientationPatient": [ + 0.06783646345138, 0.99748069047927, 0.02074741572141, + -0.0216917078942, 0.02226497046649, -0.9995167255401 + ] + }, + "PixelMeasuresSequence": { + "PixelSpacing": [0.265625, 0.265625], + "SliceThickness": 4.099999453351714 + }, + "MRImageFrameTypeSequence": { + "FrameType": ["ORIGINAL", "PRIMARY", "OTHER", "NONE"], + "PixelPresentation": "MONOCHROME", + "VolumetricProperties": "VOLUME", + "VolumeBasedCalculationTechnique": "NONE", + "ComplexImageComponent": "MAGNITUDE", + "AcquisitionContrast": "UNKNOWN" + } + } + }, + "url": "dicomweb:https://storage.googleapis.com/gradient-health-dicomseg/dicomweb/studies/1.2.40.1.13.2.1464541835.1640.1480008284780.704.2.0/series/2.25.478875556728379505207796619011601982565/instances/2.25.381554502573666068693329632131787392307/Seg2.dcm" + } + ] +} +``` + +Segmentation Series metedata is also retrieved along with other series metadata +and loaded as Segmentation From 4e76f00c2afdff1c6da7ae7d36e91ae62df8a4ab Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 10 Nov 2023 11:44:03 +0530 Subject: [PATCH 6/7] Modified the description in the GRADIENT_README.md --- extensions/ohif-gradienthealth-extension/GRADIENT_README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ohif-gradienthealth-extension/GRADIENT_README.md b/extensions/ohif-gradienthealth-extension/GRADIENT_README.md index 41b3813..d36152a 100644 --- a/extensions/ohif-gradienthealth-extension/GRADIENT_README.md +++ b/extensions/ohif-gradienthealth-extension/GRADIENT_README.md @@ -64,5 +64,5 @@ } ``` -Segmentation Series metedata is also retrieved along with other series metadata -and loaded as Segmentation +Segmentation Series metadata is also retrieved along with other series metadata of the study +and loaded as Dicom-Seg in the ThumbnailList. From f275d0d14ebc110a7a8a21c0c9f4b9f1bcbf773c Mon Sep 17 00:00:00 2001 From: Adithyan Dinesh Date: Fri, 22 Dec 2023 14:36:42 +0530 Subject: [PATCH 7/7] Viewer rebase changes --- .../PanelStudyBrowserTracking.tsx | 47 ++++++++++++++----- .../getImageSrcFromImageId.js | 2 +- .../PanelStudyBrowserTracking/index.tsx | 12 ++--- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index ed7c1c9..301c25c 100644 --- a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -7,6 +7,7 @@ import { useViewportGrid, Dialog, } from '@ohif/ui'; +import { useNavigate } from 'react-router-dom'; const { formatDate } = utils; @@ -20,7 +21,7 @@ function PanelStudyBrowserTracking({ UIDialogService, UINotificationService, getImageSrc, - getStudiesForPatientByStudyInstanceUID, + getStudiesForPatientByMRN, requestDisplaySetCreationForStudy, dataSource, }) { @@ -32,6 +33,7 @@ function PanelStudyBrowserTracking({ { activeViewportId, viewports, numCols, numRows }, viewportGridService, ] = useViewportGrid(); + const navigate=useNavigate() const [activeTabName, setActiveTabName] = useState('primary'); const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ @@ -50,7 +52,7 @@ function PanelStudyBrowserTracking({ }; const activeViewportDisplaySetInstanceUIDs = - viewports[activeViewportId]?.displaySetInstanceUIDs; + viewports.get(activeViewportId)?.displaySetInstanceUIDs; const isSingleViewport = numCols === 1 && numRows === 1; @@ -81,9 +83,26 @@ function PanelStudyBrowserTracking({ useEffect(() => { // Fetch all studies for the patient in each primary study async function fetchStudiesForPatient(StudyInstanceUID) { - const qidoStudiesForPatient = - (await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || []; - // TODO: This should be "naturalized DICOM JSON" studies + // current study qido + const qidoForStudyUID = await dataSource.query.studies.search({ + studyInstanceUid: StudyInstanceUID, + }); + + if (!qidoForStudyUID?.length) { + navigate('/notfoundstudy', '_self'); + throw new Error('Invalid study URL'); + } + + let qidoStudiesForPatient = qidoForStudyUID; + + // try to fetch the prior studies based on the patientID if the + // server can respond. + try { + qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID); + } catch (error) { + console.warn(error); + } + const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient); const actuallyMappedStudies = mappedStudies.map(qidoStudy => { return { @@ -92,16 +111,22 @@ function PanelStudyBrowserTracking({ description: qidoStudy.StudyDescription, modalities: qidoStudy.ModalitiesInStudy, numInstances: qidoStudy.NumInstances, - // displaySets: [] }; }); - setStudyDisplayList(actuallyMappedStudies); + setStudyDisplayList(prevArray => { + const ret = [...prevArray]; + for (const study of actuallyMappedStudies) { + if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) { + ret.push(study); + } + } + return ret; + }); } StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]); + }, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); // ~~ Initial Thumbnails useEffect(() => { @@ -328,7 +353,7 @@ PanelStudyBrowserTracking.propTypes = { getImageIdsForDisplaySet: PropTypes.func.isRequired, }).isRequired, getImageSrc: PropTypes.func.isRequired, - getStudiesForPatientByStudyInstanceUID: PropTypes.func.isRequired, + getStudiesForPatientByMRN: PropTypes.func.isRequired, requestDisplaySetCreationForStudy: PropTypes.func.isRequired, }; @@ -373,7 +398,7 @@ function _mapDisplaySets( const componentType = _getComponentType(ds.Modality); const viewportIdentificator = isSingleViewport ? [] - : viewports.reduce((acc, viewportData, index) => { + : Object.values(viewports).reduce((acc, viewportData, index) => { if ( viewportData?.displaySetInstanceUIDs?.includes( ds.displaySetInstanceUID diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js index af0e8a1..40e9ae8 100644 --- a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js +++ b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js @@ -6,7 +6,7 @@ function getImageSrcFromImageId(cornerstone, imageId) { return new Promise((resolve, reject) => { const canvas = document.createElement('canvas'); cornerstone.utilities - .loadImageToCanvas(canvas, imageId) + .loadImageToCanvas({canvas, imageId}) .then(imageId => { resolve(canvas.toDataURL()); }) diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx index bacfefe..df1e067 100644 --- a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx +++ b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx @@ -10,8 +10,8 @@ function _getStudyForPatientUtility(extensionManager) { '@ohif/extension-default.utilityModule.common' ); - const { getStudiesForPatientByStudyInstanceUID } = utilityModule.exports; - return getStudiesForPatientByStudyInstanceUID; + const { getStudiesForPatientByMRN } = utilityModule.exports; + return getStudiesForPatientByMRN; } /** @@ -28,10 +28,10 @@ function WrappedPanelStudyBrowserTracking({ }) { const dataSource = extensionManager.getActiveDataSource()[0]; - const getStudiesForPatientByStudyInstanceUID = _getStudyForPatientUtility( + const getStudiesForPatientByMRN = _getStudyForPatientUtility( extensionManager ); - const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind( + const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind( null, dataSource ); @@ -51,8 +51,8 @@ function WrappedPanelStudyBrowserTracking({ UINotificationService={servicesManager.services.UINotificationService} dataSource={dataSource} getImageSrc={_getImageSrcFromImageId} - getStudiesForPatientByStudyInstanceUID={ - _getStudiesForPatientByStudyInstanceUID + getStudiesForPatientByMRN={ + _getStudiesForPatientByMRN } requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy} />