UNPKG

13.4 kBJavaScriptView Raw
1import OMEXML from './omeXML';
2import {
3 isInTileBounds,
4 byteSwapInplace,
5 padTileWithZeros,
6 dimensionsFromOMEXML
7} from './utils';
8import { DTYPE_VALUES } from '../constants';
9
10const DTYPE_LOOKUP = {
11 uint8: '<u1',
12 uint16: '<u2',
13 uint32: '<u4',
14 float: '<f4',
15 // TODO: we currently need to cast these dtypes to their uint counterparts.
16 int8: '<u1',
17 int16: '<u2',
18 int32: '<u4'
19};
20
21/**
22 * This class serves as a wrapper for fetching tiff data from a file server.
23 * @param {Object} args
24 * @param {Object} args.tiff geotiffjs tiff object.
25 * @param {Object} args.pool Pool that implements a `decode` function.
26 * @param {Object} args.firstImage First image (geotiff Image object) in the tiff (containing base-resolution data).
27 * @param {String} args.omexmlString Raw OMEXML as a string.
28 * @param {Array} args.offsets The offsets of each IFD in the tiff.
29 * */
30export default class OMETiffLoader {
31 constructor({ tiff, pool, firstImage, omexmlString, offsets }) {
32 this.pool = pool;
33 this.tiff = tiff;
34 this.type = 'ome-tiff';
35 // get first image's description, which contains OMEXML
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 // We use zarr's internal format. It encodes endianness, but we leave it little for now
59 // since javascript is little endian.
60 this.dtype = DTYPE_LOOKUP[this.omexml.Type];
61 // This is experimental and will take some tuning to properly detect. For now,
62 // if the SamplesPerPixel is 3 (i.e interleaved) or if there are three uint8 channels,
63 // we flag that as rgb.
64 this.isRgb =
65 this.omexml.SamplesPerPixel === 3 ||
66 (this.channelNames.length === 3 && this.omexml.Type === 'uint8');
67 }
68
69 /**
70 * Returns an IFD index for a given loader selection.
71 * @param {number} z Z axis selection.
72 * @param {number} time Time axis selection.
73 * @param {String} channel Channel axis selection.
74 * @returns {number} IFD index.
75 */
76 _getIFDIndex({ z = 0, channel, time = 0 }) {
77 let channelIndex;
78 // Without names, enforce a numeric channel indexing scheme
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 * Handles `onTileError` within deck.gl
119 * @param {Error} err Error thrown in tile layer
120 */
121 // eslint-disable-next-line class-methods-use-this
122 onTileError(err) {
123 console.error(err);
124 }
125
126 /**
127 * Returns image tiles at tile-position (x, y) at pyramidal level z.
128 * @param {number} x positive integer
129 * @param {number} y positive integer
130 * @param {number} z positive integer (0 === highest zoom level)
131 * @param {Array} loaderSelection, Array of number Arrays specifying channel selections
132 * @returns {Object} data: TypedArray[], width: number (tileSize), height: number (tileSize).
133 * Default is `{data: [], width: tileSize, height: tileSize}`.
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 // We need to put the request for parsing the file directory into this array.
147 // This allows us to get tiff pages directly based on offset without parsing everything.
148 if (!isBioFormats6Pyramid) {
149 this._parseIFD(pyramidIndex);
150 } else {
151 // Pyramids with large z-stacks + large numbers of channels could get slow
152 // so we allow for offsets for the lowest-resolution images ("parentImage").
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 * Returns full image panes (at level z if pyramid)
175 * @param {number} z positive integer (0 === highest zoom level)
176 * @param {Array} loaderSelection, Array of number Arrays specifying channel selections
177 * @returns {Object} data: TypedArray[], width: number, height: number
178 * Default is `{data: [], width, height}`.
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 // We need to put the request for parsing the file directory into this array.
189 // This allows us to get tiff pages directly based on offset without parsing everything.
190 if (!isBioFormats6Pyramid) {
191 this._parseIFD(pyramidIndex);
192 } else {
193 // Pyramids with large z-stacks + large numbers of channels could get slow
194 // so we allow for offsets for the initial images ("parentImage").
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 // Flips bits for us for endianness.
205 const raster = await image.readRasters({ pool });
206 return raster[0];
207 })
208 );
209 // Get first selection * SizeZ * SizeT * SizeC + loaderSelection[0] size as proxy for image size.
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 // GeoTiff.js returns 32 bit uint when the tiff has 32 significant bits.
222 data = rasters.map(r => new Float32Array(r.buffer));
223 } else if (this.omexml.Type.startsWith('int')) {
224 // geotiff.js returns the correct typedarray but need to cast to Uint for viv.
225 const b = rasters[0].BYTES_PER_ELEMENT;
226 // eslint-disable-next-line no-nested-ternary
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 * Returns image width and height (at pyramid level z) without fetching data.
241 * This information is inferrable from the provided omexml.
242 * This is only used by the OverviewLayer for inferring the box size.
243 * It is NOT the actual pixel-size but rather the image size
244 * without any padding.
245 * @param {number} z positive integer (0 === highest zoom level)
246 * @returns {Object} width: number, height: number
247 */
248 getRasterSize({ z }) {
249 const { width, height } = this;
250 /* eslint-disable no-bitwise */
251 return {
252 height: height >> z,
253 width: width >> z
254 };
255 /* eslint-disable no-bitwise */
256 }
257
258 /**
259 * Get the metadata associated with an OMETiff image layer, in a human-readable format.
260 * @returns {Object} Metadata keys mapped to values.
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 * The endianness of JavaScript TypedArrays are determined by the endianness
322 * of the end-users' hardware. Nearly all desktop computers are x86 (little endian),
323 * so we flip bytes in place for big-endian buffers. This is substantially faster than using
324 * the DataView API.
325 */
326 if (!image.littleEndian) {
327 byteSwapInplace(data);
328 }
329
330 // If the tile data is not (tileSize x tileSize), pad the data with zeros
331 if (data.length < this.tileSize * this.tileSize) {
332 const { height, width } = this.getRasterSize({ z });
333 let trueHeight = height;
334 let trueWidth = width;
335 // If height * tileSize is the size of the data, then the width is the tileSize.
336 if (data.length / height === this.tileSize) {
337 trueWidth = this.tileSize;
338 }
339 // If width * tileSize is the size of the data, then the height is the tileSize.
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 // Uint view isn't correct for underling buffer, need to take an
352 // IntXArray view and cast to UintXArray.
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}