Skip to content

Support for multi bucket study loading and sheet integration in segmentation mode. #2

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 23 commits into
base: gradienthealth/Segmentation-with-DicomJSON
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
405f786
Support for multi bucket study loading and sheet integration in segme…
Adithyan-Dinesh-Trenser Jan 2, 2024
5c3fea4
Created a custom template panel which will update along with study sw…
Adithyan-Dinesh-Trenser Jan 10, 2024
1627f27
Support to get StudyInstanceUID in sheet service from bg datasource a…
Adithyan-Dinesh-Trenser Jan 12, 2024
2bb9ead
Modified auto segmentations loading and representation adding timing.
Adithyan-Dinesh-Trenser Jan 16, 2024
74d37fe
Created a function to update files in fileManager and fixed a timing …
Adithyan-Dinesh-Trenser Jan 17, 2024
8476488
Modified the auto segmentation loading and added support for auto seg…
Adithyan-Dinesh-Trenser Jan 23, 2024
ce57dfb
Implemented segment focus, added auto saving feature, added auto segm…
Adithyan-Dinesh-Trenser Feb 2, 2024
17386e0
Also added nonhydrated segmentation when calculating increment.
Adithyan-Dinesh-Trenser Feb 2, 2024
3afe9ec
Added volume support for segment focusing.
Adithyan-Dinesh-Trenser Feb 5, 2024
4bb7454
Corrected auto segmentation loading.
Adithyan-Dinesh-Trenser Feb 6, 2024
07d5d0a
Fixed the soogle sheet form values out of sync issue and active segme…
Adithyan-Dinesh-Trenser Mar 5, 2024
dd43177
Segmentation series description will be displayed using ImageLaterali…
Adithyan-Dinesh-Trenser Mar 6, 2024
391cfcf
Removing '/' from the series description when saving segmentation fil…
Adithyan-Dinesh-Trenser Mar 8, 2024
dc0c4cb
Cache the nearby studies one by one after finishing the previous study.
Adithyan-Dinesh-Trenser Mar 11, 2024
11ed29c
compressed segmentation file when saving
Adithyan-Dinesh-Trenser Mar 12, 2024
15ca599
Added more properties to the metadata json when saving segmentation a…
Adithyan-Dinesh-Trenser Mar 15, 2024
df8f5d1
Due to script update for modifying the segmentation SeriesDescription…
Adithyan-Dinesh-Trenser Mar 18, 2024
fcaf0da
Corrected cutoff calculation to allow 0 as well.
maya-trenser Apr 2, 2024
51a2d51
Modified condition for auto segmentation loading.
Adithyan-Dinesh-Trenser Apr 8, 2024
3c599da
fix: Fixed the issue of image sizing not working for Hologic images
Adithyan-Dinesh-Trenser Jun 19, 2024
eeeb037
Merge pull request #5 from gradienthealth/gradienthealth/BUG-FIX_Imag…
Adithyan-Dinesh-Trenser Jun 20, 2024
2e49eb6
Correct the orientation by auto flipping when segmentation is auto lo…
Adithyan-Dinesh-Trenser Sep 4, 2024
9d86cbc
Merge pull request #8 from gradienthealth/gradienthealth/auto_flippin…
Adithyan-Dinesh-Trenser Sep 5, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const getInstanceUrl = (url, prefix) => {
return modifiedUrl;
}

const getProperty = (serieMetadata, property) => {
return (
serieMetadata[property] || serieMetadata.instances[0].metadata[property]
);
};

