Skip to content

Commit 22c31bb

Browse files
authored
Alter DICOM endpoint response (#4334)
Return an array of objects for the image frames which allows for more flexibility in the future. Adds the image set and image frame ids for cross-referencing. Adds the accept and accept encoding headers to ensure the responses are consistent. Stores extra metadata that is required for fetching of each image frame. See DIAGNijmegen/rse-roadmap#432
1 parent b37f0e7 commit 22c31bb

6 files changed

Lines changed: 405 additions & 45 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Generated by Django 4.2.25 on 2025-10-08 11:51
2+
3+
from django.db import migrations, models
4+
5+
import grandchallenge.core.validators
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("cases", "0019_alter_dicomimagesetupload_user_uploads"),
12+
]
13+
14+
operations = [
15+
migrations.RemoveField(
16+
model_name="dicomimageset",
17+
name="image_frame_ids",
18+
),
19+
migrations.AddField(
20+
model_name="dicomimageset",
21+
name="image_frame_metadata",
22+
field=models.JSONField(
23+
default=list,
24+
editable=False,
25+
help_text="The metadata of the image frames in AWS Health Imaging",
26+
validators=[
27+
grandchallenge.core.validators.JSONValidator(
28+
schema={
29+
"$schema": "http://json-schema.org/draft-07/schema#",
30+
"items": {
31+
"additionalProperties": False,
32+
"properties": {
33+
"frame_size_in_bytes": {
34+
"min_value": 0,
35+
"type": "integer",
36+
},
37+
"image_frame_id": {
38+
"maxLength": 32,
39+
"minLength": 32,
40+
"pattern": "^[0-9a-f]{32}$",
41+
"type": "string",
42+
},
43+
"series_instance_uid": {
44+
"maxLength": 64,
45+
"minLength": 60,
46+
"pattern": "^[0-9.]*$",
47+
"type": "string",
48+
},
49+
"sop_instance_uid": {
50+
"maxLength": 64,
51+
"minLength": 60,
52+
"pattern": "^[0-9.]*$",
53+
"type": "string",
54+
},
55+
"stored_transfer_syntax_uid": {
56+
"maxLength": 25,
57+
"minLength": 19,
58+
"pattern": "^1.2.840.10008.1.2[0-9.]*$",
59+
"type": "string",
60+
},
61+
"study_instance_uid": {
62+
"maxLength": 64,
63+
"minLength": 60,
64+
"pattern": "^[0-9.]*$",
65+
"type": "string",
66+
},
67+
},
68+
"required": [
69+
"image_frame_id",
70+
"frame_size_in_bytes",
71+
"study_instance_uid",
72+
"series_instance_uid",
73+
"sop_instance_uid",
74+
"stored_transfer_syntax_uid",
75+
],
76+
"type": "object",
77+
},
78+
"minItems": 1,
79+
"type": "array",
80+
}
81+
)
82+
],
83+
),
84+
preserve_default=False,
85+
),
86+
]

