Skip to content

Commit

Permalink
Merge pull request #34 from ArcanaFramework/validated-property
Browse files Browse the repository at this point in the history
Updated to use Binary|UnicodeFile and validated-property from core
  • Loading branch information
tclose authored Sep 17, 2024
2 parents 9d465f3 + a68e448 commit e4c1c90
Show file tree
Hide file tree
Showing 17 changed files with 87 additions and 92 deletions.
1 change: 1 addition & 0 deletions .codespell-ignorewords
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
nd
te
ptd
8 changes: 6 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ repos:
--strict,
--install-types,
--non-interactive,
--no-warn-unused-ignores,
]
exclude: tests
additional_dependencies: [fileformats, pydicom, nibabel]
additional_dependencies:
- fileformats>=0.13.0
- pytest
- pydicom
- nibabel
- attrs
12 changes: 7 additions & 5 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import logging
import typing as ty
import tempfile
from pathlib import Path
import pytest
Expand Down Expand Up @@ -65,12 +66,13 @@ def dummy_mixedfmap_dicom() -> DicomDir:
# break at it
if os.getenv("_PYTEST_RAISE", "0") != "0":

@pytest.hookimpl(tryfirst=True) # type: ignore
def pytest_exception_interact(call) -> None:
raise call.excinfo.value
@pytest.hookimpl(tryfirst=True)
def pytest_exception_interact(call: pytest.CallInfo[ty.Any]) -> None:
if call.excinfo is not None:
raise call.excinfo.value

@pytest.hookimpl(tryfirst=True) # type: ignore
def pytest_internalerror(excinfo) -> None:
@pytest.hookimpl(tryfirst=True)
def pytest_internalerror(excinfo: pytest.ExceptionInfo[BaseException]) -> None:
raise excinfo.value


Expand Down
24 changes: 15 additions & 9 deletions extras/fileformats/extras/medimage/dicom.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
from pathlib import Path
import typing as ty
import typing
import pydicom
import numpy as np
import numpy
import numpy.typing
from fileformats.core import FileSet, extra_implementation
from fileformats.core import SampleFileGenerator
from fileformats.medimage import MedicalImage, DicomCollection, DicomDir, DicomSeries
from fileformats.medimage import (
MedicalImage,
DicomCollection,
DicomDir,
DicomSeries,
)
from fileformats.medimage.base import DataArrayType
import medimages4tests.dummy.dicom.mri.t1w.siemens.skyra.syngo_d13c


@extra_implementation(MedicalImage.read_array)
def dicom_read_array(
collection: DicomCollection,
) -> numpy.typing.NDArray[np.floating[ty.Any]]:
) -> DataArrayType:
image_stack = []
for dcm_file in collection.contents:
image_stack.append(pydicom.dcmread(dcm_file).pixel_array)
return np.asarray(image_stack)
return numpy.asarray(image_stack)


@extra_implementation(MedicalImage.vox_sizes)
def dicom_vox_sizes(collection: DicomCollection) -> ty.Tuple[float, float, float]:
def dicom_vox_sizes(collection: DicomCollection) -> typing.Tuple[float, float, float]:
return tuple(
collection.metadata["PixelSpacing"] + [collection.metadata["SliceThickness"]]
)