const getMetadataFromRows = (rows, prefix, seriesuidArray) => {
// TODO: bq should not have dups
let filteredRows = rows.map(row => {
Expand Down Expand Up @@ -87,6 +93,7 @@ const getMetadataFromRows = (rows, prefix, seriesuidArray) => {
SeriesDescription: row['SeriesDescription'] || 'No description',
StudyInstanceUID: row['StudyInstanceUID'],
SeriesNumber: row['SeriesNumber'],
SeriesDate: row['SeriesDate'],
SeriesTime: row['SeriesTime'],
NumInstances: isNaN(parseInt(row['NumInstances']))
? 0
Expand All @@ -101,16 +108,17 @@ const getMetadataFromRows = (rows, prefix, seriesuidArray) => {
});

return {
StudyInstanceUID: rows[0]['StudyInstanceUID'],
PatientName: rows[0]['PatientName'],
PatientSex: rows[0]['PatientSex'],
AccessionNumber: rows[0]['AccessionNumber'],
StudyDate: rows[0]['StudyDate'],
PatientID: rows[0]['PatientID'],
PatientWeight: rows[0]['PatientWeight'],
PatientAge: rows[0]['PatientAge'],
StudyDescription: rows[0]['StudyDescription'] || 'No description',
StudyTime: rows[0]['StudyTime'],
StudyInstanceUID: getProperty(rows[0], 'StudyInstanceUID'),
PatientName: getProperty(rows[0], 'PatientName'),
PatientSex: getProperty(rows[0], 'PatientSex') || '',
AccessionNumber: getProperty(rows[0], 'AccessionNumber'),
StudyDate: getProperty(rows[0], 'StudyDate'),
PatientID: getProperty(rows[0], 'PatientID'),
PatientWeight: getProperty(rows[0], 'PatientWeight') || '',
PatientAge: getProperty(rows[0], 'PatientAge') || '',
StudyDescription:
getProperty(rows[0], 'StudyDescription') || 'No description',
StudyTime: getProperty(rows[0], 'StudyTime'),
NumInstances: studyNumInstances,
Modalities: `["${rows[0]['Modality']}"]`,
series: series,
Expand Down Expand Up @@ -179,15 +187,15 @@ const getBigQueryRows = async (studyuids, seriesuid, access_token) => {

const filesFromStudyInstanceUID = async ({bucketName, prefix, studyuids, headers})=>{
const studyMetadata = studyuids.map(async (studyuid) => {
const folderPath = `${prefix}/${studyuid}/`;
const folderPath = `${prefix}/studies/${studyuid}/series/`;
const delimiter = '/'
const apiUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}/o?prefix=${folderPath}&delimiter=${delimiter}`;
const response = await fetch(apiUrl, { headers });
const res = await response.json()
const files = res.items || [];
const folders = res.prefixes || [];
const series = folders.map(async (folderPath)=>{
const objectName = `${folderPath}metadata.json.gz`;
const objectName = `${folderPath}metadata`;
const apiUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}/o/${encodeURIComponent(objectName)}?alt=media`;
const response = await fetch(apiUrl, { headers });
return response.json()
Expand Down Expand Up @@ -216,23 +224,37 @@ const mapSegSeriesFromDataSet = (dataSet) => {
SeriesDescription: dataSet.SeriesDescription,
SeriesNumber: Number(dataSet.SeriesNumber),
SeriesDate: dataSet.SeriesDate,
SeriesTime: dataSet.SeriesTime,
StudyDate: dataSet.StudyDate,
StudyTime: dataSet.StudyTime,
PatientName: dataSet.PatientName,
PatientID: dataSet.PatientID,
PatientWeight: dataSet.PatientWeight,
PatientAge: Number(dataSet.PatientAge),
AccessionNumber: Number(dataSet.AccessionNumber),
StudyInstanceUID: dataSet.StudyInstanceUID,
StudyDescription: dataSet.StudyDescription,
instances: [
{
metadata: {
FrameOfReferenceUID: dataSet.FrameOfReferenceUID,
SOPInstanceUID: dataSet.SOPInstanceUID,
SOPClassUID: dataSet.SOPClassUID,
ReferencedSeriesSequence: dataSet.ReferencedSeriesSequence,
SharedFunctionalGroupsSequence: dataSet.SharedFunctionalGroupsSequence,
SharedFunctionalGroupsSequence:
dataSet.SharedFunctionalGroupsSequence,
PerFrameFunctionalGroupsSequence:
dataSet.PerFrameFunctionalGroupsSequence,
SegmentSequence: dataSet.SegmentSequence,
Manufacturer: dataSet.Manufacturer,
},
url: dataSet.url,
}
},
],
};
};

const storeDicomSeg = async (naturalizedReport, headers) => {
const storeDicomSeg = async (naturalizedReport, headers, displaySetService) => {
const {
StudyInstanceUID,
SeriesInstanceUID,
Expand All @@ -241,24 +263,40 @@ const storeDicomSeg = async (naturalizedReport, headers) => {
} = naturalizedReport;

const params = new URLSearchParams(window.location.search);
const bucket = params.get('bucket') || 'gradient-health-search-viewer-links';
const buckets = params.getAll('bucket');
const bucket =
buckets[1] || buckets[0] || 'gradient-health-search-viewer-links';
const prefix = params.get('bucket-prefix') || 'dicomweb';
const segBucket = params.get('seg-bucket') || bucket
let segBucket = params.get('seg-bucket') || bucket
const segPrefix = params.get('seg-prefix') || prefix
const filteredDescription = SeriesDescription.replace(/[/]/g, '');

const fileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent(
SeriesDescription
let fileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent(
filteredDescription
)}.dcm`;
const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${fileName}`;

const segDisplaySet = displaySetService.getDisplaySetsBy(
(ds) =>
ds.SeriesInstanceUID === SeriesInstanceUID &&
ds.instance.SOPInstanceUID === SOPInstanceUID
)[0];
if (segDisplaySet) {
const url = segDisplaySet.instance.url;
segBucket = url.split('https://storage.googleapis.com/')[1].split('/')[0];
fileName = url.split(`https://storage.googleapis.com/${segBucket}/`)[1];
}

const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${fileName}&contentEncoding=gzip`;
const blob = datasetToBlob(naturalizedReport);
const compressedFile = pako.gzip(await blob.arrayBuffer());

await fetch(segUploadUri, {
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/dicom',
},
body: blob,
body: compressedFile,
})
.then((response) => response.json())
.then((data) => {
Expand All @@ -275,7 +313,7 @@ const storeDicomSeg = async (naturalizedReport, headers) => {
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`,
`https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/metadata&contentEncoding=gzip`,
{
method: 'POST',
headers: {
Expand Down Expand Up @@ -320,6 +358,10 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
},
initialize: async ({ params, query, url }) => {
if (!url) url = query.get('url');
if (!url) {
url = query.toString();
query.set('url', url);
}
let metaData = getMetaDataByURL(url);

// if we have already cached the data from this specific url
Expand All @@ -331,19 +373,29 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
});
}

const buckets = query.getAll('bucket');
if (buckets.length === 0)
buckets.push('gradient-health-search-viewer-links');

const { UserAuthenticationService } = servicesManager.services;
const studyMetadata = await filesFromStudyInstanceUID({
bucketName: query.get('bucket') || 'gradient-health-search-viewer-links',
prefix: query.get('bucket-prefix') || 'dicomweb',
studyuids: query.getAll('StudyInstanceUID'),
headers: UserAuthenticationService.getAuthorizationHeader()
});

const studyMetadata = [];
for (let i = 0; i < buckets.length; i++) {
const metadataPerBucket = await filesFromStudyInstanceUID({
bucketName: buckets[i],
prefix: query.get('bucket-prefix') || 'dicomweb',
studyuids: query.getAll('StudyInstanceUID'),
headers: UserAuthenticationService.getAuthorizationHeader(),
});

studyMetadata.push(...metadataPerBucket);
}

const data = getMetadataFromRows(
_.flatten(studyMetadata),
query.get('prefix'),
_.flatten(studyMetadata),
query.get('prefix'),
query.getAll('SeriesInstanceUID')
);
);

let StudyInstanceUID;
let SeriesInstanceUID;
Expand Down Expand Up @@ -422,8 +474,9 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
console.debug('Not implemented', params)
},
series: {
metadata: ({
metadata: async ({
StudyInstanceUID,
buckets = [],
madeInClient = false,
customSort,
} = {}) => {
Expand All @@ -433,7 +486,22 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
);
}

const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
let study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];

if (!study) {
// If the study is not found, initialize the study.
// If there is no buckets in the url default bucket will be used.
const params = new URLSearchParams(window.location.search);
params.set('StudyInstanceUID', StudyInstanceUID);
params.delete('bucket');
buckets.forEach((bucket) => {
params.append('bucket', bucket);
});
await implementation.initialize({ query: params });

study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
}

let series;

if (customSort) {
Expand Down Expand Up @@ -494,7 +562,11 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
if (dataset.Modality === 'SEG') {
const headers = servicesManager.services.UserAuthenticationService.getAuthorizationHeader()
try {
await storeDicomSeg(dataset, headers)
await storeDicomSeg(
dataset,
headers,
servicesManager.services.displaySetService
);
} catch (error) {
throw error
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function DisplayValue({formIndex, name, value, defaultValue, opti
<Typography sx={{ fontSize: 14, marginRight: 1 }} color="text.secondary" gutterBottom>
{ name }:
</Typography>
<Typography variant="body2">
<Typography variant="body2" className="overflow-auto invisible-scrollbar">
{ value !== null ? value.toString() : defaultValue !== null ? defaultValue.toString() : '' }
</Typography>
</Paper>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import * as React from 'react';
import { useMemo } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import Paper from '@mui/material/Paper';
import TextField from '@mui/material/TextField';
import debounce from 'lodash.debounce';

export default function Textarea({formIndex, name, value, defaultValue, options, onChange}) {
const [val, setVal] = React.useState(value !== null ? value : (defaultValue !== null ? defaultValue : ''));
const [val, setVal] = useState(value ?? defaultValue ?? '');
const { rows } = options
const debouncedOnChange = useMemo(
() => debounce((formIndex, value) => {
onChange({formIndex, value})
}, 600), [onChange]
onChange({formIndex, value})
}, 600), [onChange]
);

useEffect(() => {
setVal(value ?? defaultValue ?? '');
}, [value, defaultValue]);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setVal(event.target.value);
debouncedOnChange(formIndex, event.target.value)
Expand Down
14 changes: 13 additions & 1 deletion extensions/ohif-gradienthealth-extension/src/getPanelModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
PanelMeasurementTableTracking,
PanelStudyBrowserTracking,
PanelForm,
PanelFormAndMeasurementTable
PanelFormAndMeasurementTable,
PanelStudyBrowser,
} from './panels';

// TODO:
Expand Down Expand Up @@ -59,6 +60,17 @@ function getPanelModule({
servicesManager,
}),
},
{
name: 'seriesList-without-tracking',
iconName: 'group-layers',
iconLabel: 'Studies',
label: 'Studies',
component: PanelStudyBrowser.bind(null, {
commandsManager,
extensionManager,
servicesManager,
}),
},
];
}

Expand Down
4 changes: 4 additions & 0 deletions extensions/ohif-gradienthealth-extension/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { id } from './id.js';
import GoogleSheetsService from './services/GoogleSheetsService';
import CropDisplayAreaService from './services/CropDisplayAreaService';
import CacheAPIService from './services/CacheAPIService';
import addSegmentationLabelModifier from './utils/addSegmentationLabelModifier';

// import { CornerstoneEventTarget } from '@cornerstonejs/core/CornerstoneEventTarget';
// import { Events } from '@cornerstonejs/core/Events';
Expand All @@ -16,6 +17,9 @@ const gradientHealthExtension = {
* Only required property. Should be a unique value across all extensions.
*/
id,
onModeEnter({ servicesManager}){
addSegmentationLabelModifier(servicesManager)
},
getDataSourcesModule: ({ servicesManager }) => {
return getDataSourcesModule({ servicesManager });
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function PanelForm({ servicesManager, extensionManager }) {
useEffect(() => {
if(!firstLoad){
setLoading(true)
GoogleSheetsService.writeFormToRow(formValue).then((values)=>{
GoogleSheetsService.updateRow(formValue).then((values)=>{
setLoading(false)
})
}
Expand Down
Loading