Skip to content

Gradienthealth/Segmentation with DicomJson #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: gradienthealth/zip_deployment
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions extensions/ohif-gradienthealth-extension/GRADIENT_README.md
Original file line number Diff line number Diff line change
@@ -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 metadata is also retrieved along with other series metadata of the study
and loaded as Dicom-Seg in the ThumbnailList.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import dcmjs from 'dcmjs';
import pako from 'pako'
import {
DicomMetadataStore,
IWebApiDataSource,
Expand All @@ -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',
Expand Down Expand Up @@ -206,6 +209,99 @@ 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,
StudyInstanceUID: dataSet.StudyInstanceUID,
instances: [
{
metadata: {
FrameOfReferenceUID: dataSet.FrameOfReferenceUID,
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 segBucket = params.get('seg-bucket') || bucket
const segPrefix = params.get('seg-prefix') || prefix

const fileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent(
SeriesDescription
)}.dcm`;
const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/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}`
);
}

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/${segBucket}/o?uploadType=media&name=${segPrefix}/${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}`
);
}
})
.catch((error) => {
throw new Error(error.message || 'Failed to store DicomSeg metadata')
})
})
.catch((error) => {
throw new Error(error.message || 'Failed to store DicomSeg file')
})
};

let _dicomJsonConfig = null;

function createDicomJSONApi(dicomJsonConfig, servicesManager) {
Expand Down Expand Up @@ -394,8 +490,17 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
},
},
store: {
dicom: () => {
console.debug(' DICOMJson store dicom');
dicom: async (dataset) => {
if (dataset.Modality === 'SEG') {
const headers = servicesManager.services.UserAuthenticationService.getAuthorizationHeader()
try {
await storeDicomSeg(dataset, headers)
} catch (error) {
throw error
}
} else {
console.debug(' DICOMJson store dicom');
}
},
},
getImageIdsForDisplaySet(displaySet) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useViewportGrid,
Dialog,
} from '@ohif/ui';
import { useNavigate } from 'react-router-dom';

const { formatDate } = utils;

Expand All @@ -20,7 +21,7 @@ function PanelStudyBrowserTracking({
UIDialogService,
UINotificationService,
getImageSrc,
getStudiesForPatientByStudyInstanceUID,
getStudiesForPatientByMRN,
requestDisplaySetCreationForStudy,
dataSource,
}) {
Expand All @@ -32,6 +33,7 @@ function PanelStudyBrowserTracking({
{ activeViewportId, viewports, numCols, numRows },
viewportGridService,
] = useViewportGrid();
const navigate=useNavigate()

const [activeTabName, setActiveTabName] = useState('primary');
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
Expand All @@ -50,7 +52,7 @@ function PanelStudyBrowserTracking({
};

const activeViewportDisplaySetInstanceUIDs =
viewports[activeViewportId]?.displaySetInstanceUIDs;
viewports.get(activeViewportId)?.displaySetInstanceUIDs;

const isSingleViewport = numCols === 1 && numRows === 1;

Expand Down Expand Up @@ -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 {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ function _getStudyForPatientUtility(extensionManager) {
'@ohif/extension-default.utilityModule.common'
);

const { getStudiesForPatientByStudyInstanceUID } = utilityModule.exports;
return getStudiesForPatientByStudyInstanceUID;
const { getStudiesForPatientByMRN } = utilityModule.exports;
return getStudiesForPatientByMRN;
}

/**
Expand All @@ -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
);
Expand All @@ -51,8 +51,8 @@ function WrappedPanelStudyBrowserTracking({
UINotificationService={servicesManager.services.UINotificationService}
dataSource={dataSource}
getImageSrc={_getImageSrcFromImageId}
getStudiesForPatientByStudyInstanceUID={
_getStudiesForPatientByStudyInstanceUID
getStudiesForPatientByMRN={
_getStudiesForPatientByMRN
}
requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy}
/>
Expand Down