diff --git a/.gitignore b/.gitignore index dea060a..a444de5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ __pycache__ ~* *.venv /fileformats/medimage/_version.py +/extras/fileformats/extras/medimage/_version.py diff --git a/extras/fileformats/extras/medimage/_version.py b/extras/fileformats/extras/medimage/_version.py deleted file mode 100644 index 7519301..0000000 --- a/extras/fileformats/extras/medimage/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# file generated by setuptools_scm -# don't change, don't track in version control -__version__ = version = '0.1.6.dev12+ga52eb33' -__version_tuple__ = version_tuple = (0, 1, 6, 'dev12', 'ga52eb33') diff --git a/extras/fileformats/extras/medimage/dicom.py b/extras/fileformats/extras/medimage/dicom.py index 98167d5..e97bee3 100644 --- a/extras/fileformats/extras/medimage/dicom.py +++ b/extras/fileformats/extras/medimage/dicom.py @@ -11,7 +11,7 @@ @MedicalImage.read_array.register -def dicom_read_array(collection: DicomCollection): +def dicom_read_array(collection: DicomCollection) -> np.ndarray: image_stack = [] for dcm_file in collection.contents: image_stack.append(pydicom.dcmread(dcm_file).pixel_array) @@ -19,14 +19,14 @@ def dicom_read_array(collection: DicomCollection): @MedicalImage.vox_sizes.register -def dicom_vox_sizes(collection: DicomCollection): +def dicom_vox_sizes(collection: DicomCollection) -> ty.Tuple[float, float, float]: return tuple( collection.metadata["PixelSpacing"] + [collection.metadata["SliceThickness"]] ) @MedicalImage.dims.register -def dicom_dims(collection: DicomCollection): +def dicom_dims(collection: DicomCollection) -> ty.Tuple[int, int, int]: return tuple( ( collection.metadata["Rows"], @@ -37,12 +37,12 @@ def dicom_dims(collection: DicomCollection): @DicomCollection.series_number.register -def dicom_series_number(collection: DicomCollection): +def dicom_series_number(collection: DicomCollection) -> str: return str(collection.metadata["SeriesNumber"]) @FileSet.generate_sample_data.register -def dicom_dir_generate_sample_data(dcmdir: DicomDir, dest_dir: Path, seed: ty.Union[int, Random], stem: ty.Optional[str]): +def dicom_dir_generate_sample_data(dcmdir: DicomDir, dest_dir: Path, seed: ty.Union[int, Random] = 0, stem: ty.Optional[str] = None) -> ty.Iterable[Path]: dcm_dir = medimages4tests.dummy.dicom.mri.t1w.siemens.skyra.syngo_d13c.get_image() # Set series number to random value to make it different if isinstance(seed, Random): @@ -60,7 +60,7 @@ def dicom_dir_generate_sample_data(dcmdir: DicomDir, dest_dir: Path, seed: ty.Un @FileSet.generate_sample_data.register -def dicom_set_generate_sample_data(dcm_series: DicomSeries, dest_dir: Path, seed: int, stem: ty.Optional[str]): +def dicom_set_generate_sample_data(dcm_series: DicomSeries, dest_dir: Path, seed: ty.Union[int, Random] = 0, stem: ty.Optional[str] = None) -> ty.Iterable[Path]: rng = Random(seed) dicom_dir = dicom_dir_generate_sample_data(dcm_series, dest_dir=mkdtemp(), seed=rng, stem=None)[0] stem = gen_filename(rng, stem=stem) diff --git a/extras/fileformats/extras/medimage/diffusion.py b/extras/fileformats/extras/medimage/diffusion.py index 0b8399f..9267153 100644 --- a/extras/fileformats/extras/medimage/diffusion.py +++ b/extras/fileformats/extras/medimage/diffusion.py @@ -1,14 +1,14 @@ import numpy as np -from fileformats.medimage.diffusion import DwiEncoding, Bval, Bvec +from fileformats.medimage import DwiEncoding, Bval, Bvec @Bval.read_array.register -def bval_read_array(bval: Bval): +def bval_read_array(bval: Bval) -> np.ndarray: # noqa return np.asarray([float(ln) for ln in bval.read_contents().split()]) @DwiEncoding.read_array.register -def bvec_read_array(bvec: Bvec): +def bvec_read_array(bvec: Bvec) -> np.ndarray: # noqa bvals = bvec.b_values_file.read_array() directions = np.asarray( [[float(x) for x in ln.split()] for ln in bvec.read_contents().splitlines()] diff --git a/extras/fileformats/extras/medimage/nifti.py b/extras/fileformats/extras/medimage/nifti.py index 608871b..bb40982 100644 --- a/extras/fileformats/extras/medimage/nifti.py +++ b/extras/fileformats/extras/medimage/nifti.py @@ -1,51 +1,65 @@ from pathlib import Path import typing as ty +from random import Random import nibabel +import numpy as np from fileformats.core import FileSet +from fileformats.core.utils import gen_filename from fileformats.medimage import MedicalImage, Nifti, NiftiGz, Nifti1, NiftiGzX, NiftiX import medimages4tests.dummy.nifti @FileSet.read_metadata.register -def nifti_read_metadata(nifti: Nifti): +def nifti_read_metadata(nifti: Nifti) -> ty.Mapping[str, ty.Any]: return dict(nibabel.load(nifti.fspath).header) @MedicalImage.read_array.register -def nifti_data_array(nifti: Nifti): +def nifti_data_array(nifti: Nifti) -> np.ndarray: # noqa return nibabel.load(nifti.fspath).get_data() @MedicalImage.vox_sizes.register -def nifti_vox_sizes(nifti: Nifti): +def nifti_vox_sizes(nifti: Nifti) -> ty.Tuple[float, float, float]: # FIXME: This won't work for 4-D files - return nifti.metadata["pixdim"][1:4] + return tuple(float(d) for d in nifti.metadata["pixdim"][1:4]) @MedicalImage.dims.register -def nifti_dims(nifti: Nifti): +def nifti_dims(nifti: Nifti) -> ty.Tuple[int, int, int]: # FIXME: This won't work for 4-D files - return nifti.metadata["dim"][1:4] + return tuple(int(d) for d in nifti.metadata["dim"][1:4]) @FileSet.generate_sample_data.register -def nifti_generate_sample_data(nifti: Nifti1, dest_dir: Path, seed: int, stem: ty.Optional[str]): - return medimages4tests.dummy.nifti.get_image(out_file=dest_dir / "nifti.nii") +def nifti_generate_sample_data( + nifti: Nifti1, dest_dir: Path, seed: ty.Union[int, Random] = 0, stem: ty.Optional[str] = None +) -> ty.Iterable[Path]: + return medimages4tests.dummy.nifti.get_image( + out_file=dest_dir / gen_filename(seed, file_type=Nifti1, stem=stem) + ) @FileSet.generate_sample_data.register -def nifti_gz_generate_sample_data(nifti: NiftiGz, dest_dir: Path, seed: int, stem: ty.Optional[str]): +def nifti_gz_generate_sample_data( + nifti: NiftiGz, dest_dir: Path, seed: ty.Union[int, Random] = 0, stem: ty.Optional[str] = None +) -> ty.Iterable[Path]: return medimages4tests.dummy.nifti.get_image( - out_file=dest_dir / "nifti.nii.gz", compressed=True + out_file=dest_dir / gen_filename(seed, file_type=NiftiGz, stem=stem), + compressed=True, ) @FileSet.generate_sample_data.register -def nifti_gz_x_generate_sample_data(nifti: NiftiGzX, dest_dir: Path, seed: int, stem: ty.Optional[str]): +def nifti_gz_x_generate_sample_data( + nifti: NiftiGzX, dest_dir: Path, seed: ty.Union[int, Random] = 0, stem: ty.Optional[str] = None +) -> ty.Iterable[Path]: return medimages4tests.mri.neuro.t1w.get_image() @FileSet.generate_sample_data.register -def nifti_x_generate_sample_data(nifti: NiftiX, dest_dir: Path, seed: int, stem: ty.Optional[str]): +def nifti_x_generate_sample_data( + nifti: NiftiX, dest_dir: Path, seed: ty.Union[int, Random] = 0, stem: ty.Optional[str] = None +) -> ty.Iterable[Path]: nifti_gz_x = NiftiGzX(medimages4tests.mri.neuro.t1w.get_image()) return NiftiX.convert(nifti_gz_x) diff --git a/fileformats/medimage/__init__.py b/fileformats/medimage/__init__.py index 7873030..0dace6b 100644 --- a/fileformats/medimage/__init__.py +++ b/fileformats/medimage/__init__.py @@ -1,12 +1,13 @@ -from ._version import __version__ -from .base import MedicalImage -from fileformats.application import Dicom # imported to alias it here as well +from ._version import __version__ # noqa: F401 +from .base import MedicalImage # noqa: F401 +# import Dicom to alias to the medimage namespace it here as well +from fileformats.application import Dicom # noqa: F401 from .misc import ( # noqa: F401 Analyze, Mgh, MghGz, ) -from .nifti import ( +from .nifti import ( # noqa: F401 Nifti, Nifti1, Nifti2, @@ -14,7 +15,7 @@ NiftiX, NiftiGzX, ) -from .diffusion import ( +from .diffusion import ( # noqa: F401 DwiEncoding, Bvec, Bval, @@ -42,3 +43,6 @@ Vnd_Siemens_Biograph128Vision_Vr20b_PetCountRate, Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation, ) +from .surface import ( + Gifti # noqa: F401 +) diff --git a/fileformats/medimage/base.py b/fileformats/medimage/base.py index b72869e..5526058 100644 --- a/fileformats/medimage/base.py +++ b/fileformats/medimage/base.py @@ -19,18 +19,18 @@ class MedicalImage(FileSet): binary = True @hook.extra - def read_array(self): + def read_array(self) -> "numpy.ndarray": # noqa """ Returns the binary data of the image in a numpy array """ raise NotImplementedError @hook.extra - def vox_sizes(self) -> ty.Tuple[float]: + def vox_sizes(self) -> ty.Tuple[float, float, float]: """The length of the voxels along each dimension""" raise NotImplementedError @hook.extra - def dims(self) -> ty.Tuple[int]: + def dims(self) -> ty.Tuple[int, int, int]: """The dimensions of the image""" raise NotImplementedError diff --git a/fileformats/medimage/dicom.py b/fileformats/medimage/dicom.py index 509b03e..70bf677 100644 --- a/fileformats/medimage/dicom.py +++ b/fileformats/medimage/dicom.py @@ -26,7 +26,7 @@ def __len__(self): return len(self.contents) @hook.extra - def series_number(self): + def series_number(self) -> str: raise NotImplementedError @cached_property @@ -51,7 +51,7 @@ def from_paths( @FileSet.read_metadata.register -def dicom_collection_read_metadata(collection: DicomCollection) -> ty.Dict[str, ty.Any]: +def dicom_collection_read_metadata(collection: DicomCollection) -> ty.Mapping[str, ty.Any]: # Collated DICOM headers across series collated = copy(collection.contents[0].metadata) for i, dicom in enumerate(collection.contents[1:], start=1): diff --git a/fileformats/medimage/diffusion.py b/fileformats/medimage/diffusion.py index 849532f..0ddc89a 100644 --- a/fileformats/medimage/diffusion.py +++ b/fileformats/medimage/diffusion.py @@ -10,21 +10,21 @@ class DwiEncoding(File): iana_mime: ty.Optional[str] = None @hook.extra - def read_array(self): + def read_array(self) -> "numpy.ndarray": # noqa "Both the gradient direction and weighting combined into a single Nx4 array" raise NotImplementedError @property - def array(self): + def array(self) -> "numpy.ndarray": # noqa return self.read_array() @property - def directions(self): + def directions(self) -> "numpy.ndarray": # noqa "gradient direction and weighting combined into a single Nx4 array" return self.array[:, :3] @property - def b_values(self): + def b_values(self) -> "numpy.ndarray": # noqa "the b-value weighting" return self.array[:, 3] @@ -34,7 +34,7 @@ class Bval(File): ext = ".bval" @hook.extra - def read_array(self): + def read_array(self) -> "numpy.ndarray": # noqa raise NotImplementedError diff --git a/fileformats/medimage/radlex/__init__.py b/fileformats/medimage/radlex/__init__.py new file mode 100644 index 0000000..3d628c0 --- /dev/null +++ b/fileformats/medimage/radlex/__init__.py @@ -0,0 +1,6 @@ +from fileformats.core import ClassifierCategory + + +class AnatomicalEntity(ClassifierCategory): + pass + diff --git a/fileformats/medimage/radlex/anatomical/__init__.py b/fileformats/medimage/radlex/anatomical/__init__.py new file mode 100644 index 0000000..99c9ad9 --- /dev/null +++ b/fileformats/medimage/radlex/anatomical/__init__.py @@ -0,0 +1,5 @@ +from fileformats.core import ClassifierCategory + + +class AnatomicalEntity(ClassifierCategory): + pass diff --git a/fileformats/medimage/radlex/imaging/__init__.py b/fileformats/medimage/radlex/imaging/__init__.py new file mode 100644 index 0000000..507402c --- /dev/null +++ b/fileformats/medimage/radlex/imaging/__init__.py @@ -0,0 +1,5 @@ +from fileformats.core import ClassifierCategory + + +class ImagingSpecialty(ClassifierCategory): + pass diff --git a/fileformats/medimage/radlex/imaging/modality.py b/fileformats/medimage/radlex/imaging/modality.py new file mode 100644 index 0000000..604aea9 --- /dev/null +++ b/fileformats/medimage/radlex/imaging/modality.py @@ -0,0 +1,241 @@ +from fileformats.core import ClassifierCategory + + +class ImagingModality(ClassifierCategory): + ontology_link = 'http://www.radlex.org/RID/RID10311' + description = 'Form of imaging that depends on the way the image is produced' + dicom_modality = None + + +class CombinedModalities(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID49580' + description = 'These are cases where 2 different modalities are ' \ + 'performed in the same imaging setup without moving the ' \ + 'patient. These classes were created as sets, with the ' \ + 'individual modalities as the set members. Any reasoning ' \ + 'involving modality should accommodate this.' + + +class DualEnergyXrayAbsorptiometry(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10363' + description = None + + +class Fluoroscopy(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10361' + description = None + + +class MrFluoroscopy(Fluoroscopy): + ontology_link = 'http://www.radlex.org/RID/RID10319' + description = 'Non-invasive method of vascular imaging and determination ' \ + 'of internal anatomy without injection of contrast media or ' \ + 'radiation exposure. The technique is used especially in ' \ + 'cerebral angiography as well as for studies of other vascular ' \ + 'structures. [MeSH]' + + +class RadioFluoroscopy(Fluoroscopy): + ontology_link = 'http://www.radlex.org/RID/RID45709' + description = 'Production of an image when x-rays strike a fluorescent screen. [MeSH]' + + +class MagneticResonanceImaging(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10312' + description = 'Non-invasive method of demonstrating internal anatomy ' \ + 'based on the principle that atomic nuclei in a strong ' \ + 'magnetic field absorb pulses of radiofrequency energy and ' \ + 'emit them as radiowaves which can be reconstructed into ' \ + 'computerized images. The concept includes proton spin ' \ + 'tomographic techniques. [MeSH]' + dicom_modality = 'MR' + + +class DiffusionTensorImaging(MagneticResonanceImaging): + ontology_link = 'http://www.radlex.org/RID/RID38778' + description = None + + +class DynamicContrast(MagneticResonanceImaging): + ontology_link = 'http://www.radlex.org/RID/RID49531' + description = 'An imaging method with a timed series of T1-weighted ' \ + 'images used to detect and measure signal intensity ' \ + 'change (enhancement) over time following administration ' \ + 'of intravenous contrast agent to noninvasively access ' \ + 'tissue vascular characteristics.' + + +class EnhancedMagneticResonanceImaging(MagneticResonanceImaging): + pass + + +class FunctionalMagneticResonanceImaging(MagneticResonanceImaging): + ontology_link = 'http://www.radlex.org/RID/RID10317' + description = None + + +class MagneticResonanceAngiography(MagneticResonanceImaging): + ontology_link = 'http://www.radlex.org/RID/RID10319' + description = None + + +class MagneticResonanceSpectroscopy(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10315' + description = ' Spectroscopic method of measuring the magnetic moment of ' \ + 'elementary particles such as atomic nuclei, protons or ' \ + 'electrons. It is employed in clinical applications such as ' \ + 'nmr tomography (magnetic resonance imaging). [MeSH]' + + +class NuclearMedicineImaging(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10330' + description = None + dicom_modality = 'NM' + + +class PositronEmissionTomography(NuclearMedicineImaging): + ontology_link = 'http://www.radlex.org/RID/RID10337' + description = 'An imaging technique using compounds labelled with ' \ + 'short-lived positron-emitting radionuclides (such as ' \ + 'carbon-11, nitrogen-13, oxygen-15 and fluorine-18) to ' \ + 'measure cell metabolism. It has been useful in study of ' \ + 'soft tissues such as cancer; cardiovascular system; and ' \ + 'brain. SPECT is closely related to PET, but uses isotopes ' \ + 'with longer half-lives and resolution is lower. [MeSH]' + dicom_modality = 'PT' + +class PanographicRadiograph(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10360' + description = None + dicom_modality = 'PX' + + +class ProjectionRadiography(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10345' + description = 'Examination of any part of the body for diagnostic purposes ' \ + 'by means of roentgen rays, recording the image on a ' \ + 'sensitized surface (such as photographic film). [MeSH]' + + +class ComputedRadiography(ProjectionRadiography): + ontology_link = 'http://www.radlex.org/RID/RID10349' + dicom_modality = 'CR' + description = None + + +class DigitalRadiography(ProjectionRadiography): + ontology_link = 'http://www.radlex.org/RID/RID10351' + dicom_modality = 'DR' + description = None + + +class DualEnergySubtractionRadiograpgy(ProjectionRadiography): + ontology_link = 'http://www.radlex.org/RID/RID10356' + description = None + + +class Mammography(ProjectionRadiography): + ontology_link = 'http://www.radlex.org/RID/RID10357' + description = None + + +class ScreenFilmRadiography(ProjectionRadiography): + ontology_link = 'http://www.radlex.org/RID/RID10353' + description = 'Conventional radiography' + dicom_modality = 'RG' + +class Stereoscopy(ProjectionRadiography): + ontology_link = 'http://www.radlex.org/RID/RID50131' + description = None + + +class StereotacticRadiography(ProjectionRadiography): + ontology_link = 'http://www.radlex.org/RID/RID50260' + description = None + + +class Spectroscopy(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10377' + description = 'The measurement of the amplitude of the components of a ' \ + 'complex waveform throughout the frequency range of the ' \ + 'waveform. (McGraw-Hill Dictionary of Scientific and ' \ + 'Technical Terms, 4th ed) [MeSH]' + + +class Tomography(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID28840' + description = None + + +class ComputedTomography(Tomography): + ontology_link = 'http://www.radlex.org/RID/RID10321' + description = 'Tomography using x-ray transmission and a computer algorithm ' \ + 'to reconstruct the image. [MeSH]' + dicom_modality = 'CT' + + +class Ultrasound(ImagingModality): + ontology_link = 'http://www.radlex.org/RID/RID10326' + description = 'The visualization of deep structures of the body by ' \ + 'recording the reflections of echoes of pulses of ultrasonic ' \ + 'waves directed into the tissues. Use of ultrasound for ' \ + 'imaging or diagnostic purposes employs frequencies ranging ' \ + 'from 1.6 to 10 megahertz. [MeSH]' + dicom_modality = 'US' + + +# Some extra abbreviations +MRI = MagneticResonanceImaging +PET = PositronEmissionTomography + +# DICOM abbreviations, taken from: https://dicom.nema.org/medical/dicom/current/output/chtml/part16/sect_CID_29.html +CR = ComputedRadiography +CT = ComputedTomography +DX = DigitalRadiography +MG = Mammography +MR = MagneticResonanceImaging +NM = NuclearMedicineImaging +PT = PositronEmissionTomography +PX = PanographicRadiograph +RF = RadioFluoroscopy +RG = ScreenFilmRadiography +US = Ultrasound +# AR = Autorefraction +# BI = Biomagnetic Imaging +# BMD = Bone Mineral Densitometry +# EPS = Cardiac Electrophysiology +# DMS = Dermoscopy +# DG = Diaphanography +# ECG = Electrocardiography +# EEG = Electroencephalography +# EMG = Electromyography +# EOG = Electrooculography +# ES = Endoscopy +# XC = External-camera Photography +# GM = General Microscopy +# HD = Hemodynamic Waveform +# IO = Intra-oral Radiography +# IVOCT = Intravascular Optical Coherence Tomography +# IVUS = Intravascular Ultrasound +# KER = Keratometry +# LS = Laser Scan +# LEN = Lensometry +# OAM = Ophthalmic Axial Measurements +# OPM = Ophthalmic Mapping +# OP = Ophthalmic Photography +# OPT = Ophthalmic Tomography +# OPTBSV = Ophthalmic Tomography B-scan Volume Analysis +# OPTENF = Ophthalmic Tomography En Face +# OPV = Ophthalmic Visual Field +# OCT = Optical Coherence Tomography +# OSS = Optical Surface Scanner +# PA = Photoacoustic +# POS = Position Sensor +# RESP = Respiratory Waveform +# RTIMAGE = RT Image +# SM = Slide Microscopy +# SRF = Subjective Refraction +# TG = Thermography +# BDUS = Ultrasound Bone Densitometry +# VA = Visual Acuity +# XA = X-Ray Angiography diff --git a/fileformats/medimage/raw/pet/siemens.py b/fileformats/medimage/raw/pet/siemens.py index ee13e1d..d91b129 100644 --- a/fileformats/medimage/raw/pet/siemens.py +++ b/fileformats/medimage/raw/pet/siemens.py @@ -1,3 +1,4 @@ +from fileformats.generic import SetOf from .base import ( PetRawData, PetListMode, @@ -34,3 +35,10 @@ class Vnd_Siemens_Biograph128Vision_Vr20b_PetNormalisation( Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData, PetNormalisation ): "normalisation scan or the current cross calibration factor" + + +class Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogramSeries( + SetOf[Vnd_Siemens_Biograph128Vision_Vr20b_PetSinogram], + Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData, +): + "Series of sinogram images" diff --git a/fileformats/medimage/surface.py b/fileformats/medimage/surface.py new file mode 100644 index 0000000..3c6e39f --- /dev/null +++ b/fileformats/medimage/surface.py @@ -0,0 +1,13 @@ +import typing as ty +from fileformats.core import FileSet +from fileformats.application import Xml + + +class SurfaceMesh(FileSet): + + iana_mime: ty.Optional[str] = None + + +class Gifti(SurfaceMesh, Xml): + + ext = ".gii"