31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159 | class TiffReader(base.ImageReader):
def __init__(
self,
output: Literal["voxels", "dotprops", "raw"] = "voxels",
channel: int = 0,
threshold: Optional[Union[int, float]] = None,
thin: bool = False,
dotprop_kwargs: Dict[str, Any] = {},
fmt: str = DEFAULT_FMT,
errors: str = "raise",
attrs: Optional[Dict[str, Any]] = None,
):
if not fmt.endswith(".tif") and not fmt.endswith(".tiff"):
raise ValueError('`fmt` must end with ".tif" or ".tiff"')
super().__init__(
fmt=fmt,
attrs=attrs,
file_ext=(".tif", ".tiff"),
name_fallback="TIFF",
read_binary=True,
output=output,
threshold=threshold,
thin=thin,
dotprop_kwargs=dotprop_kwargs,
errors=errors,
)
self.channel = channel
def format_output(self, x):
# This function replaces the BaseReader.format_output()
# This is to avoid trying to convert multiple (image, header) to NeuronList
if self.output == "raw":
return x
elif x:
return core.NeuronList([n for n in x if n])
else:
return core.NeuronList([])
@base.handle_errors
def read_buffer(
self, f, attrs: Optional[Dict[str, Any]] = None
) -> Union[np.ndarray, "core.Dotprops", "core.VoxelNeuron"]:
"""Read buffer into (image, header) or a neuron.
Parameters
----------
f : IO
Readable buffer (must be bytes).
attrs : dict | None
Arbitrary attributes to include in the neuron.
Returns
-------
core.Dotprops | core.VoxelNeuron | np.ndarray
"""
import tifffile
if isinstance(f, HTTPResponse):
f = io.StringIO(f.content)
if isinstance(f, bytes):
f = io.BytesIO(f)
with tifffile.TiffFile(f) as tif:
# The header contains some but not all the info
if hasattr(tif, "imagej_metadata") and tif.imagej_metadata is not None:
header = tif.imagej_metadata
else:
header = {}
# Read the x/y resolution from the first "page" (i.e. the first slice)
res = tif.pages[0].resolution
# Resolution to spacing
header["xy_spacing"] = (1 / res[0], 1 / res[1])
# Get the axes; this will be something like "ZCYX" where:
# Z = slices, C = channels, Y = rows, X = columns, S = color(?), Q = empty(?)
axes = tif.series[0].axes
# Generate volume
data = tif.asarray()
if self.output == "raw":
return data, header
# Drop "Q" axes if they have dimenions of 1 (we're assuming these are empty)
while "Q" in axes and data.shape[axes.index("Q")] == 1:
data = np.squeeze(data, axis=axes.index("Q"))
axes = axes.replace("Q", "", 1) # Only remove the first occurrence
if "C" in axes:
# Extract the requested channel from the volume
data = data.take(self.channel, axis=axes.index("C"))
axes = axes.replace("C", "")
# At this point we expect 3D data
if data.ndim != 3:
raise ValueError(f'Expected 3D greyscale data, got {data.ndim} ("{axes}").')
# Swap axes to XYZ order
order = []
for a in ("X", "Y", "Z"):
if a not in axes:
logger.warning(
f'Expected axes to contain "Z", "Y", and "X", got "{axes}". '
"Axes will not be automatically reordered."
)
order = None
break
order.append(axes.index(a))
if order:
data = np.transpose(data, order)
# Try parsing units - this is modelled after the tif files you get from ImageJ
units = None
space_units = None
voxdim = np.array([1, 1, 1], dtype=np.float64)
if "spacing" in header:
voxdim[2] = header["spacing"]
if "xy_spacing" in header:
voxdim[:2] = header["xy_spacing"]
if "unit" in header:
space_units = header["unit"]
units = [f"{m} {space_units}" for m in voxdim]
else:
units = voxdim
return self.convert_image(data, attrs, header, voxdim, units, space_units)
|