137137from typing import Literal
138138
139139import numpy as np
140+ import numpy .typing as npt
141+ from typing_extensions import TypeVar
140142
141143from .casting import sctypes_aliases
142144from .dataobj_images import DataobjImage
150152 import io
151153 from collections .abc import Sequence
152154
153- import numpy .typing as npt
154-
155155 from .arrayproxy import ArrayLike
156156 from .fileholders import FileMap
157157
158- SpatialImgT = ty .TypeVar ('SpatialImgT' , bound = 'SpatialImage' )
159- SpatialHdrT = ty .TypeVar ('SpatialHdrT' , bound = 'SpatialHeader' )
158+ # Track whether the image is initialized with an affine or not
159+ # This will almost always be the case, but there are some exceptions
160+ # and some functions that will fail if the affine is not present
161+ Affine = npt .NDArray [np .floating ]
162+ AffT = TypeVar ('AffT' , covariant = True , bound = ty .Union [Affine , None ], default = Affine )
163+ SpatialImgT = TypeVar ('SpatialImgT' , bound = 'SpatialImage[Affine]' )
164+ SpatialHdrT = TypeVar ('SpatialHdrT' , bound = 'SpatialHeader' )
165+ AnySpatialImgT = TypeVar ('AnySpatialImgT' , bound = 'SpatialImage[Affine | None]' )
160166
161167
162168class HasDtype (ty .Protocol ):
@@ -194,7 +200,7 @@ def __init__(
194200 data_dtype : npt .DTypeLike = np .float32 ,
195201 shape : Sequence [int ] = (0 ,),
196202 zooms : Sequence [float ] | None = None ,
197- ):
203+ ) -> None :
198204 self .set_data_dtype (data_dtype )
199205 self ._zooms = ()
200206 self .set_data_shape (shape )
@@ -461,7 +467,7 @@ def slice_affine(self, slicer: object) -> np.ndarray:
461467 return self .img .affine .dot (transform )
462468
463469
464- class SpatialImage (DataobjImage ):
470+ class SpatialImage (DataobjImage , ty . Generic [ AffT ] ):
465471 """Template class for volumetric (3D/4D) images"""
466472
467473 header_class : type [SpatialHeader ] = SpatialHeader
@@ -473,11 +479,11 @@ class SpatialImage(DataobjImage):
473479 def __init__ (
474480 self ,
475481 dataobj : ArrayLike ,
476- affine : np . ndarray | None ,
482+ affine : AffT ,
477483 header : FileBasedHeader | ty .Mapping | None = None ,
478484 extra : ty .Mapping | None = None ,
479485 file_map : FileMap | None = None ,
480- ):
486+ ) -> None :
481487 """Initialize image
482488
483489 The image is a combination of (array-like, affine matrix, header), with
@@ -510,7 +516,7 @@ def __init__(
510516 # do need 4,4.
511517 # Copy affine to isolate from environment. Specify float type to
512518 # avoid surprising integer rounding when setting values into affine
513- affine = np .array (affine , dtype = np .float64 , copy = True )
519+ affine = np .array (affine , dtype = np .float64 , copy = True ) # type: ignore[assignment]
514520 if not affine .shape == (4 , 4 ):
515521 raise ValueError ('Affine should be shape 4,4' )
516522 self ._affine = affine
@@ -524,7 +530,7 @@ def __init__(
524530 self ._data_cache = None
525531
526532 @property
527- def affine (self ):
533+ def affine (self ) -> AffT :
528534 return self ._affine
529535
530536 def update_header (self ) -> None :
@@ -586,7 +592,7 @@ def set_data_dtype(self, dtype: npt.DTypeLike) -> None:
586592 self ._header .set_data_dtype (dtype )
587593
588594 @classmethod
589- def from_image (klass : type [SpatialImgT ], img : SpatialImage | FileBasedImage ) -> SpatialImgT :
595+ def from_image (klass : type [AnySpatialImgT ], img : FileBasedImage ) -> AnySpatialImgT :
590596 """Class method to create new instance of own class from `img`
591597
592598 Parameters
@@ -629,7 +635,7 @@ def slicer(self: SpatialImgT) -> SpatialFirstSlicer[SpatialImgT]:
629635 """
630636 return self .ImageSlicer (self )
631637
632- def __getitem__ (self , idx : object ) -> None :
638+ def __getitem__ (self , idx : object ) -> ty . Never :
633639 """No slicing or dictionary interface for images
634640
635641 Use the slicer attribute to perform cropping and subsampling at your
0 commit comments