@cornerstonejs/tools 1.22.0 → 1.22.1

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": "@cornerstonejs/tools",
3
- "version": "1.22.0",
3
+ "version": "1.22.1",
4
4
  "description": "Cornerstone3D Tools",
5
5
  "main": "dist/umd/index.js",
6
6
  "types": "dist/esm/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  "webpack:watch": "webpack --mode development --progress --watch --config ./.webpack/webpack.dev.js"
30
30
  },
31
31
  "dependencies": {
32
- "@cornerstonejs/core": "^1.22.0",
32
+ "@cornerstonejs/core": "^1.22.1",
33
33
  "lodash.clonedeep": "4.5.0",
34
34
  "lodash.get": "^4.4.2"
35
35
  },
@@ -52,5 +52,5 @@
52
52
  "type": "individual",
53
53
  "url": "https://ohif.org/donate"
54
54
  },
55
- "gitHead": "ff226c7a2ed426dc568b8043d3bbbb1cdc993281"
55
+ "gitHead": "c7e6f091ac9fc1e38fcaca23599b75d62af605e6"
56
56
  }
@@ -5,10 +5,10 @@ import { Enums, Types } from '@cornerstonejs/core';
5
5
  * array of scalar data after performing AVERAGE, SUM or SUBTRACT to be used to
6
6
  * create a 3D volume
7
7
  *
8
- * @param dynamicVolume4D: volume to compute time frame data from
9
- * @param operation: operation to perform on time frame data, operations include
8
+ * @param dynamicVolume4D - volume to compute time frame data from
9
+ * @param operation - operation to perform on time frame data, operations include
10
10
  * SUM, AVERAGE, and SUBTRACT (can only be used with 2 time frames provided)
11
- * @param frameNumbers: an array of frame indexs to perform the operation on, if
11
+ * @param frameNumbers - an array of frame indices to perform the operation on, if
12
12
  * left empty, all frames will be used
13
13
  * @returns
14
14
  */
@@ -1,12 +1,14 @@
1
1
  import { utilities, cache, Types } from '@cornerstonejs/core';
2
+ import { getVoxelOverlap } from '../segmentation/utilities';
3
+ import pointInShapeCallback from '../pointInShapeCallback';
2
4
 
