| from __future__ import annotations |
|
|
| from pathlib import Path |
| from zipfile import ZipFile |
|
|
| import numpy as np |
| import pytest |
| from pydicom.dataset import FileDataset, FileMetaDataset |
| from pydicom.uid import ( |
| DigitalXRayImageStorageForPresentation, |
| ExplicitVRLittleEndian, |
| JPEG2000Lossless, |
| JPEGBaseline8Bit, |
| JPEGLSLossless, |
| generate_uid, |
| ) |
| from pydicom.pixels import get_decoder |
|
|
| from radiology_trainer.study import load_study |
|
|
|
|
| def test_dicom_study_sorts_frontal_before_lateral_and_renders(tmp_path: Path) -> None: |
| lateral = _write_dicom(tmp_path / "lateral.dcm", view="LATERAL", instance=2) |
| frontal = _write_dicom(tmp_path / "frontal.dcm", view="PA", instance=1) |
| study = load_study( |
| study_id="study", |
| title="Test", |
| source="upload", |
| paths=[lateral, frontal], |
| work_dir=tmp_path, |
| ) |
| assert study.public.images[0].projection == "PA" |
| assert study.public.primary_image_id == study.public.images[0].id |
| assert study.primary().render().size == (64, 48) |
| assert study.public.images[0].metadata.pixel_spacing_mm == (0.2, 0.2) |
|
|
|
|
| def test_multiframe_dicom_becomes_ordered_study_images(tmp_path: Path) -> None: |
| path = _write_dicom(tmp_path / "multi.dcm", frames=2) |
| study = load_study( |
| study_id="study", |
| title="Multi", |
| source="upload", |
| paths=[path], |
| work_dir=tmp_path, |
| ) |
| assert len(study.public.images) == 2 |
| assert [item.metadata.frame_number for item in study.public.images] == [1, 2] |
|
|
|
|
| def test_non_chest_or_non_xray_dicom_is_rejected(tmp_path: Path) -> None: |
| path = _write_dicom(tmp_path / "ct.dcm", modality="CT") |
| with pytest.raises(ValueError, match="Only CR and DX"): |
| load_study( |
| study_id="study", |
| title="CT", |
| source="upload", |
| paths=[path], |
| work_dir=tmp_path, |
| ) |
|
|
|
|
| def test_zip_path_traversal_is_rejected(tmp_path: Path) -> None: |
| archive = tmp_path / "unsafe.zip" |
| with ZipFile(archive, "w") as output: |
| output.writestr("../outside.dcm", b"unsafe") |
| with pytest.raises(ValueError, match="unsafe path"): |
| load_study( |
| study_id="study", |
| title="Unsafe", |
| source="upload", |
| paths=[archive], |
| work_dir=tmp_path / "work", |
| ) |
|
|
|
|
| def test_monochrome1_inverts_default_render(tmp_path: Path) -> None: |
| mono1 = _write_dicom(tmp_path / "mono1.dcm", photometric="MONOCHROME1") |
| study = load_study( |
| study_id="study", |
| title="Mono", |
| source="upload", |
| paths=[mono1], |
| work_dir=tmp_path, |
| ) |
| default = np.asarray(study.primary().render()) |
| inverted = np.asarray(study.primary().render(invert=True)) |
| assert np.mean(default) > np.mean(inverted) |
|
|
|
|
| @pytest.mark.parametrize( |
| "transfer_syntax", |
| [JPEGBaseline8Bit, JPEGLSLossless, JPEG2000Lossless], |
| ) |
| def test_required_compressed_dicom_decoders_are_available(transfer_syntax) -> None: |
| assert get_decoder(transfer_syntax).is_available |
|
|
|
|
| def _write_dicom( |
| path: Path, |
| *, |
| view: str = "PA", |
| instance: int = 1, |
| modality: str = "DX", |
| photometric: str = "MONOCHROME2", |
| frames: int = 1, |
| ) -> Path: |
| meta = FileMetaDataset() |
| meta.MediaStorageSOPClassUID = DigitalXRayImageStorageForPresentation |
| meta.MediaStorageSOPInstanceUID = generate_uid() |
| meta.TransferSyntaxUID = ExplicitVRLittleEndian |
| dataset = FileDataset(str(path), {}, file_meta=meta, preamble=b"\0" * 128) |
| dataset.SOPClassUID = meta.MediaStorageSOPClassUID |
| dataset.SOPInstanceUID = meta.MediaStorageSOPInstanceUID |
| dataset.Modality = modality |
| dataset.BodyPartExamined = "CHEST" |
| dataset.ViewPosition = view |
| dataset.InstanceNumber = instance |
| dataset.Rows = 48 |
| dataset.Columns = 64 |
| dataset.SamplesPerPixel = 1 |
| dataset.PhotometricInterpretation = photometric |
| dataset.BitsAllocated = 16 |
| dataset.BitsStored = 12 |
| dataset.HighBit = 11 |
| dataset.PixelRepresentation = 0 |
| dataset.WindowCenter = 1000 |
| dataset.WindowWidth = 2000 |
| dataset.PixelSpacing = [0.2, 0.2] |
| pixels = np.linspace(0, 2000, dataset.Rows * dataset.Columns, dtype=np.uint16) |
| if frames > 1: |
| dataset.NumberOfFrames = frames |
| pixels = np.stack([pixels.reshape(dataset.Rows, dataset.Columns)] * frames) |
| dataset.PixelData = pixels.tobytes() |
| dataset.save_as(path, enforce_file_format=True) |
| return path |
|
|