17
17
import nibabel as nib
18
18
import numpy as np
19
19
from nibabel import Nifti1Image
20
+ from nibabel .analyze import _dtdefs
20
21
from numpy ._typing import NDArray
21
22
22
23
from tiledb import VFS , Config , Ctx
@@ -56,7 +57,11 @@ def __init__(
56
57
self ._binary_header = base64 .b64encode (
57
58
self ._nib_image .header .binaryblock
58
59
).decode ("utf-8" )
59
- self ._mode = "" .join (self ._nib_image .dataobj .dtype .names )
60
+ self ._mode = (
61
+ "" .join (self ._nib_image .dataobj .dtype .names )
62
+ if self ._nib_image .dataobj .dtype .names is not None
63
+ else ""
64
+ )
60
65
61
66
def __enter__ (self ) -> NiftiReader :
62
67
return self
@@ -100,10 +105,40 @@ def image_metadata(self) -> Dict[str, Any]:
100
105
101
106
@property
102
107
def axes (self ) -> Axes :
103
- if self ._mode == "L" :
104
- axes = Axes (["X" , "Y" , "Z" ])
108
+ header_dict = self .nifti1_hdr_2_dict ()
109
+ # The 0-index holds information about the number of dimensions
110
+ # according the spec https://nifti.nimh.nih.gov/pub/dist/src/niftilib/nifti1.h
111
+ dims_number = header_dict ["dim" ][0 ]
112
+ if dims_number == 4 :
113
+ # According to standard the 4th dimension corresponds to 'T' time
114
+ # but in special cases can be degnerate into channels
115
+ if header_dict ["dim" ][dims_number ] == 1 :
116
+ # The time dimension does not correspond to time
117
+ if self ._mode == "RGB" or self ._mode == "RGBA" :
118
+ # [..., ..., ..., 1, 3] or [..., ..., ..., 1, 4]
119
+ axes = Axes (["X" , "Y" , "Z" , "T" , "C" ])
120
+ else :
121
+ # The image is single-channel with 1 value in Temporal dimension
122
+ # instead of channel. So we map T to be channel.
123
+ # [..., ..., ..., 1]
124
+ axes = Axes (["X" , "Y" , "Z" , "C" ])
125
+ else :
126
+ # The time dimension does correspond to time
127
+ axes = Axes (["X" , "Y" , "Z" , "T" ])
128
+ elif dims_number < 4 :
129
+ # Only spatial dimensions
130
+ if self ._mode == "RGB" or self ._mode == "RGBA" :
131
+ axes = Axes (["X" , "Y" , "Z" , "C" ])
132
+ else :
133
+ axes = Axes (["X" , "Y" , "Z" ])
105
134
else :
106
- axes = Axes (["X" , "Y" , "Z" , "C" ])
135
+ # Has more dimensions that belong to spatial-temporal unknown attributes
136
+ # TODO: investigate sample images of this format.
137
+ if self ._mode == "RGB" or self ._mode == "RGBA" :
138
+ axes = Axes (["X" , "Y" , "Z" , "C" ])
139
+ else :
140
+ axes = Axes (["X" , "Y" , "Z" ])
141
+
107
142
self ._logger .debug (f"Reader axes: { axes } " )
108
143
return axes
109
144
@@ -124,7 +159,6 @@ def channels(self) -> Sequence[str]:
124
159
"G" : "GREEN" ,
125
160
"B" : "BLUE" ,
126
161
"A" : "ALPHA" ,
127
- "L" : "GRAYSCALE" ,
128
162
}
129
163
# Use list comprehension to convert the short form to full form
130
164
rgb_full = [color_map [color ] for color in self ._mode ]
@@ -139,12 +173,11 @@ def level_count(self) -> int:
139
173
def level_dtype (self , level : int = 0 ) -> np .dtype :
140
174
header_dict = self .nifti1_hdr_2_dict ()
141
175
142
- # Check the header first
143
- if ( dtype := header_dict [ "data_type" ]. dtype ) == np . dtype ( "S10" ):
176
+ dtype = self . get_dtype_from_code ( header_dict [ "datatype" ])
177
+ if dtype == np . dtype ([( "R" , "u1" ), ( "G" , "u1" ), ( "B" , "u1" )] ):
144
178
dtype = np .uint8
145
-
146
179
# TODO: Compare with the dtype of fields
147
- # dict(self._nib_image.dataobj.dtype.fields)
180
+
148
181
self ._logger .debug (f"Level { level } dtype: { dtype } " )
149
182
return dtype
150
183
@@ -153,15 +186,17 @@ def level_shape(self, level: int = 0) -> Tuple[int, ...]:
153
186
return ()
154
187
155
188
original_shape = self ._nib_image .shape
156
- fields = self ._nib_image .dataobj .dtype .fields
157
- if len (fields ) == 3 :
158
- # RGB convert the shape from to stack 3 channels
159
- l_shape = (* original_shape [:- 1 ], 3 )
160
- elif len (fields ) == 4 :
161
- # RGBA
162
- l_shape = (* original_shape [:- 1 ], 4 )
189
+ if (fields := self ._nib_image .dataobj .dtype .fields ) is not None :
190
+ if len (fields ) == 3 :
191
+ # RGB convert the shape from to stack 3 channels
192
+ l_shape = (* original_shape , 3 )
193
+ elif len (fields ) == 4 :
194
+ # RGBA
195
+ l_shape = (* original_shape , 4 )
196
+ else :
197
+ # Grayscale
198
+ l_shape = original_shape
163
199
else :
164
- # Grayscale
165
200
l_shape = original_shape
166
201
self ._logger .debug (f"Level { level } shape: { l_shape } " )
167
202
return l_shape
@@ -221,6 +256,13 @@ def nifti1_hdr_2_dict(self) -> Dict[str, Any]:
221
256
for field in structured_header_arr .dtype .names
222
257
}
223
258
259
+ # Function to find and return the third value based on the first value
260
+ def get_dtype_from_code (self , dtype_code : int ) -> np .dtype :
261
+ for item in _dtdefs :
262
+ if item [0 ] == dtype_code : # Check if the first value matches the input code
263
+ return item [2 ] # Return the third value (dtype)
264
+ return None # Return None if the code is not foun
265
+
224
266
@staticmethod
225
267
def _serialize_header (header_dict : Mapping [str , Any ]) -> Dict [str , Any ]:
226
268
serialized_header = {
@@ -265,9 +307,13 @@ def compute_level_metadata(
265
307
def write_group_metadata (self , metadata : Mapping [str , Any ]) -> None :
266
308
self ._group_metadata = json .loads (metadata ["json_write_kwargs" ])
267
309
268
- def _structured_dtype (self ) -> np .dtype :
310
+ def _structured_dtype (self ) -> Optional [ np .dtype ] :
269
311
if self ._original_mode == "RGB" :
270
312
return np .dtype ([("R" , "u1" ), ("G" , "u1" ), ("B" , "u1" )])
313
+ elif self ._original_mode == "RGBA" :
314
+ return np .dtype ([("R" , "u1" ), ("G" , "u1" ), ("B" , "u1" ), ("A" , "u1" )])
315
+ else :
316
+ return None
271
317
272
318
def write_level_image (
273
319
self ,
@@ -278,9 +324,14 @@ def write_level_image(
278
324
binaryblock = base64 .b64decode (self ._group_metadata ["binaryblock" ])
279
325
)
280
326
contiguous_image = np .ascontiguousarray (image )
281
- structured_arr = contiguous_image .view (dtype = self . _structured_dtype ()). reshape (
282
- * image .shape [: - 1 ]
327
+ structured_arr = contiguous_image .view (
328
+ dtype = self . _structured_dtype () if self . _structured_dtype () else image .dtype
283
329
)
330
+ if len (image .shape ) > 3 :
331
+ # If temporal is 1 and extra dim for channels RGB/RGBA
332
+ if image .shape [3 ] == 1 and (image .shape [4 ] == 3 or 4 ):
333
+ structured_arr = structured_arr .reshape (* image .shape [:4 ])
334
+
284
335
nib_image = self ._writer (
285
336
structured_arr , header = header , affine = header .get_best_affine ()
286
337
)
0 commit comments