imadreamerboy's picture
Deploy Backyard Radiology Professor
653449c verified
Raw
History Blame Contribute Delete
4.46 kB
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