@@ -413,6 +413,9 @@ def dtype(self)->type:
413413 if self .__unpacked :
414414 return self ._arr .dtype # type: ignore
415415 return self .nii .dataobj .dtype #type: ignore
416+ @dtype .setter
417+ def dtype (self , dtype :type ):
418+ self .set_dtype_ (dtype )
416419 @property
417420 def header (self ) -> Nifti1Header :
418421 if self .__unpacked :
@@ -433,6 +436,9 @@ def affine(self,affine:np.ndarray):
433436 def orientation (self ) -> AX_CODES :
434437 ort = nio .io_orientation (self .affine )
435438 return nio .ornt2axcodes (ort ) # type: ignore
439+ @orientation .setter
440+ def orientation (self , value : AX_CODES ):
441+ self .reorient_ (value , verbose = False )
436442 @property
437443 def dims (self )-> int :
438444 self ._unpack ()
@@ -448,6 +454,9 @@ def zoom(self) -> ZOOMS:
448454 z = z [:n ]
449455 #assert len(z) == 3,z
450456 return z # type: ignore
457+ @zoom .setter
458+ def zoom (self , value : tuple [float , float , float ]):
459+ self .rescale_ (value , verbose = False )
451460
452461 @property
453462 def origin (self ) -> tuple [float , float , float ]:
@@ -482,10 +491,6 @@ def direction_itk(self) -> list:
482491 a [:len (a )// 3 * 2 ]*= - 1
483492 return a .tolist ()
484493
485- @orientation .setter
486- def orientation (self , value : AX_CODES ):
487- self .reorient_ (value , verbose = False )
488-
489494
490495 def split_4D_image_to_3D (self ):
491496 assert self .get_num_dims () == 4 ,self .get_num_dims ()
@@ -799,9 +804,7 @@ def apply_pad(
799804 mode : MODES = "constant" ,
800805 inplace = False ,
801806 verbose : logging = True
802- ):
803- #TODO add other modes
804- #TODO add testcases and options for modes
807+ ):
805808 if padd is None or padd == 0 :
806809 return self if inplace else self .copy ()
807810
@@ -824,13 +827,36 @@ def apply_pad(
824827
825828 affine = self .affine @ transform
826829
830+ arr = self .get_array ()
831+
832+ # ---- 1. CROPPING (negative padding) ----
833+ slices = []
834+
835+ for i , (before , after ) in enumerate (padd [:self .dims ]):
836+ start = max (0 , - before )
837+ end = arr .shape [i ] - max (0 , - after )
838+ slices .append (slice (start , end ))
839+
840+ # keep non-spatial dims unchanged
841+ slices += [slice (None )] * (arr .ndim - self .dims )
842+
843+ arr = arr [tuple (slices )]
844+
845+ # ---- 2. PADDING (positive only) ----
846+ padd_positive = tuple (
847+ (max (0 , b ), max (0 , a )) for b , a in padd
848+ )
849+
827850 args = {}
828851 if mode == "constant" :
829852 args ["constant_values" ] = self .get_c_val ()
830853
854+ if mode == "nearest" :
855+ mode = "edge"
856+
831857 log .print (f"Padd { padd } ; { mode = } , { args } " , verbose = verbose )
832858
833- arr = np .pad (self . get_array (), padd , mode = mode , ** args )
859+ arr = np .pad (arr , padd_positive , mode = mode , ** args )
834860
835861 nii = (arr , affine , self .header )
836862
@@ -935,35 +961,38 @@ def resample_from_to(self, to_vox_map:Image_Reference|Has_Grid|tuple[SHAPE,AFFIN
935961 mapping = to_vox_map .to_gird ()
936962 else :
937963 mapping = to_vox_map if isinstance (to_vox_map , tuple ) else to_nii_optional (to_vox_map , seg = self .seg , default = to_vox_map )
938- if isinstance (mapping ,Has_Grid ) and mapping .assert_affine (self ,raise_error = False ,origin_tolerance = 0.000001 ,error_tolerance = 0.000001 ,shape_tolerance = 0 ):
939- log .print (f"resample_from_to skipped; already in space: { self } " ,verbose = verbose )
940- return self if inplace else self .copy ()
941-
942- #m1 = mapping.make_empty_POI().reorient(self.orientation)
943- #if m1.assert_affine(self,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0):
944- # log.print(f"resample_from_to only need reorientation; {self.orientation}",verbose=verbose)
945- # return self.reorient(mapping.orientation,inplace=inplace)
946- #if self.orientation == mapping.orientation and self.zoom == mapping.zoom:
947- # shift = (np.array(self.origin) - np.array(m1.origin)) / np.array(m1.zoom)
948- # if np.allclose(shift, np.round(shift), atol=1e-6):
949- # self = self.reorient(mapping.orientation,inplace=inplace) # noqa: PLW0642
950- # shift = (np.array(self.origin) - np.array(mapping.origin)) / np.array(mapping.zoom)
951- # shift = np.round(shift).astype(int)
952- # src_shape = np.array(mapping.shape)
953- # dst_shape = np.array(self.shape)
954- # # padding before = how much dst starts before src
955- # pad_before = np.maximum(-shift, 0)
956- #
957- # # where src ends inside dst
958- # src_end_in_dst = shift + src_shape
959- # # padding after = remaining dst size after src
960- # pad_after = np.maximum(dst_shape - src_end_in_dst, 0)
961- # pad = tuple((int(b), int(a)) for b, a in zip(pad_before, pad_after))
962- # ret = self.apply_pad(pad, mode=mode)
963- #
964- # log.print(f"resample_from_to only needs padding/cropping {pad}, ",verbose=verbose,)
965- # ret.assert_affine(mapping,raise_error=False,origin_tolerance=0.000001,error_tolerance=0.000001,shape_tolerance=0)
966- # return ret
964+ if isinstance (mapping ,Has_Grid ):
965+ if mapping .assert_affine (self ,raise_error = False ,origin_tolerance = 0.000001 ,error_tolerance = 0.000001 ,shape_tolerance = 0 ):
966+ log .print (f"resample_from_to skipped; already in space: { self } " ,verbose = verbose )
967+ return self if inplace else self .copy ()
968+
969+ m1 = mapping if mapping .orientation == self .orientation else mapping .make_empty_POI ().reorient (self .orientation )
970+ if m1 .assert_affine (self ,raise_error = False ,origin_tolerance = 0.00001 ,error_tolerance = 0.00001 ,shape_tolerance = 0 ):
971+ log .print (f"resample_from_to only need reorientation; { self .orientation } " ,verbose = verbose )
972+ ret = self .reorient (mapping .orientation ,inplace = inplace )
973+ ret .affine = mapping .affine #remove floating point error
974+ return ret
975+ if self .orientation == mapping .orientation and np .allclose (self .zoom , mapping .zoom , atol = 1e-6 ):
976+ shift = (np .array (self .origin ) - np .array (m1 .origin )) / np .array (m1 .zoom )
977+ if np .allclose (shift , np .round (shift ), atol = 1e-6 ):
978+ s = self .reorient (mapping .orientation ,inplace = inplace ) # noqa: PLW0642
979+ shift = (np .array (self .origin ) - np .array (mapping .origin )) / np .array (mapping .zoom )
980+ shift = np .round (shift ).astype (int )
981+ dst_shape = np .array (mapping .shape )
982+ src_shape = np .array (s .shape )
983+ # padding before = how much dst starts before src
984+ pad_before = shift
985+ # padding after = remaining dst size after src
986+ pad_after = dst_shape - shift - src_shape
987+ pad = tuple ((int (b ), int (a )) for b , a in zip (pad_before , pad_after ))
988+ ret = s .apply_pad (pad , mode = mode ,inplace = inplace ,verbose = verbose )
989+
990+ #TODO SET raise_error=False before committing
991+ valid = ret .assert_affine (mapping ,raise_error = True ,origin_tolerance = 0.0001 ,error_tolerance = 0.0001 ,shape_tolerance = 0 )
992+ if valid :
993+ log .print (f"resample_from_to only needs padding/cropping { pad } " ,verbose = verbose )
994+ ret .affine = mapping .affine #remove floating point error
995+ return ret
967996
968997
969998 assert mapping is not None
@@ -1772,7 +1801,7 @@ def infect(self: NII, reference_mask: NII, inplace=False,verbose=True,axis:int|s
17721801 """
17731802 self .assert_affine (reference_mask )
17741803 if _do_crop :
1775- crop = reference_mask .compute_crop (0 ,5 )
1804+ crop = reference_mask .compute_crop (0 ,5 , raise_error = False )
17761805 s = self .apply_crop (crop )
17771806 reference_mask = reference_mask .apply_crop (crop )
17781807 else :
0 commit comments