larvitar 1.2.9 → 1.3.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/README.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  ## Dicom Image Toolkit for CornestoneJS
8
8
 
9
- ### Current version: 1.2.9
9
+ ### Current version: 1.3.0
10
10
 
11
- ### Latest Stable version: 1.2.9
11
+ ### Latest Stable version: 1.3.0
12
12
 
13
- ### Latest Published Release: 1.2.9
13
+ ### Latest Published Release: 1.3.0
14
14
 
15
15
  This library provides common DICOM functionalities to be used in web-applications: it's wrapper that simplifies the use of cornestone-js environment.
16
16
  Orthogonal multiplanar reformat is included as well as custom loader/exporter for nrrd files and [Vuex](https://vuex.vuejs.org/) custom integration.
@@ -145,6 +145,10 @@ export const updateLoadedStack = function (
145
145
  let numberOfFrames = seriesData.metadata["x00280008"];
146
146
  let modality = seriesData.metadata["x00080060"];
147
147
  let isMultiframe = numberOfFrames > 1 ? true : false;
148
+ let numberOfTemporalPositions = seriesData.metadata["x00200105"];
149
+ let acquisitionNumberAttribute = seriesData.metadata["x00200012"];
150
+ let is4D = seriesData.metadata.is4D;
151
+
148
152
  let color = cornerstoneWADOImageLoader.isColorImage(
149
153
  seriesData.metadata["x00280004"]
150
154
  );
@@ -160,16 +164,22 @@ export const updateLoadedStack = function (
160
164
  larvitarSeriesInstanceUID: sid,
161
165
  seriesUID: sid,
162
166
  studyUID: ssid,
163
- numberOfImages: 0,
167
+ numberOfImages: is4D ? acquisitionNumberAttribute : 0,
164
168
  numberOfSlices: numberOfSlices,
165
169
  numberOfFrames: numberOfFrames,
170
+ numberOfTemporalPositions: numberOfTemporalPositions,
166
171
  isMultiframe: isMultiframe,
172
+ is4D: is4D,
167
173
  modality: modality,
168
174
  color: color,
169
175
  bytes: 0
170
176
  };
171
177
  }
172
178
 
179
+ const sortMethods = is4D
180
+ ? ["imagePosition", "contentTime"]
181
+ : ["imagePosition"];
182
+
173
183
  // if the parsed file is a new series instance, keep it
174
184
  if (isNewInstance(allSeriesStack[id].instances, iid)) {
175
185
  // generate an imageId for the file and store it
@@ -180,7 +190,9 @@ export const updateLoadedStack = function (
180
190
  );
181
191
 
182
192
  allSeriesStack[id].imageIds.push(imageId);
183
- allSeriesStack[id].numberOfImages += 1;
193
+ if (is4D === false) {
194
+ allSeriesStack[id].numberOfImages += 1;
195
+ }
184
196
  allSeriesStack[id].bytes += seriesData.file.size;
185
197
  // store needed instance tags
186
198
  allSeriesStack[id].instances[imageId] = {
@@ -191,7 +203,7 @@ export const updateLoadedStack = function (
191
203
  // order images in stack
192
204
  allSeriesStack[id].imageIds = getSortedStack(
193
205
  allSeriesStack[id],
194
- ["imagePosition"],
206
+ sortMethods,
195
207
  true
196
208
  );
197
209
  // populate the ordered dictionary of instanceUIDs
@@ -5,6 +5,7 @@
5
5
  // external libraries
6
6
  import { parseDicom } from "dicom-parser";
7
7
  import { forEach, each, has, pick } from "lodash";
8
+ import { v4 as uuidv4 } from "uuid";
8
9
 
9
10
  // internal libraries
10
11
  import { getPixelRepresentation, randomId, parseTag } from "./imageUtils.js";
@@ -126,10 +127,17 @@ export const parseDataSet = function (dataSet, metadata, customFilter) {
126
127
  * @function parseNextFile
127
128
  * @param {Array} parsingQueue - Array of queued files to be parsed
128
129
  * @param {Object} allSeriesStack - Series stack object to be populated
130
+ * @param {string} uuid - Series uuid to be used if series instance uuid is missing
129
131
  * @param {Function} resolve - Promise resolve function
130
132
  * @param {Function} reject - Promise reject function
131
133
  */
132
- let parseNextFile = function (parsingQueue, allSeriesStack, resolve, reject) {
134
+ let parseNextFile = function (
135
+ parsingQueue,
136
+ allSeriesStack,
137
+ uuid,
138
+ resolve,
139
+ reject
140
+ ) {
133
141
  // initialize t0 on first file of the queue
134
142
  if (
135
143
  Object.keys(allSeriesStack).length === 0 &&
@@ -155,22 +163,24 @@ let parseNextFile = function (parsingQueue, allSeriesStack, resolve, reject) {
155
163
  let t1 = performance.now();
156
164
  console.log(`Call to readFiles took ${t1 - t0} milliseconds.`);
157
165
  file = null;
158
- reject(allSeriesStack, "Available memory is not enough");
166
+ reject("Available memory is not enough");
159
167
  return;
160
168
  } else {
161
169
  // parse the file and wait for results
162
170
  parseFile(file)
163
171
  .then(seriesData => {
172
+ // use generated series uid if not found in dicom file
173
+ seriesData.metadata.seriesUID = seriesData.metadata.seriesUID || uuid;
164
174
  // add file to cornerstoneWADOImageLoader file manager
165
175
  updateLoadedStack(seriesData, allSeriesStack);
166
176
  // proceed with the next file to parse
167
- parseNextFile(parsingQueue, allSeriesStack, resolve, reject);
177
+ parseNextFile(parsingQueue, allSeriesStack, uuid, resolve, reject);
168
178
  seriesData = null;
169
179
  file = null;
170
180
  })
171
181
  .catch(err => {
172
182
  console.warn(err);
173
- parseNextFile(parsingQueue, allSeriesStack, resolve, reject);
183
+ parseNextFile(parsingQueue, allSeriesStack, uuid, resolve, reject);
174
184
  file = null;
175
185
  });
176
186
  }
@@ -193,7 +203,8 @@ let parseFiles = function (fileList) {
193
203
  }
194
204
  });
195
205
  return new Promise((resolve, reject) => {
196
- parseNextFile(parsingQueue, allSeriesStack, resolve, reject);
206
+ const uuid = uuidv4();
207
+ parseNextFile(parsingQueue, allSeriesStack, uuid, resolve, reject);
197
208
  });
198
209
  };
199
210
 
@@ -220,6 +231,14 @@ let parseFile = function (file) {
220
231
  let metadata = {};
221
232
  parseDataSet(dataSet, metadata);
222
233
 
234
+ let temporalPositionIdentifier = metadata["x00200100"]; // Temporal order of a dynamic or functional set of Images.
235
+ let numberOfTemporalPositions = metadata["x00200105"]; // Total number of temporal positions prescribed.
236
+ const is4D =
237
+ (temporalPositionIdentifier !== undefined) &
238
+ (numberOfTemporalPositions != undefined)
239
+ ? true
240
+ : false;
241
+
223
242
  let numberOfFrames = metadata["x00280008"];
224
243
  let isMultiframe = numberOfFrames > 1 ? true : false;
225
244
  // Overwrite SOPInstanceUID to manage multiframes.
@@ -279,6 +298,14 @@ let parseFile = function (file) {
279
298
  imageObject.metadata.frameDelay = metadata["x00181066"];
280
299
  }
281
300
  imageObject.metadata.isMultiframe = isMultiframe;
301
+ if (is4D) {
302
+ imageObject.metadata.temporalPositionIdentifier =
303
+ metadata["x00200100"];
304
+ imageObject.metadata.numberOfTemporalPositions =
305
+ metadata["x00200105"];
306
+ imageObject.metadata.contentTime = metadata["x00080033"];
307
+ }
308
+ imageObject.metadata.is4D = is4D;
282
309
  imageObject.metadata.windowCenter = metadata["x00281050"];
283
310
  imageObject.metadata.windowWidth = metadata["x00281051"];
284
311
  imageObject.metadata.minPixelValue = metadata["x00280106"];
@@ -292,7 +319,7 @@ let parseFile = function (file) {
292
319
  }
293
320
  }
294
321
  } catch (err) {
295
- console.error(err);
322
+ console.warn(err);
296
323
  reject("can not read this file");
297
324
  }
298
325
  };
@@ -369,16 +369,29 @@ export const updateImage = function (
369
369
  return;
370
370
  }
371
371
  let imageId = series.imageIds[imageIndex];
372
+
372
373
  if (imageId) {
374
+ if (series.is4D) {
375
+ const timestamp = series.instances[imageId].metadata.contentTime;
376
+ const timeId =
377
+ series.instances[imageId].metadata.temporalPositionIdentifier - 1; // timeId from 0 to N
378
+ larvitar_store.set("timeId", [elementId, timeId]);
379
+ larvitar_store.set("timestamp", [elementId, timestamp]);
380
+ }
381
+
373
382
  if (cacheImage) {
374
383
  cornerstone.loadAndCacheImage(imageId).then(function (image) {
375
384
  cornerstone.displayImage(element, image);
376
385
  larvitar_store.set("sliceId", [elementId, imageIndex]);
386
+ larvitar_store.set("minPixelValue", [elementId, image.minPixelValue]);
387
+ larvitar_store.set("maxPixelValue", [elementId, image.maxPixelValue]);
377
388
  });
378
389
  } else {
379
390
  cornerstone.loadImage(imageId).then(function (image) {
380
391
  cornerstone.displayImage(element, image);
381
392
  larvitar_store.set("sliceId", [elementId, imageIndex]);
393
+ larvitar_store.set("minPixelValue", [elementId, image.minPixelValue]);
394
+ larvitar_store.set("maxPixelValue", [elementId, image.maxPixelValue]);
382
395
  });
383
396
  }
384
397
  } else {
@@ -512,6 +525,26 @@ export const updateViewportData = function (
512
525
  case "Rotate":
513
526
  larvitar_store.set("rotation", [elementId, viewportData.rotation]);
514
527
  break;
528
+ case "mouseWheel":
529
+ const isTimeserie = larvitar_store.get(
530
+ "viewports",
531
+ elementId,
532
+ "isTimeserie"
533
+ );
534
+ if (isTimeserie) {
535
+ const index = viewportData.newImageIdIndex;
536
+ const timeId = larvitar_store.get("viewports", elementId, "timeIds")[
537
+ index
538
+ ];
539
+ const timestamp = larvitar_store.get(
540
+ "viewports",
541
+ elementId,
542
+ "timestamps"
543
+ )[index];
544
+ larvitar_store.set("timeId", [elementId, timeId]);
545
+ larvitar_store.set("timestamp", [elementId, timestamp]);
546
+ }
547
+ break;
515
548
  default:
516
549
  break;
517
550
  }
@@ -536,6 +569,19 @@ export const storeViewportData = function (image, elementId, viewport, data) {
536
569
  larvitar_store.set("minSliceId", [elementId, 0]);
537
570
  larvitar_store.set("sliceId", [elementId, data.imageIndex]);
538
571
  larvitar_store.set("maxSliceId", [elementId, data.numberOfSlices - 1]);
572
+
573
+ if (data.isTimeserie) {
574
+ larvitar_store.set("minTimeId", [elementId, 0]);
575
+ larvitar_store.set("timeId", [elementId, data.timeIndex]);
576
+ larvitar_store.set("maxTimeId", [
577
+ elementId,
578
+ data.numberOfTemporalPositions - 1
579
+ ]);
580
+ larvitar_store.set("timestamp", [elementId, data.timestamp]);
581
+ larvitar_store.set("timestamps", [elementId, data.timestamps]);
582
+ larvitar_store.set("timeIds", [elementId, data.timeIds]);
583
+ }
584
+
539
585
  larvitar_store.set("defaultViewport", [
540
586
  elementId,
541
587
  viewport.scale,
@@ -674,6 +720,23 @@ let getSeriesData = function (series, defaultProps) {
674
720
  data.numberOfSlices = series.imageIds.length;
675
721
  data.imageIndex = 0;
676
722
  data.imageId = series.imageIds[data.imageIndex];
723
+ } else if (series.is4D) {
724
+ data.isMultiframe = false;
725
+ data.isTimeserie = true;
726
+ data.numberOfSlices = series.numberOfImages;
727
+ data.numberOfTemporalPositions = series.numberOfTemporalPositions;
728
+ data.imageIndex = 0;
729
+ data.timeIndex = 0;
730
+ data.timestamp = series.instances[series.imageIds[0]].metadata["x00080033"];
731
+ data.imageId = series.imageIds[data.imageIndex];
732
+ data.timestamps = [];
733
+ data.timeIds = [];
734
+ each(series.imageIds, function (imageId) {
735
+ data.timestamps.push(series.instances[imageId].metadata.contentTime);
736
+ data.timeIds.push(
737
+ series.instances[imageId].metadata.temporalPositionIdentifier - 1 // timeId from 0 to N
738
+ );
739
+ });
677
740
  } else {
678
741
  data.isMultiframe = false;
679
742
  data.numberOfSlices =
@@ -692,7 +755,6 @@ let getSeriesData = function (series, defaultProps) {
692
755
  data.imageId = series.imageIds[data.imageIndex];
693
756
  }
694
757
  data.isColor = series.color;
695
- data.isTimeserie = false; // TODO 4D
696
758
 
697
759
  // rows, cols and x y z spacing
698
760
  data.rows = series.instances[series.imageIds[0]].metadata["x00280010"];
@@ -15,6 +15,12 @@ const DEFAULT_VIEWPORT = {
15
15
  minSliceId: 0,
16
16
  maxSliceId: 0,
17
17
  sliceId: 0,
18
+ minTimeId: 0,
19
+ maxTimeId: 0,
20
+ timeId: 0,
21
+ timestamp: 0,
22
+ timestamps: [],
23
+ timeIds: [],
18
24
  rows: 0,
19
25
  cols: 0,
20
26
  spacing_x: 0.0,
@@ -232,6 +238,18 @@ class Larvitar_Store {
232
238
  this.state["viewports"][data[0]]["maxSliceId"] = data[1];
233
239
  } else if (field == "sliceId") {
234
240
  this.state["viewports"][data[0]]["sliceId"] = data[1];
241
+ } else if (field == "minTimeId") {
242
+ this.state["viewports"][data[0]]["minTimeId"] = data[1];
243
+ } else if (field == "maxTimeId") {
244
+ this.state["viewports"][data[0]]["maxTimeId"] = data[1];
245
+ } else if (field == "timeId") {
246
+ this.state["viewports"][data[0]]["timeId"] = data[1];
247
+ } else if (field == "timestamp") {
248
+ this.state["viewports"][data[0]]["timestamp"] = data[1];
249
+ } else if (field == "timestamps") {
250
+ this.state["viewports"][data[0]]["timestamps"] = data[1];
251
+ } else if (field == "timeIds") {
252
+ this.state["viewports"][data[0]]["timeIds"] = data[1];
235
253
  } else if (field == "isColor") {
236
254
  this.state["viewports"][data[0]]["isColor"] = data[1];
237
255
  } else if (field == "isMultiframe") {
@@ -205,8 +205,8 @@ export const getPixelTypedArray = function (dataSet, pixelDataElement) {
205
205
  * @instance
206
206
  * @function getSortedStack
207
207
  * @param {Object} seriesData - The dataset
208
- * @param {Array} sortPriorities - TODO
209
- * @param {Bool} returnSuccessMethod - TODO ask @SZ
208
+ * @param {Array} sortPriorities - An array which represents the priority tasks
209
+ * @param {Bool} returnSuccessMethod - Boolean for returning the success method
210
210
  * @return {Object} The sorted stack
211
211
  */
212
212
  export const getSortedStack = function (
@@ -868,14 +868,7 @@ let sortStackCallback = function (seriesData, imageId, method) {
868
868
  return instanceNumber;
869
869
 
870
870
  case "contentTime":
871
- var cardiacNumberOfImages =
872
- seriesData.instances[imageId].metadata.x00181090;
873
- var contentTime = seriesData.instances[imageId].metadata.x00080033;
874
- if (cardiacNumberOfImages && cardiacNumberOfImages > 1 && contentTime) {
875
- return contentTime;
876
- } else {
877
- throw "Not a time series: cardiacNumberOfImages tag not available or <= 1.";
878
- }
871
+ return seriesData.instances[imageId].metadata.x00080033;
879
872
 
880
873
  case "imagePosition":
881
874
  var p = seriesData.instances[imageId].metadata.imagePosition;
@@ -62,33 +62,35 @@ export const buildNrrdImage = function (volume, seriesId, custom_header) {
62
62
  // need to extract header from nrrd file format
63
63
  // sizes, spaceDirections and spaceOrigin
64
64
 
65
+ const index = volume.header.kinds[0] == "domain" ? 0 : 1;
66
+
65
67
  let spacing_x = Math.sqrt(
66
- volume.header["space directions"][0][0] *
67
- volume.header["space directions"][0][0] +
68
- volume.header["space directions"][0][1] *
69
- volume.header["space directions"][0][1] +
70
- volume.header["space directions"][0][2] *
71
- volume.header["space directions"][0][2]
68
+ volume.header["space directions"][index + 0][0] *
69
+ volume.header["space directions"][index + 0][0] +
70
+ volume.header["space directions"][index + 0][1] *
71
+ volume.header["space directions"][index + 0][1] +
72
+ volume.header["space directions"][index + 0][2] *
73
+ volume.header["space directions"][index + 0][2]
72
74
  );
73
75
  let spacing_y = Math.sqrt(
74
- volume.header["space directions"][1][0] *
75
- volume.header["space directions"][1][0] +
76
- volume.header["space directions"][1][1] *
77
- volume.header["space directions"][1][1] +
78
- volume.header["space directions"][1][2] *
79
- volume.header["space directions"][1][2]
76
+ volume.header["space directions"][index + 1][0] *
77
+ volume.header["space directions"][index + 1][0] +
78
+ volume.header["space directions"][index + 1][1] *
79
+ volume.header["space directions"][index + 1][1] +
80
+ volume.header["space directions"][index + 1][2] *
81
+ volume.header["space directions"][index + 1][2]
80
82
  );
81
83
  let spacing_z = Math.sqrt(
82
- volume.header["space directions"][2][0] *
83
- volume.header["space directions"][2][0] +
84
- volume.header["space directions"][2][1] *
85
- volume.header["space directions"][2][1] +
86
- volume.header["space directions"][2][2] *
87
- volume.header["space directions"][2][2]
84
+ volume.header["space directions"][index + 2][0] *
85
+ volume.header["space directions"][index + 2][0] +
86
+ volume.header["space directions"][index + 2][1] *
87
+ volume.header["space directions"][index + 2][1] +
88
+ volume.header["space directions"][index + 2][2] *
89
+ volume.header["space directions"][index + 2][2]
88
90
  );
89
- header.volume.rows = volume.header.sizes[1];
90
- header.volume.cols = volume.header.sizes[0];
91
- header.volume.numberOfSlices = volume.header.sizes[2];
91
+ header.volume.rows = volume.header.sizes[index + 1];
92
+ header.volume.cols = volume.header.sizes[index + 0];
93
+ header.volume.numberOfSlices = volume.header.sizes[index + 2];
92
94
  header.volume.imagePosition = volume.header["space origin"];
93
95
  header.volume.pixelSpacing = [spacing_x, spacing_y];
94
96
  header.volume.sliceThickness = spacing_z;
@@ -107,11 +109,11 @@ export const buildNrrdImage = function (volume, seriesId, custom_header) {
107
109
  ? custom_header.acquisition_date
108
110
  : "";
109
111
 
110
- let rows = volume.header.sizes[1];
111
- let cols = volume.header.sizes[0];
112
- let frames = volume.header.sizes[2];
113
- let iop = volume.header["space directions"][0].concat(
114
- volume.header["space directions"][1]
112
+ let rows = volume.header.sizes[index + 1];
113
+ let cols = volume.header.sizes[index + 0];
114
+ let frames = volume.header.sizes[index + 2];
115
+ let iop = volume.header["space directions"][index + 0].concat(
116
+ volume.header["space directions"][index + 1]
115
117
  );
116
118
  let firstIpp = header.volume.imagePosition;
117
119
  let w = getNormalOrientation(iop);
@@ -158,6 +158,7 @@ export const toggleMouseToolsListeners = function (elementId, disable) {
158
158
  // mouse wheel handler
159
159
  function mouseWheelHandler(evt) {
160
160
  larvitar_store.set("sliceId", [evt.target.id, evt.detail.newImageIdIndex]);
161
+ updateViewportData(evt.srcElement.id, evt.detail, "mouseWheel");
161
162
  }
162
163
 
163
164
  if (disable) {
@@ -8,6 +8,12 @@ const DEFAULT_VIEWPORT = {
8
8
  minSliceId: 0,
9
9
  maxSliceId: 0,
10
10
  sliceId: 0,
11
+ minTimeId: 0,
12
+ maxTimeId: 0,
13
+ timeId: 0,
14
+ timestamp: 0,
15
+ timestamps: [],
16
+ timeIds: [],
11
17
  rows: 0,
12
18
  cols: 0,
13
19
  spacing_x: 0.0,
@@ -128,6 +134,18 @@ export default {
128
134
  commit("viewport", { id, d: { maxSliceId } }),
129
135
  setSliceId: ({ commit }, [id, sliceId]) =>
130
136
  commit("viewport", { id, d: { sliceId } }),
137
+ setMinTimeId: ({ commit }, [id, minTimeId]) =>
138
+ commit("viewport", { id, d: { minTimeId } }),
139
+ setMaxTimeId: ({ commit }, [id, maxTimeId]) =>
140
+ commit("viewport", { id, d: { maxTimeId } }),
141
+ setTimeId: ({ commit }, [id, timeId]) =>
142
+ commit("viewport", { id, d: { timeId } }),
143
+ setTimestamp: ({ commit }, [id, timestamp]) =>
144
+ commit("viewport", { id, d: { timestamp } }),
145
+ setTimestamps: ({ commit }, [id, timestamps]) =>
146
+ commit("viewport", { id, d: { timestamps } }),
147
+ setTimeIds: ({ commit }, [id, timeIds]) =>
148
+ commit("viewport", { id, d: { timeIds } }),
131
149
  setIsColor: ({ commit }, [id, isColor]) =>
132
150
  commit("viewport", { id, d: { isColor } }),
133
151
  setIsMultiframe: ({ commit }, [id, isMultiframe]) =>
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "medical",
7
7
  "cornerstone"
8
8
  ],
9
- "version": "1.2.9",
9
+ "version": "1.3.0",
10
10
  "description": "javascript library for parsing, loading, rendering and interacting with DICOM images",
11
11
  "repository": {
12
12
  "url": "https://github.com/dvisionlab/Larvitar.git",