zincjs 1.19.4 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zincjs",
3
- "version": "1.19.4",
3
+ "version": "1.20.0",
4
4
  "description": "ZincJS (Web-based-Zinc-Visualisation)",
5
5
  "main": "build/zinc.js",
6
6
  "directories": {
@@ -39,7 +39,6 @@ const defaultTextureSettings = {
39
39
  };
40
40
 
41
41
  const defaultOptions = {
42
- hideWhitePixel: false,
43
42
  hideBlackPixel: true,
44
43
  keepScalePosition: true,
45
44
  filterByValue: true,
@@ -127,197 +126,81 @@ function readNIFTI(data) {
127
126
  }
128
127
 
129
128
 
130
- function createSources(niftiHeader, niftiImage, maskHeader, maskImage, options) {
131
- if (niftiHeader?.dims && niftiHeader.dims[0] === 3) {
132
- const width = niftiHeader.dims[1];
133
- const height = niftiHeader.dims[2];
134
- const depth = niftiHeader.dims[3];
135
- const { typedData, dataType } = getTypedData(niftiHeader, niftiImage);
136
- const sliceSize = width * height;
137
- const length = sliceSize * depth * 4;
138
- const fullArray = new Uint8Array(length);
139
- let maskData = undefined;
140
- if (maskHeader && maskImage) {
141
- const maskWidth = maskHeader.dims[1];
142
- const maskHeight = maskHeader.dims[2];
143
- const maskDepth = maskHeader.dims[3];
144
- if (maskWidth === width && maskHeight === height && maskDepth === depth) {
145
- const maskedTypedData = getTypedData(maskHeader, maskImage);
146
- maskData = maskedTypedData.typedData;
147
- }
148
- }
149
- let slope = niftiHeader.scl_slope || 1;
150
- let intercept = niftiHeader.scl_inter || 0;
129
+ function convertNiftiToUint8Array(niftiHeader, niftiImage) {
130
+ // 1. Parse the raw array using the correct native typed array view
131
+ let rawData;
132
+ switch (niftiHeader.datatypeCode) {
133
+ case nifti.NIFTI1.TYPE_UINT8: rawData = new Uint8Array(niftiImage); break;
134
+ case nifti.NIFTI1.TYPE_INT8: rawData = new Int8Array(niftiImage); break;
135
+ case nifti.NIFTI1.TYPE_UINT16: rawData = new Uint16Array(niftiImage); break;
136
+ case nifti.NIFTI1.TYPE_INT16: rawData = new Int16Array(niftiImage); break;
137
+ case nifti.NIFTI1.TYPE_UINT32: rawData = new Uint32Array(niftiImage); break;
138
+ case nifti.NIFTI1.TYPE_INT32: rawData = new Int32Array(niftiImage); break;
139
+ case nifti.NIFTI1.TYPE_FLOAT32: rawData = new Float32Array(niftiImage); break;
140
+ case nifti.NIFTI1.TYPE_FLOAT64: rawData = new Float64Array(niftiImage); break;
141
+ default: throw new Error("Unsupported NIfTI data type");
142
+ }
151
143
 
152
- let min = Infinity;
153
- let max = -Infinity;
144
+ const slope = niftiHeader.scl_slope || 1.0;
145
+ const intercept = niftiHeader.scl_inter || 0.0;
154
146
 
155
- const dataLength = typedData.length;
156
- for (let i = 0; i < dataLength; i++) {
157
- let val = (typedData[i] * slope) + intercept;
158
- if (val < min) min = val;
159
- if (val > max) max = val;
160
- }
161
- let range = max - min;
147
+ let min = Infinity;
148
+ let max = -Infinity;
162
149
 
163
- for (let slice = 0; slice < depth; slice++) {
164
- const sliceOffset = sliceSize * slice;
165
- for (let row = 0; row < height; row++) {
166
- const rowOffset = row * width;
167
- for (let col = 0; col < width; col++) {
168
- const offset = sliceOffset + rowOffset + col;
169
- let trueVal = typedData[offset] * slope + intercept;
170
- let value = 0;
171
- if (range !== 0) {
172
- // Map [min, max] to [0, 255]
173
- value = ((trueVal - min) / range) * 255;
174
- }
175
- fullArray[offset * 4] = value;
176
- fullArray[offset * 4 + 1] = value;
177
- fullArray[offset * 4 + 2] = value;
178
- fullArray[offset * 4 + 3] = 255;
179
- if (maskData) {
180
- const maskedValue = maskData[offset];
181
- if (options.hideBlackPixel) {
182
- //if (maskedValue === 0 && 20 > value) {
183
- if (maskedValue === 0) {
184
- fullArray[offset * 4 + 3] = 0;
185
- }
186
- }
187
- } else if (options.filterByValue) {
188
- if (options.hideWhitePixel && value === 255) {
189
- fullArray[offset * 4 + 3] = 0;
190
- }
191
- if (options.hideBlackPixel && 2 >= value) {
192
- fullArray[offset * 4] = 240;
193
- fullArray[offset * 4 + 1] = 240;
194
- fullArray[offset * 4 + 2] = 240;
195
- fullArray[offset * 4 + 3] = 1.0;
196
- }
197
- }
198
- }
199
- }
200
- }
150
+ const len = rawData.length;
151
+ for (let i = 0; i < len; i++) {
152
+ let val = rawData[i];
153
+ if (val < min) min = val;
154
+ if (val > max) max = val;
155
+ }
201
156
 
202
- return {
203
- data: fullArray,
204
- width,
205
- height,
206
- depth,
207
- };
157
+ const uint8Data = new Uint8Array(len);
158
+ const range = max - min;
159
+
160
+ for (let i = 0; i < len; i++) {
161
+ let physicalVal = rawData[i];
162
+ let normalised = (physicalVal - min) / range;
163
+ normalised = Math.max(0.0, Math.min(1.0, normalised));
164
+ uint8Data[i] = Math.round(normalised * 255.0);
208
165
  }
209
- return undefined;
166
+
167
+ return uint8Data;
210
168
  }
211
- /*
212
- function createSources(niftiHeader, niftiImage, maskHeader, maskImage, options) {
169
+
170
+ function createSources(niftiHeader, niftiImage, maskHeader, maskImage) {
213
171
  if (niftiHeader?.dims && niftiHeader.dims[0] === 3) {
214
172
  const width = niftiHeader.dims[1];
215
173
  const height = niftiHeader.dims[2];
216
174
  const depth = niftiHeader.dims[3];
217
- const { typedData, dataType } = getTypedData(niftiHeader, niftiImage);
218
- const sliceSize = width * height;
219
- const length = sliceSize * depth * 4;
220
- const fullArray = new Uint8Array(length);
175
+ let isRGB = false;
176
+ if (niftiHeader.intent_code === 1007 || niftiHeader.intent_code === 1008 ||
177
+ niftiHeader.datatypeCode === 128 || niftiHeader.datatypeCode === 2304) {
178
+ isRGB = true;
179
+ }
180
+ const typedData = convertNiftiToUint8Array(niftiHeader, niftiImage);
221
181
  let maskData = undefined;
222
182
  if (maskHeader && maskImage) {
223
183
  const maskWidth = maskHeader.dims[1];
224
184
  const maskHeight = maskHeader.dims[2];
225
185
  const maskDepth = maskHeader.dims[3];
226
186
  if (maskWidth === width && maskHeight === height && maskDepth === depth) {
227
- const maskedTypedData = getTypedData(maskHeader, maskImage);
228
- maskData = maskedTypedData.typedData;
229
- }
230
- }
231
- let scale = 1;
232
- let valueOffset = 0.0;
233
- if (dataType === "float") {
234
- //It should be like
235
- // scl_slope = 0, intensity will be stored in value between 0, 1
236
- // Otherwise the following
237
- //y = scl_slope * x + scl_inter
238
- if (niftiHeader.scl_slope === 0) {
239
- scale = 255;
240
- } else {
241
- valueOffset = niftiHeader.scl_inter;
242
- }
243
- } else if (dataType === "uint") {
244
- scale = 255;
245
- } else if (dataType === "int16") {
246
- //scale = 1 / 255;
247
- if (niftiHeader.scl_slope === 0) {
248
- scale = 255;
249
- } else {
250
- valueOffset = niftiHeader.scl_inter;
251
- }
252
- }
253
- for (let slice = 0; slice < depth; slice++) {
254
- const sliceOffset = sliceSize * slice;
255
- for (let row = 0; row < height; row++) {
256
- const rowOffset = row * width;
257
- for (let col = 0; col < width; col++) {
258
- const offset = sliceOffset + rowOffset + col;
259
- let value = typedData[offset] * scale + valueOffset;
260
- if (value < 0) value = 0;
261
- fullArray[offset * 4] = value;
262
- fullArray[offset * 4 + 1] = value;
263
- fullArray[offset * 4 + 2] = value;
264
- fullArray[offset * 4 + 3] = 255;
265
- if (maskData) {
266
- const maskedValue = maskData[offset];
267
- if (options.hideBlackPixel) {
268
- //if (maskedValue === 0 && 20 > value) {
269
- if (maskedValue === 0) {
270
- fullArray[offset * 4 + 3] = 0;
271
- }
272
- }
273
- } else if (options.filterByValue) {
274
- if (options.hideWhitePixel && value === 255) {
275
- fullArray[offset * 4 + 3] = 0;
276
- }
277
- if (options.hideBlackPixel && 2 >= value) {
278
- fullArray[offset * 4] = 240;
279
- fullArray[offset * 4 + 1] = 240;
280
- fullArray[offset * 4 + 2] = 240;
281
- fullArray[offset * 4 + 3] = 1.0;
282
- }
283
- }
284
- }
187
+ const maskedTypedData = convertNiftiToUint8Array(maskHeader, maskImage);
188
+ maskData = maskedTypedData;
285
189
  }
286
190
  }
191
+
287
192
  return {
288
- data: fullArray,
193
+ data: typedData,
194
+ maskData: maskData,
289
195
  width,
290
196
  height,
291
197
  depth,
198
+ isRGB,
292
199
  };
293
200
  }
294
201
  return undefined;
295
202
  }
296
203
 
297
- */
298
-
299
- function getTypedData(niftiHeader, niftiImage) {
300
- if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_UINT8) {
301
- return { typedData: new Uint8Array(niftiImage), dataType: "uint" };
302
- } else if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_INT16) {
303
- return { typedData: new Int16Array(niftiImage), dataType: "int16" };
304
- } else if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_INT32) {
305
- return { typedData: new Int32Array(niftiImage), dataType: "int32" };
306
- } else if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_FLOAT32) {
307
- return { typedData: new Float32Array(niftiImage), dataType: "float" };
308
- } else if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_FLOAT64) {
309
- return { typedData: new Float64Array(niftiImage), dataType: "float" };
310
- } else if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_INT8) {
311
- return { typedData: new Int8Array(niftiImage), dataType: "int" };
312
- } else if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_UINT16) {
313
- return { typedData: new Uint16Array(niftiImage), dataType: "int" };
314
- } else if (niftiHeader.datatypeCode === nifti.NIFTI1.TYPE_UINT32) {
315
- return { typedData: new Uint32Array(niftiImage), dataType: "int" };
316
- } else {
317
- return;
318
- }
319
- }
320
-
321
204
  function getSFormTransformation(header, options) {
322
205
  if (header?.affine && header.dims) {
323
206
  const affine = header.affine;
@@ -349,26 +232,41 @@ function getTransformationFromHeader(header, options) {
349
232
  return {position: undefined, scale: undefined}
350
233
  }
351
234
 
235
+ function createDataTexture(data, width, height, depth, isRGB) {
236
+ const dataTexture = new THREE.DataTexture2DArray(
237
+ data, width, height, depth);
238
+ dataTexture.anisotropy = 4;
239
+ if (!isRGB) {
240
+ dataTexture.format = THREE.RedFormat;
241
+ }
242
+ dataTexture.minFilter = THREE.NearestFilter;
243
+ dataTexture.magFilter = THREE.NearestFilter;
244
+ dataTexture.needsUpdate = true;
245
+ return dataTexture;
246
+ }
247
+
352
248
  function createTextureArray(sources) {
353
249
  if (sources?.data) {
354
250
  const tArray = new TextureArray();
355
- tArray.impl = new THREE.DataTexture2DArray(
251
+ tArray.impl = new createDataTexture(
356
252
  sources.data, sources.width, sources.height, sources.depth);
357
- tArray.impl.anisotropy = 4;
358
253
  tArray.size = {
359
254
  width: sources.width,
360
255
  height: sources.height,
361
256
  depth: sources.depth,
362
257
  };
363
258
  tArray.isLoading = false;
364
- tArray.impl.needsUpdate = true;
365
259
  return tArray;
366
260
  }
367
261
  return undefined;
368
262
  }
369
263
 
370
- function createTexturePrimitives(niftiHeader, sources, useHeaderInfo, textureSettings, options) {
264
+ function createTexturePrimitives(niftiHeader, sources, useHeaderInfo, textureSettings, optionsIn) {
371
265
  if (sources?.data) {
266
+ const options = {...defaultOptions};
267
+ if (optionsIn) {
268
+ Object.assign(options, optionsIn);
269
+ }
372
270
  const newTexture = new TextureSlides();
373
271
  const tArray = createTextureArray(sources);
374
272
  if (tArray) {
@@ -385,36 +283,23 @@ function createTexturePrimitives(niftiHeader, sources, useHeaderInfo, textureSet
385
283
  settings.locations[0].scale = scale;
386
284
  settings.locations[0].position = position;
387
285
  }
388
- /*
389
- if (niftiHeader.qoffset_x) {
390
- settings.locations[0].position[0] = niftiHeader.qoffset_x;
391
- }
392
- if (niftiHeader.qoffset_y) {
393
- settings.locations[0].position[1] = niftiHeader.qoffset_y;
394
- }
395
- if (niftiHeader.qoffset_y) {
396
- settings.locations[0].position[2] = niftiHeader.qoffset_z;
397
- }
398
- if (niftiHeader.dims) {
399
- settings.locations[0].scale[0] = niftiHeader.dims[1];
400
- settings.locations[0].scale[1] = niftiHeader.dims[2];
401
- settings.locations[0].scale[2] = niftiHeader.dims[3];
402
- }
403
- */
404
286
  }
405
287
  newTexture.initialise(settings, undefined);
406
288
  newTexture.showEdges(0x999999);
289
+ if (sources.maskData && !options.timeEnabled) {
290
+ const maskTexture = createDataTexture(sources.maskData,
291
+ sources.width, sources.height, sources.depth, false);
292
+ newTexture.setMask(maskTexture);
293
+ }
294
+ newTexture.setNumberOfChannels(sources.isRGB ? 3 : 1);
295
+
407
296
  return newTexture;
408
297
  }
409
298
  }
410
299
  return undefined;
411
300
  }
412
301
 
413
- async function getImagesFromURL(url, maskURL, optionsIn) {
414
- const options = {...defaultOptions};
415
- if (optionsIn) {
416
- Object.assign(options, optionsIn);
417
- }
302
+ async function getImagesFromURL(url, maskURL) {
418
303
 
419
304
  // try {
420
305
  let maskHeader = undefined, maskImage = undefined;
@@ -428,13 +313,13 @@ async function getImagesFromURL(url, maskURL, optionsIn) {
428
313
  const response = await fetch(url);
429
314
  const buffer = await response.arrayBuffer();
430
315
  const {niftiHeader, niftiImage} = readNIFTI(buffer);
431
- const sources = createSources(niftiHeader, niftiImage, maskHeader, maskImage, options);
432
-
316
+ const sources = createSources(niftiHeader, niftiImage, maskHeader, maskImage);
433
317
  return {sources, niftiHeader};
434
318
  }
435
319
 
436
320
 
437
- async function createPrimitivesFromNIFTI(url, useHeaderInfo, maskURL, textureSettings, optionsIn) {
321
+ async function createPrimitivesFromNIFTI(
322
+ url, useHeaderInfo, maskURL, textureSettings, optionsIn) {
438
323
  let timeEnabled = false;
439
324
  let textureP = undefined;
440
325
  let firstURL = url;
@@ -455,6 +340,7 @@ async function createPrimitivesFromNIFTI(url, useHeaderInfo, maskURL, textureSet
455
340
  }
456
341
  textureP.timeEnabled = true;
457
342
  }
343
+
458
344
  return textureP;
459
345
  }
460
346
 
@@ -24,6 +24,9 @@ const TextureSlides = function (textureIn) {
24
24
  let flipZ = false;
25
25
  let brightness = 0.0;
26
26
  let contrast = 1.0;
27
+ let nChannels = 1;
28
+ let maskTexture = undefined;
29
+ let maskEnabled = false;
27
30
  let discardAlpha = true;
28
31
  let lt0 = 0;
29
32
  let lt1 = 0;
@@ -126,6 +129,9 @@ const TextureSlides = function (textureIn) {
126
129
  uniforms.depth.value = this.texture.size.depth;
127
130
  uniforms.flipY.value = flipY;
128
131
  uniforms.flipZ.value = flipZ;
132
+ uniforms.mask.value = maskTexture;
133
+ uniforms.maskEnabled.value = maskEnabled;
134
+ uniforms.nChannels.value = nChannels;
129
135
  const options = {
130
136
  fs: shader.fs,
131
137
  vs: shader.vs,
@@ -356,36 +362,34 @@ const TextureSlides = function (textureIn) {
356
362
  edgesLine.visible = true;
357
363
  }
358
364
 
359
- this.isAlphaPixelDiscarded = () => {
360
- return discardAlpha;
361
- }
362
365
 
363
- this.discardAlphaPixel = (flag) => {
364
- discardAlpha = flag;
366
+ this.setUniformsValue = (name, val) => {
365
367
  this.morph.children.forEach((mesh) => {
366
368
  const material = mesh.material;
367
369
  if (material.type === "ShaderMaterial") {
368
370
  const uniforms = material.uniforms;
369
- uniforms.discardAlpha.value = discardAlpha;
371
+ uniforms[name].value = val;
370
372
  material.needsUpdate = true;
371
373
  }
372
374
  });
373
375
  }
374
376
 
377
+ this.isAlphaPixelDiscarded = () => {
378
+ return discardAlpha;
379
+ }
380
+
381
+ this.discardAlphaPixel = (flag) => {
382
+ discardAlpha = flag;
383
+ this.setUniformsValue("discardAlpha", discardAlpha);
384
+ }
385
+
375
386
  this.getBrightness = () => {
376
387
  return brightness;
377
388
  }
378
389
 
379
390
  this.setBrightness = (brightnessIn) => {
380
391
  brightness = brightnessIn;
381
- this.morph.children.forEach((mesh) => {
382
- const material = mesh.material;
383
- if (material.type === "ShaderMaterial") {
384
- const uniforms = material.uniforms;
385
- uniforms.brightness.value = brightness;
386
- material.needsUpdate = true;
387
- }
388
- });
392
+ this.setUniformsValue("brightness", brightness);
389
393
  }
390
394
 
391
395
  this.getContrast = () => {
@@ -395,17 +399,29 @@ const TextureSlides = function (textureIn) {
395
399
  this.setContrast = (contrastIn) => {
396
400
  if (contrast >= 0 ) {
397
401
  contrast = contrastIn;
398
- this.morph.children.forEach((mesh) => {
399
- const material = mesh.material;
400
- if (material.type === "ShaderMaterial") {
401
- const uniforms = material.uniforms;
402
- uniforms.contrast.value = contrast;
403
- material.needsUpdate = true;
404
- }
405
- });
402
+ this.setUniformsValue("contrast", contrast);
406
403
  }
407
404
  }
408
405
 
406
+ this.getNumberOfChannels = () => {
407
+ return nChannels;
408
+ }
409
+
410
+ this.setNumberOfChannels = (numbersIn) => {
411
+ nChannels = numbersIn;
412
+ this.setUniformsValue("nChannels", nChannels);
413
+ }
414
+
415
+ this.getMask = () => {
416
+ return maskTexture;
417
+ }
418
+
419
+ this.setMask = (maskTextureIn) => {
420
+ maskTexture = maskTextureIn;
421
+ this.setUniformsValue("mask", maskTexture);
422
+ this.setUniformsValue("maskEnabled", maskTexture ? true : false);
423
+ }
424
+
409
425
  this.hideEdges = () => {
410
426
  if (edgesLine) {
411
427
  edgesLine.visible = false;
@@ -10,27 +10,35 @@ precision highp sampler2DArray;
10
10
 
11
11
  uniform sampler2DArray diffuse0;
12
12
  uniform sampler2DArray diffuse1;
13
+ uniform sampler2DArray mask;
13
14
  uniform bool discardAlpha;
15
+ uniform bool maskEnabled;
14
16
  uniform float brightness;
15
17
  uniform float contrast;
16
18
  uniform float time;
19
+ uniform int nChannels;
17
20
  in vec3 vUw;
18
21
 
19
22
  out vec4 outColor;
20
23
 
21
24
  void main() {
22
25
 
23
- vec4 color0 = texture( diffuse0, vUw );
24
- vec4 color1 = texture( diffuse1, vUw );
25
- vec4 color = mix(color0, color1, time);
26
+ float color0 = texture( diffuse0, vUw ).r;
27
+ float color1 = texture( diffuse1, vUw ).r;
28
+ float color = mix(color0, color1, time);
29
+ // discard if alpha is zero
30
+ if (maskEnabled && discardAlpha) {
31
+ float mask = texture(mask, vUw).r;
32
+ if (mask == 0.0) discard;
33
+ }
26
34
 
27
35
  // discard if alpha is zero
28
- if (discardAlpha && color.a == 0.0) discard;
36
+ //if (discardAlpha && color.a == 0.0) discard;
29
37
  // Apply brightness
30
- vec3 brightenedColor = color.rgb + vec3(brightness);
38
+ vec3 brightenedColor = vec3(color) + vec3(brightness);
31
39
  // Apply contrast
32
40
  vec3 contrastedColor = (brightenedColor - vec3(0.5)) * contrast + vec3(0.5);
33
- outColor = vec4(contrastedColor, color.a);
41
+ outColor = vec4(contrastedColor, 1.0);
34
42
  }
35
43
  `;
36
44
 
@@ -73,9 +81,12 @@ const getUniforms = function() {
73
81
  discardAlpha: {value: true},
74
82
  diffuse0: { value: undefined },
75
83
  diffuse1: { value: undefined },
76
- direction: {value: 1},
77
- flipY: { value: true},
78
- flipZ: { value: false},
84
+ direction: { value: 1 },
85
+ flipY: { value: true },
86
+ flipZ: { value: false },
87
+ nChannels: { value: 1 },
88
+ mask: { value: undefined },
89
+ maskEnabled: { value: false },
79
90
  slide: { value: new THREE.Vector3( 0, 0, 1 ) },
80
91
  time: { value: 0 }
81
92
  };