Skip to content
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

Add support for Imaging Data Commons #1450

Merged
merged 6 commits into from
Feb 15, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,6 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
msg = f'Invalid parent type: {parentType}'
raise RuntimeError(msg)

from wsidicom.uid import WSI_SOP_CLASS_UID

limit = params.get('limit')
search_filters = params.get('search_filters', {})

Expand All @@ -433,24 +431,33 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
series_uid_key = dicom_key_to_tag('SeriesInstanceUID')
instance_uid_key = dicom_key_to_tag('SOPInstanceUID')

# We are only searching for WSI datasets. Ignore all others.
# FIXME: is this actually working? For the SLIM server at
# https://imagingdatacommons.github.io/slim/, none of the series
# report a SOPClassUID, but we still get all results anyways.
search_filters = {
'SOPClassUID': WSI_SOP_CLASS_UID,
**search_filters,
}
# Search for studies. Apply the limit and search filters.
fields = [
study_uid_key,
]
if progress:
progress.update(message='Searching for studies...')

studies_results = client.search_for_studies(
limit=limit,
fields=fields,
search_filters=search_filters,
)

# Search for all series in the returned studies.
fields = [
study_uid_key,
series_uid_key,
]
if progress:
progress.update(message='Searching for series...')

# FIXME: might need to search in chunks for larger web servers
series_results = client.search_for_series(
fields=fields, limit=limit, search_filters=search_filters)
series_results = []
for study in studies_results:
study_uid = study[study_uid_key]['Value'][0]
series_results += client.search_for_series(study_uid, fields=fields)

# Create folders for each study, items for each series, and files for each instance.
items = []
for i, result in enumerate(series_results):
if progress:
Expand Down Expand Up @@ -496,8 +503,9 @@ def importData(self, parent, parentType, params, progress, user, **kwargs):
}
file['imported'] = True

# Try to infer the file size without streaming, if possible.
file['size'] = self._infer_file_size(file)
# Inferring the file size can take a long time, so don't
# do it right away, unless we figure out a way to make it faster.
# file['size'] = self._infer_file_size(file)
file = File().save(file)

items.append(item)
Expand Down
6 changes: 3 additions & 3 deletions sources/dicom/large_image_source_dicom/assetstore/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _importData(self, assetstore, params, progress):
)

if not items:
msg = 'No DICOM objects matching the search filters were found'
msg = 'No studies matching the search filters were found'
raise RestException(msg)

@access.admin(scope=TokenScope.DATA_WRITE)
Expand All @@ -77,9 +77,9 @@ def _importData(self, assetstore, params, progress):
.param('destinationType', 'The type of the parent object to import into.',
enum=('folder', 'user', 'collection'),
required=False, default='folder')
.param('limit', 'The maximum number of results to import.',
.param('limit', 'The maximum number of studies to import.',
required=False, default=None)
.param('filters', 'Any search parameters to filter DICOM objects.',
.param('filters', 'Any search parameters to filter the studies query.',
required=False, default='{}')
.param('progress', 'Whether to record progress on this operation.',
required=False, default=False, dataType='boolean')
Expand Down
15 changes: 11 additions & 4 deletions sources/dicom/large_image_source_dicom/dicomweb_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,25 @@
image_type_tag = dicom_key_to_tag('ImageType')
instance_uid_tag = dicom_key_to_tag('SOPInstanceUID')

search_filters = {
'SOPClassUID': WSI_SOP_CLASS_UID,
}
# We can't include the SOPClassUID as a search filter because Imaging Data Commons
# produces an error if we do. Perform the filtering manually instead.
class_uid_tag = dicom_key_to_tag('SOPClassUID')

fields = [
image_type_tag,
instance_uid_tag,
class_uid_tag,
]
wsi_instances = client.search_for_instances(
study_uid, series_uid, search_filters=search_filters, fields=fields)
study_uid, series_uid, fields=fields)

volume_instance = None
for instance in wsi_instances:
class_type = instance.get(class_uid_tag, {}).get('Value')
if not class_type or class_type[0] != WSI_SOP_CLASS_UID:
# Only look at WSI classes
continue

Check warning on line 51 in sources/dicom/large_image_source_dicom/dicomweb_utils.py

View check run for this annotation

Codecov / codecov/patch

sources/dicom/large_image_source_dicom/dicomweb_utils.py#L51

Added line #L51 was not covered by tests

image_type = instance.get(image_type_tag, {}).get('Value')
# It would be nice if we could have a search filter for this, but
# I didn't see one...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ form.g-dwas-import-form
.input-group-btn
button.g-open-browser.btn.btn-default(type="button")
i.icon-folder-open
label(for="g-dwas-import-limit") Limit (Series)
label(for="g-dwas-import-limit") Limit (Studies)
input#g-dwas-import-limit.form-control(
type="number", step="1", min="1", value="")
label(for="g-dwas-import-filters") Filters
textarea#g-dwas-import-filters.form-control(rows="10",
placeholder="Valid JSON. e.g.:\n\n{\n \"PatientID\": \"ABC123\"\n}")
type="number", step="1", min="1", value="10")
label(for="g-dwas-import-filters") Filters (Studies)
textarea#g-dwas-import-filters.form-control(rows="10")
| {
| "ModalitiesInStudy": "SM"
| }
.g-validation-failed-message
button.g-submit-assetstore-import.btn.btn-success(type="submit")
i.icon-link-ext
Expand Down
12 changes: 7 additions & 5 deletions sources/dicom/test_dicom/web_client_specs/dicomWebSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ describe('DICOMWeb assetstore', function () {
var dicomFileContent;

// After importing, we will verify that this item exists
const verifyItemName = '1.3.6.1.4.1.5962.99.1.3205815762.381594633.1639588388306.2.0';
const studyUID = '2.25.25644321580420796312527343668921514374';
const seriesUID = '1.3.6.1.4.1.5962.99.1.3205815762.381594633.1639588388306.2.0';
const verifyItemName = seriesUID;

runs(function () {
$('a.g-nav-link[g-target="admin"]').trigger('click');
Expand Down Expand Up @@ -173,20 +175,20 @@ describe('DICOMWeb assetstore', function () {

runs(function () {
// Perform a search where no results are returned
const filters = '{"SeriesInstanceUID": "DOES_NOT_EXIST"}';
const filters = '{"StudyInstanceUID": "DOES_NOT_EXIST"}';
$('#g-dwas-import-filters').val(filters);
$('.g-submit-assetstore-import').trigger('click');
});

waitsFor(function () {
const msg = 'No DICOM objects matching the search filters were found';
const msg = 'No studies matching the search filters were found';
return $('.g-validation-failed-message').html() === msg;
}, 'No results check');

runs(function () {
// Fix the filters
// We will only import this specific SeriesInstanceUID
const filters = '{"SeriesInstanceUID": "' + verifyItemName + '"}';
// We will only import this specific StudyInstanceUID
const filters = '{"StudyInstanceUID": "' + studyUID + '"}';
$('#g-dwas-import-filters').val(filters);

// This one should work fine
Expand Down