diff --git a/.circleci/config.yml b/.circleci/config.yml index 49c18e17d..4e0152224 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,6 +48,7 @@ jobs: - packages/streaming-image-volume-loader/dist - packages/adapters/dist - packages/dicomImageLoader/dist + - packages/nifti-volume-loader/dist - version.txt - commit.txt - version.json diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 8eff3dd86..e8f073de1 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -47,6 +47,34 @@ function addProvider(provider: (type: string, query: any) => any, priority?: num // @public (undocumented) export function addVolumesToViewports(renderingEngine: IRenderingEngine, volumeInputs: Array, viewportIds: Array, immediateRender?: boolean, suppressEvents?: boolean): Promise; +// @public (undocumented) +type AffineMatrix = [ +[ +number, +number, +number, +number +], +[ +number, +number, +number, +number +], +[ +number, +number, +number, +number +], +[ +number, +number, +number, +number +] +]; + // @public (undocumented) function applyPreset(actor: VolumeActor, preset: ViewportPreset): void; @@ -491,7 +519,7 @@ function createAndCacheDerivedVolume(referencedVolumeId: string, options: Derive function createAndCacheGeometry(geometryId: string, options: GeometryOptions): Promise; // @public (undocumented) -function createAndCacheVolume(volumeId: string, options: VolumeLoaderOptions): Promise>; +function createAndCacheVolume(volumeId: string, options?: VolumeLoaderOptions): Promise>; // @public (undocumented) function createFloat32SharedArray(length: number): Float32Array; @@ -1934,17 +1962,7 @@ function loadImageToCanvas(options: LoadImageOptions): Promise; function loadVolume(volumeId: string, options?: VolumeLoaderOptions): Promise; // @public (undocumented) -type Mat3 = [ -number, -number, -number, -number, -number, -number, -number, -number, -number -]; +type Mat3 = [number, number, number, number, number, number, number, number, number] | Float32Array; // @public (undocumented) type Metadata = { @@ -2487,7 +2505,8 @@ declare namespace Types { ColormapRegistration, PixelDataTypedArray, ImagePixelModule, - ImagePlaneModule + ImagePlaneModule, + AffineMatrix } } export { Types } diff --git a/common/reviews/api/nifti-volume-loader.api.md b/common/reviews/api/nifti-volume-loader.api.md new file mode 100644 index 000000000..c79cf7a21 --- /dev/null +++ b/common/reviews/api/nifti-volume-loader.api.md @@ -0,0 +1,1589 @@ +## API Report File for "@cornerstonejs/nifti-volume-loader" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { GetGPUTier } from 'detect-gpu'; +import type { mat4 } from 'gl-matrix'; +import type { TierResult } from 'detect-gpu'; +import type vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import type { vtkImageData } from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice'; +import type vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; + +// @public (undocumented) +type Actor = vtkActor; + +// @public +type ActorEntry = { + uid: string; + actor: Actor | VolumeActor | ImageActor; + referenceId?: string; + slabThickness?: number; +}; + +// @public +type ActorSliceRange = { + actor: VolumeActor; + viewPlaneNormal: Point3; + focalPoint: Point3; + min: number; + max: number; + current: number; +}; + +// @public (undocumented) +type AffineMatrix = [ +[number, number, number, number], +[number, number, number, number], +[number, number, number, number], +[number, number, number, number] +]; + +// @public +type CameraModifiedEvent = CustomEvent_2; + +// @public +type CameraModifiedEventDetail = { + previousCamera: ICamera; + camera: ICamera; + element: HTMLDivElement; + viewportId: string; + renderingEngineId: string; + rotation?: number; +}; + +// @public (undocumented) +type ColormapPublic = { + name?: string; + opacity?: OpacityMapping[] | number; + /** midpoint mapping between values to opacity if the colormap + * is getting used for fusion, this is an array of arrays which + * each array containing 2 values, the first value is the value + * to map to opacity and the second value is the opacity value. + * By default, the minimum value is mapped to 0 opacity and the + * maximum value is mapped to 1 opacity, but you can configure + * the points in the middle to be mapped to different opacities + * instead of a linear mapping from 0 to 1. + */ +}; + +// @public (undocumented) +type ColormapRegistration = { + ColorSpace: string; + Name: string; + RGBPoints: RGB[]; +}; + +// @public (undocumented) +type ContourData = { + points: Point3[]; + type: ContourType; + color: Point3; + segmentIndex: number; +}; + +// @public (undocumented) +type ContourSetData = { + id: string; + data: ContourData[]; + frameOfReferenceUID: string; + color?: Point3; + segmentIndex?: number; +}; + +// @public (undocumented) +type Cornerstone3DConfig = { + gpuTier?: TierResult; + detectGPUConfig: GetGPUTier; + rendering: { + // vtk.js supports 8bit integer textures and 32bit float textures. + // However, if the client has norm16 textures (it can be seen by visiting + // the webGl report at https://webglreport.com/?v=2), vtk will be default + // to use it to improve memory usage. However, if the client don't have + // it still another level of optimization can happen by setting the + // preferSizeOverAccuracy since it will reduce the size of the texture to half + // float at the cost of accuracy in rendering. This is a tradeoff that the + // client can decide. + // + // Read more in the following Pull Request: + // 1. HalfFloat: https://github.com/Kitware/vtk-js/pull/2046 + // 2. Norm16: https://github.com/Kitware/vtk-js/pull/2058 + preferSizeOverAccuracy: boolean; + // Whether the EXT_texture_norm16 extension is supported by the browser. + // WebGL 2 report (link: https://webglreport.com/?v=2) can be used to check + // if the browser supports this extension. + // In case the browser supports this extension, instead of using 32bit float + // textures, 16bit float textures will be used to reduce the memory usage where + // possible. + // Norm16 may not work currently due to the two active bugs in chrome + safari + // https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 + // https://bugs.webkit.org/show_bug.cgi?id=252039 + useNorm16Texture: boolean; + useCPURendering: boolean; + strictZSpacingForVolumeViewport: boolean; + }; +}; + +// @public (undocumented) +export function cornerstoneNiftiImageVolumeLoader(volumeId: string): IVolumeLoader; + +// @public (undocumented) +interface CPUFallbackColormap { + // (undocumented) + addColor: (rgba: Point4) => void; + // (undocumented) + buildLookupTable: (lut: CPUFallbackLookupTable) => void; + // (undocumented) + clearColors: () => void; + // (undocumented) + createLookupTable: () => CPUFallbackLookupTable; + // (undocumented) + getColor: (index: number) => Point4; + // (undocumented) + getColorRepeating: (index: number) => Point4; + // (undocumented) + getColorSchemeName: () => string; + getId: () => string; + // (undocumented) + getNumberOfColors: () => number; + // (undocumented) + insertColor: (index: number, rgba: Point4) => void; + // (undocumented) + isValidIndex: (index: number) => boolean; + // (undocumented) + removeColor: (index: number) => void; + // (undocumented) + setColor: (index: number, rgba: Point4) => void; + // (undocumented) + setColorSchemeName: (name: string) => void; + // (undocumented) + setNumberOfColors: (numColors: number) => void; +} + +// @public (undocumented) +type CPUFallbackColormapData = { + name: string; + numOfColors?: number; + colors?: Point4[]; + segmentedData?: unknown; + numColors?: number; + gamma?: number; +}; + +// @public (undocumented) +type CPUFallbackColormapsData = { + [key: string]: CPUFallbackColormapData; +}; + +// @public (undocumented) +interface CPUFallbackEnabledElement { + // (undocumented) + canvas?: HTMLCanvasElement; + // (undocumented) + colormap?: CPUFallbackColormap; + // (undocumented) + image?: IImage; + // (undocumented) + invalid?: boolean; + // (undocumented) + metadata?: { + direction?: Mat3; + dimensions?: Point3; + spacing?: Point3; + origin?: Point3; + imagePlaneModule?: ImagePlaneModule; + imagePixelModule?: ImagePixelModule; + }; + // (undocumented) + needsRedraw?: boolean; + // (undocumented) + options?: { + [key: string]: unknown; + colormap?: CPUFallbackColormap; + }; + // (undocumented) + pan?: Point2; + // (undocumented) + renderingTools?: CPUFallbackRenderingTools; + // (undocumented) + rotation?: number; + // (undocumented) + scale?: number; + // (undocumented) + transform?: CPUFallbackTransform; + // (undocumented) + viewport?: CPUFallbackViewport; + // (undocumented) + zoom?: number; +} + +// @public (undocumented) +interface CPUFallbackLookupTable { + // (undocumented) + build: (force: boolean) => void; + // (undocumented) + getColor: (scalar: number) => Point4; + // (undocumented) + setAlphaRange: (start: number, end: number) => void; + // (undocumented) + setHueRange: (start: number, end: number) => void; + // (undocumented) + setNumberOfTableValues: (number: number) => void; + // (undocumented) + setRamp: (ramp: string) => void; + // (undocumented) + setRange: (start: number, end: number) => void; + // (undocumented) + setSaturationRange: (start: number, end: number) => void; + // (undocumented) + setTableRange: (start: number, end: number) => void; + // (undocumented) + setTableValue(index: number, rgba: Point4); + // (undocumented) + setValueRange: (start: number, end: number) => void; +} + +// @public (undocumented) +type CPUFallbackLUT = { + lut: number[]; +}; + +// @public (undocumented) +type CPUFallbackRenderingTools = { + renderCanvas?: HTMLCanvasElement; + lastRenderedIsColor?: boolean; + lastRenderedImageId?: string; + lastRenderedViewport?: { + windowWidth: number | number[]; + windowCenter: number | number[]; + invert: boolean; + rotation: number; + hflip: boolean; + vflip: boolean; + modalityLUT: CPUFallbackLUT; + voiLUT: CPUFallbackLUT; + colormap: unknown; + }; + renderCanvasContext?: CanvasRenderingContext2D; + colormapId?: string; + colorLUT?: CPUFallbackLookupTable; + renderCanvasData?: ImageData; +}; + +// @public (undocumented) +interface CPUFallbackTransform { + // (undocumented) + clone: () => CPUFallbackTransform; + // (undocumented) + getMatrix: () => TransformMatrix2D; + // (undocumented) + invert: () => void; + // (undocumented) + multiply: (matrix: TransformMatrix2D) => void; + // (undocumented) + reset: () => void; + // (undocumented) + rotate: (rad: number) => void; + // (undocumented) + scale: (sx: number, sy: number) => void; + // (undocumented) + transformPoint: (point: Point2) => Point2; + // (undocumented) + translate: (x: number, y: number) => void; +} + +// @public (undocumented) +type CPUFallbackViewport = { + scale?: number; + parallelScale?: number; + focalPoint?: number[]; + translation?: { + x: number; + y: number; + }; + voi?: { + windowWidth: number; + windowCenter: number; + }; + invert?: boolean; + pixelReplication?: boolean; + rotation?: number; + hflip?: boolean; + vflip?: boolean; + modalityLUT?: CPUFallbackLUT; + voiLUT?: CPUFallbackLUT; + colormap?: CPUFallbackColormap; + displayedArea?: CPUFallbackViewportDisplayedArea; + modality?: string; +}; + +// @public (undocumented) +type CPUFallbackViewportDisplayedArea = { + tlhc: { + x: number; + y: number; + }; + brhc: { + x: number; + y: number; + }; + rowPixelSpacing: number; + columnPixelSpacing: number; + presentationSizeMode: string; +}; + +// @public (undocumented) +type CPUIImageData = { + dimensions: Point3; + direction: Mat3; + spacing: Point3; + origin: Point3; + imageData: CPUImageData; + metadata: { Modality: string }; + scalarData: PixelDataTypedArray; + scaling: Scaling; + hasPixelSpacing?: boolean; + calibration?: IImageCalibration; + + preScale?: { + scaled?: boolean; + scalingParameters?: { + modality?: string; + rescaleSlope?: number; + rescaleIntercept?: number; + suvbw?: number; + }; + }; +}; + +// @public (undocumented) +type CPUImageData = { + worldToIndex?: (point: Point3) => Point3; + indexToWorld?: (point: Point3) => Point3; + getWorldToIndex?: () => Point3; + getIndexToWorld?: () => Point3; + getSpacing?: () => Point3; + getDirection?: () => Mat3; + getScalarData?: () => PixelDataTypedArray; + getDimensions?: () => Point3; +}; + +// @public (undocumented) +interface CustomEvent_2 extends Event { + readonly detail: T; + // (undocumented) + initCustomEvent( + typeArg: string, + canBubbleArg: boolean, + cancelableArg: boolean, + detailArg: T + ): void; +} + +// @public (undocumented) +type DisplayArea = { + imageArea: [number, number]; // areaX, areaY + imageCanvasPoint: { + imagePoint: [number, number]; // imageX, imageY + canvasPoint: [number, number]; // canvasX, canvasY + }; + storeAsInitialCamera: boolean; +}; + +// @public +type DisplayAreaModifiedEvent = CustomEvent_2; + +// @public +type DisplayAreaModifiedEventDetail = { + viewportId: string; + displayArea: DisplayArea; + volumeId?: string; + storeAsInitialCamera?: boolean; +}; + +// @public +type ElementDisabledEvent = CustomEvent_2; + +// @public +type ElementDisabledEventDetail = { + element: HTMLDivElement; + viewportId: string; + renderingEngineId: string; +}; + +// @public +type ElementEnabledEvent = CustomEvent_2; + +// @public +type ElementEnabledEventDetail = { + element: HTMLDivElement; + viewportId: string; + renderingEngineId: string; +}; + +declare namespace Enums { + export { + Events + } +} +export { Enums } + +// @public (undocumented) +enum Events { + // (undocumented) + NIFTI_VOLUME_LOADED = "CORNERSTONE_NIFTI_VOLUME_LOADED", + // (undocumented) + NIFTI_VOLUME_PROGRESS = "CORNERSTONE_NIFTI_VOLUME_PROGRESS" +} + +declare namespace EventTypes { + export { + CameraModifiedEventDetail, + CameraModifiedEvent, + VoiModifiedEvent, + VoiModifiedEventDetail, + DisplayAreaModifiedEvent, + DisplayAreaModifiedEventDetail, + ElementDisabledEvent, + ElementDisabledEventDetail, + ElementEnabledEvent, + ElementEnabledEventDetail, + ImageRenderedEventDetail, + ImageRenderedEvent, + ImageVolumeModifiedEvent, + ImageVolumeModifiedEventDetail, + ImageVolumeLoadingCompletedEvent, + ImageVolumeLoadingCompletedEventDetail, + ImageLoadedEvent, + ImageLoadedEventDetail, + ImageLoadedFailedEventDetail, + ImageLoadedFailedEvent, + VolumeLoadedEvent, + VolumeLoadedEventDetail, + VolumeLoadedFailedEvent, + VolumeLoadedFailedEventDetail, + ImageCacheImageAddedEvent, + ImageCacheImageAddedEventDetail, + ImageCacheImageRemovedEvent, + ImageCacheImageRemovedEventDetail, + VolumeCacheVolumeAddedEvent, + VolumeCacheVolumeAddedEventDetail, + VolumeCacheVolumeRemovedEvent, + VolumeCacheVolumeRemovedEventDetail, + StackNewImageEvent, + StackNewImageEventDetail, + PreStackNewImageEvent, + PreStackNewImageEventDetail, + ImageSpacingCalibratedEvent, + ImageSpacingCalibratedEventDetail, + ImageLoadProgressEvent, + ImageLoadProgressEventDetail, + VolumeNewImageEvent, + VolumeNewImageEventDetail, + StackViewportNewStackEvent, + StackViewportNewStackEventDetail, + StackViewportScrollEvent, + StackViewportScrollEventDetail + } +} + +// @public (undocumented) +function fetchAndAllocateNiftiVolume(volumeId: string): Promise; + +// @public +type FlipDirection = { + flipHorizontal?: boolean; + flipVertical?: boolean; +}; + +declare namespace helpers { + export { + modalityScaleNifti, + makeVolumeMetadata, + fetchAndAllocateNiftiVolume + } +} +export { helpers } + +// @public (undocumented) +interface ICache { + getCacheSize: () => number; + getImageLoadObject: (imageId: string) => IImageLoadObject | void; + getMaxCacheSize: () => number; + getVolumeLoadObject: (volumeId: string) => IVolumeLoadObject | void; + purgeCache: () => void; + putImageLoadObject: ( + imageId: string, + imageLoadObject: IImageLoadObject + ) => Promise; + putVolumeLoadObject: ( + volumeId: string, + volumeLoadObject: IVolumeLoadObject + ) => Promise; + setMaxCacheSize: (maxCacheSize: number) => void; +} + +// @public (undocumented) +interface ICachedGeometry { + // (undocumented) + geometry?: IGeometry; + // (undocumented) + geometryId: string; + // (undocumented) + geometryLoadObject: IGeometryLoadObject; + // (undocumented) + loaded: boolean; + // (undocumented) + sizeInBytes: number; + // (undocumented) + timeStamp: number; +} + +// @public (undocumented) +interface ICachedImage { + // (undocumented) + image?: IImage; + // (undocumented) + imageId: string; + // (undocumented) + imageLoadObject: IImageLoadObject; + // (undocumented) + loaded: boolean; + // (undocumented) + sharedCacheKey?: string; + // (undocumented) + sizeInBytes: number; + // (undocumented) + timeStamp: number; +} + +// @public (undocumented) +interface ICachedVolume { + // (undocumented) + loaded: boolean; + // (undocumented) + sizeInBytes: number; + // (undocumented) + timeStamp: number; + // (undocumented) + volume?: IImageVolume; + // (undocumented) + volumeId: string; + // (undocumented) + volumeLoadObject: IVolumeLoadObject; +} + +// @public +interface ICamera { + clippingRange?: Point2; + flipHorizontal?: boolean; + flipVertical?: boolean; + focalPoint?: Point3; + parallelProjection?: boolean; + parallelScale?: number; + position?: Point3; + scale?: number; + viewAngle?: number; + viewPlaneNormal?: Point3; + viewUp?: Point3; +} + +// @public (undocumented) +interface IContour { + // (undocumented) + color: any; + // (undocumented) + getColor(): Point3; + // (undocumented) + getFlatPointsArray(): number[]; + getPoints(): Point3[]; + // (undocumented) + _getSizeInBytes(): number; + // (undocumented) + getType(): ContourType; + // (undocumented) + readonly id: string; + // (undocumented) + points: Point3[]; + // (undocumented) + readonly sizeInBytes: number; +} + +// @public +interface IContourSet { + // (undocumented) + contours: IContour[]; + // (undocumented) + _createEachContour(data: ContourData[]): void; + // (undocumented) + readonly frameOfReferenceUID: string; + // (undocumented) + getCentroid(): Point3; + // (undocumented) + getColor(): any; + getContours(): IContour[]; + getFlatPointsArray(): Point3[]; + getNumberOfContours(): number; + getNumberOfPointsArray(): number[]; + getNumberOfPointsInAContour(contourIndex: number): number; + getPointsInContour(contourIndex: number): Point3[]; + // (undocumented) + getSegmentIndex(): number; + // (undocumented) + getSizeInBytes(): number; + getTotalNumberOfPoints(): number; + // (undocumented) + readonly id: string; + // (undocumented) + readonly sizeInBytes: number; +} + +// @public +interface IDynamicImageVolume extends IImageVolume { + getScalarDataArrays(): VolumeScalarData[]; + get numTimePoints(): number; + get timePointIndex(): number; + set timePointIndex(newTimePointIndex: number); +} + +// @public +interface IEnabledElement { + FrameOfReferenceUID: string; + renderingEngine: IRenderingEngine; + renderingEngineId: string; + viewport: IStackViewport | IVolumeViewport; + viewportId: string; +} + +// @public (undocumented) +interface IGeometry { + // (undocumented) + data: IContourSet; + // (undocumented) + id: string; + // (undocumented) + sizeInBytes: number; + // (undocumented) + type: GeometryType; +} + +// @public (undocumented) +interface IGeometryLoadObject { + cancelFn?: () => void; + decache?: () => void; + promise: Promise; +} + +// @public +interface IImage { + cachedLut?: { + windowWidth?: number | number[]; + windowCenter?: number | number[]; + invert?: boolean; + lutArray?: Uint8ClampedArray; + modalityLUT?: unknown; + voiLUT?: CPUFallbackLUT; + }; + color: boolean; + colormap?: CPUFallbackColormap; + columnPixelSpacing: number; + columns: number; + // (undocumented) + getCanvas: () => HTMLCanvasElement; + getPixelData: () => PixelDataTypedArray; + height: number; + imageId: string; + intercept: number; + invert: boolean; + isPreScaled?: boolean; + // (undocumented) + maxPixelValue: number; + minPixelValue: number; + modalityLUT?: CPUFallbackLUT; + numComps: number; + photometricInterpretation?: string; + preScale?: { + scaled?: boolean; + scalingParameters?: { + modality?: string; + rescaleSlope?: number; + rescaleIntercept?: number; + suvbw?: number; + }; + }; + render?: ( + enabledElement: CPUFallbackEnabledElement, + invalidated: boolean + ) => unknown; + rgba: boolean; + rowPixelSpacing: number; + rows: number; + scaling?: { + PT?: { + // @TODO: Do these values exist? + SUVlbmFactor?: number; + SUVbsaFactor?: number; + // accessed in ProbeTool + suvbwToSuvlbm?: number; + suvbwToSuvbsa?: number; + }; + }; + // (undocumented) + sharedCacheKey?: string; + sizeInBytes: number; + sliceThickness?: number; + slope: number; + stats?: { + lastStoredPixelDataToCanvasImageDataTime?: number; + lastGetPixelDataTime?: number; + lastPutImageDataTime?: number; + lastLutGenerateTime?: number; + lastRenderedViewport?: unknown; + lastRenderTime?: number; + }; + voiLUT?: CPUFallbackLUT; + voiLUTFunction: string; + width: number; + windowCenter: number[] | number; + windowWidth: number[] | number; +} + +// @public +interface IImageCalibration { + aspect?: number; + // (undocumented) + columnPixelSpacing?: number; + rowPixelSpacing?: number; + scale?: number; + sequenceOfUltrasoundRegions?: Record[]; + tooltip?: string; + type: CalibrationTypes; +} + +// @public +interface IImageData { + // (undocumented) + calibration?: IImageCalibration; + dimensions: Point3; + direction: Mat3; + hasPixelSpacing?: boolean; + imageData: vtkImageData; + metadata: { Modality: string }; + origin: Point3; + preScale?: { + scaled?: boolean; + scalingParameters?: { + modality?: string; + rescaleSlope?: number; + rescaleIntercept?: number; + suvbw?: number; + }; + }; + scalarData: Float32Array | Uint16Array | Uint8Array | Int16Array; + scaling?: Scaling; + spacing: Point3; +} + +// @public +interface IImageLoadObject { + cancelFn?: () => void; + decache?: () => void; + promise: Promise; +} + +// @public +interface IImageVolume { + // (undocumented) + cancelLoading?: () => void; + convertToCornerstoneImage?: ( + imageId: string, + imageIdIndex: number + ) => IImageLoadObject; + destroy(): void; + dimensions: Point3; + direction: Mat3; + getImageIdIndex(imageId: string): number; + getImageURIIndex(imageURI: string): number; + getScalarData(): VolumeScalarData; + hasPixelSpacing: boolean; + imageData?: vtkImageData; + imageIds: Array; + isDynamicVolume(): boolean; + isPreScaled: boolean; + loadStatus?: Record; + metadata: Metadata; + numVoxels: number; + origin: Point3; + referencedVolumeId?: string; + scaling?: { + PT?: { + SUVlbmFactor?: number; + SUVbsaFactor?: number; + suvbwToSuvlbm?: number; + suvbwToSuvbsa?: number; + }; + }; + sizeInBytes?: number; + spacing: Point3; + readonly volumeId: string; + vtkOpenGLTexture: any; +} + +// @public +type ImageCacheImageAddedEvent = +CustomEvent_2; + +// @public +type ImageCacheImageAddedEventDetail = { + image: ICachedImage; +}; + +// @public +type ImageCacheImageRemovedEvent = +CustomEvent_2; + +// @public +type ImageCacheImageRemovedEventDetail = { + imageId: string; +}; + +// @public +type ImageLoadedEvent = CustomEvent_2; + +// @public +type ImageLoadedEventDetail = { + image: IImage; +}; + +// @public +type ImageLoadedFailedEvent = CustomEvent_2; + +// @public +type ImageLoadedFailedEventDetail = { + imageId: string; + error: unknown; +}; + +// @public +type ImageLoaderFn = ( +imageId: string, +options?: Record +) => { + promise: Promise>; + cancelFn?: () => void | undefined; + decache?: () => void | undefined; +}; + +// @public +type ImageLoadProgressEvent = CustomEvent_2; + +// @public +type ImageLoadProgressEventDetail = { + url: string; + imageId: string; + loaded: number; + total: number; + percent: number; +}; + +// @public (undocumented) +interface ImagePixelModule { + // (undocumented) + bitsAllocated: number; + // (undocumented) + bitsStored: number; + // (undocumented) + highBit: number; + // (undocumented) + modality: string; + // (undocumented) + photometricInterpretation: string; + // (undocumented) + pixelRepresentation: string; + // (undocumented) + samplesPerPixel: number; + // (undocumented) + voiLUTFunction: VOILUTFunctionType; + // (undocumented) + windowCenter: number | number[]; + // (undocumented) + windowWidth: number | number[]; +} + +// @public (undocumented) +interface ImagePlaneModule { + // (undocumented) + columnCosines?: Point3; + // (undocumented) + columnPixelSpacing?: number; + // (undocumented) + columns: number; + // (undocumented) + frameOfReferenceUID: string; + // (undocumented) + imageOrientationPatient?: Float32Array; + // (undocumented) + imagePositionPatient?: Point3; + // (undocumented) + pixelSpacing?: Point2; + // (undocumented) + rowCosines?: Point3; + // (undocumented) + rowPixelSpacing?: number; + // (undocumented) + rows: number; + // (undocumented) + sliceLocation?: number; + // (undocumented) + sliceThickness?: number; +} + +// @public +type ImageRenderedEvent = CustomEvent_2; + +// @public +type ImageRenderedEventDetail = { + element: HTMLDivElement; + viewportId: string; + renderingEngineId: string; + suppressEvents?: boolean; + viewportStatus: ViewportStatus; +}; + +// @public (undocumented) +type ImageSliceData = { + numberOfSlices: number; + imageIndex: number; +}; + +// @public +type ImageSpacingCalibratedEvent = +CustomEvent_2; + +// @public +type ImageSpacingCalibratedEventDetail = { + element: HTMLDivElement; + viewportId: string; + renderingEngineId: string; + imageId: string; + calibration: IImageCalibration; + imageData: vtkImageData; + worldToIndex: mat4; +}; + +// @public +type ImageVolumeLoadingCompletedEvent = +CustomEvent_2; + +// @public +type ImageVolumeLoadingCompletedEventDetail = { + volumeId: string; + FrameOfReferenceUID: string; +}; + +// @public +type ImageVolumeModifiedEvent = CustomEvent_2; + +// @public +type ImageVolumeModifiedEventDetail = { + imageVolume: IImageVolume; + FrameOfReferenceUID: string; +}; + +// @public +interface IRegisterImageLoader { + // (undocumented) + registerImageLoader: (scheme: string, imageLoader: ImageLoaderFn) => void; +} + +// @public (undocumented) +interface IRenderingEngine { + // (undocumented) + _debugRender(): void; + // (undocumented) + destroy(): void; + // (undocumented) + disableElement(viewportId: string): void; + // (undocumented) + enableElement(viewportInputEntry: PublicViewportInput): void; + // (undocumented) + fillCanvasWithBackgroundColor( + canvas: HTMLCanvasElement, + backgroundColor: [number, number, number] + ): void; + // (undocumented) + getStackViewports(): Array; + // (undocumented) + getViewport(id: string): IStackViewport | IVolumeViewport; + // (undocumented) + getViewports(): Array; + // (undocumented) + getVolumeViewports(): Array; + // (undocumented) + hasBeenDestroyed: boolean; + // (undocumented) + id: string; + // (undocumented) + offScreenCanvasContainer: any; + // (undocumented) + offscreenMultiRenderWindow: any; + // (undocumented) + render(): void; + // (undocumented) + renderFrameOfReference(FrameOfReferenceUID: string): void; + // (undocumented) + renderViewport(viewportId: string): void; + // (undocumented) + renderViewports(viewportIds: Array): void; + // (undocumented) + resize(immediate?: boolean, keepCamera?: boolean): void; + // (undocumented) + setViewports(viewports: Array): void; +} + +// @public +interface IStackViewport extends IViewport { + calibrateSpacing(imageId: string): void; + canvasToWorld: (canvasPos: Point2) => Point3; + customRenderViewportToCanvas: () => { + canvas: HTMLCanvasElement; + element: HTMLDivElement; + viewportId: string; + renderingEngineId: string; + }; + getCamera(): ICamera; + getCornerstoneImage: () => IImage; + getCurrentImageId: () => string; + getCurrentImageIdIndex: () => number; + getFrameOfReferenceUID: () => string; + getImageData(): IImageData | CPUIImageData; + getImageIds: () => string[]; + getProperties: () => StackViewportProperties; + getRenderer(): any; + hasImageId: (imageId: string) => boolean; + hasImageURI: (imageURI: string) => boolean; + // (undocumented) + modality: string; + resetCamera(resetPan?: boolean, resetZoom?: boolean): boolean; + resetProperties(): void; + resize: () => void; + scaling: Scaling; + setCamera(cameraInterface: ICamera): void; + setColormap(colormap: CPUFallbackColormapData | ColormapRegistration): void; + setImageIdIndex(imageIdIndex: number): Promise; + setProperties( + { voiRange, invert, interpolationType, rotation }: StackViewportProperties, + suppressEvents?: boolean + ): void; + setStack( + imageIds: Array, + currentImageIdIndex?: number + ): Promise; + unsetColormap(): void; + worldToCanvas: (worldPos: Point3) => Point2; +} + +// @public +interface IStreamingImageVolume extends ImageVolume { + clearLoadCallbacks(): void; + convertToCornerstoneImage(imageId: string, imageIdIndex: number): any; + decache(completelyRemove: boolean): void; +} + +// @public (undocumented) +interface IStreamingVolumeProperties { + imageIds: Array; + + loadStatus: { + loaded: boolean; + loading: boolean; + cancelled: boolean; + cachedFrames: Array; + callbacks: Array<() => void>; + }; +} + +// @public +interface IViewport { + _actors: Map; + addActor(actorEntry: ActorEntry): void; + addActors(actors: Array): void; + canvas: HTMLCanvasElement; + canvasToWorld: (canvasPos: Point2) => Point3; + customRenderViewportToCanvas: () => unknown; + defaultOptions: any; + element: HTMLDivElement; + getActor(actorUID: string): ActorEntry; + getActorByIndex(index: number): ActorEntry; + getActors(): Array; + getActorUIDByIndex(index: number): string; + getCamera(): ICamera; + getCanvas(): HTMLCanvasElement; + // (undocumented) + _getCorners(bounds: Array): Array[]; + getDefaultActor(): ActorEntry; + getDisplayArea(): DisplayArea | undefined; + getFrameOfReferenceUID: () => string; + getPan(): Point2; + getRenderer(): void; + getRenderingEngine(): any; + getRotation: () => number; + getZoom(): number; + id: string; + isDisabled: boolean; + options: ViewportInputOptions; + removeActors(actorUIDs: Array): void; + removeAllActors(): void; + render(): void; + renderingEngineId: string; + reset(immediate: boolean): void; + setActors(actors: Array): void; + setCamera(cameraInterface: ICamera, storeAsInitialCamera?: boolean): void; + setDisplayArea( + displayArea: DisplayArea, + callResetCamera?: boolean, + suppressEvents?: boolean + ); + setOptions(options: ViewportInputOptions, immediate: boolean): void; + setPan(pan: Point2, storeAsInitialCamera?: boolean); + setRendered(): void; + setZoom(zoom: number, storeAsInitialCamera?: boolean); + sHeight: number; + suppressEvents: boolean; + sWidth: number; + sx: number; + sy: number; + type: ViewportType; + // (undocumented) + updateRenderingPipeline: () => void; + viewportStatus: ViewportStatus; + worldToCanvas: (worldPos: Point3) => Point2; +} + +// @public +interface IViewportId { + // (undocumented) + renderingEngineId: string; + // (undocumented) + viewportId: string; +} + +// @public +interface IVolume { + dimensions: Point3; + direction: Mat3; + imageData?: vtkImageData; + metadata: Metadata; + origin: Point3; + referencedVolumeId?: string; + scalarData: VolumeScalarData | Array; + scaling?: { + PT?: { + // @TODO: Do these values exist? + SUVlbmFactor?: number; + SUVbsaFactor?: number; + // accessed in ProbeTool + suvbwToSuvlbm?: number; + suvbwToSuvbsa?: number; + }; + }; + sizeInBytes?: number; + spacing: Point3; + volumeId: string; +} + +// @public +interface IVolumeInput { + // (undocumented) + actorUID?: string; + // actorUID for segmentations, since two segmentations with the same volumeId + // can have different representations + blendMode?: BlendModes; + // actorUID for segmentations, since two segmentations with the same volumeId + // can have different representations + callback?: VolumeInputCallback; + // actorUID for segmentations, since two segmentations with the same volumeId + // can have different representations + slabThickness?: number; + // actorUID for segmentations, since two segmentations with the same volumeId + // can have different representations + visibility?: boolean; + // actorUID for segmentations, since two segmentations with the same volumeId + // can have different representations + volumeId: string; +} + +// @public +interface IVolumeLoadObject { + cancelFn?: () => void; + decache?: () => void; + promise: Promise; +} + +// @public +interface IVolumeViewport extends IViewport { + addVolumes( + volumeInputArray: Array, + immediate?: boolean, + suppressEvents?: boolean + ): Promise; + canvasToWorld: (canvasPos: Point2) => Point3; + flip(flipDirection: FlipDirection): void; + getBounds(): any; + getCurrentImageId: () => string; + getCurrentImageIdIndex: () => number; + // (undocumented) + getFrameOfReferenceUID: () => string; + getImageData(volumeId?: string): IImageData | undefined; + getImageIds: (volumeId?: string) => string[]; + getIntensityFromWorld(point: Point3): number; + getProperties: () => VolumeViewportProperties; + getSlabThickness(): number; + hasImageURI: (imageURI: string) => boolean; + hasVolumeId: (volumeId: string) => boolean; + removeVolumeActors(actorUIDs: Array, immediate?: boolean): void; + resetCamera( + resetPan?: boolean, + resetZoom?: boolean, + resetToCenter?: boolean + ): boolean; + resetProperties(volumeId?: string): void; + setBlendMode( + blendMode: BlendModes, + filterActorUIDs?: Array, + immediate?: boolean + ): void; + // (undocumented) + setOrientation(orientation: OrientationAxis): void; + setProperties( + { voiRange }: VolumeViewportProperties, + volumeId?: string, + suppressEvents?: boolean + ): void; + setSlabThickness( + slabThickness: number, + filterActorUIDs?: Array + ): void; + setVolumes( + volumeInputArray: Array, + immediate?: boolean, + suppressEvents?: boolean + ): Promise; + // (undocumented) + useCPURendering: boolean; + worldToCanvas: (worldPos: Point3) => Point2; +} + +// @public (undocumented) +function makeVolumeMetadata(niftiHeader: any, orientation: any, scalarData: any): Types.Metadata; + +// @public +type Mat3 = +| [number, number, number, number, number, number, number, number, number] +| Float32Array; + +// @public +type Metadata = { + BitsAllocated: number; + BitsStored: number; + SamplesPerPixel: number; + HighBit: number; + PhotometricInterpretation: string; + PixelRepresentation: number; + Modality: string; + SeriesInstanceUID?: string; + ImageOrientationPatient: Array; + PixelSpacing: Array; + FrameOfReferenceUID: string; + Columns: number; + Rows: number; + voiLut: Array; + VOILUTFunction: string; +}; + +// @public (undocumented) +function modalityScaleNifti(array: Float32Array | Int16Array | Uint8Array, niftiHeader: any): void; + +// @public (undocumented) +export class NiftiImageVolume extends ImageVolume { + constructor(imageVolumeProperties: Types.IVolume, streamingProperties: NiftiImageProperties); + // (undocumented) + cancelLoading: () => void; + // (undocumented) + clearLoadCallbacks(): void; + // (undocumented) + controller: AbortController; + // (undocumented) + decache(): void; + // (undocumented) + load: (callback: (...args: unknown[]) => void, priority?: number) => void; + // (undocumented) + loadStatus: LoadStatus; +} + +// @public +type OrientationVectors = { + viewPlaneNormal: Point3; + viewUp: Point3; +}; + +// @public (undocumented) +type PixelDataTypedArray = +| Float32Array +| Int16Array +| Uint16Array +| Uint8Array +| Int8Array +| Uint8ClampedArray; + +// @public +type Plane = [number, number, number, number]; + +// @public +type Point2 = [number, number]; + +// @public +type Point3 = [number, number, number]; + +// @public +type Point4 = [number, number, number, number]; + +// @public +type PreStackNewImageEvent = CustomEvent_2; + +// @public +type PreStackNewImageEventDetail = { + imageId: string; + imageIdIndex: number; + viewportId: string; + renderingEngineId: string; +}; + +// @public (undocumented) +type PTScaling = { + suvbwToSuvlbm?: number; + suvbwToSuvbsa?: number; + suvbw?: number; + suvlbm?: number; + suvbsa?: number; +}; + +// @public (undocumented) +type PublicContourSetData = ContourSetData; + +// @public +type PublicViewportInput = { + element: HTMLDivElement; + viewportId: string; + type: ViewportType; + defaultOptions?: ViewportInputOptions; +}; + +// @public +type RGB = [number, number, number]; + +// @public (undocumented) +type Scaling = { + PT?: PTScaling; +}; + +// @public (undocumented) +type ScalingParameters = { + rescaleSlope: number; + rescaleIntercept: number; + modality: string; + suvbw?: number; + suvlbm?: number; + suvbsa?: number; +}; + +// @public +type StackNewImageEvent = CustomEvent_2; + +// @public +type StackNewImageEventDetail = { + image: IImage; + imageId: string; + imageIdIndex: number; + viewportId: string; + renderingEngineId: string; +}; + +// @public +type StackViewportNewStackEvent = +CustomEvent_2; + +// @public +type StackViewportNewStackEventDetail = { + imageIds: string[]; + viewportId: string; + element: HTMLDivElement; + currentImageIdIndex: number; +}; + +// @public +type StackViewportProperties = ViewportProperties & { + interpolationType?: InterpolationType; + rotation?: number; + suppressEvents?: boolean; + isComputedVOI?: boolean; +}; + +// @public (undocumented) +type StackViewportScrollEvent = CustomEvent_2; + +// @public +type StackViewportScrollEventDetail = { + newImageIdIndex: number; + imageId: string; + direction: number; +}; + +// @public +type TransformMatrix2D = [number, number, number, number, number, number]; + +// @public +type ViewportInputOptions = { + background?: RGB; + orientation?: OrientationAxis | OrientationVectors; + displayArea?: DisplayArea; + suppressEvents?: boolean; + parallelProjection?: boolean; +}; + +// @public (undocumented) +interface ViewportPreset { + // (undocumented) + ambient: string; + // (undocumented) + colorTransfer: string; + // (undocumented) + diffuse: string; + // (undocumented) + gradientOpacity: string; + // (undocumented) + interpolation: string; + // (undocumented) + name: string; + // (undocumented) + scalarOpacity: string; + // (undocumented) + shade: string; + // (undocumented) + specular: string; + // (undocumented) + specularPower: string; +} + +// @public +type ViewportProperties = { + voiRange?: VOIRange; + VOILUTFunction?: VOILUTFunctionType; + invert?: boolean; + interpolationType?: InterpolationType; +}; + +// @public (undocumented) +type VOI = { + windowWidth: number; + windowCenter: number; +}; + +// @public +type VoiModifiedEvent = CustomEvent_2; + +// @public +type VoiModifiedEventDetail = { + viewportId: string; + range: VOIRange; + volumeId?: string; + VOILUTFunction?: VOILUTFunctionType; + invert?: boolean; + invertStateChanged?: boolean; +}; + +// @public (undocumented) +type VOIRange = { + upper: number; + lower: number; +}; + +// @public (undocumented) +type VolumeActor = vtkVolume; + +// @public +type VolumeCacheVolumeAddedEvent = +CustomEvent_2; + +// @public +type VolumeCacheVolumeAddedEventDetail = { + volume: ICachedVolume; +}; + +// @public +type VolumeCacheVolumeRemovedEvent = +CustomEvent_2; + +// @public +type VolumeCacheVolumeRemovedEventDetail = { + volumeId: string; +}; + +// @public +type VolumeInputCallback = (params: { + volumeActor: VolumeActor; + volumeId: string; +}) => unknown; + +// @public +type VolumeLoadedEvent = CustomEvent_2; + +// @public +type VolumeLoadedEventDetail = { + volume: IImageVolume; +}; + +// @public +type VolumeLoadedFailedEvent = CustomEvent_2; + +// @public +type VolumeLoadedFailedEventDetail = { + volumeId: string; + error: unknown; +}; + +// @public +type VolumeLoaderFn = ( +volumeId: string, +options?: Record +) => { + promise: Promise>; + cancelFn?: () => void | undefined; + decache?: () => void | undefined; +}; + +// @public +type VolumeNewImageEvent = CustomEvent_2; + +// @public +type VolumeNewImageEventDetail = { + imageIndex: number; + numberOfSlices: number; + viewportId: string; + renderingEngineId: string; +}; + +// @public (undocumented) +type VolumeScalarData = Float32Array | Uint8Array | Uint16Array | Int16Array; + +// @public +type VolumeViewportProperties = ViewportProperties & { + colormap?: ColormapPublic; + preset?: string; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/common/reviews/api/streaming-image-volume-loader.api.md b/common/reviews/api/streaming-image-volume-loader.api.md index 223d1fc98..454e7f8af 100644 --- a/common/reviews/api/streaming-image-volume-loader.api.md +++ b/common/reviews/api/streaming-image-volume-loader.api.md @@ -34,6 +34,14 @@ type ActorSliceRange = { current: number; }; +// @public (undocumented) +type AffineMatrix = [ +[number, number, number, number], +[number, number, number, number], +[number, number, number, number], +[number, number, number, number] +]; + // @public enum BlendModes { AVERAGE_INTENSITY_BLEND = BlendMode.AVERAGE_INTENSITY_BLEND, @@ -1344,21 +1352,12 @@ interface IVolumeViewport extends IViewport { // (undocumented) useCPURendering: boolean; worldToCanvas: (worldPos: Point3) => Point2; - } // @public -type Mat3 = [ -number, -number, -number, -number, -number, -number, -number, -number, -number -]; +type Mat3 = +| [number, number, number, number, number, number, number, number, number] +| Float32Array; // @public type Metadata = { diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index a4a0481f7..3c04ed5c0 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -72,6 +72,14 @@ export function addTool(ToolClass: any): void; // @public (undocumented) function addToolState(element: HTMLDivElement, data: CINETypes.ToolData): void; +// @public (undocumented) +type AffineMatrix = [ +[number, number, number, number], +[number, number, number, number], +[number, number, number, number], +[number, number, number, number] +]; + // @public (undocumented) interface AngleAnnotation extends Annotation { // (undocumented) @@ -3200,7 +3208,6 @@ interface IVolumeViewport extends IViewport { // (undocumented) useCPURendering: boolean; worldToCanvas: (worldPos: Point3) => Point2; - } // @public (undocumented) @@ -3428,17 +3435,9 @@ export class MagnifyTool extends BaseTool { } // @public -type Mat3 = [ -number, -number, -number, -number, -number, -number, -number, -number, -number -]; +type Mat3 = +| [number, number, number, number, number, number, number, number, number] +| Float32Array; declare namespace math { export { diff --git a/lerna.json b/lerna.json index f6a9bdc62..ce7558f56 100644 --- a/lerna.json +++ b/lerna.json @@ -5,6 +5,7 @@ "packages/core", "packages/tools", "packages/streaming-image-volume-loader", + "packages/nifti-volume-loader", "packages/dicomImageLoader" ], "npmClient": "yarn", diff --git a/package.json b/package.json index 1b5bb326f..a723ad8ca 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@babel/runtime-corejs3": "^7.15.4", "@babel/runtime": "7.21.5", "@cornerstonejs/calculate-suv": "1.0.3", - "@microsoft/api-extractor": "7.19.5", + "@microsoft/api-extractor": "7.20.1", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-json": "^6.0.0", diff --git a/packages/core/src/loaders/volumeLoader.ts b/packages/core/src/loaders/volumeLoader.ts index 7f1813b0f..8a5053519 100644 --- a/packages/core/src/loaders/volumeLoader.ts +++ b/packages/core/src/loaders/volumeLoader.ts @@ -132,7 +132,7 @@ let unknownVolumeLoader; */ function loadVolumeFromVolumeLoader( volumeId: string, - options: VolumeLoaderOptions + options?: VolumeLoaderOptions ): Types.IVolumeLoadObject { const colonIndex = volumeId.indexOf(':'); const scheme = volumeId.substring(0, colonIndex); @@ -210,7 +210,7 @@ export function loadVolume( */ export async function createAndCacheVolume( volumeId: string, - options: VolumeLoaderOptions + options?: VolumeLoaderOptions ): Promise> { if (volumeId === undefined) { throw new Error( diff --git a/packages/core/src/types/AffineMatrix.ts b/packages/core/src/types/AffineMatrix.ts new file mode 100644 index 000000000..5d75c4d04 --- /dev/null +++ b/packages/core/src/types/AffineMatrix.ts @@ -0,0 +1,8 @@ +type AffineMatrix = [ + [number, number, number, number], + [number, number, number, number], + [number, number, number, number], + [number, number, number, number] +]; + +export type { AffineMatrix }; diff --git a/packages/core/src/types/Mat3.ts b/packages/core/src/types/Mat3.ts index 1b1c9017e..28a11190c 100644 --- a/packages/core/src/types/Mat3.ts +++ b/packages/core/src/types/Mat3.ts @@ -1,16 +1,8 @@ /** * This represents a 3x3 matrix of numbers */ -type Mat3 = [ - number, - number, - number, - number, - number, - number, - number, - number, - number -]; +type Mat3 = + | [number, number, number, number, number, number, number, number, number] + | Float32Array; export default Mat3; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 636f40bf1..98ed44ea7 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -81,6 +81,7 @@ import type { ViewportProperties } from './ViewportProperties'; import type { PixelDataTypedArray } from './PixelDataTypedArray'; import type { ImagePixelModule } from './ImagePixelModule'; import type { ImagePlaneModule } from './ImagePlaneModule'; +import type { AffineMatrix } from './AffineMatrix'; export type { // config @@ -173,4 +174,5 @@ export type { PixelDataTypedArray, ImagePixelModule, ImagePlaneModule, + AffineMatrix, }; diff --git a/packages/nifti-volume-loader/.gitignore b/packages/nifti-volume-loader/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/packages/nifti-volume-loader/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/nifti-volume-loader/.webpack/webpack.dev.js b/packages/nifti-volume-loader/.webpack/webpack.dev.js new file mode 100644 index 000000000..9ba31e870 --- /dev/null +++ b/packages/nifti-volume-loader/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.common.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); +}; diff --git a/packages/nifti-volume-loader/.webpack/webpack.prod.js b/packages/nifti-volume-loader/.webpack/webpack.prod.js new file mode 100644 index 000000000..af0fb7fb5 --- /dev/null +++ b/packages/nifti-volume-loader/.webpack/webpack.prod.js @@ -0,0 +1,45 @@ +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.common.js'); +const pkg = require('./../package.json'); + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv); + + return merge(commonConfig, { + devtool: 'source-map', + entry: { + lib: path.join(__dirname, '../src/index.ts'), + }, + output: { + path: path.join(__dirname, '../dist/umd'), + library: 'cornerstoneNiftiVolumeLoader', + libraryTarget: 'umd', + filename: 'index.js', + }, + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + }, + externals: [ + { + '@cornerstonejs/core': { + root: 'cornerstone3D', + commonjs: '@cornerstonejs/core', + commonjs2: '@cornerstonejs/core', + amd: '@cornerstonejs/core', + }, + }, + ], + }); +}; diff --git a/packages/nifti-volume-loader/CHANGELOG.md b/packages/nifti-volume-loader/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/nifti-volume-loader/README.md b/packages/nifti-volume-loader/README.md new file mode 100644 index 000000000..f178146eb --- /dev/null +++ b/packages/nifti-volume-loader/README.md @@ -0,0 +1,3 @@ +# @cornerstonejs/nifti-volume-loader + +Nifti volume loader for the cornerstone3D framework. diff --git a/packages/nifti-volume-loader/api-extractor.json b/packages/nifti-volume-loader/api-extractor.json new file mode 100644 index 000000000..4ddb5f44d --- /dev/null +++ b/packages/nifti-volume-loader/api-extractor.json @@ -0,0 +1,9 @@ +{ + "extends": "../../api-extractor.json", + "projectFolder": ".", + "mainEntryPointFilePath": "/dist/esm/index.d.ts", + "apiReport": { + "reportFileName": ".api.md", + "reportFolder": "../../common/reviews/api" + } +} diff --git a/packages/nifti-volume-loader/babel.config.js b/packages/nifti-volume-loader/babel.config.js new file mode 100644 index 000000000..325ca2a8e --- /dev/null +++ b/packages/nifti-volume-loader/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/packages/nifti-volume-loader/examples/niftiBasic/index.ts b/packages/nifti-volume-loader/examples/niftiBasic/index.ts new file mode 100644 index 000000000..0b11ff48b --- /dev/null +++ b/packages/nifti-volume-loader/examples/niftiBasic/index.ts @@ -0,0 +1,100 @@ +import { + RenderingEngine, + Enums, + init as csInit, + Types, + volumeLoader, + setVolumesForViewports, +} from '@cornerstonejs/core'; +import { init as csTools3dInit } from '@cornerstonejs/tools'; +import { cornerstoneNiftiImageVolumeLoader } from '@cornerstonejs/nifti-volume-loader'; + +import { setCtTransferFunctionForVolumeActor } from '../../../../utils/demo/helpers'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const size = '500px'; + +const content = document.getElementById('content'); +const viewportGrid = document.createElement('div'); +viewportGrid.style.display = 'flex'; +viewportGrid.style.display = 'flex'; +viewportGrid.style.flexDirection = 'row'; + +const element1 = document.createElement('div'); +const element2 = document.createElement('div'); +const element3 = document.createElement('div'); +element1.style.width = size; +element1.style.height = size; +element2.style.width = size; +element2.style.height = size; +element3.style.width = size; +element3.style.height = size; + +viewportGrid.appendChild(element1); +viewportGrid.appendChild(element2); +viewportGrid.appendChild(element3); + +content.appendChild(viewportGrid); + +const viewportId1 = 'CT_NIFTI_AXIAL'; +const viewportId2 = 'CT_NIFTI_SAGITTAL'; +const viewportId3 = 'CT_NIFTI_CORONAL'; + +async function setup() { + await csInit(); + await csTools3dInit(); + + volumeLoader.registerVolumeLoader('nifti', cornerstoneNiftiImageVolumeLoader); + + const niftiURL = + 'https://ohif-assets.s3.us-east-2.amazonaws.com/nifti/MRHead.nii.gz'; + const volumeId = 'nifti:' + niftiURL; + + const volume = await volumeLoader.createAndCacheVolume(volumeId); + + const renderingEngineId = 'myRenderingEngine'; + const renderingEngine = new RenderingEngine(renderingEngineId); + + const viewportInputArray = [ + { + viewportId: viewportId1, + type: Enums.ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId2, + type: Enums.ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId3, + type: Enums.ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]; + + renderingEngine.setViewports(viewportInputArray); + + setVolumesForViewports( + renderingEngine, + [{ volumeId, callback: setCtTransferFunctionForVolumeActor }], + viewportInputArray.map((v) => v.viewportId) + ); + + renderingEngine.render(); +} + +setup(); diff --git a/packages/nifti-volume-loader/examples/niftiWithTools/index.ts b/packages/nifti-volume-loader/examples/niftiWithTools/index.ts new file mode 100644 index 000000000..8f75ae8d4 --- /dev/null +++ b/packages/nifti-volume-loader/examples/niftiWithTools/index.ts @@ -0,0 +1,282 @@ +import { + RenderingEngine, + Enums, + init as csInit, + volumeLoader, + setVolumesForViewports, + eventTarget, +} from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import { + cornerstoneNiftiImageVolumeLoader, + Enums as NiftiEnums, +} from '@cornerstonejs/nifti-volume-loader'; + +import { + addDropdownToToolbar, + setCtTransferFunctionForVolumeActor, +} from '../../../../utils/demo/helpers'; + +const { + LengthTool, + ToolGroupManager, + StackScrollMouseWheelTool, + ZoomTool, + Enums: csToolsEnums, + init: csTools3dInit, + ProbeTool, + RectangleROITool, + EllipticalROITool, + CircleROITool, + WindowLevelTool, + PanTool, + BidirectionalTool, + AngleTool, + CobbAngleTool, + ArrowAnnotateTool, +} = cornerstoneTools; + +const { ViewportType } = Enums; +const { MouseBindings } = csToolsEnums; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const size = '500px'; + +const content = document.getElementById('content'); +const viewportGrid = document.createElement('div'); +viewportGrid.style.display = 'flex'; +viewportGrid.style.display = 'flex'; +viewportGrid.style.flexDirection = 'row'; + +const element1 = document.createElement('div'); +const element2 = document.createElement('div'); +const element3 = document.createElement('div'); +element1.style.width = size; +element1.style.height = size; +element2.style.width = size; +element2.style.height = size; +element3.style.width = size; +element3.style.height = size; + +element1.oncontextmenu = () => false; +element2.oncontextmenu = () => false; +element3.oncontextmenu = () => false; + +viewportGrid.appendChild(element1); +viewportGrid.appendChild(element2); +viewportGrid.appendChild(element3); + +content.appendChild(viewportGrid); + +const pr = document.createElement('p'); +pr.innerHTML = 'Nifti Loading Progress:'; + +const progress = document.createElement('progress'); +progress.value = 0; +progress.max = 100; + +content.appendChild(pr); +content.appendChild(progress); + +const viewportId1 = 'CT_NIFTI_AXIAL'; +const viewportId2 = 'CT_NIFTI_SAGITTAL'; +const viewportId3 = 'CT_NIFTI_CORONAL'; + +const viewportIds = [viewportId1, viewportId2, viewportId3]; + +const toolsNames = [ + WindowLevelTool.toolName, + PanTool.toolName, + LengthTool.toolName, + ProbeTool.toolName, + RectangleROITool.toolName, + EllipticalROITool.toolName, + CircleROITool.toolName, + BidirectionalTool.toolName, + AngleTool.toolName, + CobbAngleTool.toolName, + ArrowAnnotateTool.toolName, +]; + +let selectedToolName = toolsNames[0]; +const toolGroupId = 'STACK_TOOL_GROUP_ID'; + +addDropdownToToolbar({ + options: { values: toolsNames, defaultValue: selectedToolName }, + onSelectedValueChange: (newSelectedToolNameAsStringOrNumber) => { + const newSelectedToolName = String(newSelectedToolNameAsStringOrNumber); + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + + // Set the new tool active + toolGroup.setToolActive(newSelectedToolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + + // Set the old tool passive + toolGroup.setToolPassive(selectedToolName); + + selectedToolName = newSelectedToolName; + }, +}); + +async function setup() { + await csInit(); + await csTools3dInit(); + + volumeLoader.registerVolumeLoader('nifti', cornerstoneNiftiImageVolumeLoader); + + const niftiURL = + 'https://ohif-assets.s3.us-east-2.amazonaws.com/nifti/CTACardio.nii.gz'; + const volumeId = 'nifti:' + niftiURL; + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(WindowLevelTool); + cornerstoneTools.addTool(PanTool); + cornerstoneTools.addTool(StackScrollMouseWheelTool); + cornerstoneTools.addTool(LengthTool); + cornerstoneTools.addTool(ZoomTool); + cornerstoneTools.addTool(ProbeTool); + cornerstoneTools.addTool(RectangleROITool); + cornerstoneTools.addTool(EllipticalROITool); + cornerstoneTools.addTool(CircleROITool); + cornerstoneTools.addTool(BidirectionalTool); + cornerstoneTools.addTool(AngleTool); + cornerstoneTools.addTool(CobbAngleTool); + cornerstoneTools.addTool(ArrowAnnotateTool); + + // Define a tool group, which defines how mouse events map to tool commands for + // Any viewport using the group + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Add the tools to the tool group and specify which volume they are pointing at + toolGroup.addTool(WindowLevelTool.toolName); + toolGroup.addTool(PanTool.toolName); + toolGroup.addTool(ZoomTool.toolName); + toolGroup.addTool(RectangleROITool.toolName); + toolGroup.addTool(EllipticalROITool.toolName); + toolGroup.addTool(StackScrollMouseWheelTool.toolName); + toolGroup.addTool(LengthTool.toolName); + toolGroup.addTool(ProbeTool.toolName); + toolGroup.addTool(RectangleROITool.toolName); + toolGroup.addTool(CircleROITool.toolName); + toolGroup.addTool(BidirectionalTool.toolName); + toolGroup.addTool(AngleTool.toolName); + toolGroup.addTool(CobbAngleTool.toolName); + toolGroup.addTool(ArrowAnnotateTool.toolName); + + // Set the initial state of the tools, here we set one tool active on left click. + // This means left click will draw that tool. + toolGroup.setToolActive(WindowLevelTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Primary, // Left Click + }, + ], + }); + + toolGroup.setToolActive(ZoomTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Secondary, // Right Click + }, + ], + }); + toolGroup.setToolActive(PanTool.toolName, { + bindings: [ + { + mouseButton: MouseBindings.Auxiliary, // Right Click + }, + ], + }); + + // As the Stack Scroll mouse wheel is a tool using the `mouseWheelCallback` + // hook instead of mouse buttons, it does not need to assign any mouse button. + toolGroup.setToolActive(StackScrollMouseWheelTool.toolName); + + toolGroup.setToolPassive(ProbeTool.toolName); + toolGroup.setToolPassive(RectangleROITool.toolName); + toolGroup.setToolPassive(EllipticalROITool.toolName); + toolGroup.setToolPassive(CircleROITool.toolName); + toolGroup.setToolPassive(BidirectionalTool.toolName); + toolGroup.setToolPassive(AngleTool.toolName); + toolGroup.setToolPassive(CobbAngleTool.toolName); + toolGroup.setToolPassive(ArrowAnnotateTool.toolName); + + const renderingEngineId = 'myRenderingEngine'; + const renderingEngine = new RenderingEngine(renderingEngineId); + + const viewportInputArray = [ + { + viewportId: viewportId1, + type: Enums.ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + }, + }, + { + viewportId: viewportId2, + type: Enums.ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + }, + }, + { + viewportId: viewportId3, + type: Enums.ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + }, + }, + ]; + + renderingEngine.setViewports(viewportInputArray); + + // Set the tool group on the viewports + viewportIds.forEach((viewportId) => + toolGroup.addViewport(viewportId, renderingEngineId) + ); + + const updateProgress = (evt) => { + const { data } = evt.detail; + + if (!data) return; + + const { total, loaded } = data; + + if (!total) return; + + const progress = Math.round((loaded / total) * 100); + + const element = document.querySelector('progress'); + element.value = progress; + }; + + eventTarget.addEventListener( + NiftiEnums.Events.NIFTI_VOLUME_PROGRESS, + updateProgress + ); + + // This will load the nifti file, no need to call .load again for nifti + await volumeLoader.createAndCacheVolume(volumeId); + + setVolumesForViewports( + renderingEngine, + [{ volumeId, callback: setCtTransferFunctionForVolumeActor }], + viewportInputArray.map((v) => v.viewportId) + ); + + renderingEngine.render(); +} + +setup(); diff --git a/packages/nifti-volume-loader/package.json b/packages/nifti-volume-loader/package.json new file mode 100644 index 000000000..8f79ea844 --- /dev/null +++ b/packages/nifti-volume-loader/package.json @@ -0,0 +1,43 @@ +{ + "name": "@cornerstonejs/nifti-volume-loader", + "version": "1.13.3", + "description": "", + "main": "dist/umd/index.js", + "types": "dist/esm/index.d.ts", + "module": "dist/esm/index.js", + "repository": "https://github.com/cornerstonejs/cornerstone3D", + "files": [ + "dist/" + ], + "directories": { + "test": "test" + }, + "sideEffects": false, + "scripts": { + "build:esm": "tsc --project ./tsconfig.esm.json", + "build:umd": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:all": "yarn run build:umd && yarn run build:esm", + "build": "yarn run build:all && yarn run copy-dts", + "clean": "shx rm -rf dist", + "copy-dts": "copyfiles -u 1 \"src/**/*.d.ts\" dist/esm", + "api-check": "api-extractor --debug run", + "build:update-api": "yarn run build && api-extractor run --local", + "prepublishOnly": "yarn run build", + "webpack:watch": "webpack --mode development --progress --watch --config ./.webpack/webpack.dev.js" + }, + "dependencies": { + "@cornerstonejs/core": "^1.4.1", + "nifti-reader-js": "^0.6.6" + }, + "contributors": [ + { + "name": "Cornerstone.js Contributors", + "url": "https://github.com/orgs/cornerstonejs/people" + } + ], + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://ohif.org/donate" + } +} diff --git a/packages/nifti-volume-loader/src/NiftiImageVolume.ts b/packages/nifti-volume-loader/src/NiftiImageVolume.ts new file mode 100644 index 000000000..da38cc6ca --- /dev/null +++ b/packages/nifti-volume-loader/src/NiftiImageVolume.ts @@ -0,0 +1,81 @@ +import { ImageVolume, cache } from '@cornerstonejs/core'; +import type { Types } from '@cornerstonejs/core'; + +type LoadStatus = { + loaded: boolean; + loading: boolean; + callbacks: Array<(...args: unknown[]) => void>; +}; + +type NiftiImageProperties = { + loadStatus: LoadStatus; + controller: AbortController; +}; + +/** + * NiftiImageVolume Class that extends ImageVolume base class. + * It implements load method to load the data from the Nifti file, and insert them into the volume. + */ +export default class NiftiImageVolume extends ImageVolume { + loadStatus: LoadStatus; + controller: AbortController; + + constructor( + imageVolumeProperties: Types.IVolume, + streamingProperties: NiftiImageProperties + ) { + super(imageVolumeProperties); + + this.loadStatus = streamingProperties.loadStatus; + this.controller = streamingProperties.controller; + } + + /** + * It cancels loading the images of the volume. It sets the loading status to false + * and filters any imageLoad request in the requestPoolManager that has the same + * volumeId + */ + public cancelLoading = () => { + const { loadStatus } = this; + + if (!loadStatus || !loadStatus.loading) { + return; + } + + // Set to not loading. + loadStatus.loading = false; + + // Remove all the callback listeners + this.clearLoadCallbacks(); + + this.controller.abort(); + }; + + /** + * Clear the load callbacks + */ + public clearLoadCallbacks(): void { + this.loadStatus.callbacks = []; + } + + /** + * It triggers a prefetch for images in the volume. + * @param callback - A callback function to be called when the volume is fully loaded + * @param priority - The priority for loading the volume images, lower number is higher priority + * @returns + */ + public load = ( + callback: (...args: unknown[]) => void, + priority = 5 + ): void => { + // This is a noop since we have to do the loading during volume creation, + // as the whole NIFTI comes in one file. + // With a clever backend you could eventually do a range read on the NIFTI + // instead, at which point the load() method would search frame by frame + // for the data, and the actual volume loader would just fetch the header. + }; + + public decache(): void { + cache.removeVolumeLoadObject(this.volumeId); + } +} diff --git a/packages/nifti-volume-loader/src/constants/index.ts b/packages/nifti-volume-loader/src/constants/index.ts new file mode 100644 index 000000000..83cf14086 --- /dev/null +++ b/packages/nifti-volume-loader/src/constants/index.ts @@ -0,0 +1,3 @@ +import NIFTI_LOADER_SCHEME from './niftiLoaderScheme'; + +export { NIFTI_LOADER_SCHEME }; diff --git a/packages/nifti-volume-loader/src/constants/niftiLoaderScheme.ts b/packages/nifti-volume-loader/src/constants/niftiLoaderScheme.ts new file mode 100644 index 000000000..490d35946 --- /dev/null +++ b/packages/nifti-volume-loader/src/constants/niftiLoaderScheme.ts @@ -0,0 +1,3 @@ +const niftiLoaderScheme = 'nifti'; + +export default niftiLoaderScheme; diff --git a/packages/nifti-volume-loader/src/cornerstoneNiftiImageLoader.ts b/packages/nifti-volume-loader/src/cornerstoneNiftiImageLoader.ts new file mode 100644 index 000000000..e1a7326be --- /dev/null +++ b/packages/nifti-volume-loader/src/cornerstoneNiftiImageLoader.ts @@ -0,0 +1,22 @@ +import NiftiImageVolume from './NiftiImageVolume'; +import { fetchAndAllocateNiftiVolume } from './helpers'; + +interface IVolumeLoader { + promise: Promise; + cancel: () => void; +} + +export default function cornerstoneNiftiImageVolumeLoader( + volumeId: string +): IVolumeLoader { + const niftiVolumePromise = fetchAndAllocateNiftiVolume(volumeId); + + return { + promise: niftiVolumePromise, + cancel: () => { + // niftiVolumePromise.then(niftiImageVolume => + // niftiImageVolume.cancelLoading() + // ); + }, + }; +} diff --git a/packages/nifti-volume-loader/src/enums/Events.ts b/packages/nifti-volume-loader/src/enums/Events.ts new file mode 100644 index 000000000..ca1fec606 --- /dev/null +++ b/packages/nifti-volume-loader/src/enums/Events.ts @@ -0,0 +1,16 @@ +/** + * Cornerstone Nifti volume loader events + */ +enum Events { + /** + * Event fired when a Nifti volume is loaded completely + * + */ + NIFTI_VOLUME_LOADED = 'CORNERSTONE_NIFTI_VOLUME_LOADED', + /** + * Event fired when a Nifti volume has some progress in loading + */ + NIFTI_VOLUME_PROGRESS = 'CORNERSTONE_NIFTI_VOLUME_PROGRESS', +} + +export default Events; diff --git a/packages/nifti-volume-loader/src/enums/index.ts b/packages/nifti-volume-loader/src/enums/index.ts new file mode 100644 index 000000000..e43e8a9d5 --- /dev/null +++ b/packages/nifti-volume-loader/src/enums/index.ts @@ -0,0 +1,3 @@ +import Events from './Events'; + +export { Events }; diff --git a/packages/nifti-volume-loader/src/helpers/affineUtilities.ts b/packages/nifti-volume-loader/src/helpers/affineUtilities.ts new file mode 100644 index 000000000..4708ba8a0 --- /dev/null +++ b/packages/nifti-volume-loader/src/helpers/affineUtilities.ts @@ -0,0 +1,79 @@ +import type { Types } from '@cornerstonejs/core'; + +/** + * Generates an affine matrix from the provided origin, orientation and spacing. + * + * @param origin - The origin of the matrix in world coordinates + * @param orientation - The orientation of the matrix. It's a 9-element array representing + * a 3x3 matrix in row-major order. + * @param spacing - image spacing along each axis + * + * @returns The generated affine matrix, a 4x4 array in row-major order. + */ +function generateAffineMatrix( + origin: Types.Point3, + orientation: Types.Mat3, + spacing: Types.Point3 +): Types.AffineMatrix { + const Ox = origin[0]; + const Oy = origin[1]; + const Oz = origin[2]; + + const S1 = spacing[0]; + const S2 = spacing[1]; + const S3 = spacing[2]; + + const D11 = orientation[0]; + const D12 = orientation[1]; + const D13 = orientation[2]; + const D21 = orientation[3]; + const D22 = orientation[4]; + const D23 = orientation[5]; + const D31 = orientation[6]; + const D32 = orientation[7]; + const D33 = orientation[8]; + + return [ + [D11 * S1, D12 * S2, D13 * S3, Ox], + [D21 * S1, D22 * S2, D23 * S3, Oy], + [D31 * S1, D32 * S2, D33 * S3, Oz], + [0, 0, 0, 1], + ]; +} + +/** + * Parses an affine matrix into its origin, orientation, and spacing components. + * + * @param affine - The input 4x4 affine matrix to be parsed, in row-major order. + * + * @returns An object with properties 'origin', 'orientation', and 'spacing'. 'origin' is a 3D point representing the origin of the affine matrix. 'orientation' is a 9-element array representing a 3x3 matrix in row-major order. 'spacing' is a 3-element array representing the spacing of the affine matrix in each dimension. + */ +function parseAffineMatrix(affine): { + origin: Types.Point3; + orientation: Types.Mat3; + spacing: Types.Point3; +} { + const origin = [affine[0][3], affine[1][3], affine[2][3]] as Types.Point3; + + const spacing = [ + Math.sqrt(affine[0][0] ** 2 + affine[1][0] ** 2 + affine[2][0] ** 2), + Math.sqrt(affine[0][1] ** 2 + affine[1][1] ** 2 + affine[2][1] ** 2), + Math.sqrt(affine[0][2] ** 2 + affine[1][2] ** 2 + affine[2][2] ** 2), + ] as Types.Point3; + + const orientation = [ + affine[0][0] / spacing[0], + affine[0][1] / spacing[1], + affine[0][2] / spacing[2], + affine[1][0] / spacing[0], + affine[1][1] / spacing[1], + affine[1][2] / spacing[2], + affine[2][0] / spacing[0], + affine[2][1] / spacing[1], + affine[2][2] / spacing[2], + ] as Types.Mat3; + + return { origin, orientation, spacing }; +} + +export { generateAffineMatrix, parseAffineMatrix }; diff --git a/packages/nifti-volume-loader/src/helpers/convert.ts b/packages/nifti-volume-loader/src/helpers/convert.ts new file mode 100644 index 000000000..a42e79364 --- /dev/null +++ b/packages/nifti-volume-loader/src/helpers/convert.ts @@ -0,0 +1,119 @@ +import type { Types } from '@cornerstonejs/core'; +import { parseAffineMatrix } from './affineUtilities'; + +/** + * This converts scalar data: LPS to RAS and RAS to LPS + */ +const invertDataPerFrame = (dimensions, imageDataArray) => { + let TypedArrayConstructor; + let bytesPerVoxel; + + if ( + imageDataArray instanceof Uint8Array || + imageDataArray instanceof ArrayBuffer || + imageDataArray instanceof SharedArrayBuffer + ) { + TypedArrayConstructor = Uint8Array; + bytesPerVoxel = 1; + } else if (imageDataArray instanceof Int16Array) { + TypedArrayConstructor = Int16Array; + bytesPerVoxel = 2; + } else if (imageDataArray instanceof Float32Array) { + TypedArrayConstructor = Float32Array; + bytesPerVoxel = 4; + } else { + throw new Error( + 'imageDataArray needs to be a Uint8Array, Int16Array or Float32Array.' + ); + } + + // Make a copy of the data first using the browser native fast TypedArray.set(). + const newImageDataArray = new TypedArrayConstructor( + imageDataArray.byteLength + ); + + const view = new TypedArrayConstructor(imageDataArray); + + newImageDataArray.set(view); + + // In order to switch from LP to RA within each slice, we just need to reverse each section. + // We can do this in place using web api which is very fast, by taking views on different parts of a single buffer. + + const numFrames = dimensions[2]; + const frameLength = dimensions[0] * dimensions[1]; + const buffer = newImageDataArray.buffer; + + for (let frame = 0; frame < numFrames; frame++) { + const byteOffset = frameLength * frame * bytesPerVoxel; + // Get view of underlying buffer for this frame. + const frameView = new TypedArrayConstructor( + buffer, + byteOffset, + frameLength + ); + + frameView.reverse(); + } + + return newImageDataArray; +}; + +function rasToLps(niftiHeader) { + const { affine } = niftiHeader; + + // RAS + const { orientation, origin, spacing } = parseAffineMatrix(affine); + + // LPS + const newOrigin = [-origin[0], -origin[1], origin[2]] as Types.Point3; + + // Change row-major to column-major for LPS orientation + const newOrientation = [ + -orientation[0], + -orientation[3], + orientation[6], + -orientation[1], + -orientation[4], + orientation[7], + -orientation[2], + -orientation[5], + orientation[8], + ]; + + return { + origin: newOrigin, + orientation: newOrientation, + spacing, + }; +} + +function lpsToRas(header) { + const { origin, orientation, spacing, dimensions, dataType } = header; + + const newOrigin = [-origin[0], -origin[1], origin[2]]; + const newOrientation = [ + -orientation[0], + -orientation[3], + orientation[6], + -orientation[1], + -orientation[4], + orientation[7], + -orientation[2], + -orientation[5], + orientation[8], + ]; + + return { + orientation: newOrientation, + origin: [newOrigin[0], newOrigin[1], newOrigin[2]], + dataType, + dimensions, + spacing, + slope: header.slope, + inter: header.inter, + max: header.max, + min: header.min, + }; +} + +export { lpsToRas, rasToLps, invertDataPerFrame }; diff --git a/packages/nifti-volume-loader/src/helpers/fetchAndAllocateNiftiVolume.ts b/packages/nifti-volume-loader/src/helpers/fetchAndAllocateNiftiVolume.ts new file mode 100644 index 000000000..10d12a8ef --- /dev/null +++ b/packages/nifti-volume-loader/src/helpers/fetchAndAllocateNiftiVolume.ts @@ -0,0 +1,266 @@ +import * as NiftiReader from 'nifti-reader-js'; +import { + cache, + utilities, + Enums, + eventTarget, + triggerEvent, +} from '@cornerstonejs/core'; +import type { Types } from '@cornerstonejs/core'; +import { vec3 } from 'gl-matrix'; +import makeVolumeMetadata from './makeVolumeMetadata'; +import NiftiImageVolume from '../NiftiImageVolume'; +import * as NIFTICONSTANTS from './niftiConstants'; +import { invertDataPerFrame, rasToLps } from './convert'; +import modalityScaleNifti from './modalityScaleNifti'; +import Events from '../enums/Events'; +import { NIFTI_LOADER_SCHEME } from '../constants'; + +const { createUint8SharedArray, createFloat32SharedArray } = utilities; + +export const urlsMap = new Map(); + +function fetchArrayBuffer(url, onProgress, signal, onload) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + + const onLoadHandler = function (e) { + if (onload && typeof onload === 'function') { + onload(); + } + + // Remove event listener for 'abort' + if (signal) { + signal.removeEventListener('abort', onAbortHandler); + } + + resolve(xhr.response); + }; + + const onAbortHandler = () => { + xhr.abort(); + + // Remove event listener for 'load' + xhr.removeEventListener('load', onLoadHandler); + + reject(new Error('Request aborted')); + }; + + xhr.addEventListener('load', onLoadHandler); + + if (onProgress && typeof onProgress === 'function') { + xhr.onprogress = function (e) { + onProgress(e.loaded, e.total); + }; + } + + if (signal && signal.aborted) { + xhr.abort(); + reject(new Error('Request aborted')); + } else if (signal) { + signal.addEventListener('abort', onAbortHandler); + } + + xhr.send(); + }); +} + +export const getTypedNiftiArray = (datatypeCode, niftiImageBuffer) => { + switch (datatypeCode) { + case NIFTICONSTANTS.NIFTI_TYPE_UINT8: + return new Uint8Array(niftiImageBuffer); + case NIFTICONSTANTS.NIFTI_TYPE_FLOAT32: + return new Float32Array(niftiImageBuffer); + case NIFTICONSTANTS.NIFTI_TYPE_INT16: + return new Int16Array(niftiImageBuffer); + default: + throw new Error(`datatypeCode ${datatypeCode} is not yet supported`); + } +}; + +export default async function fetchAndAllocateNiftiVolume( + volumeId: string +): Promise { + // nifti volumeIds start with 'nifti:' so we need to remove that + const niftiURL = volumeId.substring(NIFTI_LOADER_SCHEME.length + 1); + + const progress = (loaded, total) => { + const data = { volumeId, loaded, total }; + triggerEvent(eventTarget, Events.NIFTI_VOLUME_PROGRESS, { + data, + }); + }; + + const onLoad = () => { + const data = { volumeId }; + triggerEvent(eventTarget, Events.NIFTI_VOLUME_LOADED, { + data, + }); + }; + + const controller = new AbortController(); + const signal = controller.signal; + + urlsMap.set(niftiURL, { controller, loading: true }); + + let niftiBuffer = (await fetchArrayBuffer( + niftiURL, + progress, + signal, + onLoad + )) as ArrayBuffer; + + urlsMap.delete(niftiURL); + + let niftiHeader = null; + let niftiImage = null; + + if (NiftiReader.isCompressed(niftiBuffer)) { + niftiBuffer = NiftiReader.decompress(niftiBuffer); + } + + if (NiftiReader.isNIFTI(niftiBuffer)) { + niftiHeader = NiftiReader.readHeader(niftiBuffer); + niftiImage = NiftiReader.readImage(niftiHeader, niftiBuffer); + } + + const typedNiftiArray = getTypedNiftiArray( + niftiHeader.datatypeCode, + niftiImage + ); + + // Convert to LPS for display in OHIF. + + const { orientation, origin, spacing } = rasToLps(niftiHeader); + invertDataPerFrame(niftiHeader.dims.slice(1, 4), typedNiftiArray); + + modalityScaleNifti(typedNiftiArray, niftiHeader); + + const volumeMetadata = makeVolumeMetadata( + niftiHeader, + orientation, + typedNiftiArray + ); + + const scanAxisNormal = vec3.create(); + vec3.set(scanAxisNormal, orientation[6], orientation[7], orientation[8]); + + const { + BitsAllocated, + PixelRepresentation, + PhotometricInterpretation, + ImageOrientationPatient, + Columns, + Rows, + } = volumeMetadata; + + const rowCosineVec = vec3.fromValues( + ImageOrientationPatient[0], + ImageOrientationPatient[1], + ImageOrientationPatient[2] + ); + const colCosineVec = vec3.fromValues( + ImageOrientationPatient[3], + ImageOrientationPatient[4], + ImageOrientationPatient[5] + ); + + const { dims } = niftiHeader; + + const numFrames = dims[3]; + + // Spacing goes [1] then [0], as [1] is column spacing (x) and [0] is row spacing (y) + const dimensions = [Columns, Rows, numFrames]; + const direction = new Float32Array([ + rowCosineVec[0], + rowCosineVec[1], + rowCosineVec[2], + colCosineVec[0], + colCosineVec[1], + colCosineVec[2], + scanAxisNormal[0], + scanAxisNormal[1], + scanAxisNormal[2], + ]) as Types.Mat3; + const signed = PixelRepresentation === 1; + + // Check if it fits in the cache before we allocate data + // TODO Improve this when we have support for more types + // NOTE: We use 4 bytes per voxel as we are using Float32. + let bytesPerVoxel = 1; + if (BitsAllocated === 16 || BitsAllocated === 32) { + bytesPerVoxel = 4; + } + const sizeInBytesPerComponent = + bytesPerVoxel * dimensions[0] * dimensions[1] * dimensions[2]; + + let numComponents = 1; + if (PhotometricInterpretation === 'RGB') { + numComponents = 3; + } + + const sizeInBytes = sizeInBytesPerComponent * numComponents; + + // check if there is enough space in unallocated + image Cache + const isCacheable = cache.isCacheable(sizeInBytes); + if (!isCacheable) { + throw new Error(Enums.Events.CACHE_SIZE_EXCEEDED); + } + + cache.decacheIfNecessaryUntilBytesAvailable(sizeInBytes); + + let scalarData; + + switch (BitsAllocated) { + case 8: + if (signed) { + throw new Error( + '8 Bit signed images are not yet supported by this plugin.' + ); + } else { + scalarData = createUint8SharedArray( + dimensions[0] * dimensions[1] * dimensions[2] + ); + } + + break; + + case 16: + case 32: + scalarData = createFloat32SharedArray( + dimensions[0] * dimensions[1] * dimensions[2] + ); + + break; + } + + // Set the scalar data from the nifti typed view into the SAB + scalarData.set(typedNiftiArray); + + const niftiImageVolume = new NiftiImageVolume( + // ImageVolume properties + { + volumeId, + metadata: volumeMetadata, + dimensions, + spacing, + origin, + direction, + scalarData, + sizeInBytes, + }, + // Streaming properties + { + loadStatus: { + loaded: false, + loading: false, + callbacks: [], + }, + controller, + } + ); + + return niftiImageVolume; +} diff --git a/packages/nifti-volume-loader/src/helpers/index.ts b/packages/nifti-volume-loader/src/helpers/index.ts new file mode 100644 index 000000000..13f6c944e --- /dev/null +++ b/packages/nifti-volume-loader/src/helpers/index.ts @@ -0,0 +1,5 @@ +import makeVolumeMetadata from './makeVolumeMetadata'; +import modalityScaleNifti from './modalityScaleNifti'; +import fetchAndAllocateNiftiVolume from './fetchAndAllocateNiftiVolume'; + +export { modalityScaleNifti, makeVolumeMetadata, fetchAndAllocateNiftiVolume }; diff --git a/packages/nifti-volume-loader/src/helpers/makeVolumeMetadata.ts b/packages/nifti-volume-loader/src/helpers/makeVolumeMetadata.ts new file mode 100644 index 000000000..8abde4449 --- /dev/null +++ b/packages/nifti-volume-loader/src/helpers/makeVolumeMetadata.ts @@ -0,0 +1,75 @@ +import { Types } from '@cornerstonejs/core'; +import { vec3 } from 'gl-matrix'; + +// everything here is LPS +export default function makeVolumeMetadata( + niftiHeader, + orientation, + scalarData +): Types.Metadata { + const { numBitsPerVoxel, littleEndian, pixDims, dims } = niftiHeader; + + const rowCosines = vec3.create(); + const columnCosines = vec3.create(); + + vec3.set(rowCosines, orientation[0], orientation[1], orientation[2]); + vec3.set(columnCosines, orientation[3], orientation[4], orientation[5]); + + let min = Infinity; + let max = -Infinity; + + const xDim = dims[1]; + const yDim = dims[2]; + const zDim = dims[3]; + + const frameLength = xDim * yDim; + + const middleFrameIndex = Math.floor(zDim / 2); + + const offset = frameLength * middleFrameIndex; + + for ( + let voxelIndex = offset; + voxelIndex < offset + frameLength; + voxelIndex++ + ) { + const voxelValue = scalarData[voxelIndex]; + + if (voxelValue > max) { + max = voxelValue; + } + if (voxelValue < min) { + min = voxelValue; + } + } + + const windowCenter = (max + min) / 2; + const windowWidth = max - min; + + return { + BitsAllocated: numBitsPerVoxel, + BitsStored: numBitsPerVoxel, + SamplesPerPixel: 1, + HighBit: littleEndian ? numBitsPerVoxel - 1 : 1, + PhotometricInterpretation: 'MONOCHROME2', + PixelRepresentation: 1, + ImageOrientationPatient: [ + rowCosines[0], + rowCosines[1], + rowCosines[2], + columnCosines[0], + columnCosines[1], + columnCosines[2], + ], + PixelSpacing: [pixDims[1], pixDims[2]], + Columns: xDim, + Rows: yDim, + // This is a reshaped object and not a dicom tag: + voiLut: [{ windowCenter, windowWidth }], + // Todo: we should grab this from somewhere but it doesn't really + // prevent us from rendering the volume so we'll just hardcode it for now. + FrameOfReferenceUID: '1.2.3', + Modality: 'MR', + VOILUTFunction: 'LINEAR', + }; +} diff --git a/packages/nifti-volume-loader/src/helpers/modalityScaleNifti.ts b/packages/nifti-volume-loader/src/helpers/modalityScaleNifti.ts new file mode 100644 index 000000000..46383d1b6 --- /dev/null +++ b/packages/nifti-volume-loader/src/helpers/modalityScaleNifti.ts @@ -0,0 +1,25 @@ +/** + * Given a pixel array, rescale the pixel values using the rescale slope and + * intercept + * + * Todo: add the scaling of PT and SUV + * @param array - The array to be scaled. + * @param scalingParameters - The scaling parameters + * @returns The array being scaled + */ +export default function modalityScaleNifti( + array: Float32Array | Int16Array | Uint8Array, + niftiHeader +): void { + const arrayLength = array.length; + const { scl_slope, scl_inter } = niftiHeader; + + if (!scl_slope || scl_slope === 0 || Number.isNaN(scl_slope)) { + // No scaling encoded in NIFTI header + return; + } + + for (let i = 0; i < arrayLength; i++) { + array[i] = array[i] * scl_slope + scl_inter; + } +} diff --git a/packages/nifti-volume-loader/src/helpers/niftiConstants.ts b/packages/nifti-volume-loader/src/helpers/niftiConstants.ts new file mode 100644 index 000000000..648a336ea --- /dev/null +++ b/packages/nifti-volume-loader/src/helpers/niftiConstants.ts @@ -0,0 +1,118 @@ +/* Nifti version 2 types. */ +/*! unsigned char. */ +const NIFTI_TYPE_UINT8 = 2; +/*! signed short. */ +const NIFTI_TYPE_INT16 = 4; +/*! signed int. */ +const NIFTI_TYPE_INT32 = 8; +/*! 32 bit float. */ +const NIFTI_TYPE_FLOAT32 = 16; +/*! 64 bit complex = 2 32 bit floats. */ +const NIFTI_TYPE_COMPLEX64 = 32; +/*! 64 bit float = double. */ +const NIFTI_TYPE_FLOAT64 = 64; +/*! 3 8 bit bytes. */ +const NIFTI_TYPE_RGB24 = 128; +/*! signed char. */ +const NIFTI_TYPE_INT8 = 256; +/*! unsigned short. */ +const NIFTI_TYPE_UINT16 = 512; +/*! unsigned int. */ +const NIFTI_TYPE_UINT32 = 768; +/*! signed long long. */ +const NIFTI_TYPE_INT64 = 1024; +/*! unsigned long long. */ +const NIFTI_TYPE_UINT64 = 1280; +/*! 128 bit float = long double. */ +const NIFTI_TYPE_FLOAT128 = 1536; +/*! 128 bit complex = 2 64 bit floats. */ +const NIFTI_TYPE_COMPLEX128 = 1792; +/*! 256 bit complex = 2 128 bit floats */ +const NIFTI_TYPE_COMPLEX256 = 2048; +/*! 4 8 bit bytes. */ +const NIFTI_TYPE_RGBA32 = 2304; + +/*! Nifti version 2 header length. */ +const NIFTI2_HEADER_TOTAL_LENGTH_BYTES = 540; +const NIFTI2_HEADER_TOTAL_LENGTH_BITS = 4320; + +const NIFTI1_HEADER_TOTAL_LENGTH_BYTES = 348; +const NIFTI1_HEADER_TOTAL_LENGTH_BITS = 2784; + +/*! Nifti version 2 variables length. */ +const NIFTI2_HEADER_MAGIC_LENGTH_BYTES = 8; +const NIFTI2_HEADER_DESCRIPTION_LENGTH_BYTES = 80; +const NIFTI2_HEADER_AUX_FILE_LENGTH_BYTES = 24; +const NIFTI2_HEADER_INTENT_NAME_LENGTH_BYTES = 16; +const NIFTI2_HEADER_DIM_INFO_LENGTH_BYTES = 1; +const NIFTI2_HEADER_ENDOFFILE_LENGTH_BYTES = 15; +const NIFTI2_VOX_OFFSET_BYTES = 544; + +/*! Nifti version 2 end of file string. */ +const NIFTI2_HEADER_MAGIC_STRING = 'n+2\0'; +const NIFTI2_HEADER_ENDOFFILE_STRING = '000000000000000'; + +/*! Nifti version 2 options variables. */ +const NIFTI2_ALIGNED_ANAT = 2; +const NIFTI2_XYZT_UNITS_UNKNOWN = 0; + +const NIFTI1_HEADER_MAGIC_LENGTH_BYTES = 4; +const NIFTI1_HEADER_DESCRIPTION_LENGTH_BYTES = 80; +const NIFTI1_HEADER_AUX_FILE_LENGTH_BYTES = 24; +const NIFTI1_HEADER_INTENT_NAME_LENGTH_BYTES = 16; +const NIFTI1_HEADER_DIM_INFO_LENGTH_BYTES = 1; +const NIFTI1_HEADER_ENDOFFILE_LENGTH_BYTES = 4; +const NIFTI1_VOX_OFFSET_BYTES = 352; + +/*! Nifti version 1 end of file string. */ +const NIFTI1_HEADER_MAGIC_STRING = 'n+1\0'; +const NIFTI1_HEADER_ENDOFFILE_STRING = '0000'; + +/*! Nifti version 1 options variables. */ +const NIFTI1_ALIGNED_ANAT = 2; +const NIFTI1_XYZT_UNITS_UNKNOWN = 0; + +export { + NIFTI_TYPE_UINT8, + NIFTI_TYPE_INT16, + NIFTI_TYPE_INT32, + NIFTI_TYPE_FLOAT32, + NIFTI_TYPE_COMPLEX64, + NIFTI_TYPE_FLOAT64, + NIFTI_TYPE_RGB24, + NIFTI_TYPE_INT8, + NIFTI_TYPE_UINT16, + NIFTI_TYPE_UINT32, + NIFTI_TYPE_INT64, + NIFTI_TYPE_UINT64, + NIFTI_TYPE_FLOAT128, + NIFTI_TYPE_COMPLEX128, + NIFTI_TYPE_COMPLEX256, + NIFTI_TYPE_RGBA32, + NIFTI2_HEADER_TOTAL_LENGTH_BYTES, + NIFTI2_HEADER_TOTAL_LENGTH_BITS, + NIFTI2_HEADER_MAGIC_LENGTH_BYTES, + NIFTI2_HEADER_DESCRIPTION_LENGTH_BYTES, + NIFTI2_HEADER_AUX_FILE_LENGTH_BYTES, + NIFTI2_HEADER_INTENT_NAME_LENGTH_BYTES, + NIFTI2_HEADER_DIM_INFO_LENGTH_BYTES, + NIFTI2_HEADER_ENDOFFILE_LENGTH_BYTES, + NIFTI2_VOX_OFFSET_BYTES, + NIFTI2_HEADER_MAGIC_STRING, + NIFTI2_HEADER_ENDOFFILE_STRING, + NIFTI2_ALIGNED_ANAT, + NIFTI2_XYZT_UNITS_UNKNOWN, + NIFTI1_HEADER_TOTAL_LENGTH_BYTES, + NIFTI1_HEADER_TOTAL_LENGTH_BITS, + NIFTI1_HEADER_MAGIC_LENGTH_BYTES, + NIFTI1_HEADER_DESCRIPTION_LENGTH_BYTES, + NIFTI1_HEADER_AUX_FILE_LENGTH_BYTES, + NIFTI1_HEADER_INTENT_NAME_LENGTH_BYTES, + NIFTI1_HEADER_DIM_INFO_LENGTH_BYTES, + NIFTI1_HEADER_ENDOFFILE_LENGTH_BYTES, + NIFTI1_VOX_OFFSET_BYTES, + NIFTI1_HEADER_MAGIC_STRING, + NIFTI1_HEADER_ENDOFFILE_STRING, + NIFTI1_ALIGNED_ANAT, + NIFTI1_XYZT_UNITS_UNKNOWN, +}; diff --git a/packages/nifti-volume-loader/src/index.ts b/packages/nifti-volume-loader/src/index.ts new file mode 100644 index 000000000..2209ef5e6 --- /dev/null +++ b/packages/nifti-volume-loader/src/index.ts @@ -0,0 +1,6 @@ +import cornerstoneNiftiImageVolumeLoader from './cornerstoneNiftiImageLoader'; +import NiftiImageVolume from './NiftiImageVolume'; +import * as helpers from './helpers'; +import * as Enums from './enums'; + +export { cornerstoneNiftiImageVolumeLoader, NiftiImageVolume, helpers, Enums }; diff --git a/packages/nifti-volume-loader/tsconfig.cjs.json b/packages/nifti-volume-loader/tsconfig.cjs.json new file mode 100644 index 000000000..aebe09c10 --- /dev/null +++ b/packages/nifti-volume-loader/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.cjs.json", + "compilerOptions": { + "outDir": "./dist/cjs" + }, + "include": ["src"] +} diff --git a/packages/nifti-volume-loader/tsconfig.esm.json b/packages/nifti-volume-loader/tsconfig.esm.json new file mode 100644 index 000000000..cc9297119 --- /dev/null +++ b/packages/nifti-volume-loader/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.esm.json", + "compilerOptions": { + "outDir": "./dist/esm" + }, + "include": ["src"] +} diff --git a/packages/nifti-volume-loader/tsconfig.json b/packages/nifti-volume-loader/tsconfig.json new file mode 100644 index 000000000..b29a7b46c --- /dev/null +++ b/packages/nifti-volume-loader/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {} +} diff --git a/packages/tools/src/tools/segmentation/PaintFillTool.ts b/packages/tools/src/tools/segmentation/PaintFillTool.ts index 5a7c834b2..1bea4c898 100644 --- a/packages/tools/src/tools/segmentation/PaintFillTool.ts +++ b/packages/tools/src/tools/segmentation/PaintFillTool.ts @@ -90,7 +90,10 @@ class PaintFillTool extends BaseTool { const index = transformWorldToIndex(segmentation.imageData, worldPos); - const fixedDimension = this.getFixedDimension(viewPlaneNormal, direction); + const fixedDimension = this.getFixedDimension( + viewPlaneNormal, + direction as number[] + ); if (fixedDimension === undefined) { console.warn('Oblique paint fill not yet supported'); diff --git a/tsconfig.json b/tsconfig.json index e5564b46c..fb6fef7a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "@cornerstonejs/core": ["core/src"], "@cornerstonejs/tools": ["tools/src"], "@cornerstonejs/dicomImageLoader": ["dicomImageLoader/src"], + "@cornerstonejs/nifti-volume-loader": ["nifti-volume-loader/src"], } } } diff --git a/utils/ExampleRunner/build-all-examples-cli.js b/utils/ExampleRunner/build-all-examples-cli.js index a35b48fa7..dce3c8b2e 100644 --- a/utils/ExampleRunner/build-all-examples-cli.js +++ b/utils/ExampleRunner/build-all-examples-cli.js @@ -63,6 +63,10 @@ if (options.fromRoot === true) { path: 'packages/dicomImageLoader/examples', regexp: 'index.ts', }, + { + path: 'packages/nifti-volume-loader/examples', + regexp: 'index.ts', + }, { path: 'packages/adapters/examples', regexp: 'index.ts', diff --git a/utils/ExampleRunner/example-runner-cli.js b/utils/ExampleRunner/example-runner-cli.js index e979d8475..7af2e774c 100755 --- a/utils/ExampleRunner/example-runner-cli.js +++ b/utils/ExampleRunner/example-runner-cli.js @@ -77,6 +77,10 @@ const configuration = { path: 'packages/dicomImageLoader/examples', regexp: 'index.ts', }, + { + path: 'packages/nifti-volume-loader/examples', + regexp: 'index.ts', + }, { path: 'packages/adapters/examples', regexp: 'index.ts', diff --git a/utils/ExampleRunner/template-config.js b/utils/ExampleRunner/template-config.js index c4baf9d01..a2af4facd 100644 --- a/utils/ExampleRunner/template-config.js +++ b/utils/ExampleRunner/template-config.js @@ -9,6 +9,7 @@ const csStreamingBasePath = path.resolve( const csDICOMImageLoaderDistPath = path.resolve( 'packages/dicomImageLoader/dist/dynamic-import/cornerstoneDICOMImageLoader.min.js' ); +const csNiftiPath = path.resolve('packages/nifti-volume-loader/src/index'); module.exports = function buildConfig( name, @@ -69,6 +70,10 @@ module.exports = { alias: { '@cornerstonejs/core': '${csRenderBasePath.replace(/\\/g, '/')}', '@cornerstonejs/tools': '${csToolsBasePath.replace(/\\/g, '/')}', + '@cornerstonejs/nifti-volume-loader': '${csNiftiPath.replace( + /\\/g, + '/' + )}', '@cornerstonejs/adapters': '${csAdapters.replace(/\\/g, '/')}', '@cornerstonejs/streaming-image-volume-loader': '${csStreamingBasePath.replace( /\\/g, diff --git a/yarn.lock b/yarn.lock index 1afac139a..f74fe6367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2950,21 +2950,21 @@ resolved "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== -"@microsoft/api-extractor-model@7.15.4": - version "7.15.4" - resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.15.4.tgz#59fc1e00530b7a604c719221a59dc265c76e9bc5" - integrity sha512-9bIXQKKQr5jAH1c9atXrukr4ua30fhqxMwWIOOjEnexPBPu3nhy9lC4/GpE0kPUp1Al3wSXgFnOEGzEH+HFz+w== +"@microsoft/api-extractor-model@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.16.0.tgz#3db7360897115f26a857f1f684fb5af82b0ef9f6" + integrity sha512-0FOrbNIny8mzBrzQnSIkEjAXk0JMSnPmWYxt3ZDTPVg9S8xIPzB6lfgTg9+Mimu0RKCpGKBpd+v2WcR5vGzyUQ== dependencies: "@microsoft/tsdoc" "0.13.2" "@microsoft/tsdoc-config" "~0.15.2" "@rushstack/node-core-library" "3.45.1" -"@microsoft/api-extractor@7.19.5": - version "7.19.5" - resolved "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.19.5.tgz#85b2404135a4158864d448fde8991ef7dc39b6a7" - integrity sha512-ra5r8P7PocOpemrZRccI3Tf1+wukI0gT6ncRB448QSxSYlmqKuvez95YUSYPwHIN/ztKL0cn5PfMOauU1lZfGQ== +"@microsoft/api-extractor@7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.20.1.tgz#e7ccf6f7618eef792a1936674183023109183d0d" + integrity sha512-T7cqcK+JpvHGOj7cD2ZCCWS7Xgru1uOqZwrV/FSUdyKVs5fopZcbBSuetwD/akst3O7Ypryg3UOLP54S/vnVmA== dependencies: - "@microsoft/api-extractor-model" "7.15.4" + "@microsoft/api-extractor-model" "7.16.0" "@microsoft/tsdoc" "0.13.2" "@microsoft/tsdoc-config" "~0.15.2" "@rushstack/node-core-library" "3.45.1" @@ -2979,7 +2979,7 @@ "@microsoft/tsdoc-config@0.16.2": version "0.16.2" - resolved "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" integrity sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw== dependencies: "@microsoft/tsdoc" "0.14.2" @@ -2989,7 +2989,7 @@ "@microsoft/tsdoc-config@~0.15.2": version "0.15.2" - resolved "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz#eb353c93f3b62ab74bdc9ab6f4a82bcf80140f14" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz#eb353c93f3b62ab74bdc9ab6f4a82bcf80140f14" integrity sha512-mK19b2wJHSdNf8znXSMYVShAHktVr/ib0Ck2FA3lsVBSEhSI/TfXT7DJQkAYgcztTuwazGcg58ZjYdk0hTCVrA== dependencies: "@microsoft/tsdoc" "0.13.2" @@ -2999,7 +2999,7 @@ "@microsoft/tsdoc@0.13.2": version "0.13.2" - resolved "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz#3b0efb6d3903bd49edb073696f60e90df08efb26" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz#3b0efb6d3903bd49edb073696f60e90df08efb26" integrity sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg== "@microsoft/tsdoc@0.14.2": @@ -4061,7 +4061,7 @@ "@rushstack/node-core-library@3.45.1": version "3.45.1" - resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.1.tgz#787361b61a48d616eb4b059641721a3dc138f001" + resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.45.1.tgz#787361b61a48d616eb4b059641721a3dc138f001" integrity sha512-BwdssTNe007DNjDBxJgInHg8ePytIPyT0La7ZZSQZF9+rSkT42AygXPGvbGsyFfEntjr4X37zZSJI7yGzL16cQ== dependencies: "@types/node" "12.20.24" @@ -4076,7 +4076,7 @@ "@rushstack/rig-package@0.3.8": version "0.3.8" - resolved "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.8.tgz#0e8b2fbc7a35d96f6ccf34e773f7c1adb1524296" + resolved "https://registry.yarnpkg.com/@rushstack/rig-package/-/rig-package-0.3.8.tgz#0e8b2fbc7a35d96f6ccf34e773f7c1adb1524296" integrity sha512-MDWg1xovea99PWloSiYMjFcCLsrdjFtYt6aOyHNs5ojn5mxrzR6U9F83hvbQjTWnKPMvZtr0vcek+4n+OQOp3Q== dependencies: resolve "~1.17.0" @@ -4084,7 +4084,7 @@ "@rushstack/ts-command-line@4.10.7": version "4.10.7" - resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.7.tgz#21e3757a756cbd4f7eeab8f89ec028a64d980efc" + resolved "https://registry.yarnpkg.com/@rushstack/ts-command-line/-/ts-command-line-4.10.7.tgz#21e3757a756cbd4f7eeab8f89ec028a64d980efc" integrity sha512-CjS+DfNXUSO5Ab2wD1GBGtUTnB02OglRWGqfaTcac9Jn45V5MeUOsq/wA8wEeS5Y/3TZ2P1k+IWdVDiuOFP9Og== dependencies: "@types/argparse" "1.0.38" @@ -4675,7 +4675,7 @@ "@types/node@12.20.24": version "12.20.24" - resolved "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz#c37ac69cb2948afb4cef95f424fa0037971a9a5c" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.24.tgz#c37ac69cb2948afb4cef95f424fa0037971a9a5c" integrity sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ== "@types/node@^17.0.5": @@ -10017,6 +10017,11 @@ fetch-node-website@^7.3.0: got "^12.3.1" is-plain-obj "^4.1.0" +fflate@*: + version "0.8.0" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.0.tgz#f93ad1dcbe695a25ae378cf2386624969a7cda32" + integrity sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg== + fflate@0.7.3: version "0.7.3" resolved "https://registry.npmjs.org/fflate/-/fflate-0.7.3.tgz#288b034ff0e9c380eaa2feff48c787b8371b7fa5" @@ -15306,6 +15311,13 @@ netlify@^13.1.5: p-wait-for "^4.0.0" qs "^6.9.6" +nifti-reader-js@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/nifti-reader-js/-/nifti-reader-js-0.6.6.tgz#c1c66f4b89bcd0da2b5d2ed8150ce7391e4d0b27" + integrity sha512-MsqoX2MxZ7wkRPgLMNNRY3IrbMijfTtnirfhe7laR/YuqSOJytUV4BbKCmynKylbLz5JNjdLi86NRvrOEp0zuQ== + dependencies: + fflate "*" + nise@^5.1.4: version "5.1.4" resolved "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" @@ -18778,7 +18790,7 @@ resolve@^2.0.0-next.1: resolve@~1.17.0: version "1.17.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" @@ -19126,7 +19138,7 @@ semver@7.3.4: semver@7.3.8, semver@~7.3.0: version "7.3.8" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" @@ -20528,7 +20540,7 @@ timed-out@^4.0.1: timsort@~0.3.0: version "0.3.0" - resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A== tiny-invariant@^1.0.2: @@ -20943,7 +20955,7 @@ typedoc@^0.24.6: typescript@4.6.4: version "4.6.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== "typescript@^3 || ^4": @@ -20953,12 +20965,12 @@ typescript@4.6.4: typescript@^5.0.0, typescript@^5.0.4: version "5.0.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== typescript@~4.5.2: version "4.5.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== ua-parser-js@^0.7.30: