@cornerstonejs/core 1.31.0 → 1.32.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.
Files changed (45) hide show
  1. package/dist/cjs/RenderingEngine/VideoViewport.d.ts +15 -2
  2. package/dist/cjs/RenderingEngine/VideoViewport.js +97 -33
  3. package/dist/cjs/RenderingEngine/VideoViewport.js.map +1 -1
  4. package/dist/cjs/metaData.d.ts +1 -1
  5. package/dist/cjs/metaData.js +1 -2
  6. package/dist/cjs/metaData.js.map +1 -1
  7. package/dist/cjs/types/IVideoViewport.d.ts +6 -1
  8. package/dist/cjs/utilities/getViewportsWithImageURI.js +6 -5
  9. package/dist/cjs/utilities/getViewportsWithImageURI.js.map +1 -1
  10. package/dist/cjs/utilities/imageRetrieveMetadataProvider.d.ts +1 -1
  11. package/dist/cjs/utilities/imageRetrieveMetadataProvider.js +1 -4
  12. package/dist/cjs/utilities/imageRetrieveMetadataProvider.js.map +1 -1
  13. package/dist/cjs/utilities/spatialRegistrationMetadataProvider.d.ts +1 -1
  14. package/dist/cjs/utilities/spatialRegistrationMetadataProvider.js +1 -2
  15. package/dist/cjs/utilities/spatialRegistrationMetadataProvider.js.map +1 -1
  16. package/dist/esm/RenderingEngine/VideoViewport.js +97 -33
  17. package/dist/esm/RenderingEngine/VideoViewport.js.map +1 -1
  18. package/dist/esm/metaData.js +1 -2
  19. package/dist/esm/metaData.js.map +1 -1
  20. package/dist/esm/utilities/getViewportsWithImageURI.js +6 -5
  21. package/dist/esm/utilities/getViewportsWithImageURI.js.map +1 -1
  22. package/dist/esm/utilities/imageRetrieveMetadataProvider.js +1 -4
  23. package/dist/esm/utilities/imageRetrieveMetadataProvider.js.map +1 -1
  24. package/dist/esm/utilities/spatialRegistrationMetadataProvider.js +1 -2
  25. package/dist/esm/utilities/spatialRegistrationMetadataProvider.js.map +1 -1
  26. package/dist/types/RenderingEngine/VideoViewport.d.ts +15 -2
  27. package/dist/types/RenderingEngine/VideoViewport.d.ts.map +1 -1
  28. package/dist/types/metaData.d.ts +1 -1
  29. package/dist/types/metaData.d.ts.map +1 -1
  30. package/dist/types/types/IVideoViewport.d.ts +6 -1
  31. package/dist/types/types/IVideoViewport.d.ts.map +1 -1
  32. package/dist/types/utilities/getViewportsWithImageURI.d.ts.map +1 -1
  33. package/dist/types/utilities/imageRetrieveMetadataProvider.d.ts +1 -1
  34. package/dist/types/utilities/imageRetrieveMetadataProvider.d.ts.map +1 -1
  35. package/dist/types/utilities/spatialRegistrationMetadataProvider.d.ts +1 -1
  36. package/dist/types/utilities/spatialRegistrationMetadataProvider.d.ts.map +1 -1
  37. package/dist/umd/index.js +1 -1
  38. package/dist/umd/index.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/RenderingEngine/VideoViewport.ts +146 -28
  41. package/src/metaData.ts +2 -3
  42. package/src/types/IVideoViewport.ts +44 -4
  43. package/src/utilities/getViewportsWithImageURI.ts +6 -13
  44. package/src/utilities/imageRetrieveMetadataProvider.ts +1 -4
  45. package/src/utilities/spatialRegistrationMetadataProvider.ts +1 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/core",
3
- "version": "1.31.0",
3
+ "version": "1.32.1",
4
4
  "description": "",
5
5
  "main": "src/index.ts",
6
6
  "types": "dist/types/index.d.ts",
@@ -47,5 +47,5 @@
47
47
  "type": "individual",
48
48
  "url": "https://ohif.org/donate"
49
49
  },
50
- "gitHead": "280c27226cb94a0b422f28656e69dbc059f95323"
50
+ "gitHead": "bb586320e1b8c8d07ae7f695cc838ed529631050"
51
51
  }
@@ -25,6 +25,8 @@ import { getOrCreateCanvas } from './helpers';
25
25
  * looking into an internal scene, and an associated target output `canvas`.
26
26
  */
27
27
  class VideoViewport extends Viewport implements IVideoViewport {
28
+ public static frameRangeExtractor = /(\/frames\/|[&?]frameNumber=)([^/&?]*)/i;
29
+
28
30
  public modality;
29
31
  // Viewport Data
30
32
  protected imageId: string;
@@ -35,11 +37,15 @@ class VideoViewport extends Viewport implements IVideoViewport {
35
37
  private videoWidth = 0;
36
38
  private videoHeight = 0;
37
39
 
38
- private loop = false;
40
+ private loop = true;
39
41
  private mute = true;
40
42
  private isPlaying = false;
41
43
  private scrollSpeed = 1;
42
44
  private playbackRate = 1;
45
+ /**
46
+ * The range is the set of frames to play
47
+ */
48
+ private frameRange: [number, number] = [0, 0];
43
49
 
44
50
  protected metadata;
45
51
 
@@ -50,6 +56,9 @@ class VideoViewport extends Viewport implements IVideoViewport {
50
56
  */
51
57
  private fps = 30;
52
58
 
59
+ /** The number of frames in the video */
60
+ private numberOfFrames = 0;
61
+
53
62
  private videoCamera: InternalVideoCamera = {
54
63
  panWorld: [0, 0],
55
64
  parallelScale: 1,
@@ -156,14 +165,15 @@ class VideoViewport extends Viewport implements IVideoViewport {
156
165
  origin = [0, 0, 0];
157
166
  }
158
167
 
159
- const xSpacing = imagePlaneModule.columnPixelSpacing;
160
- const ySpacing = imagePlaneModule.rowPixelSpacing;
168
+ const xSpacing = imagePlaneModule.columnPixelSpacing || 1;
169
+ const ySpacing = imagePlaneModule.rowPixelSpacing || 1;
161
170
  const xVoxels = imagePlaneModule.columns;
162
171
  const yVoxels = imagePlaneModule.rows;
163
172
 
164
173
  const zSpacing = 1;
165
174
  const zVoxels = 1;
166
175
 
176
+ this.hasPixelSpacing = !!imagePlaneModule.columnPixelSpacing;
167
177
  return {
168
178
  bitsAllocated: 8,
169
179
  numComps: 3,
@@ -171,6 +181,7 @@ class VideoViewport extends Viewport implements IVideoViewport {
171
181
  direction: [...rowCosineVec, ...colCosineVec, ...scanAxisNormal],
172
182
  dimensions: [xVoxels, yVoxels, zVoxels],
173
183
  spacing: [xSpacing, ySpacing, zSpacing],
184
+ hasPixelSpacing: this.hasPixelSpacing,
174
185
  numVoxels: xVoxels * yVoxels * zVoxels,
175
186
  imagePlaneModule,
176
187
  };
@@ -181,23 +192,33 @@ class VideoViewport extends Viewport implements IVideoViewport {
181
192
  * Requirements are to have the imageUrlModule in the metadata
182
193
  * with the rendered endpoint being the raw video in video/mp4 format.
183
194
  */
184
- public setVideo(
185
- imageIds: string | string[],
186
- frameNumber?: number
187
- ): Promise<unknown> {
188
- this.imageId = Array.isArray(imageIds) ? imageIds[0] : imageIds;
189
- const { imageId } = this;
195
+ public setVideo(imageId: string, frameNumber?: number): Promise<unknown> {
196
+ this.imageId = Array.isArray(imageId) ? imageId[0] : imageId;
190
197
  const { rendered } = metaData.get(MetadataModules.IMAGE_URL, imageId);
191
198
  const generalSeries = metaData.get(MetadataModules.GENERAL_SERIES, imageId);
192
199
  this.modality = generalSeries?.Modality;
193
200
  this.metadata = this._getImageDataMetadata();
194
201
 
195
202
  return this.setVideoURL(rendered).then(() => {
196
- const { cineRate = 30 } = metaData.get(MetadataModules.CINE, imageId);
203
+ let { cineRate, numberOfFrames } = metaData.get(
204
+ MetadataModules.CINE,
205
+ imageId
206
+ );
207
+ if (!numberOfFrames) {
208
+ numberOfFrames = Math.round(
209
+ this.videoElement.duration * (cineRate || 30)
210
+ );
211
+ }
212
+ if (!cineRate) {
213
+ cineRate = Math.round(numberOfFrames / this.videoElement.duration);
214
+ }
197
215
  this.fps = cineRate;
216
+ this.numberOfFrames = numberOfFrames;
217
+ // 1 based range setting
218
+ this.setFrameRange([1, numberOfFrames]);
198
219
  if (frameNumber !== undefined) {
199
220
  this.pause();
200
- this.setFrame(frameNumber);
221
+ this.setFrameNumber(frameNumber);
201
222
  }
202
223
  });
203
224
  }
@@ -326,10 +347,31 @@ class VideoViewport extends Viewport implements IVideoViewport {
326
347
  }
327
348
 
328
349
  // Sets the frame number - note according to DICOM, this is 1 based
329
- public async setFrame(frame: number) {
350
+ public async setFrameNumber(frame: number) {
330
351
  this.setTime((frame - 1) / this.fps);
331
352
  }
332
353
 
354
+ /**
355
+ * Sets the playback frame range. The video will play over the given set
356
+ * of frames (assuming it is playing).
357
+ * @param frameRange - the minimum to maximum (inclusive) frames to play over
358
+ * @returns
359
+ */
360
+ public setFrameRange(frameRange: number[]) {
361
+ if (!frameRange) {
362
+ this.frameRange = [1, this.numberOfFrames];
363
+ return;
364
+ }
365
+ if (frameRange.length !== 2 || frameRange[0] === frameRange[1]) {
366
+ return;
367
+ }
368
+ this.frameRange = [frameRange[0], frameRange[1]];
369
+ }
370
+
371
+ public getFrameRange(): [number, number] {
372
+ return this.frameRange;
373
+ }
374
+
333
375
  public setProperties(props: VideoViewportProperties) {
334
376
  if (props.loop !== undefined) {
335
377
  this.videoElement.loop = props.loop;
@@ -421,6 +463,7 @@ class VideoViewport extends Viewport implements IVideoViewport {
421
463
  origin: metadata.origin,
422
464
  direction: metadata.direction,
423
465
  metadata: { Modality: this.modality },
466
+ getScalarData: () => this.getScalarData(),
424
467
  imageData: {
425
468
  getDirection: () => metadata.direction,
426
469
  getDimensions: () => metadata.dimensions,
@@ -445,6 +488,32 @@ class VideoViewport extends Viewport implements IVideoViewport {
445
488
  };
446
489
  }
447
490
 
491
+ /**
492
+ * Checks to see if the imageURI is currently being displayed. The imageURI
493
+ * may contain frame numbers according to the DICOM standard format, which
494
+ * will be stripped to compare the base image URI, and then the values used
495
+ * to check if that frame is currently being displayed.
496
+ *
497
+ * The DICOM standard allows for comma separated values as well, however,
498
+ * this is not supported here, with only a single range or single value
499
+ * being tested.
500
+ *
501
+ * For a single value, the time range +/- 5 frames is permitted to allow
502
+ * the detection to actually succeed when nearby without requiring an exact
503
+ * time frame to be matched.
504
+ *
505
+ * @param imageURI - containing frame number or range.
506
+ * @returns
507
+ */
508
+ public hasImageURI(imageURI: string) {
509
+ // TODO - move annotationFrameRange into core so it can be used here.
510
+ const framesMatch = imageURI.match(VideoViewport.frameRangeExtractor);
511
+ const testURI = framesMatch
512
+ ? imageURI.substring(0, framesMatch.index)
513
+ : imageURI;
514
+ return this.imageId.indexOf(testURI) !== -1;
515
+ }
516
+
448
517
  public setVOI(voiRange: VOIRange): void {
449
518
  this.voiRange = voiRange;
450
519
  this.setColorTransform();
@@ -470,8 +539,6 @@ class VideoViewport extends Viewport implements IVideoViewport {
470
539
  const white = this.averageWhite || [255, 255, 255];
471
540
  const maxWhite = Math.max(...white);
472
541
  const scaleWhite = white.map((c) => maxWhite / c);
473
- // From the DICOM standard: ((x - (c - 0.5)) / (w-1) + 0.5) * (ymax- ymin) + ymin
474
- // which is x/(w-1) - (c - 0.5) / (w-1) + 0.5 for this case
475
542
  const { lower = 0, upper = 255 } = this.voiRange || {};
476
543
  const wlScale = (upper - lower + 1) / 255;
477
544
  const wlDelta = lower / 255;
@@ -496,7 +563,8 @@ class VideoViewport extends Viewport implements IVideoViewport {
496
563
  // NOTE: the parallel scale should be done first
497
564
  // because it affects the focal point later
498
565
  if (camera.parallelScale !== undefined) {
499
- this.videoCamera.parallelScale = 1 / parallelScale;
566
+ this.videoCamera.parallelScale =
567
+ this.element.clientHeight / 2 / parallelScale;
500
568
  }
501
569
 
502
570
  if (focalPoint !== undefined) {
@@ -526,6 +594,33 @@ class VideoViewport extends Viewport implements IVideoViewport {
526
594
  }
527
595
  }
528
596
 
597
+ /**
598
+ * This function returns the imageID associated with either the current
599
+ * frame being displayed, or the range of frames being played. This may not
600
+ * correspond to any particular imageId that has imageId metadata, as the
601
+ * format is one of:
602
+ * `<DICOMweb URI>/frames/<Start Frame>(-<End Frame>)?`
603
+ * or
604
+ * `<Other URI>[?&]frameNumber=<Start Frame>(-<EndFrame>)?`
605
+ * for a URL parameter.
606
+ *
607
+ * @returns an imageID for video
608
+ */
609
+ public getCurrentImageId() {
610
+ const current = this.imageId.replace(
611
+ '/frames/1',
612
+ this.isPlaying
613
+ ? `/frames/1-${this.numberOfFrames}`
614
+ : `/frames/${this.getFrameNumber()}`
615
+ );
616
+ return current;
617
+ }
618
+
619
+ public getFrameNumber() {
620
+ // Need to round this as the fps/time isn't exact
621
+ return 1 + Math.round(this.videoElement.currentTime * this.fps);
622
+ }
623
+
529
624
  public getCamera(): ICamera {
530
625
  const { parallelScale } = this.videoCamera;
531
626
 
@@ -543,7 +638,8 @@ class VideoViewport extends Viewport implements IVideoViewport {
543
638
  parallelProjection: true,
544
639
  focalPoint: canvasCenterWorld,
545
640
  position: [0, 0, 0],
546
- parallelScale: 1 / parallelScale, // Reverse zoom direction back
641
+ viewUp: [0, -1, 0],
642
+ parallelScale: this.element.clientHeight / 2 / parallelScale, // Reverse zoom direction back
547
643
  viewPlaneNormal: [0, 0, 1],
548
644
  };
549
645
  }
@@ -561,7 +657,9 @@ class VideoViewport extends Viewport implements IVideoViewport {
561
657
  };
562
658
 
563
659
  public getNumberOfSlices = (): number => {
564
- return (this.videoElement.duration * this.fps) / this.scrollSpeed;
660
+ return Math.round(
661
+ (this.videoElement.duration * this.fps) / this.scrollSpeed
662
+ );
565
663
  };
566
664
 
567
665
  public getFrameOfReferenceUID = (): string => {
@@ -636,18 +734,23 @@ class VideoViewport extends Viewport implements IVideoViewport {
636
734
  return canvasPos;
637
735
  };
638
736
 
737
+ public getPan(): Point2 {
738
+ const worldPan = this.videoCamera.panWorld;
739
+ return [worldPan[0], worldPan[1]];
740
+ }
741
+
742
+ public getRotation = () => 0;
743
+
639
744
  protected canvasToIndex = (canvasPos: Point2): Point2 => {
640
- const [x, y] = canvasPos;
641
- const ratio = this.videoWidth / this.canvas.width;
642
- const pan = this.getPan();
643
- return [(x + pan[0]) * ratio, (y + pan[1]) * ratio];
745
+ const transform = this.getTransform();
746
+ transform.invert();
747
+
748
+ return transform.transformPoint(canvasPos);
644
749
  };
645
750
 
646
751
  protected indexToCanvas = (indexPos: Point2): Point2 => {
647
- const [x, y] = indexPos;
648
- const ratio = this.canvas.width / this.videoWidth;
649
- const pan = this.getPan();
650
- return [x * ratio - pan[0], y * ratio - pan[1]];
752
+ const transform = this.getTransform();
753
+ return transform.transformPoint(indexPos);
651
754
  };
652
755
 
653
756
  private refreshRenderValues() {
@@ -692,17 +795,15 @@ class VideoViewport extends Viewport implements IVideoViewport {
692
795
  this.renderFrame();
693
796
  };
694
797
 
695
- private renderFrame = () => {
798
+ protected getTransform() {
696
799
  const panWorld: Point2 = this.videoCamera.panWorld;
697
800
  const worldToCanvasRatio: number = this.getWorldToCanvasRatio();
698
801
  const canvasToWorldRatio: number = this.getCanvasToWorldRatio();
699
-
700
802
  const halfCanvas = [this.canvas.width / 2, this.canvas.height / 2];
701
803
  const halfCanvasWorldCoordinates = [
702
804
  halfCanvas[0] * canvasToWorldRatio,
703
805
  halfCanvas[1] * canvasToWorldRatio,
704
806
  ];
705
-
706
807
  const transform = new Transform();
707
808
 
708
809
  // Translate to the center of the canvas (move origin of the transform
@@ -720,6 +821,10 @@ class VideoViewport extends Viewport implements IVideoViewport {
720
821
  -halfCanvasWorldCoordinates[0],
721
822
  -halfCanvasWorldCoordinates[1]
722
823
  );
824
+ return transform;
825
+ }
826
+ private renderFrame = () => {
827
+ const transform = this.getTransform();
723
828
  const transformationMatrix: number[] = transform.getMatrix();
724
829
 
725
830
  this.canvasContext.transform(
@@ -749,6 +854,19 @@ class VideoViewport extends Viewport implements IVideoViewport {
749
854
  time: this.videoElement.currentTime,
750
855
  duration: this.videoElement.duration,
751
856
  });
857
+
858
+ const frame = this.getFrameNumber();
859
+ if (this.isPlaying) {
860
+ if (frame < this.frameRange[0]) {
861
+ this.setFrameNumber(this.frameRange[0]);
862
+ } else if (frame > this.frameRange[1]) {
863
+ if (this.loop) {
864
+ this.setFrameNumber(this.frameRange[0]);
865
+ } else {
866
+ this.pause();
867
+ }
868
+ }
869
+ }
752
870
  };
753
871
 
754
872
  private renderWhilstPlaying = () => {
package/src/metaData.ts CHANGED
@@ -11,7 +11,7 @@ const providers = [];
11
11
  * @category MetaData
12
12
  */
13
13
  export function addProvider(
14
- provider: (type: string, query: any) => any,
14
+ provider: (type: string, ...query: string[]) => any,
15
15
  priority = 0
16
16
  ): void {
17
17
  let i;
@@ -73,10 +73,9 @@ export function removeAllProviders(): void {
73
73
  * @category MetaData
74
74
  */
75
75
  function getMetaData(type: string, ...queries): any {
76
- const query = queries.length === 1 ? queries[0] : queries;
77
76
  // Invoke each provider in priority order until one returns something
78
77
  for (let i = 0; i < providers.length; i++) {
79
- const result = providers[i].provider(type, query);
78
+ const result = providers[i].provider(type, ...queries);
80
79
 
81
80
  if (result !== undefined) {
82
81
  return result;
@@ -19,11 +19,26 @@ export default interface IVideoViewport extends IViewport {
19
19
  */
20
20
  getProperties: () => VideoViewportProperties;
21
21
 
22
- setVideo: (
23
- imageIds: string | string[],
24
- imageIdIndex?: number
25
- ) => Promise<unknown>;
22
+ /**
23
+ * Sets the video to play.
24
+ * The video should have at least some metadata in the metadata provider,
25
+ * including:
26
+ * * study/series/sop common module for UIDs
27
+ * * `cineModule` for information on number of frames and playback rate
28
+ * * `imageUrlModule` - to get the URL for the image under the `rendered` attribute
29
+ *
30
+ * Without these, other tools requiring metadata wont work, although basic
31
+ * playback does work if the setVideoURL is used instead.
32
+ */
33
+ setVideo: (imageIds: string, imageIdIndex?: number) => Promise<unknown>;
26
34
 
35
+ /**
36
+ * Displays a raw video, without any metadata associated with it. Plays back,
37
+ * but does not permit tools to apply to the viewport, which requires providing
38
+ * additional metadata for the study.
39
+ *
40
+ * @param url - to display
41
+ */
27
42
  setVideoURL: (url: string) => void;
28
43
 
29
44
  play: () => void;
@@ -35,6 +50,31 @@ export default interface IVideoViewport extends IViewport {
35
50
  */
36
51
  resetProperties(): void;
37
52
 
53
+ /**
54
+ * Gets the current image id, including frame selction or frameless.
55
+ */
56
+ getCurrentImageId(): string;
57
+
58
+ /**
59
+ * Gets the current frame, 1 based
60
+ */
61
+ getFrameNumber(): number;
62
+
63
+ /**
64
+ * Sets the current frame
65
+ */
66
+ setFrameNumber(frameNo: number);
67
+
68
+ /**
69
+ * Sets the range of frames for displaying. This is the range of frames
70
+ * that are shown/looped over when the video is playing.
71
+ * Note that ability to playback a frame range depends on the server
72
+ * implementing byte range requests, OR the video being easily cached in memory.
73
+ */
74
+ setFrameRange(range?: [number, number]);
75
+
76
+ getFrameRange(): [number, number];
77
+
38
78
  /**
39
79
  * Centers Pan and resets the zoom for stack viewport.
40
80
  */
@@ -26,20 +26,13 @@ export default function getViewportsWithImageURI(
26
26
 
27
27
  const viewports = [];
28
28
  renderingEngines.forEach((renderingEngine) => {
29
- const stackViewports = renderingEngine.getStackViewports();
29
+ const viewportsForRenderingEngine = renderingEngine.getViewports();
30
30
 
31
- const filteredStackViewports = stackViewports.filter((viewport) =>
32
- viewport.hasImageURI(imageURI)
33
- );
34
-
35
- // If no stack viewport found but a volumeViewport is rendering the same data
36
- const volumeViewports = renderingEngine.getVolumeViewports();
37
-
38
- const filteredVolumeViewports = volumeViewports.filter((viewport) =>
39
- viewport.hasImageURI(imageURI)
40
- );
41
-
42
- viewports.push(...filteredStackViewports, ...filteredVolumeViewports);
31
+ viewportsForRenderingEngine.forEach((viewport) => {
32
+ if (viewport.hasImageURI(imageURI)) {
33
+ viewports.push(viewport);
34
+ }
35
+ });
43
36
  });
44
37
 
45
38
  return viewports;
@@ -20,10 +20,7 @@ const imageRetrieveMetadataProvider = {
20
20
  retrieveConfigurationState.set(key, payload);
21
21
  },
22
22
 
23
- get: (type: string, queriesOrQuery: string | string[]) => {
24
- const queries = Array.isArray(queriesOrQuery)
25
- ? queriesOrQuery
26
- : [queriesOrQuery];
23
+ get: (type: string, ...queries: string[]) => {
27
24
  if (type === IMAGE_RETRIEVE_CONFIGURATION) {
28
25
  return queries
29
26
  .map((query) => retrieveConfigurationState.get(query))
@@ -19,13 +19,11 @@ const spatialRegistrationMetadataProvider = {
19
19
  state[entryId] = payload;
20
20
  },
21
21
 
22
- get: (type: string, query: string[]): mat4 => {
22
+ get: (type: string, viewportId1: string, viewportId2: string): mat4 => {
23
23
  if (type !== 'spatialRegistrationModule') {
24
24
  return;
25
25
  }
26
26
 
27
- const [viewportId1, viewportId2] = query;
28
-
29
27
  // check both ways
30
28
  const entryId = `${viewportId1}_${viewportId2}`;
31
29