Skip to content

Commit d451701

Browse files
authored
Merge pull request #952 from NHSDigital/fix/save-valid-jpeg-from-dicom
FIX: Save valid jpeg from DICOM pixel data
2 parents d794702 + ebf25f7 commit d451701

File tree

4 files changed

+50
-2
lines changed

4 files changed

+50
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ coverage
3939
.structurizr
4040
workspace.json
4141
pyrightconfig.json
42+
43+
# API local file uploads containing DICOM data
44+
manage_breast_screening/data/dicom

manage_breast_screening/config/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ def list_env(key):
209209
# In non-production environments without a connection string, use local file storage
210210
dicom_storage_options = {
211211
"BACKEND": "django.core.files.storage.FileSystemStorage",
212+
"OPTIONS": {
213+
"location": BASE_DIR / "data" / "dicom",
214+
},
212215
}
213216
else:
214217
# In production, use DefaultAzureCredential for authentication

manage_breast_screening/dicom/dicom_recorder.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import io
22
from datetime import datetime
33

4+
import numpy as np
45
import pydicom
6+
from django.core.files.uploadedfile import InMemoryUploadedFile
57
from PIL import Image as PILImage
68

79
from .models import Image, Series, Study
@@ -46,8 +48,7 @@ def get_or_create_records(
4648
if created:
4749
image.dicom_file.save(f"{sop_uid}.dcm", dicom_file)
4850
image.image_file.save(
49-
f"{sop_uid}.jpeg",
50-
io.BytesIO(PILImage.fromarray(ds.pixel_array).tobytes()),
51+
f"{sop_uid}.jpeg", __class__.dataset_to_jpeg(sop_uid, ds)
5152
)
5253

5354
return study, series, image
@@ -61,3 +62,27 @@ def study_date_and_time(ds) -> datetime | None:
6162
study_date + study_time.split(".")[0], "%Y%m%d%H%M%S"
6263
)
6364
return None
65+
66+
@staticmethod
67+
def dataset_to_jpeg(sop_uid: str, ds: pydicom.Dataset) -> InMemoryUploadedFile:
68+
"""Convert a DICOM dataset to a JPEG image and return it as an InMemoryUploadedFile."""
69+
# Normalize pixel data to 0-255 and convert to uint8
70+
pixel_array = ds.pixel_array
71+
pixel_array = pixel_array.astype(np.float32)
72+
pixel_array -= pixel_array.min()
73+
pixel_array /= pixel_array.max()
74+
pixel_array *= 255.0
75+
pixel_array = pixel_array.astype(np.uint8)
76+
# Create a PIL image from the pixel array
77+
image = PILImage.fromarray(pixel_array, mode="L")
78+
in_memory_file = io.BytesIO()
79+
image.save(in_memory_file, format="JPEG")
80+
81+
return InMemoryUploadedFile(
82+
in_memory_file,
83+
name=f"{sop_uid}.jpeg",
84+
field_name="file",
85+
content_type="image/jpeg",
86+
size=in_memory_file.getbuffer().nbytes,
87+
charset=None,
88+
)

manage_breast_screening/dicom/tests/test_dicom_recorder.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import tempfile
22
from datetime import datetime
3+
from unittest.mock import patch
34

5+
import numpy as np
46
import pydicom
57
import pytest
68

@@ -84,3 +86,18 @@ def test_get_or_create_records_invalid_dicom(self, source_message_id):
8486
with open(temp_file.name, "rb") as dicom_file:
8587
with pytest.raises(AttributeError):
8688
DicomRecorder.get_or_create_records(source_message_id, dicom_file)
89+
90+
def test_dataset_to_jpeg(self, dataset):
91+
converted_pixel_array = dataset.pixel_array.astype(np.float32)
92+
converted_pixel_array -= converted_pixel_array.min()
93+
converted_pixel_array /= converted_pixel_array.max()
94+
converted_pixel_array *= 255.0
95+
converted_pixel_array = converted_pixel_array.astype(np.uint8)
96+
97+
with patch(f"{DicomRecorder.__module__}.PILImage.fromarray") as mock_fromarray:
98+
DicomRecorder.dataset_to_jpeg(dataset.SOPInstanceUID, dataset)
99+
100+
assert mock_fromarray.call_count == 1
101+
assert mock_fromarray.call_args[0][0].shape == converted_pixel_array.shape
102+
assert np.array_equal(mock_fromarray.call_args[0][0], converted_pixel_array)
103+
assert mock_fromarray.call_args[1]["mode"] == "L"

0 commit comments

Comments
 (0)