app/grandchallenge/cases/models.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -337,19 +337,61 @@ class DICOMImageSet(UUIDModel):
337337
help_text="The ID of the image set in AWS Health Imaging",
338338
editable=False,
339339
)
340-
image_frame_ids = models.JSONField(
340+
image_frame_metadata = models.JSONField(
341341
editable=False,
342-
help_text="The IDs of the image frames in AWS Health Imaging",
342+
help_text="The metadata of the image frames in AWS Health Imaging",
343343
validators=[
344344
JSONValidator(
345345
schema={
346346
"$schema": "http://json-schema.org/draft-07/schema#",
347347
"type": "array",
348348
"items": {
349-
"type": "string",
350-
"pattern": "^[0-9a-f]{32}$",
351-
"minLength": 32,
352-
"maxLength": 32,
349+
"type": "object",
350+
"required": [
351+
"image_frame_id",
352+
"frame_size_in_bytes",
353+
"study_instance_uid",
354+
"series_instance_uid",
355+
"sop_instance_uid",
356+
"stored_transfer_syntax_uid",
357+
],
358+
"additionalProperties": False,
359+
"properties": {
360+
"image_frame_id": {
361+
"type": "string",
362+
"pattern": "^[0-9a-f]{32}$",
363+
"minLength": 32,
364+
"maxLength": 32,
365+
},
366+
"frame_size_in_bytes": {
367+
"type": "integer",
368+
"min_value": 0,
369+
},
370+
"study_instance_uid": {
371+
"type": "string",
372+
"pattern": "^[0-9.]*$",
373+
"minLength": 60,
374+
"maxLength": 64,
375+
},
376+
"series_instance_uid": {
377+
"type": "string",
378+
"pattern": "^[0-9.]*$",
379+
"minLength": 60,
380+
"maxLength": 64,
381+
},
382+
"sop_instance_uid": {
383+
"type": "string",
384+
"pattern": "^[0-9.]*$",
385+
"minLength": 60,
386+
"maxLength": 64,
387+
},
388+
"stored_transfer_syntax_uid": {
389+
"type": "string",
390+
"pattern": "^1.2.840.10008.1.2[0-9.]*$",
391+
"minLength": 19,
392+
"maxLength": 25,
393+
},
394+
},
353395
},
354396
"minItems": 1,
355397
}
@@ -1116,10 +1158,22 @@ def _get_image_set_metadata(self, *, image_set_id):
11161158

11171159
return metadata
11181160

1119-
def _get_image_frame_ids(self, *, image_set_id):
1161+
def _get_image_frame_metadata(self, *, image_set_id):
11201162
metadata = self._get_image_set_metadata(image_set_id=image_set_id)
1163+
11211164
return [
1122-
frame["ID"]
1165+
{
1166+
"study_instance_uid": metadata["Study"]["DICOM"][
1167+
"StudyInstanceUID"
1168+
],
1169+
"series_instance_uid": series["DICOM"]["SeriesInstanceUID"],
1170+
"sop_instance_uid": instance["DICOM"]["SOPInstanceUID"],
1171+
"stored_transfer_syntax_uid": instance[
1172+
"StoredTransferSyntaxUID"
1173+
],
1174+
"image_frame_id": frame["ID"],
1175+
"frame_size_in_bytes": frame["FrameSizeInBytes"],
1176+
}
11231177
for series in metadata["Study"]["Series"].values()
11241178
for instance in series["Instances"].values()
11251179
for frame in instance["ImageFrames"]
@@ -1309,7 +1363,7 @@ def revert_image_set_to_initial_version(*, image_set_summary):
13091363
def convert_image_set_to_internal(self, *, image_set_id):
13101364
dicom_image_set = DICOMImageSet(
13111365
image_set_id=image_set_id,
1312-
image_frame_ids=self._get_image_frame_ids(
1366+
image_frame_metadata=self._get_image_frame_metadata(
13131367
image_set_id=image_set_id
13141368
),
13151369
dicom_image_set_upload=self,

app/grandchallenge/cases/views.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,34 @@ def serialize_aws_request(request):
110110
"headers": dict(request.headers.items()),
111111
}
112112

113+
@staticmethod
114+
def get_frame_encoding(stored_transfer_syntax_uid):
115+
# From https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/medical-imaging/client/get_image_frame.html
116+
transfer_syntax_to_encoding = {
117+
"1.2.840.10008.1.2.1": "application/octet-stream",
118+
"1.2.840.10008.1.2.4.50": "image/jpeg",
119+
"1.2.840.10008.1.2.4.91": "image/j2c",
120+
"1.2.840.10008.1.2.4.100": "video/mpeg",
121+
"1.2.840.10008.1.2.4.100.1": "video/mpeg",
122+
"1.2.840.10008.1.2.4.101": "video/mpeg",
123+
"1.2.840.10008.1.2.4.101.1": "video/mpeg",
124+
"1.2.840.10008.1.2.4.102": "video/mp4",
125+
"1.2.840.10008.1.2.4.102.1": "video/mp4",
126+
"1.2.840.10008.1.2.4.103": "video/mp4",
127+
"1.2.840.10008.1.2.4.103.1": "video/mp4",
128+
"1.2.840.10008.1.2.4.104": "video/mp4",
129+
"1.2.840.10008.1.2.4.104.1": "video/mp4",
130+
"1.2.840.10008.1.2.4.105": "video/mp4",
131+
"1.2.840.10008.1.2.4.105.1": "video/mp4",
132+
"1.2.840.10008.1.2.4.106": "video/mp4",
133+
"1.2.840.10008.1.2.4.106.1": "video/mp4",
134+
"1.2.840.10008.1.2.4.107": "video/H256",
135+
"1.2.840.10008.1.2.4.108": "video/H256",
136+
"1.2.840.10008.1.2.4.202": "image/jph",
137+
"1.2.840.10008.1.2.4.203": "image/jphc",
138+
}
139+
return transfer_syntax_to_encoding[stored_transfer_syntax_uid]
140+
113141
@action(detail=True, url_path="dicom")
114142
def dicom(self, request, pk=None):
115143
image = self.get_object()
@@ -130,33 +158,50 @@ def dicom(self, request, pk=None):
130158

131159
image_set_url = f"https://runtime-medical-imaging.{settings.AWS_DEFAULT_REGION}.amazonaws.com/datastore/{settings.AWS_HEALTH_IMAGING_DATASTORE_ID}/imageSet/{image.dicom_image_set.image_set_id}"
132160

133-
image_frame_requests = {}
161+
serialized_image_frames = []
162+
163+
for image_frame in image.dicom_image_set.image_frame_metadata:
164+
image_frame_id = image_frame["image_frame_id"]
134165

135-
for frame_id in image.dicom_image_set.image_frame_ids:
136-
frame_request = AWSRequest(
166+
image_frame_request = AWSRequest(
137167
method="POST",
138168
url=f"{image_set_url}/getImageFrame",
139-
data=json.dumps({"imageFrameId": frame_id}),
169+
data=json.dumps({"imageFrameId": image_frame_id}),
170+
headers={
171+
"Accept": self.get_frame_encoding(
172+
image_frame["stored_transfer_syntax_uid"]
173+
),
174+
},
140175
)
141-
medical_imaging_auth.add_auth(frame_request)
176+
medical_imaging_auth.add_auth(image_frame_request)
142177

143-
image_frame_requests[frame_id] = self.serialize_aws_request(
144-
frame_request
178+
serialized_image_frames.append(
179+
{
180+
"image_frame_id": image_frame_id,
181+
"get_image_frame": self.serialize_aws_request(
182+
image_frame_request
183+
),
184+
}
145185
)
146186

147187
metadata_request = AWSRequest(
148188
method="POST",
149189
url=f"{image_set_url}/getImageSetMetadata",
150190
data=json.dumps({"versionId": "1"}),
191+
headers={
192+
"Accept-Encoding": "gzip",
193+
"Accept": "application/json",
194+
},
151195
)
152196
medical_imaging_auth.add_auth(metadata_request)
153197

154198
return JsonResponse(
155199
{
200+
"image_set_id": image.dicom_image_set.image_set_id,
156201
"get_image_set_metadata": self.serialize_aws_request(
157202
metadata_request
158203
),
159-
"get_image_frames": image_frame_requests,
204+
"image_frames": serialized_image_frames,
160205
}
161206
)
162207

app/tests/cases_tests/factories.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,24 @@ def fake_image_frame_id():
161161
return "".join(random.choices(characters, k=32))
162162

163163

164+
def fake_dicom_instance_uid():
165+
characters = ".0123456789"
166+
return "".join(random.choices(characters, k=64))
167+
168+
164169
class DICOMImageSetFactory(factory.django.DjangoModelFactory):
165-
image_frame_ids = factory.LazyAttribute(
166-
lambda _: [fake_image_frame_id() for _ in range(5)]
170+
image_frame_metadata = factory.LazyAttribute(
171+
lambda _: [
172+
{
173+
"image_frame_id": fake_image_frame_id(),
174+
"frame_size_in_bytes": 1337,
175+
"study_instance_uid": fake_dicom_instance_uid(),
176+
"series_instance_uid": fake_dicom_instance_uid(),
177+
"sop_instance_uid": fake_dicom_instance_uid(),
178+
"stored_transfer_syntax_uid": "1.2.840.10008.1.2.4.202",
179+
}
180+
for _ in range(5)
181+
]
167182
)
168183
dicom_image_set_upload = factory.SubFactory(DICOMImageSetUploadFactory)
169184

0 commit comments

Comments
 (0)