@extra_implementation(MedicalImage.dims)
def dicom_dims(collection: DicomCollection) -> ty.Tuple[int, int, int]:
def dicom_dims(collection: DicomCollection) -> typing.Tuple[int, int, int]:
return tuple(
(
collection.metadata["Rows"],
Expand All @@ -46,7 +52,7 @@ def dicom_series_number(collection: DicomCollection) -> str:
def dicom_dir_generate_sample_data(
dcmdir: DicomDir,
generator: SampleFileGenerator,
) -> ty.List[Path]:
) -> typing.List[Path]:
dcm_dir = medimages4tests.dummy.dicom.mri.t1w.siemens.skyra.syngo_d13c.get_image()
series_number = generator.rng.randint(1, SERIES_NUMBER_RANGE)
dest = generator.generate_fspath(DicomDir)
Expand All @@ -62,7 +68,7 @@ def dicom_dir_generate_sample_data(
def dicom_series_generate_sample_data(
dcm_series: DicomSeries,
generator: SampleFileGenerator,
) -> ty.List[Path]:
) -> typing.List[Path]:
dicom_dir: Path = dicom_dir_generate_sample_data(dcm_series, generator=generator)[0] # type: ignore[arg-type]
stem = generator.generate_fspath().stem
fspaths = []
Expand Down
7 changes: 4 additions & 3 deletions extras/fileformats/extras/medimage/diffusion.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import typing as ty
import numpy as np
import typing # noqa: F401
import numpy.typing
from fileformats.core import extra_implementation
from fileformats.medimage import DwiEncoding, Bval, Bvec
from fileformats.medimage.diffusion import EncodingArrayType


@extra_implementation(Bval.read_array)
def bval_read_array(bval: Bval) -> numpy.typing.NDArray[np.floating[ty.Any]]: # noqa
def bval_read_array(bval: Bval) -> EncodingArrayType:
return np.asarray([float(ln) for ln in bval.read_contents().split()])


@extra_implementation(DwiEncoding.read_array)
def bvec_read_array(bvec: Bvec) -> numpy.typing.NDArray[np.floating[ty.Any]]: # noqa
def bvec_read_array(bvec: Bvec) -> EncodingArrayType:
bvals = bvec.b_values_file.read_array()
directions = np.asarray(
[[float(x) for x in ln.split()] for ln in bvec.read_contents().splitlines()]
Expand Down
13 changes: 5 additions & 8 deletions extras/fileformats/extras/medimage/nifti.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from pathlib import Path
import typing as ty
import nibabel
import numpy as np
import numpy.typing
import typing # noqa: F401
import numpy.typing # noqa: F401
from fileformats.core import FileSet, SampleFileGenerator, extra_implementation
from fileformats.medimage import (
MedicalImage,
Expand All @@ -18,24 +18,21 @@
Dmri,
Brain,
)
from fileformats.medimage.base import DataArrayType
import medimages4tests.dummy.nifti
import medimages4tests.mri.neuro.t1w
import medimages4tests.mri.neuro.dwi
import medimages4tests.mri.neuro.bold


@extra_implementation(FileSet.read_metadata)
def nifti_read_metadata(
nifti: Nifti, selected_keys: ty.Optional[ty.Collection[str]] = None
) -> ty.Mapping[str, ty.Any]:
def nifti_read_metadata(nifti: Nifti, **kwargs: ty.Any) -> ty.Mapping[str, ty.Any]:
metadata = dict(nibabel.load(nifti.fspath).header) # type: ignore[call-overload, attr-defined]
if selected_keys:
metadata = {k: v for k, v in metadata.items() if k in selected_keys}
return metadata # type: ignore[no-any-return]


@extra_implementation(MedicalImage.read_array)
def nifti_data_array(nifti: Nifti) -> numpy.typing.NDArray[np.floating[ty.Any]]: # noqa
def nifti_data_array(nifti: Nifti) -> DataArrayType: # noqa
return nibabel.load(nifti.fspath).get_data() # type: ignore[attr-defined]


Expand Down
7 changes: 4 additions & 3 deletions extras/fileformats/extras/medimage/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
@extra_implementation(FileSet.read_metadata)
def siemens_pet_raw_data_read_metadata(
pet_raw_data: Vnd_Siemens_Biograph128Vision_Vr20b_PetRawData,
selected_keys: ty.Optional[ty.Collection[str]] = None,
specific_tags: ty.Optional[ty.Collection[str]] = None,
**kwargs: ty.Any,
) -> ty.Mapping[str, ty.Any]:

with pet_raw_data.open() as f:
Expand All @@ -17,7 +18,7 @@ def siemens_pet_raw_data_read_metadata(
pet_raw_data.dicom_header_offset,
pet_raw_data.dcm_hdr_size_int_offset,
)
if selected_keys is not None:
selected_keys = list(selected_keys)
if specific_tags is not None:
selected_keys = list(specific_tags)
dcm = pydicom.dcmread(window, specific_tags=selected_keys)
return dcm # type: ignore[return-value]
9 changes: 3 additions & 6 deletions fileformats/medimage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,26 @@
from typing_extensions import TypeAlias

if ty.TYPE_CHECKING:
import numpy as np
import numpy.typing
import numpy.typing # noqa: F401


# =====================================================================
# Custom loader functions for different image types
# =====================================================================

DataArrayType: TypeAlias = (
"numpy.typing.NDArray[ty.Union[np.floating[ty.Any], np.integer[ty.Any]]]"
)
ty.Any
) # In Py<3.9 this is problematic "numpy.typing.NDArray[typing.Union[numpy.floating[typing.Any], numpy.integer[typing.Any]]]"


class MedicalImage(WithClassifiers, FileSet):

iana_mime: ty.Optional[str] = None
INCLUDE_HDR_KEYS: ty.Optional[ty.Tuple[str, ...]] = None
IGNORE_HDR_KEYS: ty.Optional[ty.Tuple[str, ...]] = None
binary = True
classifiers_attr_name = "image_contents"
image_contents = ()
allowed_classifiers = (ContentsClassifier,)
multiple_classifiers = True
exclusive_classifiers = (ImagingModality, AnatomicalEntity, Derivative)

@extra
Expand Down
4 changes: 1 addition & 3 deletions fileformats/medimage/dicom.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def contents(self) -> ty.List[Dicom]: # type: ignore[override]

@extra_implementation(FileSet.read_metadata)
def dicom_collection_read_metadata(
collection: DicomCollection, selected_keys: ty.Optional[ty.Collection[str]] = None
collection: DicomCollection, **kwargs: ty.Any
) -> ty.Mapping[str, ty.Any]:
# Collated DICOM headers across series
collated: ty.Dict[str, ty.Any] = {}
Expand All @@ -102,8 +102,6 @@ def dicom_collection_read_metadata(
TypedSet if isinstance(collection, DicomSeries) else Directory
)
for dicom in base_class.contents.__get__(collection):
if selected_keys is not None:
dicom = Dicom(dicom, metadata_keys=selected_keys)
for key, val in dicom.metadata.items():
try:
prev_val = collated[key]
Expand Down
37 changes: 19 additions & 18 deletions fileformats/medimage/diffusion.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,59 @@
import typing as ty
from fileformats.core import extra
import typing
from fileformats.core import extra, validated_property
from fileformats.core.typing import TypeAlias
from fileformats.core.mixin import WithAdjacentFiles
from fileformats.generic import File
from fileformats.generic import BinaryFile
from .nifti import NiftiGzX, NiftiGz, Nifti1, NiftiX


if ty.TYPE_CHECKING:
import numpy as np
import numpy.typing
if typing.TYPE_CHECKING:
import numpy.typing # noqa: F401

EncodingArrayType: TypeAlias = (
typing.Any
) # In Py<3.9 this is problematic "numpy.typing.NDArray[numpy.floating[typing.Any]]"

class DwiEncoding(File):

iana_mime: ty.Optional[str] = None

class DwiEncoding:
@extra
def read_array(self) -> "numpy.typing.NDArray[np.floating[ty.Any]]":
def read_array(self) -> EncodingArrayType:
"Both the gradient direction and weighting combined into a single Nx4 array"
raise NotImplementedError

def array(self) -> "numpy.typing.NDArray[np.floating[ty.Any]]":
def array(self) -> EncodingArrayType:
return self.read_array()

def directions(self) -> "numpy.typing.NDArray[np.floating[ty.Any]]":
def directions(self) -> EncodingArrayType:
"gradient direction and weighting combined into a single Nx4 array"
return self.array()[:, :3]

def b_values(self) -> "numpy.typing.NDArray[np.floating[ty.Any]]":
def b_values(self) -> EncodingArrayType:
"the b-value weighting"
return self.array()[:, 3]


class Bval(File):
class Bval(BinaryFile):

ext = ".bval"

@extra
def read_array(self) -> "numpy.typing.NDArray[np.floating[ty.Any]]":
def read_array(self) -> EncodingArrayType:
raise NotImplementedError


class Bvec(WithAdjacentFiles, DwiEncoding):
class Bvec(WithAdjacentFiles, DwiEncoding, BinaryFile):
"""FSL-style diffusion encoding, in two separate files"""

ext = ".bvec"

@property
@validated_property
def b_values_file(self) -> Bval:
return Bval(self.select_by_ext(Bval))


# NIfTI file format gzipped with BIDS side car
class WithBvec(WithAdjacentFiles):
@property
@validated_property
def encoding(self) -> Bvec:
return Bvec(self.select_by_ext(Bvec)) # type: ignore[attr-defined]

Expand Down
11 changes: 6 additions & 5 deletions fileformats/medimage/misc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fileformats.application import Gzip
from fileformats.generic import File
from fileformats.generic import BinaryFile
from fileformats.core import validated_property
from fileformats.core.mixin import WithSeparateHeader, WithMagicVersion
from .base import MedicalImage
from fileformats.core.exceptions import FormatMismatchError
Expand All @@ -10,18 +11,18 @@
# ==================


class AnalyzeHeader(File):
class AnalyzeHeader(BinaryFile):

ext = ".hdr"


class Analyze(WithSeparateHeader, MedicalImage, File):
class Analyze(WithSeparateHeader, MedicalImage, BinaryFile):

ext = ".img"
header_type = AnalyzeHeader


class Mgh(WithMagicVersion, File):
class Mgh(WithMagicVersion, BinaryFile):
"""
FreeSurfer 4-dimensional brain images
Expand All @@ -31,7 +32,7 @@ class Mgh(WithMagicVersion, File):
ext = ".mgh"
magic_pattern = rb"(....)" # First integer is the version string

@property
@validated_property
def _is_supported_version(self) -> None:
assert isinstance(self.version, str)
if int(self.version) != 1:
Expand Down
11 changes: 5 additions & 6 deletions fileformats/medimage/nifti.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import typing as ty
from fileformats.generic import File
from fileformats.generic import BinaryFile
from fileformats.core import validated_property
from fileformats.core.mixin import WithSideCars, WithMagicNumber, WithAdjacentFiles
from fileformats.application import Json
from fileformats.application.archive import BaseGzip
from .base import MedicalImage


class Nifti(MedicalImage, File):
class Nifti(MedicalImage, BinaryFile):

ext: str = ".nii"
iana_mime: ty.Optional[str] = None


class WithBids(WithSideCars):

primary_type = Nifti
side_car_types = (Json,)

@property
@validated_property
def json_file(self) -> Json:
return Json(self.select_by_ext(Json)) # type: ignore[attr-defined]

Expand Down Expand Up @@ -61,6 +60,6 @@ class NiftiWithDataFile(WithAdjacentFiles, Nifti1):
magic_number = "6E693100"
alternate_exts = (".hdr",)

@property
@validated_property
def data_file(self) -> NiftiDataFile:
return NiftiDataFile(self.select_by_ext(NiftiDataFile))
Loading

0 comments on commit e4c1c90

Please sign in to comment.