3
5
  /**
4
6
  * Gets the scalar data for a series of time points for either a single
5
7
  * coordinate or a segmentation mask, it will return the an array of scalar
6
8
  * data for a single coordinate or an array of arrays for a segmentation.
7
9
  *
8
- * @param dynamicVolume: 4D volume to compute time point data from
9
- * @param options: frameNumbers: which frames to use as timepoints, if left
10
+ * @param dynamicVolume - 4D volume to compute time point data from
11
+ * @param options - frameNumbers: which frames to use as timepoints, if left
10
12
  * blank, gets data timepoints over all frames
11
13
  * maskVolumeId: segmentationId to get timepoint data of
12
14
  * imageCoordinate: world coordinate to get timepoint data of
@@ -31,26 +33,23 @@ function getDataInTime(
31
33
  // Throws error if neither maskVolumeId or imageCoordinate is given,
32
34
  // throws error if BOTH maskVolumeId and imageCoordinate is given
33
35
  if (!options.maskVolumeId && !options.imageCoordinate) {
34
- throw new Error('No ROI provided');
36
+ throw new Error(
37
+ 'You should provide either maskVolumeId or imageCoordinate'
38
+ );
35
39
  }
36
40
 
37
41
  if (options.maskVolumeId && options.imageCoordinate) {
38
- throw new Error('Please provide only one ROI');
42
+ throw new Error('You can only use one of maskVolumeId or imageCoordinate');
39
43
  }
40
44
 
41
45
  if (options.maskVolumeId) {
42
46
  const segmentationVolume = cache.getVolume(options.maskVolumeId);
43
- const segScalarData = segmentationVolume.getScalarData();
44
- const indexArray = [];
45
-
46
- // Get the index of every non-zero voxel in mask
47
- for (let i = 0, len = segScalarData.length; i < len; i++) {
48
- if (segScalarData[i] !== 0) {
49
- indexArray.push(i);
50
- }
51
- }
52
47
 
53
- const dataInTime = _getTimePointDataMask(frames, indexArray, dynamicVolume);
48
+ const dataInTime = _getTimePointDataMask(
49
+ frames,
50
+ dynamicVolume,
51
+ segmentationVolume
52
+ );
54
53
 
55
54
  return dataInTime;
56
55
  }
@@ -95,19 +94,110 @@ function _getTimePointDataCoordinate(frames, coordinate, volume) {
95
94
  return value;
96
95
  }
97
96
 
98
- function _getTimePointDataMask(frames, indexArray, volume) {
99
- const allScalarData = volume.getScalarDataArrays();
100
- const value = [];
97
+ function _getTimePointDataMask(frames, dynamicVolume, segmentationVolume) {
98
+ const { imageData: maskImageData } = segmentationVolume;
99
+ const segScalarData = segmentationVolume.getScalarData();
101
100
 
102
- for (let i = 0; i < indexArray.length; i++) {
103
- const indexValues = [];
104
- frames.forEach((frame) => {
105
- const activeScalarData = allScalarData[frame];
106
- indexValues.push(activeScalarData[indexArray[i]]);
107
- });
108
- value.push(indexValues);
101
+ const len = segScalarData.length;
102
+
103
+ // Pre-allocate memory for array
104
+ const nonZeroVoxelIndices = [];
105
+ nonZeroVoxelIndices.length = len;
106
+
107
+ // Get the index of every non-zero voxel in mask
108
+ let actualLen = 0;
109
+ for (let i = 0, len = segScalarData.length; i < len; i++) {
110
+ if (segScalarData[i] !== 0) {
111
+ nonZeroVoxelIndices[actualLen++] = i;
112
+ }
109
113
  }
110
- return value;
114
+
115
+ // Trim the array to actual size
116
+ nonZeroVoxelIndices.length = actualLen;
117
+
118
+ const dynamicVolumeScalarDataArray = dynamicVolume.getScalarDataArrays();
119
+ const values = [];
120
+ const isSameVolume =
121
+ dynamicVolumeScalarDataArray[0].length === len &&
122
+ JSON.stringify(dynamicVolume.spacing) ===
123
+ JSON.stringify(segmentationVolume.spacing);
124
+
125
+ // if the segmentation mask is the same size as the dynamic volume (one frame)
126
+ // means we can just return the scalar data for the non-zero voxels
127
+ if (isSameVolume) {
128
+ for (let i = 0; i < nonZeroVoxelIndices.length; i++) {
129
+ const indexValues = [];
130
+ frames.forEach((frame) => {
131
+ const activeScalarData = dynamicVolumeScalarDataArray[frame];
132
+ indexValues.push(activeScalarData[nonZeroVoxelIndices[i]]);
133
+ });
134
+ values.push(indexValues);
135
+ }
136
+
137
+ return values;
138
+ }
139
+
140
+ // In case that the segmentation mask is not the same size as the dynamic volume (one frame)
141
+ // then we need to consider each voxel in the segmentation mask and check if it
142
+ // overlaps with the other volume, and if so we need to average the values of the
143
+ // overlapping voxels.
144
+ const callback = ({ pointLPS: segPointLPS, value: segValue }) => {
145
+ // see if the value is non-zero
146
+ if (segValue === 0) {
147
+ // not interested
148
+ return;
149
+ }
150
+
151
+ // Then for each non-zero voxel in the segmentation mask, we should
152
+ // again perform the pointInShapeCallback to run the averaging callback
153
+ // function to get the average value of the overlapping voxels.
154
+ const overlapIJKMinMax = getVoxelOverlap(
155
+ dynamicVolume.imageData,
156
+ dynamicVolume.dimensions,
157
+ dynamicVolume.spacing,
158
+ segPointLPS
159
+ );
160
+
161
+ // count represents the number of voxels of the dynamic volume that represents
162
+ // one voxel of the segmentation mask
163
+ let count = 0;
164
+ const perFrameSum = new Map();
165
+
166
+ // Pre-initialize the Map
167
+ frames.forEach((frame) => perFrameSum.set(frame, 0));
168
+
169
+ const averageCallback = ({ index }) => {
170
+ for (let i = 0; i < frames.length; i++) {
171
+ const value = dynamicVolumeScalarDataArray[i][index];
172
+ const frame = frames[i];
173
+ perFrameSum.set(frame, perFrameSum.get(frame) + value);
174
+ }
175
+ count++;
176
+ };
177
+
178
+ pointInShapeCallback(
179
+ dynamicVolume.imageData,
180
+ () => true,
181
+ averageCallback,
182
+ overlapIJKMinMax
183
+ );
184
+
185
+ // average the values
186
+ const averageValues = [];
187
+ perFrameSum.forEach((sum) => {
188
+ averageValues.push(sum / count);
189
+ });
190
+
191
+ values.push(averageValues);
192
+ };
193
+
194
+ // Since we have the non-zero voxel indices of the segmentation mask,
195
+ // we theoretically can use them, however, we kind of need to compute the
196
+ // pointLPS for each of the non-zero voxel indices, which is a bit of a pain.
197
+ // Todo: consider using the nonZeroVoxelIndices to compute the pointLPS
198
+ pointInShapeCallback(maskImageData, () => true, callback);
199
+
200
+ return values;
111
201
  }
112
202
 
113
203
  export default getDataInTime;
@@ -56,7 +56,7 @@ export function getVoxelOverlap(
56
56
  for (let i = 0; i < 2; i++) {
57
57
  for (let j = 0; j < 2; j++) {
58
58
  for (let k = 0; k < 2; k++) {
59
- const point = voxelCenter;
59
+ const point = [...voxelCenter]; // Create a new point from voxelCenter
60
60
  point[0] = point[0] + ((i * 2 - 1) * voxelSpacing[0]) / 2;
61
61
  point[1] = point[1] + ((j * 2 - 1) * voxelSpacing[1]) / 2;
62
62
  point[2] = point[2] + ((k * 2 - 1) * voxelSpacing[2]) / 2;
@@ -79,8 +79,7 @@ export function processVolumes(
79
79
  segmentationVolume: Types.IImageVolume,
80
80
  thresholdVolumeInformation: ThresholdInformation[]
81
81
  ) {
82
- const { spacing: segmentationSpacing, imageData: segmentationImageData } =
83
- segmentationVolume;
82
+ const { spacing: segmentationSpacing } = segmentationVolume;
84
83
  const scalarData = segmentationVolume.getScalarData();
85
84
 
86
85
  // prepare a list of volume information objects for callback functions