1 | import OMEXML from './omeXML';
|
2 | import {
|
3 | isInTileBounds,
|
4 | byteSwapInplace,
|
5 | padTileWithZeros,
|
6 | dimensionsFromOMEXML
|
7 | } from './utils';
|
8 | import { DTYPE_VALUES } from '../constants';
|
9 |
|
10 | const DTYPE_LOOKUP = {
|
11 | uint8: '<u1',
|
12 | uint16: '<u2',
|
13 | uint32: '<u4',
|
14 | float: '<f4',
|
15 |
|
16 | int8: '<u1',
|
17 | int16: '<u2',
|
18 | int32: '<u4'
|
19 | };
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | export default class OMETiffLoader {
|
31 | constructor({ tiff, pool, firstImage, omexmlString, offsets }) {
|
32 | this.pool = pool;
|
33 | this.tiff = tiff;
|
34 | this.type = 'ome-tiff';
|
35 |
|
36 | this.omexml = new OMEXML(omexmlString);
|
37 | this.physicalSizes = {
|
38 | x: {
|
39 | value: this.omexml.PhysicalSizeX,
|
40 | unit: this.omexml.PhysicalSizeXUnit
|
41 | },
|
42 | y: {
|
43 | value: this.omexml.PhysicalSizeY,
|
44 | unit: this.omexml.PhysicalSizeYUnit
|
45 | }
|
46 | };
|
47 | this.software = firstImage.fileDirectory.Software;
|
48 | this.offsets = offsets || [];
|
49 | this.channelNames = this.omexml.getChannelNames();
|
50 | this.width = this.omexml.SizeX;
|
51 | this.height = this.omexml.SizeY;
|
52 | this.tileSize = firstImage.getTileWidth();
|
53 | const { SubIFDs } = firstImage.fileDirectory;
|
54 | this.numLevels = SubIFDs?.length || this.omexml.getNumberOfImages();
|
55 | this.isBioFormats6Pyramid = SubIFDs;
|
56 | this.isPyramid = this.numLevels > 1;
|
57 | this.dimensions = dimensionsFromOMEXML(this.omexml);
|
58 |
|
59 |
|
60 | this.dtype = DTYPE_LOOKUP[this.omexml.Type];
|
61 |
|
62 |
|
63 |
|
64 | this.isRgb =
|
65 | this.omexml.SamplesPerPixel === 3 ||
|
66 | (this.channelNames.length === 3 && this.omexml.Type === 'uint8');
|
67 | }
|
68 |
|
69 | |
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | _getIFDIndex({ z = 0, channel, time = 0 }) {
|
77 | let channelIndex;
|
78 |
|
79 | if (this.channelNames.every(v => !v)) {
|
80 | console.warn(
|
81 | 'No channel names found in OMEXML. Please be sure to use numeric indexing.'
|
82 | );
|
83 | channelIndex = channel;
|
84 | } else if (typeof channel === 'string') {
|
85 | channelIndex = this.channelNames.indexOf(channel);
|
86 | } else if (typeof channel === 'number') {
|
87 | channelIndex = channel;
|
88 | } else {
|
89 | throw new Error('Channel selection must be numeric index or string');
|
90 | }
|
91 | const { SizeZ, SizeT, SizeC, DimensionOrder } = this.omexml;
|
92 | switch (DimensionOrder) {
|
93 | case 'XYZCT': {
|
94 | return time * SizeZ * SizeC + channelIndex * SizeZ + z;
|
95 | }
|
96 | case 'XYZTC': {
|
97 | return channelIndex * SizeZ * SizeT + time * SizeZ + z;
|
98 | }
|
99 | case 'XYCTZ': {
|
100 | return z * SizeC * SizeT + time * SizeC + channelIndex;
|
101 | }
|
102 | case 'XYCZT': {
|
103 | return time * SizeC * SizeZ + z * SizeC + channelIndex;
|
104 | }
|
105 | case 'XYTCZ': {
|
106 | return z * SizeT * SizeC + channelIndex * SizeT + time;
|
107 | }
|
108 | case 'XYTZC': {
|
109 | return channelIndex * SizeT * SizeZ + z * SizeT + time;
|
110 | }
|
111 | default: {
|
112 | throw new Error('Dimension order is required for OMETIFF');
|
113 | }
|
114 | }
|
115 | }
|
116 |
|
117 | |
118 |
|
119 |
|
120 |
|
121 |
|
122 | onTileError(err) {
|
123 | console.error(err);
|
124 | }
|
125 |
|
126 | |
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | async getTile({ x, y, z, loaderSelection = [], signal }) {
|
136 | if (!this._tileInBounds({ x, y, z })) {
|
137 | return null;
|
138 | }
|
139 | const { tiff, isBioFormats6Pyramid, omexml, tileSize } = this;
|
140 | const { SizeZ, SizeT, SizeC } = omexml;
|
141 | const pyramidOffset = z * SizeZ * SizeT * SizeC;
|
142 | let image;
|
143 | const tileRequests = loaderSelection.map(async sel => {
|
144 | const index = this._getIFDIndex(sel);
|
145 | const pyramidIndex = pyramidOffset + index;
|
146 |
|
147 |
|
148 | if (!isBioFormats6Pyramid) {
|
149 | this._parseIFD(pyramidIndex);
|
150 | } else {
|
151 |
|
152 |
|
153 | this._parseIFD(index);
|
154 | const parentImage = await tiff.getImage(index);
|
155 | if (z !== 0) {
|
156 | tiff.ifdRequests[pyramidIndex] = tiff.parseFileDirectoryAt(
|
157 | parentImage.fileDirectory.SubIFDs[z - 1]
|
158 | );
|
159 | }
|
160 | }
|
161 | image = await tiff.getImage(pyramidIndex);
|
162 | return this._getChannel({ image, x, y, z, signal });
|
163 | });
|
164 | const tiles = await Promise.all(tileRequests);
|
165 | if (signal?.aborted) return null;
|
166 | return {
|
167 | data: tiles,
|
168 | width: tileSize,
|
169 | height: tileSize
|
170 | };
|
171 | }
|
172 |
|
173 | |
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 | async getRaster({ z, loaderSelection }) {
|
182 | const { tiff, omexml, isBioFormats6Pyramid, pool } = this;
|
183 | const { SizeZ, SizeT, SizeC } = omexml;
|
184 | const rasters = await Promise.all(
|
185 | loaderSelection.map(async sel => {
|
186 | const index = this._getIFDIndex(sel);
|
187 | const pyramidIndex = z * SizeZ * SizeT * SizeC + index;
|
188 |
|
189 |
|
190 | if (!isBioFormats6Pyramid) {
|
191 | this._parseIFD(pyramidIndex);
|
192 | } else {
|
193 |
|
194 |
|
195 | this._parseIFD(index);
|
196 | const parentImage = await tiff.getImage(index);
|
197 | if (z !== 0) {
|
198 | tiff.ifdRequests[pyramidIndex] = tiff.parseFileDirectoryAt(
|
199 | parentImage.fileDirectory.SubIFDs[z - 1]
|
200 | );
|
201 | }
|
202 | }
|
203 | const image = await tiff.getImage(pyramidIndex);
|
204 |
|
205 | const raster = await image.readRasters({ pool });
|
206 | return raster[0];
|
207 | })
|
208 | );
|
209 |
|
210 | if (!loaderSelection || loaderSelection.length === 0) {
|
211 | return { data: [], ...this.getRasterSize({ z }) };
|
212 | }
|
213 | const image = await tiff.getImage(
|
214 | z * SizeZ * SizeT * SizeC + this._getIFDIndex(loaderSelection[0])
|
215 | );
|
216 | const width = image.getWidth();
|
217 | const height = image.getHeight();
|
218 |
|
219 | let data;
|
220 | if (this.dtype === '<f4') {
|
221 |
|
222 | data = rasters.map(r => new Float32Array(r.buffer));
|
223 | } else if (this.omexml.Type.startsWith('int')) {
|
224 |
|
225 | const b = rasters[0].BYTES_PER_ELEMENT;
|
226 |
|
227 | const T = b === 1 ? Uint8Array : b === 2 ? Uint16Array : Uint32Array;
|
228 | data = rasters.map(r => new T(r));
|
229 | } else {
|
230 | data = rasters;
|
231 | }
|
232 | return {
|
233 | data,
|
234 | width,
|
235 | height
|
236 | };
|
237 | }
|
238 |
|
239 | |
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | getRasterSize({ z }) {
|
249 | const { width, height } = this;
|
250 |
|
251 | return {
|
252 | height: height >> z,
|
253 | width: width >> z
|
254 | };
|
255 |
|
256 | }
|
257 |
|
258 | |
259 |
|
260 |
|
261 |
|
262 | getMetadata() {
|
263 | const { omexml } = this;
|
264 | const {
|
265 | metadataOMEXML: {
|
266 | Image: { AcquisitionDate },
|
267 | StructuredAnnotations
|
268 | },
|
269 | SizeX,
|
270 | SizeY,
|
271 | SizeZ,
|
272 | SizeT,
|
273 | SizeC,
|
274 | Type,
|
275 | PhysicalSizeX,
|
276 | PhysicalSizeXUnit,
|
277 | PhysicalSizeY,
|
278 | PhysicalSizeYUnit,
|
279 | PhysicalSizeZ,
|
280 | PhysicalSizeZUnit
|
281 | } = omexml;
|
282 |
|
283 | const physicalSizeAndUnitX =
|
284 | PhysicalSizeX && PhysicalSizeXUnit
|
285 | ? `${PhysicalSizeX} (${PhysicalSizeXUnit})`
|
286 | : '-';
|
287 | const physicalSizeAndUnitY =
|
288 | PhysicalSizeY && PhysicalSizeYUnit
|
289 | ? `${PhysicalSizeY} (${PhysicalSizeYUnit})`
|
290 | : '-';
|
291 | const physicalSizeAndUnitZ =
|
292 | PhysicalSizeZ && PhysicalSizeZUnit
|
293 | ? `${PhysicalSizeZ} (${PhysicalSizeZUnit})`
|
294 | : '-';
|
295 | let roiCount;
|
296 | if (StructuredAnnotations) {
|
297 | const { MapAnnotation } = StructuredAnnotations;
|
298 | roiCount =
|
299 | MapAnnotation && MapAnnotation.Value
|
300 | ? Object.entries(MapAnnotation.Value).length
|
301 | : 0;
|
302 | }
|
303 | return {
|
304 | 'Acquisition Date': AcquisitionDate,
|
305 | 'Dimensions (XY)': `${SizeX} x ${SizeY}`,
|
306 | 'Pixels Type': Type,
|
307 | 'Pixels Size (XYZ)': `${physicalSizeAndUnitX} x ${physicalSizeAndUnitY} x ${physicalSizeAndUnitZ}`,
|
308 | 'Z-sections/Timepoints': `${SizeZ} x ${SizeT}`,
|
309 | Channels: SizeC,
|
310 | 'ROI Count': roiCount
|
311 | };
|
312 | }
|
313 |
|
314 | async _getChannel({ image, x, y, z, signal }) {
|
315 | const { dtype } = this;
|
316 | const { TypedArray } = DTYPE_VALUES[dtype];
|
317 | const tile = await image.getTileOrStrip(x, y, 0, this.pool, signal);
|
318 | const data = new TypedArray(tile.data);
|
319 | if (signal?.aborted) return null;
|
320 | |
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 | if (!image.littleEndian) {
|
327 | byteSwapInplace(data);
|
328 | }
|
329 |
|
330 |
|
331 | if (data.length < this.tileSize * this.tileSize) {
|
332 | const { height, width } = this.getRasterSize({ z });
|
333 | let trueHeight = height;
|
334 | let trueWidth = width;
|
335 |
|
336 | if (data.length / height === this.tileSize) {
|
337 | trueWidth = this.tileSize;
|
338 | }
|
339 |
|
340 | if (data.length / width === this.tileSize) {
|
341 | trueHeight = this.tileSize;
|
342 | }
|
343 | return padTileWithZeros(
|
344 | { data, width: trueWidth, height: trueHeight },
|
345 | this.tileSize,
|
346 | this.tileSize
|
347 | );
|
348 | }
|
349 |
|
350 | if (this.omexml.Type.startsWith('int')) {
|
351 |
|
352 |
|
353 | if (data.BYTES_PER_ELEMENT === 1) {
|
354 | return new Uint8Array(new Int8Array(data.buffer));
|
355 | }
|
356 | if (data.BYTES_PER_ELEMENT === 2) {
|
357 | return new Uint16Array(new Int16Array(data.buffer));
|
358 | }
|
359 | return new Uint32Array(new Int32Array(data.buffer));
|
360 | }
|
361 |
|
362 | return data;
|
363 | }
|
364 |
|
365 | _tileInBounds({ x, y, z }) {
|
366 | const { width, height, tileSize, numLevels } = this;
|
367 | return isInTileBounds({
|
368 | x,
|
369 | y,
|
370 | z,
|
371 | width,
|
372 | height,
|
373 | tileSize,
|
374 | numLevels
|
375 | });
|
376 | }
|
377 |
|
378 | _parseIFD(index) {
|
379 | const { tiff, offsets } = this;
|
380 | if (offsets.length > 0) {
|
381 | tiff.ifdRequests[index] = tiff.parseFileDirectoryAt(offsets[index]);
|
382 | }
|
383 | }
|
384 | }
|