From e33fedbde81f8806bd92b54831542dafbd1be08e Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 14 Mar 2024 14:04:07 -0400 Subject: [PATCH 01/16] feat: dynamic threshold sphere * Add dynamic sphere * fix: Performance issues on threshold sphere * performance * feat: Create segmentation voxel manager in initial create of volume * Faster fill for fixing features * performance: Fix the performance of island removal * fix restore of mixed islands * fix external island removal on non-acquisition * Performance and planar filling improvements * Fix flood fill not being planar * Remove invalid points in shape * feat: Add a labelmap statistics calculator (#2) * fix: Change to names for statistics so that other implementations can be added * Fix build issues * feat: Add working statistics calcs * fix: Add stats to the labelmap calculator * Compute labelmap statistics and lesion glycolysis * PR comments * PR comments * PR - comments added * PR review comments * PR fixes - mostly cleanup * Improvements to threshold out of plane * Missed a fix on the sphere change --- common/reviews/api/core.api.md | 95 ++- common/reviews/api/tools.api.md | 39 +- .../multiVolumeCanvasToWorld/index.ts | 2 +- .../vtkStreamingOpenGLVolumeMapper.js | 2 +- .../core/src/cache/classes/ImageVolume.ts | 11 +- packages/core/src/enums/VoxelManagerEnum.ts | 29 + packages/core/src/enums/index.ts | 2 + packages/core/src/loaders/volumeLoader.ts | 33 +- packages/core/src/types/ImageVolumeProps.ts | 6 +- .../core/src/types/PixelDataTypedArray.ts | 4 +- packages/core/src/utilities/PointsManager.ts | 8 + packages/core/src/utilities/RLEVoxelMap.ts | 296 +++++++++- packages/core/src/utilities/VoxelManager.ts | 122 +++- packages/core/src/utilities/index.ts | 2 + .../index.ts | 315 +++++----- .../examples/labelmapStatistics/index.ts | 549 ++++++++++++++++++ packages/tools/src/enums/StrategyCallbacks.ts | 3 + .../segmentation/triggerSegmentationEvents.ts | 8 +- .../src/tools/annotation/CircleROITool.ts | 4 +- .../src/tools/annotation/EllipticalROITool.ts | 4 +- .../tools/annotation/PlanarFreehandROITool.ts | 7 +- .../src/tools/annotation/RectangleROITool.ts | 5 +- packages/tools/src/tools/base/BaseTool.ts | 8 +- .../tools/src/tools/segmentation/BrushTool.ts | 48 +- .../CircleROIStartEndThresholdTool.ts | 10 +- .../src/tools/segmentation/PaintFillTool.ts | 3 +- .../RectangleROIStartEndThresholdTool.ts | 8 +- .../segmentation/strategies/BrushStrategy.ts | 18 +- .../compositions/dynamicThreshold.ts | 24 +- .../strategies/compositions/index.ts | 2 + .../strategies/compositions/islandRemoval.ts | 394 ++++++++----- .../compositions/labelmapStatistics.ts | 51 ++ .../strategies/compositions/preview.ts | 25 +- .../strategies/compositions/regionFill.ts | 4 +- .../strategies/compositions/setValue.ts | 2 +- .../segmentation/strategies/fillCircle.ts | 6 +- .../segmentation/strategies/fillSphere.ts | 19 +- .../utils/normalizeViewportPlane.ts | 59 ++ packages/tools/src/types/CalculatorTypes.ts | 5 + packages/tools/src/types/EventTypes.ts | 5 + packages/tools/src/types/FloodFillTypes.ts | 4 +- .../math/basic/BasicStatsCalculator.ts | 42 +- .../src/utilities/pointInShapeCallback.ts | 165 +++--- .../segmentation/VolumetricCalculator.ts | 31 + .../src/utilities/segmentation/floodFill.ts | 27 +- .../tools/src/utilities/segmentation/index.ts | 2 + utils/ExampleRunner/example-info.json | 4 + 47 files changed, 2014 insertions(+), 498 deletions(-) create mode 100644 packages/core/src/enums/VoxelManagerEnum.ts create mode 100644 packages/tools/examples/labelmapStatistics/index.ts create mode 100644 packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts create mode 100644 packages/tools/src/tools/segmentation/strategies/utils/normalizeViewportPlane.ts create mode 100644 packages/tools/src/utilities/segmentation/VolumetricCalculator.ts diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 2b3b065f11..50a87617c5 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -767,7 +767,8 @@ declare namespace Enums { ViewportStatus, VideoEnums, MetadataModules, - ImageQualityStatus + ImageQualityStatus, + VoxelManagerEnum } } export { Enums } @@ -1869,6 +1870,8 @@ export class ImageVolume implements IImageVolume { // (undocumented) readonly volumeId: string; // (undocumented) + voxelManager?: VoxelManager | VoxelManager; + // (undocumented) vtkOpenGLTexture: any; } @@ -1896,6 +1899,8 @@ interface ImageVolumeProps extends VolumeProps { imageIds: Array; // (undocumented) referencedImageIds?: Array; + // (undocumented) + voxelManager?: VoxelManager | VoxelManager; } // @public (undocumented) @@ -2514,7 +2519,7 @@ function performCacheOptimizationForVolume(volume: any): void; type PixelDataTypedArray = Float32Array | Int16Array | Uint16Array | Uint8Array | Int8Array | Uint8ClampedArray; // @public (undocumented) -type PixelDataTypedArrayString = 'Float32Array' | 'Int16Array' | 'Uint16Array' | 'Uint8Array' | 'Int8Array' | 'Uint8ClampedArray'; +type PixelDataTypedArrayString = 'Float32Array' | 'Int16Array' | 'Uint16Array' | 'Uint8Array' | 'Int8Array' | 'Uint8ClampedArray' | 'none'; declare namespace planar { export { @@ -2571,6 +2576,8 @@ class PointsManager { // (undocumented) getPointArray(index: number): T; // (undocumented) + getTypedArray(): Float32Array; + // (undocumented) protected grow(additionalSize?: number, growSize?: number): void; // (undocumented) growSize: number; @@ -2842,6 +2849,75 @@ export interface RetrieveStage { // @public (undocumented) type RGB = [number, number, number]; +// @public (undocumented) +class RLEVoxelMap { + constructor(width: number, height: number, depth?: number); + // (undocumented) + clear(): void; + // (undocumented) + defaultValue: T; + // (undocumented) + delete(index: number): void; + // (undocumented) + protected depth: number; + // (undocumented) + fillFrom(getter: (i: number, j: number, k: number) => T, boundsIJK: BoundsIJK): void; + // (undocumented) + findAdjacents(item: [RLERun, number, number, Point3[]?], { diagonals, planar, singlePlane }: { + diagonals?: boolean; + planar?: boolean; + singlePlane?: boolean; + }): any[]; + // (undocumented) + protected findIndex(row: RLERun[], i: number): number; + // (undocumented) + floodFill(i: number, j: number, k: number, value: T, options?: { + planar?: boolean; + diagonals?: boolean; + singlePlane?: boolean; + }): number; + // (undocumented) + forEach(callback: any, options?: { + rowModified?: boolean; + }): void; + // (undocumented) + forEachRow(callback: any): void; + // (undocumented) + get: (index: number) => T; + // (undocumented) + getPixelData(k?: number, pixelData?: PixelDataTypedArray): PixelDataTypedArray; + // (undocumented) + protected getRLE(i: number, j: number, k?: number): RLERun; + // (undocumented) + getRun: (j: number, k: number) => RLERun[]; + // (undocumented) + has(index: number): boolean; + // (undocumented) + protected height: number; + // (undocumented) + protected jMultiple: number; + // (undocumented) + keys(): number[]; + // (undocumented) + protected kMultiple: number; + // (undocumented) + normalizer: PlaneNormalizer; + // (undocumented) + protected numComps: number; + // (undocumented) + pixelDataConstructor: Uint8ArrayConstructor; + // (undocumented) + protected rows: Map[]>; + // (undocumented) + set: (index: number, value: T) => void; + // (undocumented) + toIJK(index: number): Point3; + // (undocumented) + toIndex([i, j, k]: Point3): number; + // (undocumented) + protected width: number; +} + // @public (undocumented) function roundNumber(value: string | number | (string | number)[], precision?: number): string; @@ -3425,6 +3501,7 @@ declare namespace utilities { isVideoTransferSyntax, getBufferConfiguration, VoxelManager, + RLEVoxelMap, generateVolumePropsFromImageIds, convertStackToVolumeViewport, convertVolumeToStackViewport, @@ -4114,6 +4191,8 @@ class VoxelManager { // (undocumented) static createRGBVolumeVoxelManager(dimensions: Point3, scalarData: any, numComponents: any): VoxelManager; // (undocumented) + static createRLEHistoryVoxelManager(sourceVoxelManager: VoxelManager): VoxelManager; + // (undocumented) static createRLEVoxelManager(dimensions: Point3): VoxelManager; // (undocumented) static createVolumeVoxelManager(dimensions: Point3, scalarData: any, numComponents?: number): VoxelManager | VoxelManager; @@ -4146,12 +4225,16 @@ class VoxelManager { // (undocumented) map: Map | RLEVoxelMap; // (undocumented) + mapForEach(callback: any, options?: any): void; + // (undocumented) modifiedSlices: Set; // (undocumented) numComps: number; // (undocumented) points: Set; // (undocumented) + rleForEach(callback: any, options?: any): void; + // (undocumented) scalarData: PixelDataTypedArray; // (undocumented) _set: (index: number, v: T) => boolean | void; @@ -4171,6 +4254,14 @@ class VoxelManager { width: number; } +// @public (undocumented) +enum VoxelManagerEnum { + // (undocumented) + RLE = "RLE", + // (undocumented) + Volume = "Volume" +} + declare namespace windowLevel { export { toWindowLevel, diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index a8a91f566d..badddcc537 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -640,7 +640,7 @@ export abstract class BaseTool implements IBaseTool { // (undocumented) applyActiveStrategy(enabledElement: Types_2.IEnabledElement, operationData: unknown): any; // (undocumented) - applyActiveStrategyCallback(enabledElement: Types_2.IEnabledElement, operationData: unknown, callbackType: StrategyCallbacks | string): any; + applyActiveStrategyCallback(enabledElement: Types_2.IEnabledElement, operationData: unknown, callbackType: StrategyCallbacks | string, ...extraArgs: any[]): any; // (undocumented) configuration: Record; // (undocumented) @@ -673,11 +673,18 @@ declare namespace BasicStatsCalculator { // @public (undocumented) class BasicStatsCalculator_2 extends Calculator { // (undocumented) - static getStatistics: () => NamedStatistics; + static getStatistics: (options?: { + unit: string; + }) => NamedStatistics; // (undocumented) - static statsCallback: ({ value: newValue }: { + static statsCallback: ({ value: newValue, pointLPS }: { value: any; + pointLPS?: any; }) => void; + // (undocumented) + static statsInit(options: { + noPointsCollection: boolean; + }): void; } // @public (undocumented) @@ -837,6 +844,8 @@ export class BrushTool extends BaseTool { referencedVolumeId?: string; }; // (undocumented) + getStatistics(element: any, segmentIndices?: any): any; + // (undocumented) invalidateBrushCursor(): void; // (undocumented) mouseMoveCallback: (evt: EventTypes_2.InteractionEventType) => void; @@ -2365,12 +2374,13 @@ type FloodFillOptions = { onBoundary?: (x: number, y: number, z?: number) => void; equals?: (a: any, b: any) => boolean; diagonals?: boolean; + bounds?: Map; + filter?: (point: any) => boolean; }; // @public (undocumented) type FloodFillResult = { flooded: Types_2.Point2[] | Types_2.Point3[]; - boundaries: Types_2.Point2[] | Types_2.Point3[]; }; // @public (undocumented) @@ -3541,6 +3551,9 @@ type NamedStatistics = { max: Statistics & { name: 'max'; }; + min: Statistics & { + name: 'min'; + }; stdDev: Statistics & { name: 'stdDev'; }; @@ -3560,6 +3573,7 @@ type NamedStatistics = { name: 'circumferance'; }; array: Statistics[]; + pointsInShape?: Types_2.PointsManager; }; // @public (undocumented) @@ -3870,7 +3884,7 @@ const pointCanProjectOnLine: (p: Types_2.Point2, p1: Types_2.Point2, p2: Types_2 function pointInEllipse(ellipse: any, pointLPS: any, inverts?: Inverts): boolean; // @public (undocumented) -function pointInShapeCallback(imageData: vtkImageData | Types_2.CPUImageData, pointInShapeFn: ShapeFnCriteria, callback?: PointInShapeCallback, boundsIJK?: BoundsIJK_2): Array; +function pointInShapeCallback(imageData: vtkImageData | Types_2.CPUImageData, pointInShapeFn: ShapeFnCriteria, callback: PointInShapeCallback, boundsIJK?: BoundsIJK_2): void; // @public (undocumented) function pointInSurroundingSphereCallback(imageData: vtkImageData, circlePoints: [Types_2.Point3, Types_2.Point3], callback: PointInShapeCallback, viewport?: Types_2.IVolumeViewport): void; @@ -4623,6 +4637,7 @@ declare namespace segmentation_2 { setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, + VolumetricCalculator, thresholdSegmentationByRange, createImageIdReferenceMap, contourAndFindLargestBidirectional, @@ -4640,6 +4655,7 @@ declare namespace segmentation_2 { type SegmentationDataModifiedEventDetail = { segmentationId: string; modifiedSlicesToUse?: number[]; + segmentIndex?: number; }; // @public (undocumented) @@ -5203,6 +5219,8 @@ enum StrategyCallbacks { // (undocumented) Fill = "fill", // (undocumented) + GetStatistics = "getStatistics", + // (undocumented) Initialize = "initialize", // (undocumented) INTERNAL_setValue = "setValue", @@ -5696,7 +5714,7 @@ function triggerAnnotationRenderForViewportIds(renderingEngine: Types_2.IRenderi function triggerEvent(el: EventTarget, type: string, detail?: unknown): boolean; // @public (undocumented) -function triggerSegmentationDataModified(segmentationId: string, modifiedSlicesToUse?: number[]): void; +function triggerSegmentationDataModified(segmentationId: string, modifiedSlicesToUse?: number[], segmentIndex?: number): void; declare namespace triggerSegmentationEvents { export { @@ -6150,6 +6168,15 @@ type VolumeScrollOutOfBoundsEventDetail = { // @public (undocumented) type VolumeScrollOutOfBoundsEventType = Types_2.CustomEventType; +// @public (undocumented) +class VolumetricCalculator extends BasicStatsCalculator_2 { + // (undocumented) + static getStatistics(options: { + spacing?: number; + unit?: string; + }): NamedStatistics; +} + // @public (undocumented) export class WindowLevelTool extends BaseTool { constructor(toolProps?: {}, defaultToolProps?: { diff --git a/packages/core/examples/multiVolumeCanvasToWorld/index.ts b/packages/core/examples/multiVolumeCanvasToWorld/index.ts index 2a598ad55e..a1e50e152b 100644 --- a/packages/core/examples/multiVolumeCanvasToWorld/index.ts +++ b/packages/core/examples/multiVolumeCanvasToWorld/index.ts @@ -171,7 +171,7 @@ async function run() { Math.floor(evt.clientX - rect.left), Math.floor(evt.clientY - rect.top), ]; - // Convert canvas coordiantes to world coordinates + // Convert canvas coordinates to world coordinates const worldPos = viewport.canvasToWorld(canvasPos); canvasPosElement.innerText = `canvas: (${canvasPos[0]}, ${canvasPos[1]})`; diff --git a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js index 542e2f4ee9..1c1e72e3be 100644 --- a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js +++ b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLVolumeMapper.js @@ -33,7 +33,7 @@ function vtkStreamingOpenGLVolumeMapper(publicAPI, model) { return; } - const scalars = image.getPointData() && image.getPointData().getScalars(); + const scalars = image.getPointData()?.getScalars(); if (!scalars) { return; } diff --git a/packages/core/src/cache/classes/ImageVolume.ts b/packages/core/src/cache/classes/ImageVolume.ts index 76449ff920..b86a40c854 100644 --- a/packages/core/src/cache/classes/ImageVolume.ts +++ b/packages/core/src/cache/classes/ImageVolume.ts @@ -7,7 +7,7 @@ import { imageIdToURI, } from '../../utilities'; import { vtkStreamingOpenGLTexture } from '../../RenderingEngine/vtkClasses'; -import { +import type { Metadata, Point3, IImageVolume, @@ -16,9 +16,11 @@ import { ImageVolumeProps, IImage, IImageLoadObject, + RGB, } from '../../types'; import cache from '../cache'; import * as metaData from '../../metaData'; +import type VoxelManager from '../../utilities/VoxelManager'; /** The base class for volume data. It includes the volume metadata * and the volume data along with the loading status. @@ -80,6 +82,8 @@ export class ImageVolume implements IImageVolume { hasPixelSpacing: boolean; /** Property to store additional information */ additionalDetails?: Record; + /** Store a voxel manager to access scalar data */ + voxelManager?: VoxelManager | VoxelManager; constructor(props: ImageVolumeProps) { const { @@ -97,6 +101,7 @@ export class ImageVolume implements IImageVolume { metadata, referencedImageIds, additionalDetails, + voxelManager, } = props; this.imageIds = imageIds; @@ -108,6 +113,7 @@ export class ImageVolume implements IImageVolume { this.direction = direction; this.scalarData = scalarData; this.sizeInBytes = sizeInBytes; + this.voxelManager = voxelManager; this.vtkOpenGLTexture = vtkStreamingOpenGLTexture.newInstance(); this.numVoxels = this.dimensions[0] * this.dimensions[1] * this.dimensions[2]; @@ -191,6 +197,9 @@ export class ImageVolume implements IImageVolume { if (isTypedArray(this.scalarData)) { return this.scalarData; } + if (!this.scalarData) { + return null; + } throw new Error('Unknown scalar data type'); } diff --git a/packages/core/src/enums/VoxelManagerEnum.ts b/packages/core/src/enums/VoxelManagerEnum.ts new file mode 100644 index 0000000000..d96d817581 --- /dev/null +++ b/packages/core/src/enums/VoxelManagerEnum.ts @@ -0,0 +1,29 @@ +/** + * The voxel manager enum is used to select from various voxel managers. + * This allows different representations of the underlying imaging data for + * a volume or image slice. Some representations are better for some types of + * operations, or are required to support specific sizes of operation data. + */ +enum VoxelManagerEnum { + /** + * The RLE Voxel manager defines rows within a volume as a set of Run Length + * encoded values, where all the values between successive i indices on the + * same row/slice have the given value. + * This is very efficient when there are long runs on i values all having the + * same value, as is typical in many segmentations. + * It is also allows for multi-valued segmentations, for example, having + * segments 1 and 3 for a single run. Note that such segmentations need to + * be converted to simple segmentations for actual display. + */ + RLE = 'RLE', + + /** + * The volume voxel manager represents data in a TypeArray that is pixel selection first, + * column second, row third, and finally slice number. This is the same representation + * as Image data used in ITK and VTK. + * This requires a full pixel data TypedArray instance. + */ + Volume = 'Volume', +} + +export default VoxelManagerEnum; diff --git a/packages/core/src/enums/index.ts b/packages/core/src/enums/index.ts index 9792df0315..bb10abf7c5 100644 --- a/packages/core/src/enums/index.ts +++ b/packages/core/src/enums/index.ts @@ -14,6 +14,7 @@ import ViewportStatus from './ViewportStatus'; import ImageQualityStatus from './ImageQualityStatus'; import * as VideoEnums from './VideoEnums'; import MetadataModules from './MetadataModules'; +import VoxelManagerEnum from './VoxelManagerEnum'; export { Events, @@ -32,4 +33,5 @@ export { VideoEnums, MetadataModules, ImageQualityStatus, + VoxelManagerEnum, }; diff --git a/packages/core/src/loaders/volumeLoader.ts b/packages/core/src/loaders/volumeLoader.ts index bf41dc836c..929bcd9291 100644 --- a/packages/core/src/loaders/volumeLoader.ts +++ b/packages/core/src/loaders/volumeLoader.ts @@ -8,8 +8,10 @@ import cloneDeep from 'lodash.clonedeep'; import { ImageVolume } from '../cache/classes/ImageVolume'; import cache from '../cache/cache'; import Events from '../enums/Events'; +import VoxelManagerEnum from '../enums/VoxelManagerEnum'; import eventTarget from '../eventTarget'; import triggerEvent from '../utilities/triggerEvent'; +import VoxelManager from '../utilities/VoxelManager'; import { generateVolumePropsFromImageIds, getBufferConfiguration, @@ -43,6 +45,12 @@ interface DerivedVolumeOptions { type: PixelDataTypedArrayString; sharedArrayBuffer?: boolean; }; + /** + * Use a voxel representation of the specified type. + * This allows efficient representation of the data to be selected and then + * treated as though all the representations were equivalent. + */ + voxelRepresentation?: VoxelManagerEnum; } interface LocalVolumeOptions { metadata: Metadata; @@ -291,7 +299,8 @@ export async function createAndCacheDerivedVolume( } let { volumeId } = options; - const { targetBuffer } = options; + const { targetBuffer, voxelRepresentation } = options; + const { type } = targetBuffer; if (volumeId === undefined) { volumeId = uuidv4(); @@ -311,6 +320,8 @@ export async function createAndCacheDerivedVolume( name: 'Pixels', numberOfComponents: 1, values: volumeScalarData, + size: numBytes, + dataType: !type || type === 'none' ? 'Uint8Array' : type, }); const derivedImageData = vtkImageData.newInstance(); @@ -321,6 +332,19 @@ export async function createAndCacheDerivedVolume( derivedImageData.setOrigin(origin); derivedImageData.getPointData().setScalars(scalarArray); + const internalScalarData = derivedImageData + .getPointData() + .getScalars() + .getData() as PixelDataTypedArray; + + const voxelManager = + (voxelRepresentation === VoxelManagerEnum.RLE && + VoxelManager.createRLEVoxelManager(dimensions)) || + (VoxelManager.createVolumeVoxelManager( + dimensions, + internalScalarData, + 1 + ) as VoxelManager); const derivedVolume = new ImageVolume({ volumeId, metadata: cloneDeep(metadata), @@ -329,7 +353,8 @@ export async function createAndCacheDerivedVolume( origin, direction, imageData: derivedImageData, - scalarData: volumeScalarData, + scalarData: internalScalarData, + voxelManager, sizeInBytes: numBytes, imageIds: [], referencedVolumeId, @@ -578,6 +603,7 @@ export async function createAndCacheDerivedSegmentationVolume( ...options, targetBuffer: { type: 'Uint8Array', + ...options?.targetBuffer, }, }); } @@ -624,6 +650,9 @@ function generateVolumeScalarData( ) { const { useNorm16Texture } = getConfiguration().rendering; + if (targetBuffer?.type === 'none') { + return { volumeScalarData: null, numBytes: scalarLength }; + } const { TypedArrayConstructor, numBytes } = getBufferConfiguration( targetBuffer?.type, scalarLength, diff --git a/packages/core/src/types/ImageVolumeProps.ts b/packages/core/src/types/ImageVolumeProps.ts index 7960580943..5e00bea60f 100644 --- a/packages/core/src/types/ImageVolumeProps.ts +++ b/packages/core/src/types/ImageVolumeProps.ts @@ -1,4 +1,6 @@ -import { VolumeProps } from '.'; +import type { VolumeProps } from '.'; +import type VoxelManager from '../utilities/VoxelManager'; +import type Point3 from './Point3'; /** * ImageVolume which is considered a special case of a Volume, which is @@ -10,6 +12,8 @@ interface ImageVolumeProps extends VolumeProps { imageIds: Array; /** if the volume is created from a stack, the imageIds of the stack */ referencedImageIds?: Array; + /** A voxel manager for this data */ + voxelManager?: VoxelManager | VoxelManager; } export { ImageVolumeProps }; diff --git a/packages/core/src/types/PixelDataTypedArray.ts b/packages/core/src/types/PixelDataTypedArray.ts index 3079399869..d8f607d9b4 100644 --- a/packages/core/src/types/PixelDataTypedArray.ts +++ b/packages/core/src/types/PixelDataTypedArray.ts @@ -12,4 +12,6 @@ export type PixelDataTypedArrayString = | 'Uint16Array' | 'Uint8Array' | 'Int8Array' - | 'Uint8ClampedArray'; + | 'Uint8ClampedArray' + // Used to not create an array object + | 'none'; diff --git a/packages/core/src/utilities/PointsManager.ts b/packages/core/src/utilities/PointsManager.ts index 9b5fbd96b0..1ca85d2db5 100644 --- a/packages/core/src/utilities/PointsManager.ts +++ b/packages/core/src/utilities/PointsManager.ts @@ -153,6 +153,14 @@ export default class PointsManager { } } + /** + * Gets the raw underlying data - note this can change. Use for fast calculations + * on a fully filled array. + */ + public getTypedArray() { + return this.data; + } + /** * Push a new point onto this arrays object */ diff --git a/packages/core/src/utilities/RLEVoxelMap.ts b/packages/core/src/utilities/RLEVoxelMap.ts index 7dbdd10157..9ee74e3b91 100644 --- a/packages/core/src/utilities/RLEVoxelMap.ts +++ b/packages/core/src/utilities/RLEVoxelMap.ts @@ -1,3 +1,5 @@ +import type Point3 from '../types/Point3'; +import type BoundsIJK from '../types/BoundsIJK'; import { PixelDataTypedArray } from '../types'; /** @@ -11,6 +13,45 @@ export type RLERun = { end: number; }; +/** + * Performs adjacent flood fill in all directions, for a true flood fill + */ +const ADJACENT_ALL = [ + [0, -1, 0], + [0, 1, 0], + [0, 0, -1], + [0, 0, 1], +]; + +const ADJACENT_SINGLE_PLANE = [ + [0, -1, 0], + [0, 1, 0], +]; + +/** + * Adjacent in and out do a flood fill in only one of depth (in or out) directions. + * That improves the performance, as well as looks much nicer for many flood operations. + */ +const ADJACENT_IN = [ + [0, -1, 0], + [0, 1, 0], + [0, 0, -1], +]; +const ADJACENT_OUT = [ + [0, -1, 0], + [0, 1, 0], + [0, 0, 1], +]; + +/** + * A type that has converts to and from an integer plane representation. + */ +export type PlaneNormalizer = { + toIJK: (ijkPrime: Point3) => Point3; + fromIJK: (ijk: Point3) => Point3; + boundsIJKPrime: BoundsIJK; +}; + /** * RLE based implementation of a voxel map. * This can be used as single or multi-plane, as the underlying indexes are @@ -18,6 +59,7 @@ export type RLERun = { * incrementing for all rows in the multi-plane voxel. */ export default class RLEVoxelMap { + public normalizer: PlaneNormalizer; /** * The rows for the voxel map is a map from the j index location (or for * volumes, `j + k*height`) to a list of RLE runs. That is, each entry in @@ -52,7 +94,7 @@ export default class RLEVoxelMap { * default value for unset values. * Set to 0 by default, but any maps where 0 not in T should update this value. */ - public defaultValue: T = 0 as unknown as T; + public defaultValue: T; /** * The constructor for creating pixel data. @@ -79,9 +121,20 @@ export default class RLEVoxelMap { const i = index % this.jMultiple; const j = (index - i) / this.jMultiple; const rle = this.getRLE(i, j); - return rle?.value || this.defaultValue; + return rle?.value ?? this.defaultValue; }; + public toIJK(index: number): Point3 { + const i = index % this.jMultiple; + const j = ((index - i) / this.jMultiple) % this.height; + const k = Math.floor(index / this.kMultiple); + return [i, j, k]; + } + + public toIndex([i, j, k]: Point3) { + return i + k * this.kMultiple + j * this.jMultiple; + } + /** * Gets a list of RLERun values which specify the data on the row j * This allows applying or modifying the run directly. See CanvasActor @@ -97,6 +150,61 @@ export default class RLEVoxelMap { return i >= rle?.start ? rle : undefined; } + /** + * Indicate if the map has the given value + */ + public has(index: number): boolean { + const i = index % this.jMultiple; + const j = (index - i) / this.jMultiple; + const rle = this.getRLE(i, j); + return rle?.value !== undefined; + } + + /** + * Delete any value at the given index; + */ + public delete(index: number) { + const i = index % this.width; + const j = (index - i) / this.width; + const row = this.rows.get(j); + if (!row) { + return; + } + const rleIndex = this.findIndex(row, i); + const rle = row[rleIndex]; + if (!rle || rle.start > i) { + // Value not in RLE, so no need to delete + return; + } + if (rle.end === i + 1) { + // Value at end, so decrease the length. + // This also handles hte case of the value at the beginning and deleting + // the final value in the RLE + rle.end--; + if (rle.start >= rle.end) { + // Last value in the RLE + row.splice(rleIndex, 1); + if (!row.length) { + this.rows.delete(j); + } + } + return; + } + if (rle.start === i) { + // Not the only value, otherwise this is checked by the previous code + rle.start++; + return; + } + // Need to split the rle since the value occurs in the middle. + const newRle = { + value: rle.value, + start: i + 1, + end: rle.end, + }; + rle.end = i; + row.splice(rleIndex + 1, 0, newRle); + } + /** * Finds the index in the row that i is contained in, OR that i would be * before. That is, the rle value for the returned index in that row @@ -114,6 +222,28 @@ export default class RLEVoxelMap { return row.length; } + /** + * For each RLE element, call the given callback + */ + public forEach(callback, options?: { rowModified?: boolean }) { + const rowModified = options?.rowModified; + for (const [baseIndex, row] of this.rows) { + const rowToUse = rowModified ? [...row] : row; + for (const rle of rowToUse) { + callback(baseIndex * this.width, rle, row); + } + } + } + + /** + * For each row, call the callback with the base index and the row data + */ + public forEachRow(callback) { + for (const [baseIndex, row] of this.rows) { + callback(baseIndex * this.width, row); + } + } + /** * Gets the run for the given j,k indices. This is used to allow fast access * to runs for data for things like rendering entire rows of data. @@ -297,6 +427,168 @@ export default class RLEVoxelMap { } return pixelData; } + + /** + * Performs a flood fill on the RLE values at the given position, replacing + * the current value with the new value (which must be different) + * Note that this is, by default, a planar fill, which will fill each plane + * given the starting point, in a true flood fill fashion, but then not + * re-fill the given plane. + * + * @param i,j,k - starting point to fill from, as integer indices into + * the voxel volume. These are converted internally to RLE indices + * @param value - to replace the existing value with. Must be different from + * the starting value. + * @param options - to control the flood. + * * planar means to flood the current k plane entirely, and then use the + * points from the current plane as seed points in the k+1 and k-1 planes, + * but not returning to the current plane + * * singlePlane is just a single k plane, not filling any other planes + * * diagonals means to use the diagonally adjacent points. + */ + public floodFill( + i: number, + j: number, + k: number, + value: T, + options?: { planar?: boolean; diagonals?: boolean; singlePlane?: boolean } + ): number { + const rle = this.getRLE(i, j, k); + if (!rle) { + throw new Error(`Initial point ${i},${j},${k} isn't in the RLE`); + } + const stack = [[rle, j, k]]; + const replaceValue = rle.value; + if (replaceValue === value) { + throw new Error( + `source (${replaceValue}) and destination (${value}) are identical` + ); + } + return this.flood(stack, replaceValue, value, options); + } + + /** + * Performs a flood fill on the stack. + * + * @param stack - list of points/rle runs to try filling + * @param sourceValue - the value that is being replaced in the flood + * @param value - the destination value for the flood + * @param options - see floodFill + */ + private flood(stack, sourceValue, value, options) { + let sum = 0; + const { + planar = true, + diagonals = true, + singlePlane = false, + } = options || {}; + const childOptions = { planar, diagonals, singlePlane }; + while (stack.length) { + const top = stack.pop(); + const [current] = top; + if (current.value !== sourceValue) { + continue; + } + current.value = value; + sum += current.end - current.start; + const adjacents = this.findAdjacents(top, childOptions).filter( + (adjacent) => adjacent && adjacent[0].value === sourceValue + ); + stack.push(...adjacents); + } + return sum; + } + + /** + * Fills an RLE from a given getter result, skipping undefined values only. + * @param getter - a function taking i,j,k values (indices) and returning the new + * value at the given point. + * @param boundsIJK - a set of boundary values to flood up to and including both values. + */ + public fillFrom( + getter: (i: number, j: number, k: number) => T, + boundsIJK: BoundsIJK + ) { + for (let k = boundsIJK[2][0]; k <= boundsIJK[2][1]; k++) { + for (let j = boundsIJK[1][0]; j <= boundsIJK[1][1]; j++) { + let rle; + let row; + for (let i = boundsIJK[0][0]; i <= boundsIJK[0][1]; i++) { + const value = getter(i, j, k); + if (value === undefined) { + rle = undefined; + continue; + } + if (!row) { + row = []; + this.rows.set(j + k * this.height, row); + } + if (rle && rle.value !== value) { + rle = undefined; + } + if (!rle) { + rle = { start: i, end: i, value }; + row.push(rle); + } + rle.end++; + } + } + } + } + + /** + * Finds adjacent RLE runs, in all directions. + * The planar value (true by default) does plane at a time fills. + * @param item - an RLE being sepecified to find adjacent values for + * @param options - see floodFill + */ + public findAdjacents( + item: [RLERun, number, number, Point3[]?], + { diagonals = true, planar = true, singlePlane = false } + ) { + const [rle, j, k, adjacentsDelta] = item; + const { start, end } = rle; + const leftRle = start > 0 && this.getRLE(start - 1, j, k); + const rightRle = end < this.width && this.getRLE(end, j, k); + const range = diagonals + ? [start > 0 ? start - 1 : start, end < this.width ? end + 1 : end] + : [start, end]; + const adjacents = []; + if (leftRle) { + adjacents.push([leftRle, j, k]); + } + if (rightRle) { + adjacents.push([rightRle, j, k]); + } + for (const delta of adjacentsDelta || + (singlePlane ? ADJACENT_SINGLE_PLANE : ADJACENT_ALL)) { + const [, delta1, delta2] = delta; + const testJ = delta1 + j; + const testK = delta2 + k; + if (testJ < 0 || testJ >= this.height) { + continue; + } + if (testK < 0 || testK >= this.depth) { + continue; + } + const row = this.getRun(testJ, testK); + if (!row) { + continue; + } + for (const testRle of row) { + const newAdjacentDelta = + adjacentsDelta || + (singlePlane && ADJACENT_SINGLE_PLANE) || + (planar && delta2 > 0 && ADJACENT_OUT) || + (planar && delta2 < 0 && ADJACENT_IN) || + ADJACENT_ALL; + if (!(testRle.end <= range[0] || testRle.start >= range[1])) { + adjacents.push([testRle, testJ, testK, newAdjacentDelta]); + } + } + } + return adjacents; + } } // This is some code to allow debugging RLE maps diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index f59315c776..b3dc5a7908 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -173,28 +173,55 @@ export default class VoxelManager { const boundsIJK = options?.boundsIJK || this.getBoundsIJK(); const { isWithinObject } = options || {}; if (this.map) { - // Optimize this for only values in the map - for (const index of this.map.keys()) { - const pointIJK = this.toIJK(index); - const value = this._get(index); - const callbackArguments = { value, index, pointIJK }; - if (isWithinObject?.(callbackArguments) === false) { - continue; + if (this.map instanceof RLEVoxelMap) { + return this.rleForEach(callback, options); + } + return this.mapForEach(callback, options); + } + + for (let k = boundsIJK[2][0]; k <= boundsIJK[2][1]; k++) { + const kIndex = k * this.frameSize; + for (let j = boundsIJK[1][0]; j <= boundsIJK[1][1]; j++) { + const jIndex = kIndex + j * this.width; + for ( + let i = boundsIJK[0][0], index = jIndex + i; + i <= boundsIJK[0][1]; + i++, index++ + ) { + const value = this.getAtIndex(index); + const callbackArguments = { value, index, pointIJK: [i, j, k] }; + if (isWithinObject?.(callbackArguments) === false) { + continue; + } + callback(callbackArguments); } - callback(callbackArguments); } - } else { - for (let k = boundsIJK[2][0]; k <= boundsIJK[2][1]; k++) { - const kIndex = k * this.frameSize; - for (let j = boundsIJK[1][0]; j <= boundsIJK[1][1]; j++) { - const jIndex = kIndex + j * this.width; - for ( - let i = boundsIJK[0][0], index = jIndex + i; - i <= boundsIJK[0][1]; - i++, index++ - ) { - const value = this.getAtIndex(index); - const callbackArguments = { value, index, pointIJK: [i, j, k] }; + } + }; + + /** + * Foreach callback optimized for RLE testing + */ + public rleForEach(callback, options?) { + const boundsIJK = options?.boundsIJK || this.getBoundsIJK(); + const { isWithinObject } = options || {}; + const map = this.map as RLEVoxelMap; + map.defaultValue = undefined; + for (let k = boundsIJK[2][0]; k <= boundsIJK[2][1]; k++) { + for (let j = boundsIJK[1][0]; j <= boundsIJK[1][1]; j++) { + const row = map.getRun(j, k); + if (!row) { + continue; + } + for (const rle of row) { + const { start, end, value } = rle; + const baseIndex = this.toIndex([0, j, k]); + for (let i = start; i < end; i++) { + const callbackArguments = { + value, + index: baseIndex + i, + pointIJK: [i, j, k], + }; if (isWithinObject?.(callbackArguments) === false) { continue; } @@ -203,7 +230,24 @@ export default class VoxelManager { } } } - }; + } + + /** + * Foreach callback optimized for basic map callbacks. + */ + public mapForEach(callback, options?) { + const { isWithinObject } = options || {}; + // Optimize this for only values in the map + for (const index of this.map.keys()) { + const pointIJK = this.toIJK(index); + const value = this._get(index); + const callbackArguments = { value, index, pointIJK }; + if (isWithinObject?.(callbackArguments) === false) { + continue; + } + callback(callbackArguments); + } + } /** * Clears any map specific data, as wellas the modified slices, points and @@ -393,6 +437,42 @@ export default class VoxelManager { return voxelManager; } + /** + * Creates a history remembering voxel manager, based on the RLE endpoint + * rather than a map endpoint. + * This will remember the original values in the voxels, and will apply the + * update to the underlying source voxel manager. + */ + public static createRLEHistoryVoxelManager( + sourceVoxelManager: VoxelManager + ): VoxelManager { + const { dimensions } = sourceVoxelManager; + const map = new RLEVoxelMap(dimensions[0], dimensions[1], dimensions[2]); + const voxelManager = new VoxelManager( + dimensions, + (index) => map.get(index), + function (index, v) { + const originalV = map.get(index); + if (originalV === undefined) { + const oldV = this.sourceVoxelManager.getAtIndex(index); + if (oldV === v || oldV === undefined || v === null) { + // No-op + return false; + } + map.set(index, oldV); + } else if (v === originalV || v === null) { + map.delete(index); + v = originalV; + } + this.sourceVoxelManager.setAtIndex(index, v); + } + ); + voxelManager.map = map; + voxelManager.scalarData = sourceVoxelManager.scalarData; + voxelManager.sourceVoxelManager = sourceVoxelManager; + return voxelManager; + } + /** * Creates a lazy voxel manager that will create an image plane as required * for each slice of a volume as it gets changed. This can be used to diff --git a/packages/core/src/utilities/index.ts b/packages/core/src/utilities/index.ts index ab6d31d3f6..899d18ad79 100644 --- a/packages/core/src/utilities/index.ts +++ b/packages/core/src/utilities/index.ts @@ -66,6 +66,7 @@ import { generateVolumePropsFromImageIds } from './generateVolumePropsFromImageI import { convertStackToVolumeViewport } from './convertStackToVolumeViewport'; import { convertVolumeToStackViewport } from './convertVolumeToStackViewport'; import VoxelManager from './VoxelManager'; +import RLEVoxelMap from './RLEVoxelMap'; import roundNumber, { roundToPrecision } from './roundNumber'; import convertToGrayscale from './convertToGrayscale'; import getViewportImageIds from './getViewportImageIds'; @@ -150,6 +151,7 @@ export { isVideoTransferSyntax, getBufferConfiguration, VoxelManager, + RLEVoxelMap, generateVolumePropsFromImageIds, convertStackToVolumeViewport, convertVolumeToStackViewport, diff --git a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts index 6b9d15ff9a..dc01875ac9 100644 --- a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts +++ b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts @@ -16,6 +16,7 @@ import { setCtTransferFunctionForVolumeActor, getLocalUrl, addButtonToToolbar, + addManipulationBindings, } from '../../../../utils/demo/helpers'; import * as cornerstoneTools from '@cornerstonejs/tools'; @@ -34,10 +35,6 @@ const { CircleScissorsTool, BrushTool, PaintFillTool, - PanTool, - ZoomTool, - StackScrollTool, - StackScrollMouseWheelTool, utilities: cstUtils, } = cornerstoneTools; @@ -99,50 +96,125 @@ instructions.innerText = ` content.append(instructions); -const brushInstanceNames = { - ThresholdCircle: 'ThresholdCircle', - CircularBrush: 'CircularBrush', - CircularEraser: 'CircularEraser', - SphereBrush: 'SphereBrush', - SphereEraser: 'SphereEraser', - ScissorsEraser: 'ScissorsEraser', +const interpolationTools = new Map(); +const previewColors = { + 0: [255, 255, 255, 128], + 1: [0, 255, 255, 192], + 2: [255, 0, 255, 255], }; - -const brushStrategies = { - [brushInstanceNames.CircularBrush]: 'FILL_INSIDE_CIRCLE', - [brushInstanceNames.CircularEraser]: 'ERASE_INSIDE_CIRCLE', - [brushInstanceNames.SphereBrush]: 'FILL_INSIDE_SPHERE', - [brushInstanceNames.SphereEraser]: 'ERASE_INSIDE_SPHERE', - [brushInstanceNames.ThresholdCircle]: 'THRESHOLD_INSIDE_CIRCLE', - [brushInstanceNames.ScissorsEraser]: 'ERASE_INSIDE', +const preview = { + enabled: true, + previewColors, +}; +const configuration = { + preview, + strategySpecificConfiguration: { + useCenterSegmentIndex: true, + }, }; +const thresholdOptions = new Map(); +thresholdOptions.set('Dynamic Radius 0', { isDynamic: true, dynamicRadius: 0 }); +thresholdOptions.set('Dynamic Radius 1', { isDynamic: true, dynamicRadius: 1 }); +thresholdOptions.set('Dynamic Radius 2', { isDynamic: true, dynamicRadius: 2 }); +thresholdOptions.set('Dynamic Radius 3', { isDynamic: true, dynamicRadius: 3 }); +thresholdOptions.set('Dynamic Radius 4', { isDynamic: true, dynamicRadius: 4 }); +thresholdOptions.set('Use Existing Threshold', { + isDynamic: false, + dynamicRadius: 5, +}); +thresholdOptions.set('CT Fat: (-150, -70)', { + threshold: [-150, -70], + isDynamic: false, +}); +thresholdOptions.set('CT Bone: (200, 1000)', { + threshold: [200, 1000], + isDynamic: false, +}); +const defaultThresholdOption = [...thresholdOptions.keys()][2]; +const thresholdArgs = thresholdOptions.get(defaultThresholdOption); + +interpolationTools.set('ThresholdSphereIsland', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', + strategySpecificConfiguration: { + ...configuration.strategySpecificConfiguration, + THRESHOLD: { ...thresholdArgs }, + }, + }, +}); -const brushValues = [ - brushInstanceNames.ThresholdCircle, - brushInstanceNames.CircularBrush, - brushInstanceNames.CircularEraser, - brushInstanceNames.SphereBrush, - brushInstanceNames.SphereEraser, -]; +interpolationTools.set('ThresholdCircle', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + strategySpecificConfiguration: { + ...configuration.strategySpecificConfiguration, + THRESHOLD: { ...thresholdArgs }, + }, + }, +}); + +interpolationTools.set('ThresholdSphere', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'THRESHOLD_INSIDE_SPHERE', + strategySpecificConfiguration: { + ...configuration.strategySpecificConfiguration, + THRESHOLD: { ...thresholdArgs }, + }, + }, +}); + +interpolationTools.set('CircularBrush', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'FILL_INSIDE_CIRCLE', + }, +}); + +interpolationTools.set('CircularEraser', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'ERASE_INSIDE_CIRCLE', + }, +}); + +interpolationTools.set('SphereBrush', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'FILL_INSIDE_SPHERE', + }, +}); +interpolationTools.set('SphereEraser', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'ERASE_INSIDE_SPHERE', + }, +}); +interpolationTools.set('ScissorsEraser', { + baseTool: SphereScissorsTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'ERASE_INSIDE', + }, +}); const optionsValues = [ - ...brushValues, + ...interpolationTools.keys(), RectangleScissorsTool.toolName, CircleScissorsTool.toolName, SphereScissorsTool.toolName, - brushInstanceNames.ScissorsEraser, PaintFillTool.toolName, ]; -const previewColors = { - 0: [255, 255, 255, 128], - 1: [0, 255, 255, 255], -}; -const preview = { - enabled: true, - previewColors, -}; - // ============================= // addDropdownToToolbar({ options: { values: optionsValues, defaultValue: BrushTool.toolName }, @@ -157,40 +229,12 @@ addDropdownToToolbar({ toolGroup.setToolDisabled(toolName); } - if (brushValues.includes(name)) { - toolGroup.setToolActive(name, { - bindings: [{ mouseButton: MouseBindings.Primary }], - }); - } else { - const toolName = name; - - toolGroup.setToolActive(toolName, { - bindings: [{ mouseButton: MouseBindings.Primary }], - }); - } + toolGroup.setToolActive(name, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); }, }); -const thresholdOptions = new Map(); -thresholdOptions.set('Dynamic Radius 0', { isDynamic: true, dynamicRadius: 0 }); -thresholdOptions.set('Dynamic Radius 1', { isDynamic: true, dynamicRadius: 1 }); -thresholdOptions.set('Dynamic Radius 3', { isDynamic: true, dynamicRadius: 3 }); -thresholdOptions.set('Dynamic Radius 5', { isDynamic: true, dynamicRadius: 5 }); -thresholdOptions.set('Use Existing Threshold', { - isDynamic: false, - dynamicRadius: 5, -}); -thresholdOptions.set('CT Fat: (-150, -70)', { - threshold: [-150, -70], - isDynamic: false, -}); -thresholdOptions.set('CT Bone: (200, 1000)', { - threshold: [200, 1000], - isDynamic: false, -}); - -const defaultThresholdOption = [...thresholdOptions.keys()][2]; - addDropdownToToolbar({ options: { values: Array.from(thresholdOptions.keys()), @@ -211,7 +255,7 @@ addDropdownToToolbar({ addSliderToToolbar({ title: 'Brush Size', - range: [5, 50], + range: [5, 100], defaultValue: 25, onSelectedValueChange: (valueAsStringOrNumber) => { const value = Number(valueAsStringOrNumber); @@ -247,6 +291,10 @@ async function addSegmentationsToState() { // Create a segmentation of the same resolution as the source data await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, { volumeId: segmentationId, + // The following doesn't quite work yet + // TODO, allow RLE to be used instead of scalars. + // targetBuffer: { type: 'none' }, + // voxelRepresentation: 'rleVoxelManager', }); // Add the segmentations to state @@ -279,10 +327,6 @@ async function run() { ); // Add tools to Cornerstone3D - cornerstoneTools.addTool(PanTool); - cornerstoneTools.addTool(ZoomTool); - cornerstoneTools.addTool(StackScrollMouseWheelTool); - cornerstoneTools.addTool(StackScrollTool); cornerstoneTools.addTool(SegmentationDisplayTool); cornerstoneTools.addTool(RectangleScissorsTool); cornerstoneTools.addTool(CircleScissorsTool); @@ -294,124 +338,38 @@ async function run() { const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); // Manipulation Tools - toolGroup.addTool(PanTool.toolName); - toolGroup.addTool(ZoomTool.toolName); - toolGroup.addTool(StackScrollMouseWheelTool.toolName); + addManipulationBindings(toolGroup); // Segmentation Tools toolGroup.addTool(SegmentationDisplayTool.toolName); toolGroup.addTool(RectangleScissorsTool.toolName); toolGroup.addTool(CircleScissorsTool.toolName); toolGroup.addTool(SphereScissorsTool.toolName); - toolGroup.addToolInstance( - brushInstanceNames.ScissorsEraser, - SphereScissorsTool.toolName, - { - activeStrategy: brushStrategies.ScissorsEraser, - } - ); toolGroup.addTool(PaintFillTool.toolName); - toolGroup.addTool(StackScrollTool.toolName); - toolGroup.addToolInstance( - brushInstanceNames.CircularBrush, - BrushTool.toolName, - { - activeStrategy: brushStrategies.CircularBrush, - preview, - strategySpecificConfiguration: { - useCenterSegmentIndex: true, - }, - } - ); - toolGroup.addToolInstance( - brushInstanceNames.CircularEraser, - BrushTool.toolName, - { - activeStrategy: brushStrategies.CircularEraser, - preview, - } - ); - toolGroup.addToolInstance( - brushInstanceNames.SphereBrush, - BrushTool.toolName, - { - activeStrategy: brushStrategies.SphereBrush, - preview, - } - ); - toolGroup.addToolInstance( - brushInstanceNames.SphereEraser, - BrushTool.toolName, - { - activeStrategy: brushStrategies.SphereEraser, - previewColors, + toolGroup.addTool(BrushTool.toolName); + + for (const [toolName, config] of interpolationTools.entries()) { + if (config.baseTool) { + toolGroup.addToolInstance( + toolName, + config.baseTool, + config.configuration + ); + } else { + toolGroup.addTool(toolName, config.configuration); } - ); - toolGroup.setToolActive(StackScrollTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Primary, // Left Click - modifierKey: KeyboardBindings.Alt, - }, - { - numTouchPoints: 1, - modifierKey: KeyboardBindings.Meta, - }, - ], - }); - - // Setup threshold and the default strategy arguments - const thresholdArgs = thresholdOptions.get(defaultThresholdOption); - toolGroup.addToolInstance( - brushInstanceNames.ThresholdCircle, - BrushTool.toolName, - { - activeStrategy: brushStrategies.ThresholdCircle, - preview, - strategySpecificConfiguration: { - useCenterSegmentIndex: true, - THRESHOLD: { ...thresholdArgs }, - }, + if (config.passive) { + // This can be applied during add/remove contours + toolGroup.setToolPassive(toolName); } - ); + } toolGroup.setToolEnabled(SegmentationDisplayTool.toolName); - toolGroup.setToolActive(brushInstanceNames.ThresholdCircle, { + toolGroup.setToolActive(interpolationTools.keys().next().value, { bindings: [{ mouseButton: MouseBindings.Primary }], }); - toolGroup.setToolActive(ZoomTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Primary, // Shift Left Click - modifierKey: KeyboardBindings.Shift, - }, - ], - }); - - toolGroup.setToolActive(PanTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Auxiliary, // Middle Click - }, - { - mouseButton: MouseBindings.Primary, - modifierKey: KeyboardBindings.Ctrl, - }, - ], - }); - toolGroup.setToolActive(ZoomTool.toolName, { - bindings: [ - { - mouseButton: MouseBindings.Secondary, // 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); - // Get Cornerstone imageIds for the source data and fetch metadata into RAM const imageIds = await createImageIdsAndCacheMetaData({ StudyInstanceUID: @@ -485,10 +443,6 @@ async function run() { [viewportId1, viewportId2, viewportId3] ); - segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 3); - segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 4); - segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 1); - // Add the segmentation representation to the toolgroup await segmentation.addSegmentationRepresentations(toolGroupId, [ { @@ -496,6 +450,7 @@ async function run() { type: csToolsEnums.SegmentationRepresentations.Labelmap, }, ]); + segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 1); // Render the image renderingEngine.renderViewports([viewportId1, viewportId2, viewportId3]); diff --git a/packages/tools/examples/labelmapStatistics/index.ts b/packages/tools/examples/labelmapStatistics/index.ts new file mode 100644 index 0000000000..773d87250c --- /dev/null +++ b/packages/tools/examples/labelmapStatistics/index.ts @@ -0,0 +1,549 @@ +import { + RenderingEngine, + Types, + Enums, + setVolumesForViewports, + volumeLoader, + ProgressiveRetrieveImages, + utilities, + eventTarget, +} from '@cornerstonejs/core'; +import { + initDemo, + createImageIdsAndCacheMetaData, + setTitleAndDescription, + addDropdownToToolbar, + addSliderToToolbar, + setCtTransferFunctionForVolumeActor, + getLocalUrl, + addButtonToToolbar, + addManipulationBindings, +} from '../../../../utils/demo/helpers'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +// This is for debugging purposes +console.warn( + 'Click on index.ts to open source code for this example --------->' +); + +const { + SegmentationDisplayTool, + ToolGroupManager, + Enums: csToolsEnums, + segmentation, + RectangleScissorsTool, + SphereScissorsTool, + CircleScissorsTool, + BrushTool, + PaintFillTool, + utilities: cstUtils, +} = cornerstoneTools; + +const { MouseBindings, Events } = csToolsEnums; +const { ViewportType } = Enums; +const { segmentation: segmentationUtils, roundNumber } = cstUtils; + +// Define a unique id for the volume +const volumeName = 'CT_VOLUME_ID'; // Id of the volume less loader prefix +const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use +const volumeId = `${volumeLoaderScheme}:${volumeName}`; // VolumeId with loader id + volume id +const segmentationId = 'MY_SEGMENTATION_ID'; +const toolGroupId = 'MY_TOOLGROUP_ID'; +const viewports = []; + +const DEFAULT_BRUSH_SIZE = 10; + +// ======== Set up page ======== // +setTitleAndDescription( + 'Labelmap Segmentation Statistics', + 'Here we demonstrate calculating labelmap statistics' +); + +const size = '500px'; +const content = document.getElementById('content'); + +const statsGrid = document.createElement('div'); +statsGrid.style.display = 'flex'; +statsGrid.style.display = 'flex'; +statsGrid.style.flexDirection = 'row'; +statsGrid.style.fontSize = 'smaller'; + +const statsIds = ['statsCurrent', 'statsPreview', 'statsCombined']; +const statsStyle = { + width: '20em', + height: '10em', +}; + +for (const statsId of statsIds) { + const statsDiv = document.createElement('div'); + statsDiv.id = statsId; + statsDiv.innerText = statsId; + Object.assign(statsDiv.style, statsStyle); + statsGrid.appendChild(statsDiv); +} + +content.appendChild(statsGrid); + +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; + +// Disable right click context menu so we can have right click tools +element1.oncontextmenu = (e) => e.preventDefault(); +element2.oncontextmenu = (e) => e.preventDefault(); +element3.oncontextmenu = (e) => e.preventDefault(); + +viewportGrid.appendChild(element1); +viewportGrid.appendChild(element2); +viewportGrid.appendChild(element3); + +content.appendChild(viewportGrid); + +const instructions = document.createElement('p'); +instructions.innerText = ` + Hover - show preview of segmentation tool + Left drag to extend preview + Left Click (or enter) to accept preview + Reject preview by button (or esc) + Hover outside of region to reset to hovered over segment index + Shift Left - zoom, Ctrl Left - Pan, Alt Left - Stack Scroll + `; + +content.append(instructions); + +const interpolationTools = new Map(); +const previewColors = { + 0: [255, 255, 255, 128], + 1: [0, 255, 255, 192], + 2: [255, 0, 255, 255], +}; +const preview = { + enabled: true, + previewColors, +}; +const configuration = { + preview, + strategySpecificConfiguration: { + useCenterSegmentIndex: true, + }, +}; +const thresholdOptions = new Map(); +thresholdOptions.set('Dynamic Radius 0', { isDynamic: true, dynamicRadius: 0 }); +thresholdOptions.set('Dynamic Radius 1', { isDynamic: true, dynamicRadius: 1 }); +thresholdOptions.set('Dynamic Radius 3', { isDynamic: true, dynamicRadius: 3 }); +thresholdOptions.set('Dynamic Radius 5', { isDynamic: true, dynamicRadius: 5 }); +thresholdOptions.set('Use Existing Threshold', { + isDynamic: false, + dynamicRadius: 5, +}); +thresholdOptions.set('CT Fat: (-150, -70)', { + threshold: [-150, -70], + isDynamic: false, +}); +thresholdOptions.set('CT Bone: (200, 1000)', { + threshold: [200, 1000], + isDynamic: false, +}); +const defaultThresholdOption = [...thresholdOptions.keys()][2]; +const thresholdArgs = thresholdOptions.get(defaultThresholdOption); + +interpolationTools.set('CircularBrush', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'FILL_INSIDE_CIRCLE', + }, +}); + +interpolationTools.set('ThresholdSphereIsland', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL', + strategySpecificConfiguration: { + ...configuration.strategySpecificConfiguration, + THRESHOLD: { ...thresholdArgs }, + }, + }, +}); + +interpolationTools.set('ThresholdCircle', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + strategySpecificConfiguration: { + ...configuration.strategySpecificConfiguration, + THRESHOLD: { ...thresholdArgs }, + }, + }, +}); + +interpolationTools.set('ThresholdSphere', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'THRESHOLD_INSIDE_SPHERE', + strategySpecificConfiguration: { + ...configuration.strategySpecificConfiguration, + THRESHOLD: { ...thresholdArgs }, + }, + }, +}); + +interpolationTools.set('CircularEraser', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'ERASE_INSIDE_CIRCLE', + }, +}); + +interpolationTools.set('SphereBrush', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'FILL_INSIDE_SPHERE', + }, +}); +interpolationTools.set('SphereEraser', { + baseTool: BrushTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'ERASE_INSIDE_SPHERE', + }, +}); +interpolationTools.set('ScissorsEraser', { + baseTool: SphereScissorsTool.toolName, + configuration: { + ...configuration, + activeStrategy: 'ERASE_INSIDE', + }, +}); + +const optionsValues = [ + ...interpolationTools.keys(), + RectangleScissorsTool.toolName, + CircleScissorsTool.toolName, + SphereScissorsTool.toolName, + PaintFillTool.toolName, +]; + +// ============================= // +addDropdownToToolbar({ + options: { values: optionsValues, defaultValue: BrushTool.toolName }, + onSelectedValueChange: (nameAsStringOrNumber) => { + const name = String(nameAsStringOrNumber); + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + + // Set the currently active tool disabled + const toolName = toolGroup.getActivePrimaryMouseButtonTool(); + + if (toolName) { + toolGroup.setToolDisabled(toolName); + } + + toolGroup.setToolActive(name, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + }, +}); + +addDropdownToToolbar({ + options: { + values: Array.from(thresholdOptions.keys()), + defaultValue: defaultThresholdOption, + }, + onSelectedValueChange: (nameAsStringOrNumber) => { + const name = String(nameAsStringOrNumber); + + const thresholdArgs = thresholdOptions.get(name); + + segmentationUtils.setBrushThresholdForToolGroup( + toolGroupId, + thresholdArgs.threshold, + thresholdArgs + ); + }, +}); + +addSliderToToolbar({ + title: 'Brush Size', + range: [5, 100], + defaultValue: DEFAULT_BRUSH_SIZE, + onSelectedValueChange: (valueAsStringOrNumber) => { + const value = Number(valueAsStringOrNumber); + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, value); + }, +}); + +// ============================= // +addDropdownToToolbar({ + options: { values: ['1', '2', '3'], defaultValue: '1' }, + labelText: 'Segment', + onSelectedValueChange: (segmentIndex) => { + segmentation.segmentIndex.setActiveSegmentIndex( + segmentationId, + Number(segmentIndex) + ); + }, +}); + +addButtonToToolbar({ + title: 'Statistics 1,2,3', + onClick: () => calculateStatistics(statsIds[2], [1, 2, 3]), +}); + +function displayStat(stat) { + if (!stat) { + return; + } + return `${stat.label || stat.name}: ${roundNumber(stat.value)} ${ + stat.unit ? stat.unit : '' + }`; +} + +function calculateStatistics(id, indices) { + const [viewport] = viewports; + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + const activeName = toolGroup.getActivePrimaryMouseButtonTool(); + const brush = toolGroup.getToolInstance(activeName); + const stats = brush.getStatistics(viewport.element, { indices }); + const items = [`Statistics on ${indices.join(', ')}`]; + stats.count.label = 'Voxels'; + const lesionGlycolysis = { + name: 'Lesion Glycolysis', + value: stats.volume.value * stats.stdDev.value, + unit: 'HU \xB7 mm \xb3', + }; + stats.stdDev.label = 'SUV'; + items.push( + displayStat(stats.volume), + displayStat(stats.count), + displayStat(stats.stdDev), + displayStat(lesionGlycolysis), + displayStat(stats.mean), + displayStat(stats.max), + displayStat(stats.min) + ); + const statsDiv = document.getElementById(id); + statsDiv.innerHTML = items.map((span) => `${span}
\n`).join('\n'); +} + +let timeoutId; + +function segmentationModifiedCallback(evt) { + const { detail } = evt; + if (!detail) { + return; + } + if (timeoutId) { + window.clearTimeout(timeoutId); + timeoutId = null; + } + const { segmentIndex } = detail; + if (!segmentIndex) { + // Both undefined and 0 segment indices are returns + return; + } + const statsId = statsIds[segmentIndex === 255 ? 1 : 0]; + + window.setTimeout(() => { + timeoutId = null; + calculateStatistics(statsId, [segmentIndex]); + }, 100); +} + +// ============================= // + +async function addSegmentationsToState() { + // Create a segmentation of the same resolution as the source data + await volumeLoader.createAndCacheDerivedSegmentationVolume(volumeId, { + volumeId: segmentationId, + // The following doesn't quite work yet + // TODO, allow RLE to be used instead of scalars. + // targetBuffer: { type: 'none' }, + // voxelRepresentation: VoxelManagerEnum.RLE, + }); + + // Add the segmentations to state + segmentation.addSegmentations([ + { + segmentationId, + representation: { + // The type of segmentation + type: csToolsEnums.SegmentationRepresentations.Labelmap, + // The actual segmentation data, in the case of labelmap this is a + // reference to the source volume of the segmentation. + data: { + volumeId: segmentationId, + }, + }, + }, + ]); + + eventTarget.addEventListener( + Events.SEGMENTATION_DATA_MODIFIED, + segmentationModifiedCallback + ); +} + +/** + * Runs the demo + */ +async function run() { + // Init Cornerstone and related libraries + await initDemo(); + + utilities.imageRetrieveMetadataProvider.add( + 'volume', + ProgressiveRetrieveImages.interleavedRetrieveStages + ); + + // Add tools to Cornerstone3D + cornerstoneTools.addTool(SegmentationDisplayTool); + cornerstoneTools.addTool(RectangleScissorsTool); + cornerstoneTools.addTool(CircleScissorsTool); + cornerstoneTools.addTool(SphereScissorsTool); + cornerstoneTools.addTool(PaintFillTool); + cornerstoneTools.addTool(BrushTool); + + // Define tool groups to add the segmentation display tool to + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + + // Manipulation Tools + addManipulationBindings(toolGroup); + + // Segmentation Tools + toolGroup.addTool(SegmentationDisplayTool.toolName); + toolGroup.addTool(RectangleScissorsTool.toolName); + toolGroup.addTool(CircleScissorsTool.toolName); + toolGroup.addTool(SphereScissorsTool.toolName); + toolGroup.addTool(PaintFillTool.toolName); + toolGroup.addTool(BrushTool.toolName); + + for (const [toolName, config] of interpolationTools.entries()) { + if (config.baseTool) { + toolGroup.addToolInstance( + toolName, + config.baseTool, + config.configuration + ); + } else { + toolGroup.addTool(toolName, config.configuration); + } + if (config.passive) { + // This can be applied during add/remove contours + toolGroup.setToolPassive(toolName); + } + } + + toolGroup.setToolEnabled(SegmentationDisplayTool.toolName); + + toolGroup.setToolActive(interpolationTools.keys().next().value, { + bindings: [{ mouseButton: MouseBindings.Primary }], + }); + + // Get Cornerstone imageIds for the source data and fetch metadata into RAM + const imageIds = await createImageIdsAndCacheMetaData({ + StudyInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463', + SeriesInstanceUID: + '1.3.6.1.4.1.14519.5.2.1.7009.2403.226151125820845824875394858561', + wadoRsRoot: + getLocalUrl() || 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + }); + + // Define a volume in memory + const volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds, + }); + + // Add some segmentations based on the source data volume + await addSegmentationsToState(); + + // Instantiate a rendering engine + const renderingEngineId = 'myRenderingEngine'; + const renderingEngine = new RenderingEngine(renderingEngineId); + + // Create the viewports + const viewportId1 = 'CT_AXIAL'; + const viewportId2 = 'CT_SAGITTAL'; + const viewportId3 = 'CT_CORONAL'; + + const viewportInputArray = [ + { + viewportId: viewportId1, + type: ViewportType.ORTHOGRAPHIC, + element: element1, + defaultOptions: { + orientation: Enums.OrientationAxis.AXIAL, + background: [0, 0, 0], + }, + }, + { + viewportId: viewportId2, + type: ViewportType.ORTHOGRAPHIC, + element: element2, + defaultOptions: { + orientation: Enums.OrientationAxis.SAGITTAL, + background: [0, 0, 0], + }, + }, + { + viewportId: viewportId3, + type: ViewportType.ORTHOGRAPHIC, + element: element3, + defaultOptions: { + orientation: Enums.OrientationAxis.CORONAL, + background: [0, 0, 0], + }, + }, + ]; + + renderingEngine.setViewports(viewportInputArray); + + toolGroup.addViewport(viewportId1, renderingEngineId); + toolGroup.addViewport(viewportId2, renderingEngineId); + toolGroup.addViewport(viewportId3, renderingEngineId); + + viewports.push(...renderingEngine.getViewports()); + + // Set the volume to load + volume.load(); + + // Set volumes on the viewports + await setVolumesForViewports( + renderingEngine, + [{ volumeId, callback: setCtTransferFunctionForVolumeActor }], + [viewportId1, viewportId2, viewportId3] + ); + + // Add the segmentation representation to the toolgroup + await segmentation.addSegmentationRepresentations(toolGroupId, [ + { + segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + }, + ]); + segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, 1); + + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, DEFAULT_BRUSH_SIZE); + + // Render the image + renderingEngine.renderViewports([viewportId1, viewportId2, viewportId3]); +} + +run(); diff --git a/packages/tools/src/enums/StrategyCallbacks.ts b/packages/tools/src/enums/StrategyCallbacks.ts index 7c13b83a42..c0bb140c60 100644 --- a/packages/tools/src/enums/StrategyCallbacks.ts +++ b/packages/tools/src/enums/StrategyCallbacks.ts @@ -50,6 +50,9 @@ enum StrategyCallbacks { /** inner circle size */ ComputeInnerCircleRadius = 'computeInnerCircleRadius', + + /** Compute statistics on this instance */ + GetStatistics = 'getStatistics', } export default StrategyCallbacks; diff --git a/packages/tools/src/stateManagement/segmentation/triggerSegmentationEvents.ts b/packages/tools/src/stateManagement/segmentation/triggerSegmentationEvents.ts index 56cbd73306..2c9241f8cc 100644 --- a/packages/tools/src/stateManagement/segmentation/triggerSegmentationEvents.ts +++ b/packages/tools/src/stateManagement/segmentation/triggerSegmentationEvents.ts @@ -133,15 +133,19 @@ function triggerSegmentationModified(segmentationId?: string): void { /** * Trigger an event that a segmentation data has been modified - * @param segmentationId - The Id of segmentation + * @param segmentIndex - the primary segment index modified. This can + * be set to a value that the user is actively using - that doesn't + * mean other segments aren't touched, just that the specified one is primary. */ function triggerSegmentationDataModified( segmentationId: string, - modifiedSlicesToUse?: number[] + modifiedSlicesToUse?: number[], + segmentIndex?: number ): void { const eventDetail: SegmentationDataModifiedEventDetail = { segmentationId, modifiedSlicesToUse, + segmentIndex, }; // set it to dirty to force the next call to getUniqueSegmentIndices to diff --git a/packages/tools/src/tools/annotation/CircleROITool.ts b/packages/tools/src/tools/annotation/CircleROITool.ts index efba98d01e..c889d56557 100644 --- a/packages/tools/src/tools/annotation/CircleROITool.ts +++ b/packages/tools/src/tools/annotation/CircleROITool.ts @@ -965,7 +965,7 @@ class CircleROITool extends AnnotationTool { modalityUnitOptions ); - const pointsInShape = pointInShapeCallback( + pointInShapeCallback( imageData, (pointLPS) => pointInEllipse(ellipseObj, pointLPS, { @@ -982,9 +982,9 @@ class CircleROITool extends AnnotationTool { area, mean: stats.mean?.value, max: stats.max?.value, + pointsInShape: stats.pointsInShape.points, stdDev: stats.stdDev?.value, statsArray: stats.array, - pointsInShape: pointsInShape, isEmptyArea, areaUnit: getCalibratedAreaUnits(null, image), radius: worldWidth / 2 / scale, diff --git a/packages/tools/src/tools/annotation/EllipticalROITool.ts b/packages/tools/src/tools/annotation/EllipticalROITool.ts index 449e188075..fad0fe5152 100644 --- a/packages/tools/src/tools/annotation/EllipticalROITool.ts +++ b/packages/tools/src/tools/annotation/EllipticalROITool.ts @@ -1087,7 +1087,7 @@ class EllipticalROITool extends AnnotationTool { modalityUnitOptions ); - const pointsInShape = pointInShapeCallback( + pointInShapeCallback( imageData, (pointLPS) => pointInEllipse(ellipseObj, pointLPS, { fast: true }), this.configuration.statsCalculator.statsCallback, @@ -1101,9 +1101,9 @@ class EllipticalROITool extends AnnotationTool { area, mean: stats.mean?.value, max: stats.max?.value, + pointsInShape: stats.pointsInShape.points, stdDev: stats.stdDev?.value, statsArray: stats.array, - pointsInShape, isEmptyArea, areaUnit: getCalibratedAreaUnits(null, image), modalityUnit, diff --git a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts index 743a92b45f..cb24fff8ab 100644 --- a/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts +++ b/packages/tools/src/tools/annotation/PlanarFreehandROITool.ts @@ -777,9 +777,9 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { let curRow = 0; let intersections = []; let intersectionCounter = 0; - const pointsInShape = pointInShapeCallback( + pointInShapeCallback( imageData, - (pointLPS, pointIJK) => { + (pointLPS) => { let result = true; const point = viewport.worldToCanvas(pointLPS); if (point[1] != curRow) { @@ -839,7 +839,8 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool { max: stats.max?.value, stdDev: stats.stdDev?.value, statsArray: stats.array, - pointsInShape: pointsInShape, + pointsInShape: stats.pointsInShape.points, + areaUnit: getCalibratedAreaUnits(null, image), modalityUnit, }; diff --git a/packages/tools/src/tools/annotation/RectangleROITool.ts b/packages/tools/src/tools/annotation/RectangleROITool.ts index a74874039c..8f91a77275 100644 --- a/packages/tools/src/tools/annotation/RectangleROITool.ts +++ b/packages/tools/src/tools/annotation/RectangleROITool.ts @@ -942,7 +942,7 @@ class RectangleROITool extends AnnotationTool { modalityUnitOptions ); - const pointsInShape = pointInShapeCallback( + pointInShapeCallback( imageData, () => true, this.configuration.statsCalculator.statsCallback, @@ -958,7 +958,8 @@ class RectangleROITool extends AnnotationTool { stdDev: stats.stdDev?.value, max: stats.max?.value, statsArray: stats.array, - pointsInShape: pointsInShape, + pointsInShape: stats.pointsInShape.points, + areaUnit: getCalibratedAreaUnits(null, image), modalityUnit, }; diff --git a/packages/tools/src/tools/base/BaseTool.ts b/packages/tools/src/tools/base/BaseTool.ts index 4596b9916d..1b4180e0b4 100644 --- a/packages/tools/src/tools/base/BaseTool.ts +++ b/packages/tools/src/tools/base/BaseTool.ts @@ -102,8 +102,9 @@ abstract class BaseTool implements IBaseTool { public applyActiveStrategyCallback( enabledElement: Types.IEnabledElement, operationData: unknown, - callbackType: StrategyCallbacks | string - ): any { + callbackType: StrategyCallbacks | string, + ...extraArgs + ) { const { strategies, activeStrategy } = this.configuration; if (!strategies[activeStrategy]) { @@ -115,7 +116,8 @@ abstract class BaseTool implements IBaseTool { return strategies[activeStrategy][callbackType]?.call( this, enabledElement, - operationData + operationData, + ...extraArgs ); } diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index 2679a96f76..0529e7033c 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -12,6 +12,7 @@ import { BaseTool } from '../base'; import { fillInsideSphere, thresholdInsideSphere, + thresholdInsideSphereIsland, } from './strategies/fillSphere'; import { eraseInsideSphere } from './strategies/eraseSphere'; import { @@ -98,13 +99,37 @@ class BrushTool extends BaseTool { supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { strategies: { + /** Perform fill of the active segment index inside a (2d) circle */ FILL_INSIDE_CIRCLE: fillInsideCircle, + /** Erase (to 0) inside a circle */ ERASE_INSIDE_CIRCLE: eraseInsideCircle, + /** Fill a 3d sphere with the active segment index */ FILL_INSIDE_SPHERE: fillInsideSphere, + /** Erase inside a 3d sphere, clearing any segment index (to 0) */ ERASE_INSIDE_SPHERE: eraseInsideSphere, + /** + * Threshold inside a circle, either with a dynamic threshold value + * based on the voxels in a 2d plane around the center click. + * Performs island removal. + */ THRESHOLD_INSIDE_CIRCLE: thresholdInsideCircle, + /** + * Threshold inside a sphere, either dynamic or pre-configured. + * For dynamic, base the threshold on a 2d CIRCLE around the center click. + * Do not perform island removal (this may be slow) + * Users may see delays dragging the sphere for large radius values and + * for complex mixtures of texture. + */ THRESHOLD_INSIDE_SPHERE: thresholdInsideSphere, + /** + * Threshold inside a sphere, but also include island removal. + * The current implementation of this is fairly fast now, but users may + * see delays when island removal occurs on large sections of the volume. + */ + THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL: + thresholdInsideSphereIsland, }, + strategySpecificConfiguration: { THRESHOLD: { threshold: [-150, -70], // E.g. CT Fat // Only used during threshold strategies. @@ -136,14 +161,6 @@ class BrushTool extends BaseTool { }, ], }, - [StrategyCallbacks.RejectPreview]: { - method: StrategyCallbacks.RejectPreview, - bindings: [ - { - key: 'Escape', - }, - ], - }, }, }, } @@ -634,6 +651,21 @@ class BrushTool extends BaseTool { } }; + public getStatistics(element, segmentIndices?) { + if (!element) { + return; + } + const enabledElement = getEnabledElement(element); + const stats = this.applyActiveStrategyCallback( + enabledElement, + this.getOperationData(element), + StrategyCallbacks.GetStatistics, + segmentIndices + ); + + return stats; + } + /** * Cancels any preview view being shown, resetting any segments being shown. */ diff --git a/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts b/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts index 88f92861cd..ca645d2903 100644 --- a/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts +++ b/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts @@ -541,16 +541,16 @@ class CircleROIStartEndThresholdTool extends CircleROITool { zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2, }; - const pointsInShape = pointInShapeCallback( + const points = []; + + pointInShapeCallback( imageData, //@ts-ignore (pointLPS) => pointInEllipse(ellipseObj, pointLPS), - null, + ({ pointLPS }) => points.push(pointLPS.slice()), boundsIJK ); - - //@ts-ignore - pointsInsideVolume.push(pointsInShape); + pointsInsideVolume.push(points); } } data.cachedStats.pointsInVolume = pointsInsideVolume; diff --git a/packages/tools/src/tools/segmentation/PaintFillTool.ts b/packages/tools/src/tools/segmentation/PaintFillTool.ts index 0ff18e526e..07a66b27be 100644 --- a/packages/tools/src/tools/segmentation/PaintFillTool.ts +++ b/packages/tools/src/tools/segmentation/PaintFillTool.ts @@ -194,7 +194,8 @@ class PaintFillTool extends BaseTool { fixedDimensionValue: number, floodFillResult: FloodFillResult ): number[] => { - const { boundaries } = floodFillResult; + // TODO - call the boundary function as it proceeds + const { flooded: boundaries } = floodFillResult; if (fixedDimension === 2) { return [fixedDimensionValue]; diff --git a/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts b/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts index 3fed236a05..e9e6a84291 100644 --- a/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts +++ b/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts @@ -380,15 +380,15 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool { [kMin, kMax], ] as [Types.Point2, Types.Point2, Types.Point2]; - const pointsInShape = pointInShapeCallback( + const points = []; + pointInShapeCallback( imageData, () => true, - null, + ({ pointLPS }) => points.push(pointLPS.slice()), boundsIJK ); - //@ts-ignore - pointsInsideVolume.push(pointsInShape); + pointsInsideVolume.push(points); } } data.cachedStats.pointsInVolume = pointsInsideVolume; diff --git a/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts b/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts index 8d2f3990b3..d6961ec50c 100644 --- a/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts +++ b/packages/tools/src/tools/segmentation/strategies/BrushStrategy.ts @@ -114,6 +114,9 @@ export default class BrushStrategy { [StrategyCallbacks.ComputeInnerCircleRadius]: addListMethod( StrategyCallbacks.ComputeInnerCircleRadius ), + [StrategyCallbacks.GetStatistics]: addSingletonMethod( + StrategyCallbacks.GetStatistics + ), // Add other exposed fields below // initializers is exposed on the function to allow extension of the composition object compositions: null, @@ -187,15 +190,20 @@ export default class BrushStrategy { segmentationVoxelManager, previewVoxelManager, previewSegmentIndex, + segmentIndex, } = initializedData; + const isPreview = + previewSegmentIndex && previewVoxelManager.modifiedSlices.size; + triggerSegmentationDataModified( initializedData.segmentationId, - segmentationVoxelManager.getArrayOfSlices() + segmentationVoxelManager.getArrayOfSlices(), + isPreview ? previewSegmentIndex : segmentIndex ); // We are only previewing if there is a preview index, and there is at // least one slice modified - if (!previewSegmentIndex || !previewVoxelManager.modifiedSlices.size) { + if (!isPreview) { return null; } // Use the original initialized data set to preserve preview info @@ -242,7 +250,7 @@ export default class BrushStrategy { } = data; const previewVoxelManager = operationData.preview?.previewVoxelManager || - VoxelManager.createHistoryVoxelManager(segmentationVoxelManager); + VoxelManager.createRLEHistoryVoxelManager(segmentationVoxelManager); const previewEnabled = !!operationData.previewColors; const previewSegmentIndex = previewEnabled ? 255 : undefined; @@ -383,11 +391,11 @@ function addSingletonMethod(name: string, isInitialized = true) { } brushStrategy[name] = isInitialized ? func - : (enabledElement, operationData) => { + : (enabledElement, operationData, ...args) => { // Store the enabled element in the operation data so we can use single // argument calls operationData.enabledElement = enabledElement; - return func.call(brushStrategy, operationData); + return func.call(brushStrategy, operationData, ...args); }; }; } diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts index f8025a555b..e0da6a0e5d 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts @@ -21,6 +21,7 @@ export default { segmentationVoxelManager: segmentationVoxelManager, imageVoxelManager: imageVoxelManager, segmentIndex, + viewport, } = operationData; const { THRESHOLD } = strategySpecificConfiguration; @@ -37,6 +38,8 @@ export default { const { boundsIJK } = segmentationVoxelManager; const { threshold: oldThreshold, dynamicRadius = 0 } = THRESHOLD; const useDelta = oldThreshold ? 0 : dynamicRadius; + const { viewPlaneNormal } = viewport.getCamera(); + const nestedBounds = boundsIJK.map((ijk, idx) => { const [min, max] = ijk; return [ @@ -44,10 +47,25 @@ export default { Math.min(max, centerIJK[idx] + useDelta), ]; }) as BoundsIJK; + // Squash the bounds to the plane in view when it is orthogonal, or close + // to orthogonal to one of the bounding planes. + // Otherwise just use the full area for now. + if (Math.abs(viewPlaneNormal[0]) > 0.8) { + nestedBounds[0] = [centerIJK[0], centerIJK[0]]; + } else if (Math.abs(viewPlaneNormal[1]) > 0.8) { + nestedBounds[1] = [centerIJK[1], centerIJK[1]]; + } else if (Math.abs(viewPlaneNormal[2]) > 0.8) { + nestedBounds[2] = [centerIJK[2], centerIJK[2]]; + } const threshold = oldThreshold || [Infinity, -Infinity]; // TODO - threshold on all three values separately - const callback = ({ value }) => { + const useDeltaSqr = useDelta * useDelta; + const callback = ({ value, pointIJK }) => { + const distance = vec3.sqrDist(centerIJK, pointIJK); + if (distance > useDeltaSqr) { + return; + } const gray = Array.isArray(value) ? vec3.len(value as any) : value; threshold[0] = Math.min(gray, threshold[0]); threshold[1] = Math.max(gray, threshold[1]); @@ -105,7 +123,9 @@ export default { strategySpecificConfiguration[activeStrategy] = {}; } + // Add a couple of pixels to the radius to make it more obvious what is + // included. strategySpecificConfiguration[activeStrategy].dynamicRadiusInCanvas = - dynamicRadiusInCanvas; + 3 + dynamicRadiusInCanvas; }, }; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/index.ts b/packages/tools/src/tools/segmentation/strategies/compositions/index.ts index 56542944ca..22687a9c75 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/index.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/index.ts @@ -6,6 +6,7 @@ import preview from './preview'; import regionFill from './regionFill'; import setValue from './setValue'; import threshold from './threshold'; +import labelmapStatistics from './labelmapStatistics'; export default { determineSegmentIndex, @@ -16,4 +17,5 @@ export default { regionFill, setValue, threshold, + labelmapStatistics, }; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts b/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts index ba36d090f4..789ae23552 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts @@ -1,7 +1,26 @@ +import { utilities } from '@cornerstonejs/core'; +import type { Types } from '@cornerstonejs/core'; import type { InitializedOperationData } from '../BrushStrategy'; -import floodFill from '../../../../utilities/segmentation/floodFill'; import { triggerSegmentationDataModified } from '../../../../stateManagement/segmentation/triggerSegmentationEvents'; import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; +import normalizeViewportPlane from '../utils/normalizeViewportPlane'; + +const { RLEVoxelMap } = utilities; + +// The maximum size of a dimension on an image in DICOM +// Note, does not work for whole slide imaging +const MAX_IMAGE_SIZE = 65535; + +export enum SegmentationEnum { + // Segment means it is in the segment or preview of interest + SEGMENT = 1, + // Island means it is connected to a selected point + ISLAND = 2, + // Interior means it is inside the island, or possibly inside + INTERIOR = 3, + // Exterior means it is outside the island + EXTERIOR = 4, +} /** * Removes external islands and fills internal islands. @@ -14,165 +33,256 @@ export default { [StrategyCallbacks.OnInteractionEnd]: ( operationData: InitializedOperationData ) => { - const { - previewVoxelManager: previewVoxelManager, - segmentationVoxelManager: segmentationVoxelManager, - strategySpecificConfiguration, - previewSegmentIndex, - segmentIndex, - } = operationData; - - if (!strategySpecificConfiguration.THRESHOLD || segmentIndex === null) { + const { strategySpecificConfiguration, previewSegmentIndex, segmentIndex } = + operationData; + + if ( + !strategySpecificConfiguration.THRESHOLD || + segmentIndex === null || + previewSegmentIndex === undefined + ) { return; } - const clickedPoints = previewVoxelManager.getPoints(); - if (!clickedPoints?.length) { + const segmentSet = createSegmentSet(operationData); + if (!segmentSet) { return; } - - if (previewSegmentIndex === undefined) { + const externalRemoved = removeExternalIslands(operationData, segmentSet); + if (externalRemoved === undefined) { + // Nothing to remove return; } - - // Ensure the bounds includes the clicked points, otherwise the fill - // fails. - const boundsIJK = previewVoxelManager - .getBoundsIJK() - .map((bound, i) => [ - Math.min(bound[0], ...clickedPoints.map((point) => point[i])), - Math.max(bound[1], ...clickedPoints.map((point) => point[i])), - ]); - - if (boundsIJK.find((it) => it[0] < 0 || it[1] > 65535)) { - // Nothing done, so just skip this + const arrayOfSlices = removeInternalIslands(operationData, segmentSet); + if (!arrayOfSlices) { return; } - const floodedSet = new Set(); - // Returns true for new colour, and false otherwise - const getter = (i, j, k) => { - if ( - i < boundsIJK[0][0] || - i > boundsIJK[0][1] || - j < boundsIJK[1][0] || - j > boundsIJK[1][1] || - k < boundsIJK[2][0] || - k > boundsIJK[2][1] - ) { - return -1; - } - const index = segmentationVoxelManager.toIndex([i, j, k]); - if (floodedSet.has(index)) { - // Values already flooded - return -2; - } - const oldVal = segmentationVoxelManager.getAtIndex(index); - const isIn = - oldVal === previewSegmentIndex || oldVal === segmentIndex ? 1 : 0; - if (!isIn) { - segmentationVoxelManager.addPoint(index); - } - // 1 is values that are preview/segment index, 0 is everything else - return isIn; - }; + triggerSegmentationDataModified( + operationData.segmentationId, + arrayOfSlices, + previewSegmentIndex + ); + }, +}; - let floodedCount = 0; +/** + * Creates a segment set - an RLE based map of points to segment data. + * This function returns the data in the appropriate planar orientation according + * to the view, with SegmentationEnum.SEGMENT set for any point within the segment, + * either preview or base segment colour. + * + * Returns undefined if the data is invalid for some reason. + */ +export function createSegmentSet(operationData: InitializedOperationData) { + const { + segmentationVoxelManager, + previewSegmentIndex, + previewVoxelManager, + segmentIndex, + viewport, + } = operationData; + + const clickedPoints = previewVoxelManager.getPoints(); + if (!clickedPoints?.length) { + return; + } + // Ensure the bounds includes the clicked points, otherwise the fill + // fails. + const boundsIJK = previewVoxelManager + .getBoundsIJK() + .map((bound, i) => [ + Math.min(bound[0], ...clickedPoints.map((point) => point[i])), + Math.max(bound[1], ...clickedPoints.map((point) => point[i])), + ]) as Types.BoundsIJK; + + if (boundsIJK.find((it) => it[0] < 0 || it[1] > MAX_IMAGE_SIZE)) { + // Nothing done, so just skip this + return; + } + + // First get the set of points which are directly connected to the points + // that the user clicked on/dragged over. + const { toIJK, fromIJK, boundsIJKPrime, error } = normalizeViewportPlane( + viewport, + boundsIJK + ); + + if (error) { + console.warn( + 'Not performing island removal for planes not orthogonal to acquisition plane', + error + ); + return; + } + + const [width, height, depth] = fromIJK(segmentationVoxelManager.dimensions); + const floodedSet = new RLEVoxelMap(width, height, depth); - const onFlood = (i, j, k) => { - const index = segmentationVoxelManager.toIndex([i, j, k]); - if (floodedSet.has(index)) { - return; + // Returns true for new colour, and false otherwise + const getter = (i, j, k) => { + const index = segmentationVoxelManager.toIndex(toIJK([i, j, k])); + const oldVal = segmentationVoxelManager.getAtIndex(index); + if (oldVal === previewSegmentIndex || oldVal === segmentIndex) { + // Values are initially false for indexed values. + return SegmentationEnum.SEGMENT; + } + }; + floodedSet.fillFrom(getter, boundsIJKPrime); + floodedSet.normalizer = { toIJK, fromIJK, boundsIJKPrime }; + return floodedSet; +} + +/** + * Handle islands which are internal to the flood fill - these are points which + * are surrounded entirely by the filled area. + * Start by getting the island map - that is, the output from the previous + * external island removal. Then, mark all the points in between two islands + * as being "Interior". The set of points marked interior is within a boundary + * point on the left and right, but may still be open above or below. To + * test that, perform a flood fill on the interior points, and see if it is + * entirely contained ('covered') on the top and bottom. + * Note this is done in a planar fashion, that is one plane at a time, but + * covering all planes that have interior data. That removes islands that + * are interior to the currently displayed view to be handled. + */ +function removeInternalIslands( + operationData: InitializedOperationData, + floodedSet +) { + const { height, normalizer } = floodedSet; + const { toIJK } = normalizer; + const { previewVoxelManager, previewSegmentIndex } = operationData; + + floodedSet.forEachRow((baseIndex, row) => { + let lastRle; + for (const rle of [...row]) { + if (rle.value !== SegmentationEnum.ISLAND) { + continue; } - // Fill this point with an indicator that this point is connected - previewVoxelManager.setAtIJK(i, j, k, previewSegmentIndex); - floodedSet.add(index); - floodedCount++; - }; - clickedPoints.forEach((clickedPoint) => { - // @ts-ignore - need to ignore the spread appication to array params - if (getter(...clickedPoint) === 1) { - floodFill(getter, clickedPoint, { - onFlood, - diagonals: true, - }); + if (!lastRle) { + lastRle = rle; + continue; } - }); - - let clearedCount = 0; - let previewCount = 0; - - const callback = ({ index, pointIJK, value: trackValue }) => { - const value = segmentationVoxelManager.getAtIndex(index); - if (floodedSet.has(index)) { - previewCount++; - const newValue = - trackValue === segmentIndex ? segmentIndex : previewSegmentIndex; - previewVoxelManager.setAtIJKPoint(pointIJK, newValue); - } else if (value === previewSegmentIndex) { - clearedCount++; - const newValue = trackValue ?? 0; - previewVoxelManager.setAtIJKPoint(pointIJK, newValue); + for (let iPrime = lastRle.end; iPrime < rle.start; iPrime++) { + floodedSet.set(baseIndex + iPrime, SegmentationEnum.INTERIOR); } - }; - - previewVoxelManager.forEach(callback, {}); - - if (floodedCount - previewCount !== 0) { - console.warn( - 'There were flooded=', - floodedCount, - 'cleared=', - clearedCount, - 'preview count=', - previewCount, - 'not handled', - floodedCount - previewCount + lastRle = rle; + } + }); + // Next, remove the island sets which are adjacent to an opening + floodedSet.forEach((baseIndex, rle) => { + if (rle.value !== SegmentationEnum.INTERIOR) { + // Already filled/handled + return; + } + const [, jPrime, kPrime] = floodedSet.toIJK(baseIndex); + const rowPrev = jPrime > 0 ? floodedSet.getRun(jPrime - 1, kPrime) : null; + const rowNext = + jPrime + 1 < height ? floodedSet.getRun(jPrime + 1, kPrime) : null; + const prevCovers = covers(rle, rowPrev); + const nextCovers = covers(rle, rowNext); + if (rle.end - rle.start > 2 && (!prevCovers || !nextCovers)) { + floodedSet.floodFill( + rle.start, + jPrime, + kPrime, + SegmentationEnum.EXTERIOR, + { singlePlane: true } ); } - const islandMap = new Set(segmentationVoxelManager.points || []); - floodedSet.clear(); + }); - for (const index of islandMap.keys()) { - if (floodedSet.has(index)) { - continue; - } - let isInternal = true; - const internalSet = new Set(); - const onFloodInternal = (i, j, k) => { - const floodIndex = previewVoxelManager.toIndex([i, j, k]); - floodedSet.add(floodIndex); - if ( - (boundsIJK[0][0] !== boundsIJK[0][1] && - (i === boundsIJK[0][0] || i === boundsIJK[0][1])) || - (boundsIJK[1][0] !== boundsIJK[1][1] && - (j === boundsIJK[1][0] || j === boundsIJK[1][1])) || - (boundsIJK[2][0] !== boundsIJK[2][1] && - (k === boundsIJK[2][0] || k === boundsIJK[2][1])) - ) { - isInternal = false; - } - if (isInternal) { - internalSet.add(floodIndex); - } - }; - const pointIJK = previewVoxelManager.toIJK(index); - if (getter(...pointIJK) !== 0) { - continue; + // Finally, for all the islands, fill them in with the preview colour as + // they are now internal + floodedSet.forEach((baseIndex, rle) => { + if (rle.value !== SegmentationEnum.INTERIOR) { + return; + } + for (let iPrime = rle.start; iPrime < rle.end; iPrime++) { + const clearPoint = toIJK(floodedSet.toIJK(baseIndex + iPrime)); + previewVoxelManager.setAtIJKPoint(clearPoint, previewSegmentIndex); + } + }); + return previewVoxelManager.getArrayOfSlices(); +} + +/** + * This part removes external islands. External islands are regions of voxels which + * are not connected to the selected/click points. The algorithm is to + * start with all of the clicked points, performing a flood fill along all + * sections that are within the given segment, replacing the "SEGMENT" + * indicator with a new "ISLAND" indicator. Then, every point in the + * preview that is not marked as ISLAND is now external and can be reset to + * the value it had before the flood fill was initiated. + */ +function removeExternalIslands( + operationData: InitializedOperationData, + floodedSet +) { + const { previewVoxelManager } = operationData; + const { toIJK, fromIJK } = floodedSet.normalizer; + const clickedPoints = previewVoxelManager.getPoints(); + + // Just used to count up how many points got filled. + let floodedCount = 0; + + // First mark everything as island that is connected to a start point + clickedPoints.forEach((clickedPoint) => { + const ijkPrime = fromIJK(clickedPoint); + const index = floodedSet.toIndex(ijkPrime); + const [iPrime, jPrime, kPrime] = ijkPrime; + if (floodedSet.get(index) === SegmentationEnum.SEGMENT) { + floodedCount += floodedSet.floodFill( + iPrime, + jPrime, + kPrime, + SegmentationEnum.ISLAND + ); + } + }); + + if (floodedCount === 0) { + return; + } + // Next, iterate over all points which were set to a new value in the preview + // For everything NOT connected to something in set of clicked points, + // remove it from the preview. + + const callback = (index, rle) => { + const [, jPrime, kPrime] = floodedSet.toIJK(index); + if (rle.value !== SegmentationEnum.ISLAND) { + for (let iPrime = rle.start; iPrime < rle.end; iPrime++) { + const clearPoint = toIJK([iPrime, jPrime, kPrime]); + // preview voxel manager knows to reset on null + previewVoxelManager.setAtIJKPoint(clearPoint, null); } - floodFill(getter, pointIJK, { - onFlood: onFloodInternal, - diagonals: false, - }); - if (isInternal) { - for (const index of internalSet) { - previewVoxelManager.setAtIndex(index, previewSegmentIndex); - } + } + }; + + floodedSet.forEach(callback, { rowModified: true }); + + return floodedCount; +} + +/** + * Determine if the rle `[start...end)` is covered by row completely, by which + * it is meant that the row has RLE elements from the start to the end of the + * RLE section, matching every index i in the start to end. + */ +export function covers(rle, row) { + if (!row) { + return false; + } + let { start } = rle; + const { end } = rle; + for (const rowRle of row) { + if (start >= rowRle.start && start < rowRle.end) { + start = rowRle.end; + if (start >= end) { + return true; } } - triggerSegmentationDataModified( - operationData.segmentationId, - previewVoxelManager.getArrayOfSlices() - ); - }, -}; + } + return false; +} diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts b/packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts new file mode 100644 index 0000000000..424eda6273 --- /dev/null +++ b/packages/tools/src/tools/segmentation/strategies/compositions/labelmapStatistics.ts @@ -0,0 +1,51 @@ +import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; +import type { InitializedOperationData } from '../BrushStrategy'; +import { VolumetricCalculator } from '../../../../utilities/segmentation'; +import { segmentIndex } from '../../../../stateManagement/segmentation'; +import { getStrategyData } from '../utils/getStrategyData'; + +/** + * Compute basic labelmap segmentation statistics. + */ +export default { + [StrategyCallbacks.GetStatistics]: function ( + enabledElement, + operationData: InitializedOperationData, + options?: { indices?: number | number[] } + ) { + const { viewport } = enabledElement; + let { indices } = options; + const { segmentationId } = operationData; + if (!indices) { + indices = [segmentIndex.getActiveSegmentIndex(segmentationId)]; + } else if (!Array.isArray(indices)) { + // Include the preview index + indices = [indices, 255]; + } + const indicesArr = indices as number[]; + + const { + segmentationVoxelManager, + imageVoxelManager, + segmentationImageData, + } = getStrategyData({ + operationData, + viewport, + }); + + const spacing = segmentationImageData.getSpacing(); + // Turning this off more than doubles the speed of the stats collection... + VolumetricCalculator.statsInit({ noPointsCollection: true }); + + segmentationVoxelManager.forEach((voxel) => { + const { value, pointIJK } = voxel; + if (indicesArr.indexOf(value) === -1) { + return; + } + const imageValue = imageVoxelManager.getAtIJKPoint(pointIJK); + VolumetricCalculator.statsCallback({ value: imageValue }); + }); + + return VolumetricCalculator.getStatistics({ spacing }); + }, +}; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts b/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts index c623363dcf..42f9246b92 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/preview.ts @@ -57,7 +57,11 @@ export default { operationData.previewVoxelManager = preview.previewVoxelManager; } - if (segmentIndex === null || !previewSegmentIndex) { + if ( + segmentIndex === undefined || + segmentIndex === null || + !previewSegmentIndex + ) { // Null means to reset the value, so we don't change the preview colour return; } @@ -71,7 +75,9 @@ export default { if (!configColor && !segmentColor) { return; } - const previewColor = configColor || segmentColor.map((it) => it * 0.9); + const previewColor = + configColor || + segmentColor.map((it, idx) => (idx === 3 ? 64 : Math.round(it * 0.9))); segmentationConfig.color.setColorForSegmentIndex( toolGroupId, segmentationRepresentationUID, @@ -85,7 +91,7 @@ export default { ) => { const { segmentationVoxelManager: segmentationVoxelManager, - previewVoxelManager: previewVoxelManager, + previewVoxelManager, previewSegmentIndex, preview, } = operationData; @@ -108,7 +114,8 @@ export default { triggerSegmentationDataModified( operationData.segmentationId, - tracking.getArrayOfSlices() + tracking.getArrayOfSlices(), + preview.segmentIndex ); tracking.clear(); }, @@ -116,10 +123,7 @@ export default { [StrategyCallbacks.RejectPreview]: ( operationData: InitializedOperationData ) => { - const { - previewVoxelManager: previewVoxelManager, - segmentationVoxelManager: segmentationVoxelManager, - } = operationData; + const { previewVoxelManager, segmentationVoxelManager } = operationData; if (previewVoxelManager.modifiedSlices.size === 0) { return; } @@ -129,9 +133,12 @@ export default { }; previewVoxelManager.forEach(callback); + // Primarily rejects back to zero, so use 0 as the segment index - even + // if somtimes it modifies the data to other values on reject. triggerSegmentationDataModified( operationData.segmentationId, - previewVoxelManager.getArrayOfSlices() + previewVoxelManager.getArrayOfSlices(), + 0 ); previewVoxelManager.clear(); }, diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts b/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts index b60473e678..490344da19 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts @@ -14,8 +14,8 @@ export default { segmentsLocked, segmentationImageData, segmentationVoxelManager: segmentationVoxelManager, - previewVoxelManager: previewVoxelManager, - imageVoxelManager: imageVoxelManager, + previewVoxelManager, + imageVoxelManager, brushStrategy, centerIJK, } = operationData; diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts b/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts index df43a1df68..9a5be1427c 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts @@ -16,7 +16,7 @@ export default { const { segmentsLocked, segmentIndex, - previewVoxelManager: previewVoxelManager, + previewVoxelManager, previewSegmentIndex, segmentationVoxelManager: segmentationVoxelManager, } = operationData; diff --git a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts index 5cc4c54f71..4f1a5c6d4c 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillCircle.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillCircle.ts @@ -126,7 +126,8 @@ const CIRCLE_STRATEGY = new BrushStrategy( compositions.setValue, initializeCircle, compositions.determineSegmentIndex, - compositions.preview + compositions.preview, + compositions.labelmapStatistics ); const CIRCLE_THRESHOLD_STRATEGY = new BrushStrategy( @@ -138,7 +139,8 @@ const CIRCLE_THRESHOLD_STRATEGY = new BrushStrategy( compositions.dynamicThreshold, compositions.threshold, compositions.preview, - compositions.islandRemoval + compositions.islandRemoval, + compositions.labelmapStatistics ); /** diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 1d80643161..361c4fb16f 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -70,7 +70,8 @@ const SPHERE_STRATEGY = new BrushStrategy( compositions.setValue, sphereComposition, compositions.determineSegmentIndex, - compositions.preview + compositions.preview, + compositions.labelmapStatistics ); /** @@ -82,6 +83,13 @@ const SPHERE_STRATEGY = new BrushStrategy( const fillInsideSphere = SPHERE_STRATEGY.strategyFunction; const SPHERE_THRESHOLD_STRATEGY = new BrushStrategy( + 'SphereThreshold', + ...SPHERE_STRATEGY.compositions, + compositions.dynamicThreshold, + compositions.threshold +); + +const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy( 'SphereThreshold', ...SPHERE_STRATEGY.compositions, compositions.dynamicThreshold, @@ -97,6 +105,8 @@ const SPHERE_THRESHOLD_STRATEGY = new BrushStrategy( */ const thresholdInsideSphere = SPHERE_THRESHOLD_STRATEGY.strategyFunction; +const thresholdInsideSphereIsland = + SPHERE_THRESHOLD_STRATEGY_ISLAND.strategyFunction; /** * Fill outside a sphere with the given segment index in the given operation data. The @@ -108,4 +118,9 @@ export function fillOutsideSphere(): void { throw new Error('fill outside sphere not implemented'); } -export { fillInsideSphere, thresholdInsideSphere, SPHERE_STRATEGY }; +export { + fillInsideSphere, + thresholdInsideSphere, + SPHERE_STRATEGY, + thresholdInsideSphereIsland, +}; diff --git a/packages/tools/src/tools/segmentation/strategies/utils/normalizeViewportPlane.ts b/packages/tools/src/tools/segmentation/strategies/utils/normalizeViewportPlane.ts new file mode 100644 index 0000000000..13502a32bb --- /dev/null +++ b/packages/tools/src/tools/segmentation/strategies/utils/normalizeViewportPlane.ts @@ -0,0 +1,59 @@ +import type { Types } from '@cornerstonejs/core'; +import { BaseVolumeViewport, utilities } from '@cornerstonejs/core'; + +const { isEqual } = utilities; + +const acquisitionMapping = { + toIJK: (ijkPrime) => ijkPrime, + fromIJK: (ijk) => ijk, + type: 'acquistion', +}; + +const jkMapping = { + toIJK: ([j, k, i]) => [i, j, k], + fromIJK: ([i, j, k]) => [j, k, i], + type: 'jk', +}; + +const ikMapping = { + toIJK: ([i, k, j]) => [i, j, k], + fromIJK: ([i, j, k]) => [i, k, j], + type: 'ik', +}; + +/** + * This function returns a set of functions that normalize the viewport plane + * into `i', j', k'` from the image space `i,j,k` such that + * `i', j'` are within viewport indices corresponding to 1 pixel distance on + * the underlying view space. + * As well, the function returns a dimension for the total view space that + * corresponds to a `[0,dimension)` index for the given bounds. + */ +export default function normalizeViewportPlane( + viewport: Types.IViewport, + boundsIJK: Types.BoundsIJK +) { + if (!(viewport instanceof BaseVolumeViewport)) { + // This is the case for acquisition plane, which includes all non-volume viewports: + return { ...acquisitionMapping, boundsIJKPrime: boundsIJK }; + } + + const { viewPlaneNormal } = viewport.getCamera(); + // This doesn't really handle non-coplanar views, but it sort of works even for those, so leave it for now. + const mapping = + (isEqual(Math.abs(viewPlaneNormal[0]), 1) && jkMapping) || + (isEqual(Math.abs(viewPlaneNormal[1]), 1) && ikMapping) || + (isEqual(Math.abs(viewPlaneNormal[2]), 1) && acquisitionMapping); + if (!mapping) { + // Non-orthogonal to acquisition plane isn't handled, but doesn't prevent + // options from working, so return an error indicator. + return { + toIJK: null, + boundsIJKPrime: null, + fromIJK: null, + error: `Only mappings orthogonal to acquisition plane are permitted, but requested ${viewPlaneNormal}`, + }; + } + + return { ...mapping, boundsIJKPrime: mapping.fromIJK(boundsIJK) }; +} diff --git a/packages/tools/src/types/CalculatorTypes.ts b/packages/tools/src/types/CalculatorTypes.ts index 78814d3540..b4d99efc5c 100644 --- a/packages/tools/src/types/CalculatorTypes.ts +++ b/packages/tools/src/types/CalculatorTypes.ts @@ -1,3 +1,5 @@ +import type { Types } from '@cornerstonejs/core'; + type Statistics = { name: string; label?: string; @@ -8,6 +10,7 @@ type Statistics = { type NamedStatistics = { mean: Statistics & { name: 'mean' }; max: Statistics & { name: 'max' }; + min: Statistics & { name: 'min' }; stdDev: Statistics & { name: 'stdDev' }; stdDevWithSumSquare: Statistics & { name: 'stdDevWithSumSquare' }; count: Statistics & { name: 'count' }; @@ -15,6 +18,8 @@ type NamedStatistics = { volume?: Statistics & { name: 'volume' }; circumferance?: Statistics & { name: 'circumferance' }; array: Statistics[]; + /** The array of points that this statistic is calculated on. */ + pointsInShape?: Types.PointsManager; }; export type { Statistics, NamedStatistics }; diff --git a/packages/tools/src/types/EventTypes.ts b/packages/tools/src/types/EventTypes.ts index 94da7ca655..7cdc7da0ed 100644 --- a/packages/tools/src/types/EventTypes.ts +++ b/packages/tools/src/types/EventTypes.ts @@ -231,6 +231,11 @@ type SegmentationDataModifiedEventDetail = { /** array of slice indices in a labelmap which have been modified */ // TODO: This is labelmap-specific and needs to be a labelmap-specific event modifiedSlicesToUse?: number[]; + /** + * The segment index being modified as a primary action - other segments + * indices may also be modified as a side affect of the primary change. + */ + segmentIndex?: number; }; /** diff --git a/packages/tools/src/types/FloodFillTypes.ts b/packages/tools/src/types/FloodFillTypes.ts index 826b24e9bb..509975058c 100644 --- a/packages/tools/src/types/FloodFillTypes.ts +++ b/packages/tools/src/types/FloodFillTypes.ts @@ -2,7 +2,6 @@ import { Types } from '@cornerstonejs/core'; type FloodFillResult = { flooded: Types.Point2[] | Types.Point3[]; - boundaries: Types.Point2[] | Types.Point3[]; }; type FloodFillGetter3D = (x: number, y: number, z: number) => unknown; @@ -14,6 +13,9 @@ type FloodFillOptions = { onBoundary?: (x: number, y: number, z?: number) => void; equals?: (a, b) => boolean; // Equality operation for your datastructure. Defaults to a === b. diagonals?: boolean; // Whether to flood fill across diagonals. Default false. + bounds?: Map; //Store the bounds + // Return false to exclude + filter?: (point) => boolean; }; export { FloodFillResult, FloodFillGetter, FloodFillOptions }; diff --git a/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts b/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts index cdabbf9493..9bb3dbb951 100644 --- a/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts +++ b/packages/tools/src/utilities/math/basic/BasicStatsCalculator.ts @@ -1,28 +1,42 @@ -import { NamedStatistics, Statistics } from '../../../types'; +import { utilities } from '@cornerstonejs/core'; +import { NamedStatistics } from '../../../types'; import Calculator from './Calculator'; +const { PointsManager } = utilities; + export default class BasicStatsCalculator extends Calculator { private static max = [-Infinity]; + private static min = [-Infinity]; private static sum = [0]; private static sumSquares = [0]; private static squaredDiffSum = [0]; private static count = 0; + // Collect the points to be returned + private static pointsInShape = PointsManager.create3(1024); + + public static statsInit(options: { noPointsCollection: boolean }) { + if (options.noPointsCollection) { + this.pointsInShape = null; + } + } /** * This callback is used when we verify if the point is in the annotion drawn so we can get every point * in the shape to calculate the statistics * @param value of the point in the shape of the annotation */ - static statsCallback = ({ value: newValue }): void => { + static statsCallback = ({ value: newValue, pointLPS = null }): void => { if ( Array.isArray(newValue) && newValue.length > 1 && this.max.length === 1 ) { this.max.push(this.max[0], this.max[0]); + this.min.push(this.min[0], this.min[0]); this.sum.push(this.sum[0], this.sum[0]); this.sumSquares.push(this.sumSquares[0], this.sumSquares[0]); this.squaredDiffSum.push(this.squaredDiffSum[0], this.squaredDiffSum[0]); + this.pointsInShape?.push(pointLPS); } const newArray = Array.isArray(newValue) ? newValue : [newValue]; @@ -31,6 +45,9 @@ export default class BasicStatsCalculator extends Calculator { this.max.forEach( (it, idx) => (this.max[idx] = Math.max(it, newArray[idx])) ); + this.min.forEach( + (it, idx) => (this.min[idx] = Math.min(it, newArray[idx])) + ); this.sum.map((it, idx) => (this.sum[idx] += newArray[idx])); this.sumSquares.map( (it, idx) => (this.sumSquares[idx] += newArray[idx] ** 2) @@ -42,6 +59,7 @@ export default class BasicStatsCalculator extends Calculator { 2 )) ); + this.pointsInShape = PointsManager.create3(1024); }; /** @@ -54,7 +72,7 @@ export default class BasicStatsCalculator extends Calculator { * array : An array of hte above values, in order. */ - static getStatistics = (): NamedStatistics => { + static getStatistics = (options?: { unit: string }): NamedStatistics => { const mean = this.sum.map((sum) => sum / this.count); const stdDev = this.squaredDiffSum.map((squaredDiffSum) => Math.sqrt(squaredDiffSum / this.count) @@ -63,29 +81,37 @@ export default class BasicStatsCalculator extends Calculator { Math.sqrt(this.sumSquares[idx] / this.count - mean[idx] ** 2) ); + const unit = options?.unit || null; + const named: NamedStatistics = { max: { name: 'max', label: 'Max Pixel', value: singleArrayAsNumber(this.max), - unit: null, + unit, + }, + min: { + name: 'min', + label: 'Min Pixel', + value: singleArrayAsNumber(this.min), + unit, }, mean: { name: 'mean', label: 'Mean Pixel', value: singleArrayAsNumber(mean), - unit: null, + unit, }, stdDev: { name: 'stdDev', label: 'Standard Deviation', value: singleArrayAsNumber(stdDev), - unit: null, + unit, }, stdDevWithSumSquare: { name: 'stdDevWithSumSquare', value: singleArrayAsNumber(stdDevWithSumSquare), - unit: null, + unit, }, count: { name: 'count', @@ -93,6 +119,7 @@ export default class BasicStatsCalculator extends Calculator { value: this.count, unit: null, }, + pointsInShape: this.pointsInShape, array: [], }; named.array.push( @@ -104,6 +131,7 @@ export default class BasicStatsCalculator extends Calculator { ); this.max = [-Infinity]; + this.min = [Infinity]; this.sum = [0]; this.sumSquares = [0]; this.squaredDiffSum = [0]; diff --git a/packages/tools/src/utilities/pointInShapeCallback.ts b/packages/tools/src/utilities/pointInShapeCallback.ts index b401f0c36e..a4d0608514 100644 --- a/packages/tools/src/utilities/pointInShapeCallback.ts +++ b/packages/tools/src/utilities/pointInShapeCallback.ts @@ -1,8 +1,10 @@ import { vec3 } from 'gl-matrix'; -import type { Types } from '@cornerstonejs/core'; +import { Types, utilities } from '@cornerstonejs/core'; import type { vtkImageData } from '@kitware/vtk.js/Common/DataModel/ImageData'; import BoundsIJK from '../types/BoundsIJK'; +const { PointsManager } = utilities; + export type PointInShape = { value: number; index: number; @@ -18,18 +20,99 @@ export type PointInShapeCallback = ({ }: { value: number; index: number; - pointIJK: vec3; - pointLPS: vec3; + pointIJK: Types.Point3; + pointLPS: Types.Point3; }) => void; export type ShapeFnCriteria = (pointLPS: vec3, pointIJK: vec3) => boolean; +/** + * Returns a function that takes an ijk position and efficiently returns + * the world position. Only works for integer ijk, AND values within the bounds. + * The position array is re-used, so don't preserve it/compare for different + * values, although you can provide an instance position to copy into. + * + * This function is safe to use out of order, and is stable in terms of calculations. + */ +export function createPositionCallback(imageData) { + const currentPos = vec3.create(); + const dimensions = imageData.getDimensions(); + const positionI = PointsManager.create3(dimensions[0]); + const positionJ = PointsManager.create3(dimensions[1]); + const positionK = PointsManager.create3(dimensions[2]); + + const direction = imageData.getDirection(); + const rowCosines = direction.slice(0, 3); + const columnCosines = direction.slice(3, 6); + const scanAxisNormal = direction.slice(6, 9); + + const spacing = imageData.getSpacing(); + const [rowSpacing, columnSpacing, scanAxisSpacing] = spacing; + + // @ts-ignore will be fixed in vtk-master + const worldPosStart = imageData.indexToWorld([0, 0, 0]); + + const rowStep = vec3.fromValues( + rowCosines[0] * rowSpacing, + rowCosines[1] * rowSpacing, + rowCosines[2] * rowSpacing + ); + + const columnStep = vec3.fromValues( + columnCosines[0] * columnSpacing, + columnCosines[1] * columnSpacing, + columnCosines[2] * columnSpacing + ); + + const scanAxisStep = vec3.fromValues( + scanAxisNormal[0] * scanAxisSpacing, + scanAxisNormal[1] * scanAxisSpacing, + scanAxisNormal[2] * scanAxisSpacing + ); + + const scaled = vec3.create(); + // Add the world position start to the I component so we don't need to add it + for (let i = 0; i < dimensions[0]; i++) { + positionI.push( + vec3.add( + scaled, + worldPosStart, + vec3.scale(scaled, rowStep, i) + ) as Types.Point3 + ); + } + for (let j = 0; j < dimensions[0]; j++) { + positionJ.push(vec3.scale(scaled, columnStep, j) as Types.Point3); + } + for (let k = 0; k < dimensions[0]; k++) { + positionK.push(vec3.scale(scaled, scanAxisStep, k) as Types.Point3); + } + + const dataI = positionI.getTypedArray(); + const dataJ = positionJ.getTypedArray(); + const dataK = positionK.getTypedArray(); + + return (ijk, destPoint = currentPos) => { + const [i, j, k] = ijk; + const offsetI = i * 3; + const offsetJ = j * 3; + const offsetK = k * 3; + destPoint[0] = dataI[offsetI] + dataJ[offsetJ] + dataK[offsetK]; + destPoint[1] = dataI[offsetI + 1] + dataJ[offsetJ + 1] + dataK[offsetK + 1]; + destPoint[2] = dataI[offsetI + 2] + dataJ[offsetJ + 2] + dataK[offsetK + 2]; + return destPoint as Types.Point3; + }; +} + /** * For each point in the image (If boundsIJK is not provided, otherwise, for each * point in the provided bounding box), It runs the provided callback IF the point * passes the provided criteria to be inside the shape (which is defined by the * provided pointInShapeFn) * + * You must record points in the callback function if you wish to have an array + * of the called points. + * * @param imageData - The image data object. * @param dimensions - The dimensions of the image. * @param pointInShapeFn - A function that takes a point in LPS space and returns @@ -41,9 +124,9 @@ export type ShapeFnCriteria = (pointLPS: vec3, pointIJK: vec3) => boolean; export default function pointInShapeCallback( imageData: vtkImageData | Types.CPUImageData, pointInShapeFn: ShapeFnCriteria, - callback?: PointInShapeCallback, + callback: PointInShapeCallback, boundsIJK?: BoundsIJK -): Array { +) { let iMin, iMax, jMin, jMax, kMin, kMax; let scalarData; @@ -72,36 +155,8 @@ export default function pointInShapeCallback( [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK; } - const start = vec3.fromValues(iMin, jMin, kMin); - - const direction = imageData.getDirection(); - const rowCosines = direction.slice(0, 3); - const columnCosines = direction.slice(3, 6); - const scanAxisNormal = direction.slice(6, 9); - - const spacing = imageData.getSpacing(); - const [rowSpacing, columnSpacing, scanAxisSpacing] = spacing; - - // @ts-ignore will be fixed in vtk-master - const worldPosStart = imageData.indexToWorld(start); - - const rowStep = vec3.fromValues( - rowCosines[0] * rowSpacing, - rowCosines[1] * rowSpacing, - rowCosines[2] * rowSpacing - ); - - const columnStep = vec3.fromValues( - columnCosines[0] * columnSpacing, - columnCosines[1] * columnSpacing, - columnCosines[2] * columnSpacing - ); - - const scanAxisStep = vec3.fromValues( - scanAxisNormal[0] * scanAxisSpacing, - scanAxisNormal[1] * scanAxisSpacing, - scanAxisNormal[2] * scanAxisSpacing - ); + const indexToWorld = createPositionCallback(imageData); + const pointIJK = [0, 0, 0] as Types.Point3; const xMultiple = numComps || @@ -109,22 +164,21 @@ export default function pointInShapeCallback( const yMultiple = dimensions[0] * xMultiple; const zMultiple = dimensions[1] * yMultiple; - const pointsInShape: Array = []; - - const currentPos = vec3.clone(worldPosStart); - for (let k = kMin; k <= kMax; k++) { - const startPosJ = vec3.clone(currentPos); + pointIJK[2] = k; + const indexK = k * zMultiple; for (let j = jMin; j <= jMax; j++) { - const startPosI = vec3.clone(currentPos); + pointIJK[1] = j; + const indexJK = indexK + j * yMultiple; for (let i = iMin; i <= iMax; i++) { - const pointIJK: Types.Point3 = [i, j, k]; + pointIJK[0] = i; + const pointLPS = indexToWorld(pointIJK); // The current world position (pointLPS) is now in currentPos - if (pointInShapeFn(currentPos as Types.Point3, pointIJK)) { - const index = k * zMultiple + j * yMultiple + i * xMultiple; + if (pointInShapeFn(pointLPS, pointIJK)) { + const index = indexJK + i * xMultiple; let value; if (xMultiple > 2) { value = [ @@ -136,30 +190,9 @@ export default function pointInShapeCallback( value = scalarData[index]; } - pointsInShape.push({ - value, - index, - pointIJK, - pointLPS: currentPos.slice(), - }); - if (callback) { - callback({ value, index, pointIJK, pointLPS: currentPos }); - } + callback({ value, index, pointIJK, pointLPS }); } - - // Increment currentPos by rowStep for the next iteration - vec3.add(currentPos, currentPos, rowStep); } - - // Reset currentPos to the start of the next J line and increment by columnStep - vec3.copy(currentPos, startPosI); - vec3.add(currentPos, currentPos, columnStep); } - - // Reset currentPos to the start of the next K slice and increment by scanAxisStep - vec3.copy(currentPos, startPosJ); - vec3.add(currentPos, currentPos, scanAxisStep); } - - return pointsInShape; } diff --git a/packages/tools/src/utilities/segmentation/VolumetricCalculator.ts b/packages/tools/src/utilities/segmentation/VolumetricCalculator.ts new file mode 100644 index 0000000000..36932a7424 --- /dev/null +++ b/packages/tools/src/utilities/segmentation/VolumetricCalculator.ts @@ -0,0 +1,31 @@ +import { NamedStatistics } from '../../types'; +import { BasicStatsCalculator } from '../math/basic'; + +/** + * A basic stats calculator for volumetric data, generally for use with + * segmentations. + */ +export default class VolumetricCalculator extends BasicStatsCalculator { + public static getStatistics(options: { + spacing?: number; + unit?: string; + }): NamedStatistics { + const { spacing } = options; + // Get the basic units + const stats = BasicStatsCalculator.getStatistics(); + + // Add the volumetric units + const volumeUnit = spacing ? 'mm\xb3' : 'voxels\xb3'; + const volumeScale = spacing ? spacing[0] * spacing[1] * spacing[2] : 1; + + stats.volume = { + value: Array.isArray(stats.count.value) + ? stats.count.value.map((v) => v * volumeScale) + : stats.count.value * volumeScale, + unit: volumeUnit, + name: 'volume', + }; + stats.array.push(stats.volume); + return stats; + } +} diff --git a/packages/tools/src/utilities/segmentation/floodFill.ts b/packages/tools/src/utilities/segmentation/floodFill.ts index 71f6151aeb..76e07bddd0 100644 --- a/packages/tools/src/utilities/segmentation/floodFill.ts +++ b/packages/tools/src/utilities/segmentation/floodFill.ts @@ -24,6 +24,10 @@ import { Types } from '@cornerstonejs/core'; * @param options.equals - An optional equality method for your datastructure. * Default is simply value1 = value2. * @param options.diagonals - Whether you allow flooding through diagonals. Defaults to false. + * @param options.bounds - An optional min/max value bounds in the form boundsIJK. Allows controlling + * the fill to a single plane. + * @param options.filter - An optional filter function to include/exclude points. + * If the filter returns false, then the point is excluded. * * @returns Flood fill results */ @@ -35,13 +39,14 @@ function floodFill( const onFlood = options.onFlood; const onBoundary = options.onBoundary; const equals = options.equals; + const filter = options.filter; const diagonals = options.diagonals || false; const startNode = get(seed); const permutations = prunedPermutations(); const stack = []; const flooded = []; const visits = new Set(); - const bounds = new Map(); + const bounds = options.bounds; stack.push({ currentArgs: seed }); @@ -51,7 +56,6 @@ function floodFill( return { flooded, - boundaries: boundaries(), }; function flood(job) { @@ -108,7 +112,7 @@ function floodFill( // Use an integer key value for checking visited, since JavaScript does not // provide a generic hash key indexed hash map. const iKey = x + 32768 + 65536 * (y + 32768 + 65536 * (z + 32768)); - bounds.set(iKey, prevArgs); + bounds?.set(iKey, prevArgs); if (onBoundary) { //@ts-ignore onBoundary(...prevArgs); @@ -123,6 +127,12 @@ function floodFill( for (let j = 0; j < getArgs.length; j += 1) { nextArgs[j] += perm[j]; } + if (filter?.(nextArgs) === false) { + continue; + } + if (visited(nextArgs)) { + continue; + } stack.push({ currentArgs: nextArgs, @@ -174,17 +184,16 @@ function floodFill( return perms; } - function boundaries() { + function boundaries(): Types.Point2[] | Types.Point3[] { + if (!bounds) { + throw new Error('bounds not recorded'); + } const array = Array.from(bounds.values()); array.reverse(); - return array; + return array as Types.Point2[] | Types.Point3[]; } } -function defaultEquals(a, b) { - return a === b; -} - function countNonZeroes(array) { let count = 0; diff --git a/packages/tools/src/utilities/segmentation/index.ts b/packages/tools/src/utilities/segmentation/index.ts index 1c58f1b675..e8db3934bf 100644 --- a/packages/tools/src/utilities/segmentation/index.ts +++ b/packages/tools/src/utilities/segmentation/index.ts @@ -14,6 +14,7 @@ import { getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, } from './brushThresholdForToolGroup'; +import VolumetricCalculator from './VolumetricCalculator'; import thresholdSegmentationByRange from './thresholdSegmentationByRange'; import { createImageIdReferenceMap } from './createImageIdReferenceMap'; import contourAndFindLargestBidirectional from './contourAndFindLargestBidirectional'; @@ -38,6 +39,7 @@ export { setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, + VolumetricCalculator, thresholdSegmentationByRange, createImageIdReferenceMap, contourAndFindLargestBidirectional, diff --git a/utils/ExampleRunner/example-info.json b/utils/ExampleRunner/example-info.json index 9ccb61aa8c..4827253668 100644 --- a/utils/ExampleRunner/example-info.json +++ b/utils/ExampleRunner/example-info.json @@ -312,6 +312,10 @@ "name": "Segmentation Tools (Labelmap) - Brush, Scissors", "description": "Demonstrates how to use manual segmentation tools to modify the segmentation data" }, + "labelmapStatistics": { + "name": "Labelmap Statistics", + "description": "Show labelmap statistics" + }, "labelmapSegmentationDynamicThreshold": { "name": "Labelmap Segmentation Dynamic Threshold and Preview", "description": "Demonstrates how to use dynamic threshold with prevview to modify the segmentation data" From fc05f3ae3dca2891a1fe0bd3e199ec7671798eee Mon Sep 17 00:00:00 2001 From: sedghi Date: Wed, 27 Nov 2024 17:09:23 -0500 Subject: [PATCH 02/16] fix --- packages/core/src/utilities/RLEVoxelMap.ts | 25 +-- packages/core/src/utilities/VoxelManager.ts | 36 ++++ .../src/utilities/pointInShapeCallback.ts | 167 ++++++++++-------- .../strategies/compositions/islandRemoval.ts | 2 +- .../segmentation/strategies/fillSphere.ts | 10 -- .../src/utilities/segmentation/floodFill.ts | 13 +- 6 files changed, 150 insertions(+), 103 deletions(-) diff --git a/packages/core/src/utilities/RLEVoxelMap.ts b/packages/core/src/utilities/RLEVoxelMap.ts index 8c292ceab3..05979628cc 100644 --- a/packages/core/src/utilities/RLEVoxelMap.ts +++ b/packages/core/src/utilities/RLEVoxelMap.ts @@ -1,6 +1,6 @@ import type Point3 from '../types/Point3'; import type BoundsIJK from '../types/BoundsIJK'; -import { PixelDataTypedArray } from '../types'; +import type { PixelDataTypedArray } from '../types'; /** * The RLERun specifies a contigous run of values for a row, @@ -86,7 +86,7 @@ export default class RLEVoxelMap { */ protected kMultiple = 1; /** Number of components in the value */ - protected numberOfComponents = 1; + protected numComps = 1; /** * The default value returned for get. @@ -140,7 +140,7 @@ export default class RLEVoxelMap { * This allows applying or modifying the run directly. See CanvasActor * for an example in the RLE rendering. */ - protected getRLE(i: number, j: number, k = 0): RLERun | undefined { + protected getRLE(i: number, j: number, k = 0): RLERun { const row = this.rows.get(j + k * this.height); if (!row) { return; @@ -301,7 +301,7 @@ export default class RLEVoxelMap { let rleNext = isAfter ? row[rleIndex + 1] : rle1; // Can merge with previous value, so no insert - if (rlePrev?.value === value && rlePrev.end === i) { + if (rlePrev?.value === value && rlePrev?.end === i) { rlePrev.end++; if (rleNext?.value === value && rleNext.start === i + 1) { rlePrev.end = rleNext.end; @@ -340,7 +340,7 @@ export default class RLEVoxelMap { if (rleNext?.start === i && rleNext.end === i + 1) { rleNext.value = value; const nextnext = row[rleIndex + 1]; - if (nextnext?.start == i + 1 && nextnext?.value === value) { + if (nextnext?.start == i + 1 && nextnext.value === value) { row.splice(rleIndex + 1, 1); rleNext.end = nextnext.end; } @@ -393,19 +393,19 @@ export default class RLEVoxelMap { ): PixelDataTypedArray { if (!pixelData) { pixelData = new this.pixelDataConstructor( - this.width * this.height * this.numberOfComponents + this.width * this.height * this.numComps ); } else { pixelData.fill(0); } - const { width, height, numberOfComponents } = this; + const { width, height, numComps } = this; for (let j = 0; j < height; j++) { const row = this.getRun(j, k); if (!row) { continue; } - if (numberOfComponents === 1) { + if (numComps === 1) { for (const rle of row) { const rowOffset = j * width; const { start, end, value } = rle; @@ -415,10 +415,10 @@ export default class RLEVoxelMap { } } else { for (const rle of row) { - const rowOffset = j * width * numberOfComponents; + const rowOffset = j * width * numComps; const { start, end, value } = rle; - for (let i = start; i < end; i += numberOfComponents) { - for (let comp = 0; comp < numberOfComponents; comp++) { + for (let i = start; i < end; i += numComps) { + for (let comp = 0; comp < numComps; comp++) { pixelData[rowOffset + i + comp] = value[comp]; } } @@ -603,6 +603,7 @@ export default class RLEVoxelMap { // const { start, end, value } = rle; // if (start < 0 || end > 1920 || start >= end) { // console.log('Wrong order', ...inputs); +// debugger; // } // if (!lastRle) { // lastRle = rle; @@ -612,9 +613,11 @@ export default class RLEVoxelMap { // lastRle = rle; // if (start < lastEnd) { // console.log('inputs for wrong overlap', ...inputs); +// debugger; // } // if (start === lastEnd && value === lastValue) { // console.log('inputs for two in a row same', ...inputs); +// debugger; // } // } // } diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index c77fce80d5..601d9e2ac0 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -310,6 +310,9 @@ export default class VoxelManager { // in the IJK coordinate system if (this.map) { + if (this.map instanceof RLEVoxelMap) { + return this.rleForEach(callback, options); + } // Optimize this for only values in the map for (const index of this.map.keys()) { const pointIJK = this.toIJK(index); @@ -361,6 +364,39 @@ export default class VoxelManager { } }; + /** + * Foreach callback optimized for RLE testing + */ + public rleForEach(callback, options?) { + const boundsIJK = options?.boundsIJK || this.getBoundsIJK(); + const { isWithinObject } = options || {}; + const map = this.map as RLEVoxelMap; + map.defaultValue = undefined; + for (let k = boundsIJK[2][0]; k <= boundsIJK[2][1]; k++) { + for (let j = boundsIJK[1][0]; j <= boundsIJK[1][1]; j++) { + const row = map.getRun(j, k); + if (!row) { + continue; + } + for (const rle of row) { + const { start, end, value } = rle; + const baseIndex = this.toIndex([0, j, k]); + for (let i = start; i < end; i++) { + const callbackArguments = { + value, + index: baseIndex + i, + pointIJK: [i, j, k], + }; + if (isWithinObject?.(callbackArguments) === false) { + continue; + } + callback(callbackArguments); + } + } + } + } + } + /** * Retrieves the scalar data. * If the scalar data is already available, it will be returned. diff --git a/packages/core/src/utilities/pointInShapeCallback.ts b/packages/core/src/utilities/pointInShapeCallback.ts index 8f8e9a8355..281c391abd 100644 --- a/packages/core/src/utilities/pointInShapeCallback.ts +++ b/packages/core/src/utilities/pointInShapeCallback.ts @@ -2,6 +2,7 @@ import { vec3 } from 'gl-matrix'; import type { vtkImageData } from '@kitware/vtk.js/Common/DataModel/ImageData'; import type BoundsIJK from '../types/BoundsIJK'; import type { CPUImageData, Point3 } from '../types'; +import PointsManager from './PointsManager'; export type PointInShape = { value: number; @@ -44,18 +45,95 @@ export interface PointInShapeOptions { } /** - * @deprecated - * You should use the voxelManager.forEach method instead. - * This method is deprecated and will be removed in a future version. + * Returns a function that takes an ijk position and efficiently returns + * the world position. Only works for integer ijk, AND values within the bounds. + * The position array is re-used, so don't preserve it/compare for different + * values, although you can provide an instance position to copy into. * + * This function is safe to use out of order, and is stable in terms of calculations. + */ +export function createPositionCallback(imageData) { + const currentPos = vec3.create(); + const dimensions = imageData.getDimensions(); + const positionI = PointsManager.create3(dimensions[0]); + const positionJ = PointsManager.create3(dimensions[1]); + const positionK = PointsManager.create3(dimensions[2]); + + const direction = imageData.getDirection(); + const rowCosines = direction.slice(0, 3); + const columnCosines = direction.slice(3, 6); + const scanAxisNormal = direction.slice(6, 9); + + const spacing = imageData.getSpacing(); + const [rowSpacing, columnSpacing, scanAxisSpacing] = spacing; + + // @ts-ignore will be fixed in vtk-master + const worldPosStart = imageData.indexToWorld([0, 0, 0]); + + const rowStep = vec3.fromValues( + rowCosines[0] * rowSpacing, + rowCosines[1] * rowSpacing, + rowCosines[2] * rowSpacing + ); + + const columnStep = vec3.fromValues( + columnCosines[0] * columnSpacing, + columnCosines[1] * columnSpacing, + columnCosines[2] * columnSpacing + ); + + const scanAxisStep = vec3.fromValues( + scanAxisNormal[0] * scanAxisSpacing, + scanAxisNormal[1] * scanAxisSpacing, + scanAxisNormal[2] * scanAxisSpacing + ); + + const scaled = vec3.create(); + // Add the world position start to the I component so we don't need to add it + for (let i = 0; i < dimensions[0]; i++) { + positionI.push( + vec3.add(scaled, worldPosStart, vec3.scale(scaled, rowStep, i)) as Point3 + ); + } + for (let j = 0; j < dimensions[0]; j++) { + positionJ.push(vec3.scale(scaled, columnStep, j) as Point3); + } + for (let k = 0; k < dimensions[0]; k++) { + positionK.push(vec3.scale(scaled, scanAxisStep, k) as Point3); + } + + const dataI = positionI.getTypedArray(); + const dataJ = positionJ.getTypedArray(); + const dataK = positionK.getTypedArray(); + + return (ijk, destPoint = currentPos) => { + const [i, j, k] = ijk; + const offsetI = i * 3; + const offsetJ = j * 3; + const offsetK = k * 3; + destPoint[0] = dataI[offsetI] + dataJ[offsetJ] + dataK[offsetK]; + destPoint[1] = dataI[offsetI + 1] + dataJ[offsetJ + 1] + dataK[offsetK + 1]; + destPoint[2] = dataI[offsetI + 2] + dataJ[offsetJ + 2] + dataK[offsetK + 2]; + return destPoint as Point3; + }; +} + +/** * For each point in the image (If boundsIJK is not provided, otherwise, for each * point in the provided bounding box), It runs the provided callback IF the point * passes the provided criteria to be inside the shape (which is defined by the * provided pointInShapeFn) * + * You must record points in the callback function if you wish to have an array + * of the called points. + * * @param imageData - The image data object. - * @param options - Configuration options for the shape callback. - * @returns An array of points in the shape if returnPoints is true, otherwise undefined. + * @param dimensions - The dimensions of the image. + * @param pointInShapeFn - A function that takes a point in LPS space and returns + * true if the point is in the shape and false if it is not. + * @param callback - A function that will be called for + * every point in the shape. + * @param boundsIJK - The bounds of the volume in IJK coordinates. */ export function pointInShapeCallback( imageData: vtkImageData | CPUImageData, @@ -68,11 +146,10 @@ export function pointInShapeCallback( returnPoints = false, // Destructure other options here as needed } = options; - let iMin, iMax, jMin, jMax, kMin, kMax; let scalarData; - const { numComps } = imageData as unknown as { numComps: number }; + const { numComps } = imageData as { numComps: number }; // if getScalarData is a method on imageData if ((imageData as CPUImageData).getScalarData) { @@ -84,11 +161,6 @@ export function pointInShapeCallback( .getData(); } - if (!scalarData) { - console.warn('No scalar data found for imageData', imageData); - return; - } - const dimensions = imageData.getDimensions(); if (!boundsIJK) { @@ -102,59 +174,31 @@ export function pointInShapeCallback( [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK; } - const start = vec3.fromValues(iMin, jMin, kMin); - - const direction = imageData.getDirection(); - const rowCosines = direction.slice(0, 3); - const columnCosines = direction.slice(3, 6); - const scanAxisNormal = direction.slice(6, 9); - - const spacing = imageData.getSpacing(); - const [rowSpacing, columnSpacing, scanAxisSpacing] = spacing; - - // @ts-ignore will be fixed in vtk-master - const worldPosStart = imageData.indexToWorld(start); - - const rowStep = vec3.fromValues( - rowCosines[0] * rowSpacing, - rowCosines[1] * rowSpacing, - rowCosines[2] * rowSpacing - ); - - const columnStep = vec3.fromValues( - columnCosines[0] * columnSpacing, - columnCosines[1] * columnSpacing, - columnCosines[2] * columnSpacing - ); - - const scanAxisStep = vec3.fromValues( - scanAxisNormal[0] * scanAxisSpacing, - scanAxisNormal[1] * scanAxisSpacing, - scanAxisNormal[2] * scanAxisSpacing - ); + const indexToWorld = createPositionCallback(imageData); + const pointIJK = [0, 0, 0] as Point3; const xMultiple = numComps || scalarData.length / dimensions[2] / dimensions[1] / dimensions[0]; const yMultiple = dimensions[0] * xMultiple; const zMultiple = dimensions[1] * yMultiple; - const pointsInShape: Array = []; - const currentPos = vec3.clone(worldPosStart); - for (let k = kMin; k <= kMax; k++) { - const startPosJ = vec3.clone(currentPos); + pointIJK[2] = k; + const indexK = k * zMultiple; for (let j = jMin; j <= jMax; j++) { - const startPosI = vec3.clone(currentPos); + pointIJK[1] = j; + const indexJK = indexK + j * yMultiple; for (let i = iMin; i <= iMax; i++) { - const pointIJK: Point3 = [i, j, k]; + pointIJK[0] = i; + const pointLPS = indexToWorld(pointIJK); // The current world position (pointLPS) is now in currentPos - if (pointInShapeFn(currentPos as Point3, pointIJK)) { - const index = k * zMultiple + j * yMultiple + i * xMultiple; + if (pointInShapeFn(pointLPS, pointIJK)) { + const index = indexJK + i * xMultiple; let value; if (xMultiple > 2) { value = [ @@ -170,30 +214,13 @@ export function pointInShapeCallback( value, index, pointIJK, - pointLPS: currentPos.slice(), + pointLPS: pointLPS.slice(), }); - if (callback) { - callback({ - value, - index, - pointIJK, - pointLPS: currentPos as Point3, - }); - } - } - // Increment currentPos by rowStep for the next iteration - vec3.add(currentPos, currentPos, rowStep); + callback({ value, index, pointIJK, pointLPS }); + } } - - // Reset currentPos to the start of the next J line and increment by columnStep - vec3.copy(currentPos, startPosI); - vec3.add(currentPos, currentPos, columnStep); } - - // Reset currentPos to the start of the next K slice and increment by scanAxisStep - vec3.copy(currentPos, startPosJ); - vec3.add(currentPos, currentPos, scanAxisStep); } // Modify the return statement diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts b/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts index 789ae23552..05c27319c1 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/islandRemoval.ts @@ -204,7 +204,7 @@ function removeInternalIslands( previewVoxelManager.setAtIJKPoint(clearPoint, previewSegmentIndex); } }); - return previewVoxelManager.getArrayOfSlices(); + return previewVoxelManager.getArrayOfModifiedSlices(); } /** diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 8d34b73735..8774dfc570 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -84,14 +84,6 @@ const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy( compositions.islandRemoval ); -const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy( - 'SphereThreshold', - ...SPHERE_STRATEGY.compositions, - compositions.dynamicThreshold, - compositions.threshold, - compositions.islandRemoval -); - /** * Fill inside the circular region segment inside the segmentation defined by the operationData. * It fills the segmentation pixels inside the defined circle. @@ -100,8 +92,6 @@ const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy( */ const thresholdInsideSphere = SPHERE_THRESHOLD_STRATEGY.strategyFunction; -const thresholdInsideSphereIsland = - SPHERE_THRESHOLD_STRATEGY_ISLAND.strategyFunction; const thresholdInsideSphereIsland = SPHERE_THRESHOLD_STRATEGY_ISLAND.strategyFunction; diff --git a/packages/tools/src/utilities/segmentation/floodFill.ts b/packages/tools/src/utilities/segmentation/floodFill.ts index a67351b6d2..4609d6fe1e 100644 --- a/packages/tools/src/utilities/segmentation/floodFill.ts +++ b/packages/tools/src/utilities/segmentation/floodFill.ts @@ -13,8 +13,8 @@ import type { Types } from '@cornerstonejs/core'; * map to work on keys. * * @param getter The getter to the elements of your data structure, - * e.g. getter(x,y) for a 2D interprettation of your structure. - * @param seed The seed for your fill. The dimensionality is infered + * e.g. getter(x,y) for a 2D interpretation of your structure. + * @param seed The seed for your fill. The dimensionality is inferred * by the number of dimensions of the seed. * @param options.onFlood - An optional callback to execute when each pixel is flooded. * e.g. onFlood(x,y). @@ -183,15 +183,6 @@ function floodFill( return perms; } - - function boundaries(): Types.Point2[] | Types.Point3[] { - if (!bounds) { - throw new Error('bounds not recorded'); - } - const array = Array.from(bounds.values()); - array.reverse(); - return array as Types.Point2[] | Types.Point3[]; - } } function countNonZeroes(array) { From 9763efc804c861db96c397d161428cb531ac95b7 Mon Sep 17 00:00:00 2001 From: sedghi Date: Wed, 27 Nov 2024 21:24:46 -0500 Subject: [PATCH 03/16] fix --- packages/core/src/loaders/volumeLoader.ts | 4 ++-- .../src/tools/segmentation/CircleROIStartEndThresholdTool.ts | 2 +- .../tools/segmentation/RectangleROIStartEndThresholdTool.ts | 2 +- .../segmentation/strategies/compositions/dynamicThreshold.ts | 3 ++- packages/tools/src/types/CalculatorTypes.ts | 1 - .../tools/src/utilities/segmentation/VolumetricCalculator.ts | 4 +++- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/core/src/loaders/volumeLoader.ts b/packages/core/src/loaders/volumeLoader.ts index 7de13deea6..b73ecf39eb 100644 --- a/packages/core/src/loaders/volumeLoader.ts +++ b/packages/core/src/loaders/volumeLoader.ts @@ -3,7 +3,7 @@ import '@kitware/vtk.js/Rendering/Profiles/Volume'; import { ImageVolume } from '../cache/classes/ImageVolume'; import cache from '../cache/cache'; import Events from '../enums/Events'; -import VoxelManagerEnum from '../enums/VoxelManagerEnum'; +import type VoxelManagerEnum from '../enums/VoxelManagerEnum'; import eventTarget from '../eventTarget'; import triggerEvent from '../utilities/triggerEvent'; @@ -39,6 +39,7 @@ interface DerivedVolumeOptions { targetBuffer?: { type: PixelDataTypedArrayString; }; + voxelRepresentation?: VoxelManagerEnum; } export interface LocalVolumeOptions { @@ -510,7 +511,6 @@ export function createAndCacheDerivedLabelmapVolume( type: 'Uint8Array', ...options?.targetBuffer, }, - targetBuffer: { type: 'Uint8Array' }, }); } diff --git a/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts b/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts index 2d9e8a9432..26a47e98af 100644 --- a/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts +++ b/packages/tools/src/tools/segmentation/CircleROIStartEndThresholdTool.ts @@ -751,7 +751,7 @@ class CircleROIStartEndThresholdTool extends CircleROITool { returnPoints: this.configuration.storePointData, } ); - pointsInsideVolume.push(points); + pointsInsideVolume.push(pointsInShape); } } const stats = this.configuration.statsCalculator.getStatistics(); diff --git a/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts b/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts index ba9eba2217..be9d7e102e 100644 --- a/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts +++ b/packages/tools/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts @@ -451,7 +451,7 @@ class RectangleROIStartEndThresholdTool extends RectangleROITool { } ); - pointsInsideVolume.push(points); + pointsInsideVolume.push(pointsInShape); } } const stats = this.configuration.statsCalculator.getStatistics(); diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts index 0af11de6bf..1a801a9f71 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/dynamicThreshold.ts @@ -66,7 +66,8 @@ export default { if (distance > useDeltaSqr) { return; } - const gray = Array.isArray(value) ? vec3.len(value as any) : value; + // @ts-ignore + const gray = Array.isArray(value) ? vec3.len(value) : value; threshold[0] = Math.min(gray, threshold[0]); threshold[1] = Math.max(gray, threshold[1]); }; diff --git a/packages/tools/src/types/CalculatorTypes.ts b/packages/tools/src/types/CalculatorTypes.ts index 597117de9d..bbc5ee0c89 100644 --- a/packages/tools/src/types/CalculatorTypes.ts +++ b/packages/tools/src/types/CalculatorTypes.ts @@ -19,7 +19,6 @@ type NamedStatistics = { pointsInShape?: Types.IPointsManager; array: Statistics[]; /** The array of points that this statistic is calculated on. */ - pointsInShape?: Types.PointsManager; }; export type { Statistics, NamedStatistics }; diff --git a/packages/tools/src/utilities/segmentation/VolumetricCalculator.ts b/packages/tools/src/utilities/segmentation/VolumetricCalculator.ts index 91f9182d43..cc1607fb7a 100644 --- a/packages/tools/src/utilities/segmentation/VolumetricCalculator.ts +++ b/packages/tools/src/utilities/segmentation/VolumetricCalculator.ts @@ -16,7 +16,9 @@ export default class VolumetricCalculator extends BasicStatsCalculator { // Add the volumetric units const volumeUnit = spacing ? 'mm\xb3' : 'voxels\xb3'; - const volumeScale = spacing ? spacing[0] * spacing[1] * spacing[2] : 1; + const volumeScale = spacing + ? spacing[0] * spacing[1] * spacing[2] * 1000 + : 1; stats.volume = { value: Array.isArray(stats.count.value) From 5bdc3802d4ae1a3e678f9c9dc52bcb0ac22592cd Mon Sep 17 00:00:00 2001 From: sedghi Date: Wed, 27 Nov 2024 21:25:19 -0500 Subject: [PATCH 04/16] fix --- common/reviews/api/core.api.md | 45 +++++++++++++++++++++++++++++---- common/reviews/api/tools.api.md | 1 - 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 649afdadf9..a07a8ca811 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -989,6 +989,7 @@ declare namespace Enums { VideoEnums, MetadataModules, ImageQualityStatus, + VoxelManagerEnum, GenerateImageType } } @@ -3058,7 +3059,7 @@ type RGB = [number, number, number]; function rgbToHex(r: any, g: any, b: any): string; // @public (undocumented) -interface RLERun { +interface RLERun_2 { // (undocumented) end: number; // (undocumented) @@ -3075,18 +3076,42 @@ class RLEVoxelMap { // (undocumented) defaultValue: T; // (undocumented) + delete(index: number): void; + // (undocumented) protected depth: number; // (undocumented) + fillFrom(getter: (i: number, j: number, k: number) => T, boundsIJK: BoundsIJK): void; + // (undocumented) + findAdjacents(item: [RLERun, number, number, Point3[]?], { diagonals, planar, singlePlane }: { + diagonals?: boolean; + planar?: boolean; + singlePlane?: boolean; + }): any[]; + // (undocumented) protected findIndex(row: RLERun[], i: number): number; // (undocumented) + floodFill(i: number, j: number, k: number, value: T, options?: { + planar?: boolean; + diagonals?: boolean; + singlePlane?: boolean; + }): number; + // (undocumented) + forEach(callback: any, options?: { + rowModified?: boolean; + }): void; + // (undocumented) + forEachRow(callback: any): void; + // (undocumented) get: (index: number) => T; // (undocumented) getPixelData(k?: number, pixelData?: PixelDataTypedArray): PixelDataTypedArray; // (undocumented) - protected getRLE(i: number, j: number, k?: number): RLERun | undefined; + protected getRLE(i: number, j: number, k?: number): RLERun; // (undocumented) getRun: (j: number, k: number) => RLERun[]; // (undocumented) + has(index: number): boolean; + // (undocumented) protected height: number; // (undocumented) protected jMultiple: number; @@ -3095,7 +3120,9 @@ class RLEVoxelMap { // (undocumented) protected kMultiple: number; // (undocumented) - protected numberOfComponents: number; + normalizer: PlaneNormalizer; + // (undocumented) + protected numComps: number; // (undocumented) pixelDataConstructor: Uint8ArrayConstructor; // (undocumented) @@ -3103,6 +3130,10 @@ class RLEVoxelMap { // (undocumented) set: (index: number, value: T) => void; // (undocumented) + toIJK(index: number): Point3; + // (undocumented) + toIndex([i, j, k]: Point3): number; + // (undocumented) protected width: number; } @@ -3781,7 +3812,7 @@ declare namespace Types { LocalVolumeOptions, IVoxelManager, IRLEVoxelMap, - RLERun, + RLERun_2 as RLERun, ViewportInput, ImageLoadRequests, IBaseVolumeViewport, @@ -4810,6 +4841,8 @@ class VoxelManager { dimension: Point3; }): IVoxelManager; // (undocumented) + static createRLEHistoryVoxelManager(sourceVoxelManager: VoxelManager): VoxelManager; + // (undocumented) static createRLEVoxelManager({ dimensions, }: { dimensions: Point3; }): VoxelManager; @@ -4839,7 +4872,7 @@ class VoxelManager { isInObject?: (pointLPS: any, pointIJK: any) => boolean; returnPoints?: boolean; imageData?: vtkImageData | CPUImageData; - }) => any[]; + }) => void | any[]; // (undocumented) frameSize: number; // (undocumented) @@ -4899,6 +4932,8 @@ class VoxelManager { // (undocumented) resetModifiedSlices(): void; // (undocumented) + rleForEach(callback: any, options?: any): void; + // (undocumented) _set: (index: number, v: T) => boolean; // (undocumented) setAtIJK: (i: number, j: number, k: number, v: any) => boolean; diff --git a/common/reviews/api/tools.api.md b/common/reviews/api/tools.api.md index cc2c1c8106..e6fa382fbb 100644 --- a/common/reviews/api/tools.api.md +++ b/common/reviews/api/tools.api.md @@ -3732,7 +3732,6 @@ type NamedStatistics = { }; pointsInShape?: Types_2.IPointsManager; array: Statistics[]; - pointsInShape?: Types_2.PointsManager; }; // @public (undocumented) From 1e84fe93539d97fe5661c336cb10e69bacb6b5ee Mon Sep 17 00:00:00 2001 From: sedghi Date: Wed, 27 Nov 2024 21:51:51 -0500 Subject: [PATCH 05/16] wip --- packages/core/src/loaders/imageLoader.ts | 26 +++++++++++++------ packages/core/src/loaders/volumeLoader.ts | 6 +++-- packages/core/src/utilities/VoxelManager.ts | 16 ++++++++++-- .../core/test/utilities/RLEVoxelMap.jest.js | 2 +- .../core/test/utilities/VoxelManager.jest.js | 4 +-- .../index.ts | 3 +-- .../src/tools/annotation/CircleROITool.ts | 2 +- .../tools/src/tools/segmentation/BrushTool.ts | 1 - .../strategies/compositions/regionFill.ts | 3 +-- .../segmentation/strategies/fillSphere.ts | 3 ++- packages/tools/src/types/CalculatorTypes.ts | 1 - 11 files changed, 44 insertions(+), 23 deletions(-) diff --git a/packages/core/src/loaders/imageLoader.ts b/packages/core/src/loaders/imageLoader.ts index 382a6bf2aa..4c30bcfd81 100644 --- a/packages/core/src/loaders/imageLoader.ts +++ b/packages/core/src/loaders/imageLoader.ts @@ -21,6 +21,7 @@ import type { } from '../types'; import imageLoadPoolManager from '../requestPool/imageLoadPoolManager'; import * as metaData from '../metaData'; +import VoxelManagerEnum from '../enums/VoxelManagerEnum'; export interface ImageLoaderOptions { priority: number; @@ -35,6 +36,7 @@ interface LocalImageOptions { targetBuffer?: { type: PixelDataTypedArrayString; }; + voxelRepresentation?: VoxelManagerEnum; dimensions?: Point2; spacing?: Point2; origin?: Point3; @@ -240,7 +242,8 @@ export function createAndCacheDerivedImage( options.imageId = `derived:${uuidv4()}`; } - const { imageId, skipCreateBuffer, onCacheAdd } = options; + const { imageId, skipCreateBuffer, onCacheAdd, voxelRepresentation } = + options; const imagePlaneModule = metaData.get('imagePlaneModule', referencedImageId); @@ -303,6 +306,7 @@ export function createAndCacheDerivedImage( targetBuffer: { type: imageScalarData.constructor.name as PixelDataTypedArrayString, }, + voxelRepresentation, dimensions: [imagePlaneModule.columns, imagePlaneModule.rows], spacing: [ imagePlaneModule.columnPixelSpacing, @@ -339,6 +343,7 @@ export function createAndCacheDerivedImages( targetBuffer?: { type: PixelDataTypedArrayString; }; + voxelRepresentation?: VoxelManagerEnum; } = {} ): IImage[] { if (referencedImageIds.length === 0) { @@ -376,6 +381,7 @@ export function createAndCacheLocalImage( skipCreateBuffer, onCacheAdd, frameOfReferenceUID, + voxelRepresentation, } = options; const dimensions = options.dimensions; @@ -426,7 +432,7 @@ export function createAndCacheLocalImage( scalarDataToUse = scalarData; } else if (!skipCreateBuffer) { // Todo: need to handle numberOfComponents > 1 - const { numBytes, TypedArrayConstructor } = getBufferConfiguration( + const { TypedArrayConstructor } = getBufferConfiguration( targetBuffer?.type, length ); @@ -485,12 +491,16 @@ export function createAndCacheLocalImage( }); }); - const voxelManager = VoxelManager.createImageVoxelManager({ - height, - width, - numberOfComponents, - scalarData: scalarDataToUse, - }); + // Todo: probably here we need to consider the RLE voxel manager as well + const voxelManager = + (voxelRepresentation === VoxelManagerEnum.RLE && + VoxelManager.createRLEImageVoxelManager({ dimensions })) || + (VoxelManager.createImageVoxelManager({ + height, + width, + numberOfComponents, + scalarData: scalarDataToUse, + }) as VoxelManager); // Calculate min and max pixel values let minPixelValue = scalarDataToUse[0]; diff --git a/packages/core/src/loaders/volumeLoader.ts b/packages/core/src/loaders/volumeLoader.ts index b73ecf39eb..6763eb8dc0 100644 --- a/packages/core/src/loaders/volumeLoader.ts +++ b/packages/core/src/loaders/volumeLoader.ts @@ -206,8 +206,7 @@ export function createAndCacheDerivedVolume( } let { volumeId } = options; - const { targetBuffer, voxelRepresentation } = options; - const { type } = targetBuffer; + const { voxelRepresentation } = options; if (volumeId === undefined) { volumeId = uuidv4(); @@ -239,6 +238,7 @@ export function createAndCacheDerivedVolume( // images const derivedImages = createAndCacheDerivedImages(referencedImageIds, { targetBuffer: options.targetBuffer, + voxelRepresentation, }); const dataType = derivedImages[0].dataType; @@ -321,6 +321,8 @@ export function createAndCacheVolumeFromImagesSync( return cachedVolume; } + // Todo: implement rle based voxel manager here for ultrasound later + const volumeProps = generateVolumePropsFromImageIds(imageIds, volumeId); const derivedVolume = new ImageVolume({ diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index 601d9e2ac0..294f8a6231 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -9,6 +9,7 @@ import type { CPUImageData, IVoxelManager, IRLEVoxelMap, + Point2, } from '../types'; import RLEVoxelMap from './RLEVoxelMap'; import isEqual from './isEqual'; @@ -1270,7 +1271,7 @@ export default class VoxelManager { * Creates a RLE based voxel manager. This is effective for storing * segmentation maps or already RLE encoded data such as ultrasounds. */ - public static createRLEVoxelManager({ + public static createRLEVolumeVoxelManager({ dimensions, }: { dimensions: Point3; @@ -1292,6 +1293,17 @@ export default class VoxelManager { return voxelManager; } + public static createRLEImageVoxelManager({ + dimensions, + }: { + dimensions: Point2; + }): VoxelManager { + const [width, height] = dimensions; + return VoxelManager.createRLEVolumeVoxelManager({ + dimensions: [width, height, 1], + }); + } + /** * This method adds a voxelManager instance to the image object * where the object added is of type: @@ -1316,7 +1328,7 @@ export default class VoxelManager { // This case occurs when the image data is a dummy image data set // created just to prevent exceptions in the caching logic. Then, the // RLE voxel manager can be created to store the data instead. - image.voxelManager = VoxelManager.createRLEVoxelManager({ + image.voxelManager = VoxelManager.createRLEVolumeVoxelManager({ dimensions: [width, height, 1], }); // The RLE voxel manager knows how to get scalar data pixel data representations. diff --git a/packages/core/test/utilities/RLEVoxelMap.jest.js b/packages/core/test/utilities/RLEVoxelMap.jest.js index a553040ad4..62462afa49 100644 --- a/packages/core/test/utilities/RLEVoxelMap.jest.js +++ b/packages/core/test/utilities/RLEVoxelMap.jest.js @@ -80,7 +80,7 @@ xdescribe('RLEVoxelMap', () => { describe('RLEVoxelManager', () => { it('sets', () => { - const map = VoxelManager.createRLEVoxelManager({ dimension }); + const map = VoxelManager.createRLEVolumeVoxelManager({ dimension }); map.setAtIJK(...ijkPoint, 15); expect(map.getAtIJK(...ijkPoint)).toBe(15); expect(map.getAtIJKPoint(ijkPoint)).toBe(15); diff --git a/packages/core/test/utilities/VoxelManager.jest.js b/packages/core/test/utilities/VoxelManager.jest.js index fb49242010..78468774c1 100644 --- a/packages/core/test/utilities/VoxelManager.jest.js +++ b/packages/core/test/utilities/VoxelManager.jest.js @@ -151,8 +151,8 @@ describe('VoxelManager', () => { }); }); - it('createRLEVoxelManager', () => { - const map = VoxelManager.createRLEVoxelManager({ dimensions }); + it('createRLEVolumeVoxelManager', () => { + const map = VoxelManager.createRLEVolumeVoxelManager({ dimensions }); map.setAtIJKPoint(ijkPoint, 1); expect(map.getAtIJKPoint(ijkPoint)).toBe(1); }); diff --git a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts index 97da487e7a..05727aa825 100644 --- a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts +++ b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts @@ -222,8 +222,7 @@ async function addSegmentationsToState() { volumeId: segmentationId, // The following doesn't quite work yet // TODO, allow RLE to be used instead of scalars. - // targetBuffer: { type: 'none' }, - // voxelRepresentation: 'rleVoxelManager', + // voxelRepresentation: Enums.VoxelManagerEnum.RLE, }); // Add the segmentations to state diff --git a/packages/tools/src/tools/annotation/CircleROITool.ts b/packages/tools/src/tools/annotation/CircleROITool.ts index ef6a71359c..2fd8512183 100644 --- a/packages/tools/src/tools/annotation/CircleROITool.ts +++ b/packages/tools/src/tools/annotation/CircleROITool.ts @@ -981,7 +981,7 @@ class CircleROITool extends AnnotationTool { area, mean: stats.mean?.value, max: stats.max?.value, - pointsInShape: stats.pointsInShape.points, + pointsInShape, stdDev: stats.stdDev?.value, statsArray: stats.array, isEmptyArea, diff --git a/packages/tools/src/tools/segmentation/BrushTool.ts b/packages/tools/src/tools/segmentation/BrushTool.ts index 2aed4f0000..b6949c330b 100644 --- a/packages/tools/src/tools/segmentation/BrushTool.ts +++ b/packages/tools/src/tools/segmentation/BrushTool.ts @@ -142,7 +142,6 @@ class BrushTool extends BaseTool { THRESHOLD_INSIDE_SPHERE_WITH_ISLAND_REMOVAL: thresholdInsideSphereIsland, }, - strategySpecificConfiguration: { THRESHOLD: { threshold: [-150, -70], // E.g. CT Fat // Only used during threshold strategies. diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts b/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts index 4f3c718787..482a4a1fde 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/regionFill.ts @@ -12,9 +12,8 @@ export default { const { segmentsLocked, segmentationImageData, - segmentationVoxelManager: segmentationVoxelManager, + segmentationVoxelManager, previewVoxelManager, - imageVoxelManager, brushStrategy, centerIJK, } = operationData; diff --git a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts index 8774dfc570..0af5b13fe9 100644 --- a/packages/tools/src/tools/segmentation/strategies/fillSphere.ts +++ b/packages/tools/src/tools/segmentation/strategies/fillSphere.ts @@ -73,7 +73,8 @@ const SPHERE_THRESHOLD_STRATEGY = new BrushStrategy( 'SphereThreshold', ...SPHERE_STRATEGY.compositions, compositions.dynamicThreshold, - compositions.threshold + compositions.threshold, + compositions.islandRemoval ); const SPHERE_THRESHOLD_STRATEGY_ISLAND = new BrushStrategy( diff --git a/packages/tools/src/types/CalculatorTypes.ts b/packages/tools/src/types/CalculatorTypes.ts index bbc5ee0c89..49aba1720d 100644 --- a/packages/tools/src/types/CalculatorTypes.ts +++ b/packages/tools/src/types/CalculatorTypes.ts @@ -18,7 +18,6 @@ type NamedStatistics = { circumference?: Statistics & { name: 'circumference' }; pointsInShape?: Types.IPointsManager; array: Statistics[]; - /** The array of points that this statistic is calculated on. */ }; export type { Statistics, NamedStatistics }; From 225da7e9947dcaf7a211f990cb4a4eb689898817 Mon Sep 17 00:00:00 2001 From: sedghi Date: Wed, 27 Nov 2024 22:02:11 -0500 Subject: [PATCH 06/16] api --- common/reviews/api/core.api.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index a07a8ca811..9b39c547f4 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -806,6 +806,7 @@ function createAndCacheDerivedImages(referencedImageIds: string[], options?: Der targetBuffer?: { type: PixelDataTypedArrayString; }; + voxelRepresentation?: VoxelManagerEnum; }): IImage[]; // @public (undocumented) @@ -4843,7 +4844,11 @@ class VoxelManager { // (undocumented) static createRLEHistoryVoxelManager(sourceVoxelManager: VoxelManager): VoxelManager; // (undocumented) - static createRLEVoxelManager({ dimensions, }: { + static createRLEImageVoxelManager({ dimensions, }: { + dimensions: Point2; + }): VoxelManager; + // (undocumented) + static createRLEVolumeVoxelManager({ dimensions, }: { dimensions: Point3; }): VoxelManager; // (undocumented) From 97df0184df3d7f38586a42ae89383587e3ef74ae Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Thu, 28 Nov 2024 18:26:02 -0500 Subject: [PATCH 07/16] fix: RLE rendering for segmentations --- .../helpers/setDefaultVolumeVOI.ts | 4 +- .../vtkClasses/vtkStreamingOpenGLTexture.js | 3 +- packages/core/src/utilities/RLEVoxelMap.ts | 41 ++- packages/core/src/utilities/VoxelManager.ts | 259 ++++++++++-------- .../core/test/utilities/RLEVoxelMap.jest.js | 9 +- .../index.ts | 4 +- .../strategies/compositions/setValue.ts | 1 - 7 files changed, 194 insertions(+), 127 deletions(-) diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index 51c9ac2ce8..3b2547d83b 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -180,10 +180,8 @@ async function getVOIFromMiddleSliceMinMax( image = await loadAndCacheImage(imageId, { ...options, ignoreCache: true }); } - const imageScalarData = image.getPixelData(); - // Get the min and max pixel values of the middle slice - const { min, max } = getMinMax(imageScalarData); + const { min, max } = image.voxelManager.getMinMax(); return { lower: min, upper: max, diff --git a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js index f91e11d249..20992a1c7a 100644 --- a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js +++ b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js @@ -122,7 +122,8 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { continue; } - let data = image.voxelManager.getScalarData(); + // Get a temporary copy + let data = image.voxelManager.getScalarData(true); const gl = model.context; if (volume.dataType !== data.constructor.name) { diff --git a/packages/core/src/utilities/RLEVoxelMap.ts b/packages/core/src/utilities/RLEVoxelMap.ts index 05979628cc..713ce98018 100644 --- a/packages/core/src/utilities/RLEVoxelMap.ts +++ b/packages/core/src/utilities/RLEVoxelMap.ts @@ -1,6 +1,6 @@ import type Point3 from '../types/Point3'; import type BoundsIJK from '../types/BoundsIJK'; -import type { PixelDataTypedArray } from '../types'; +import { PixelDataTypedArray } from '../types'; /** * The RLERun specifies a contigous run of values for a row, @@ -60,6 +60,7 @@ export type PlaneNormalizer = { */ export default class RLEVoxelMap { public normalizer: PlaneNormalizer; + /** * The rows for the voxel map is a map from the j index location (or for * volumes, `j + k*height`) to a list of RLE runs. That is, each entry in @@ -69,14 +70,14 @@ export default class RLEVoxelMap { */ protected rows = new Map[]>(); /** The height of the images stored in the voxel map (eg the height of each plane) */ - protected height = 1; + public height = 1; /** The width of the image planes */ - protected width = 1; + public width = 1; /** * The number of image planes stored (the depth of the indices), with the k * index going from 0...depth. */ - protected depth = 1; + public depth = 1; /** * A multiplier value to go from j values to overall index values. */ @@ -90,7 +91,7 @@ export default class RLEVoxelMap { /** * The default value returned for get. - * This allows treting the voxel map more like scalar data, returning the right + * This allows treating the voxel map more like scalar data, returning the right * default value for unset values. * Set to 0 by default, but any maps where 0 not in T should update this value. */ @@ -101,6 +102,18 @@ export default class RLEVoxelMap { */ public pixelDataConstructor = Uint8Array; + /** + * Copies the data in source into the map. + */ + public static copyMap( + destination: RLEVoxelMap, + source: RLEVoxelMap + ) { + for (const [index, row] of source.rows) { + destination.rows.set(index, structuredClone(row)); + } + } + constructor(width: number, height: number, depth = 1) { this.width = width; this.height = height; @@ -109,6 +122,24 @@ export default class RLEVoxelMap { this.kMultiple = this.jMultiple * height; } + /** This is a function on the voxel manager, to get the RLE scalar data. */ + public static getScalarData = function () { + const scalarData = new Float32Array(this.frameSize); + this.map.updateScalarData(scalarData); + return scalarData; + }; + + public updateScalarData = function (scalarData: ArrayLike) { + scalarData.set(0); + const callback = (index, rle, row) => { + const { start, end, value } = rle; + for (let i = start; i < end; i++) { + scalarData[index + i] = value; + } + }; + this.forEach(callback); + }; + /** * Gets the value encoded in the map at the given index, which is * an integer `[i,j,k]` voxel index, equal to `index=i+(j+k*height)*width` diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index 294f8a6231..5c26ce5e20 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -37,7 +37,7 @@ export default class VoxelManager { public sourceVoxelManager: IVoxelManager; public isInObject: (pointLPS, pointIJK) => boolean; public readonly dimensions: Point3; - public numberOfComponents = 1; + public readonly numberOfComponents; public getCompleteScalarDataArray?: () => ArrayLike; public setCompleteScalarDataArray?: (scalarData: ArrayLike) => void; @@ -48,14 +48,17 @@ export default class VoxelManager { // a limit on the number of slices to cache since it can grow indefinitely private _sliceDataCache = null as Map; + public readonly managerType: string; + points: Set; width: number; frameSize: number; - _get: (index: number) => T; - _set: (index: number, v: T) => boolean; - _getConstructor?: () => new (length: number) => PixelDataTypedArray; + readonly _get: (index: number) => T; + readonly _set: (index: number, v: T) => boolean; + readonly _getConstructor?: () => new (length: number) => PixelDataTypedArray; _getScalarDataLength?: () => number; - _getScalarData?: () => PixelDataTypedArray; + _getScalarData?: () => ArrayLike; + _updateScalarData?: (scalarData: ArrayLike) => PixelDataTypedArray; _getSliceData: (args: { sliceIndex: number; slicePlane: number; @@ -65,19 +68,35 @@ export default class VoxelManager { * Creates a generic voxel value accessor, with access to the values * provided by the _get and optionally _set values. * @param dimensions - for the voxel volume - * @param _get - called to get a value by index - * @param _set - called when setting a value + * @param options._get - called to get a value by index + * @param options._set - called when setting a value */ constructor( dimensions, - _get: (index: number) => T, - _set?: (index: number, v: T) => boolean + options: { + _get: (index: number) => T; + _set?: (index: number, v: T) => boolean; + _getScalarData?: () => ArrayLike; + managerType: string; + _updateScalarData?: ( + scalarData: ArrayLike + ) => PixelDataTypedArray; + numberOfComponents?: number; + scalarData?: ArrayLike; + _getConstructor?: () => new (length: number) => PixelDataTypedArray; + } ) { this.dimensions = dimensions; this.width = dimensions[0]; this.frameSize = this.width * dimensions[1]; - this._get = _get; - this._set = _set; + this._get = options._get; + this._set = options._set; + this.managerType = options.managerType; + this._getConstructor = options._getConstructor; + this.numberOfComponents = this.numberOfComponents || 1; + this.scalarData = this.scalarData; + this._getScalarData = options._getScalarData; + this._updateScalarData = options._updateScalarData; } /** @@ -125,6 +144,29 @@ export default class VoxelManager { */ public getAtIndex = (index) => this._get(index); + /** Gets the min/max pair - as array for RGB */ + public getMinMax() { + let min, max; + const callback = (v) => { + const isArray = Array.isArray(v); + if (min === undefined) { + min = isArray ? [...v] : v; + max = isArray ? [...v] : v; + } + if (isArray) { + for (let i = 0; i < v.length; i++) { + min[i] = Math.min(min[i], v[i]); + max[i] = Math.max(max[i], v[i]); + } + } else { + min = Math.min(min, v); + max = Math.max(max, v); + } + }; + this.forEach(callback); + return { min, max }; + } + /** * Sets the value at the given index */ @@ -407,13 +449,18 @@ export default class VoxelManager { * @returns The scalar data. * @throws {Error} If no scalar data is available. */ - public getScalarData() { + public getScalarData(transient = null) { if (this.scalarData) { + this._updateScalarData?.(this.scalarData); return this.scalarData; } if (this._getScalarData) { - return this._getScalarData(); + const scalarData = this._getScalarData(); + if (!transient) { + console.log('Not transient, should store value', scalarData); + } + return scalarData; } throw new Error('No scalar data available'); @@ -471,6 +518,10 @@ export default class VoxelManager { this.points?.clear(); } + /** + * @returns a constructor for a typed array for the pixel data of the given length + * and the right type. Defaults to float32array. + */ public getConstructor(): new (length: number) => PixelDataTypedArray { if (this.scalarData) { return this.scalarData.constructor as new ( @@ -667,23 +718,23 @@ export default class VoxelManager { scalarData; numberOfComponents; }): VoxelManager { - const voxels = new VoxelManager( - dimensions, - (index) => { + const voxels = new VoxelManager(dimensions, { + _get: (index) => { index *= numberOfComponents; return [scalarData[index++], scalarData[index++], scalarData[index++]]; }, - (index, v) => { + managerType: '_createRGBScalarVolumeVoxelManager', + _set: (index, v) => { index *= 3; const isChanged = !isEqual(scalarData[index], v); scalarData[index++] = v[0]; scalarData[index++] = v[1]; scalarData[index++] = v[2]; return isChanged; - } - ); - voxels.numberOfComponents = numberOfComponents; - voxels.scalarData = scalarData; + }, + numberOfComponents, + scalarData, + }); return voxels; } @@ -707,6 +758,9 @@ export default class VoxelManager { function getPixelInfo(index) { const sliceIndex = Math.floor(index / pixelsPerSlice); + if (sliceIndex < 0 || sliceIndex >= dimensions[2]) { + return {}; + } const imageId = imageIds[sliceIndex]; if (!imageId) { @@ -721,74 +775,56 @@ export default class VoxelManager { return { pixelData: null, pixelIndex: null }; } - const pixelData = image.voxelManager.getScalarData(); - const pixelIndex = (index % pixelsPerSlice) * numberOfComponents; + const voxelManager = image.voxelManager; + const pixelIndex = index % pixelsPerSlice; - return { pixelData, pixelIndex }; + return { voxelManager, pixelIndex }; } function getVoxelValue(index) { - const { pixelData, pixelIndex } = getPixelInfo(index); + const { voxelManager, pixelIndex } = getPixelInfo(index); - if (!pixelData || pixelIndex === null) { + if (!voxelManager || pixelIndex === null) { return null; } - if (numberOfComponents === 1) { - return pixelData[pixelIndex]; - } else { - return [ - pixelData[pixelIndex], - pixelData[pixelIndex + 1], - pixelData[pixelIndex + 2], - ] as RGB; - } + return voxelManager.getAtIndex(pixelIndex) as Number; } function setVoxelValue(index, v) { - const { pixelData, pixelIndex } = getPixelInfo(index); + const { voxelManager, pixelIndex } = getPixelInfo(index); - if (!pixelData || pixelIndex === null) { + if (!voxelManager || pixelIndex === null) { return false; } - let isChanged = false; + const currentValue = voxelManager.getAtIndex(pixelIndex); + const isChanged = !isEqual(v, currentValue); - if (numberOfComponents === 1) { - if (pixelData[pixelIndex] !== v) { - pixelData[pixelIndex] = v as number; - isChanged = true; - } - } else { - const rgbValue = v as RGB; - for (let i = 0; i < numberOfComponents; i++) { - if (pixelData[pixelIndex + i] !== rgbValue[i]) { - pixelData[pixelIndex + i] = rgbValue[i]; - isChanged = true; - } - } + if (!isChanged) { + return isChanged; } + voxelManager.setAtIndex(pixelIndex, v as number); - return isChanged; + return true; } - const voxelManager = new VoxelManager( - dimensions, - (index) => getVoxelValue(index), - (index, v) => setVoxelValue(index, v) - ); - - voxelManager.numberOfComponents = numberOfComponents; - - // @ts-ignore - voxelManager._getConstructor = () => { + const _getConstructor = () => { const pixelInfo = getPixelInfo(0); - if (!pixelInfo.pixelData) { + if (!pixelInfo?.pixelData) { return null; } return pixelInfo.pixelData.constructor; }; + const voxelManager = new VoxelManager(dimensions, { + _get: getVoxelValue, + _set: setVoxelValue, + numberOfComponents, + _getConstructor, + managerType: 'createImageVolumeVoxelManager', + }); + voxelManager.getMiddleSliceData = () => { const middleSliceIndex = Math.floor(dimensions[2] / 2); return voxelManager.getSliceData({ @@ -1014,14 +1050,12 @@ export default class VoxelManager { }); // Create a VoxelManager that will manage the active voxel group - const voxelManager = new VoxelManager( - dimensions, - (index) => voxelGroups[timePoint]._get(index), - // @ts-ignore - (index, v) => voxelGroups[timePoint]._set(index, v) - ) as IVoxelManager | IVoxelManager; - - voxelManager.numberOfComponents = numberOfComponents; + const voxelManager = new VoxelManager(dimensions, { + _get: (index) => voxelGroups[timePoint]._get(index), + _set: (index, v) => voxelGroups[timePoint]._set(index, v), + numberOfComponents, + managerType: 'createScalarDynamicVolumeVoxelManager', + }) as IVoxelManager | IVoxelManager; voxelManager.getScalarDataLength = () => { return voxelGroups[timePoint].getScalarDataLength(); @@ -1120,15 +1154,15 @@ export default class VoxelManager { dimensions: Point3; scalarData: PixelDataTypedArray; }): IVoxelManager { - const voxels = new VoxelManager( - dimensions, - (index) => scalarData[index], - (index, v) => { + const voxels = new VoxelManager(dimensions, { + _get: (index) => scalarData[index], + _set: (index, v) => { const isChanged = scalarData[index] !== v; scalarData[index] = v; return isChanged; - } - ); + }, + managerType: '_createNumberVolumeVoxelManager', + }); voxels.scalarData = scalarData; voxels.getMiddleSliceData = () => { @@ -1153,11 +1187,11 @@ export default class VoxelManager { dimension: Point3; }): IVoxelManager { const map = new Map(); - const voxelManager = new VoxelManager( - dimension, - map.get.bind(map), - (index, v) => map.set(index, v) && true - ); + const voxelManager = new VoxelManager(dimension, { + _get: map.get.bind(map), + _set: (index, v) => map.set(index, v) && true, + managerType: 'createMapVoxelManager', + }); voxelManager.map = map; return voxelManager; } @@ -1174,10 +1208,9 @@ export default class VoxelManager { }): VoxelManager { const map = new Map(); const { dimensions } = sourceVoxelManager; - const voxelManager = new VoxelManager( - dimensions, - (index) => map.get(index), - function (index, v) { + const voxelManager = new VoxelManager(dimensions, { + _get: (index) => map.get(index), + _set: function (index, v) { if (!map.has(index)) { const oldV = this.sourceVoxelManager.getAtIndex(index); if (oldV === v) { @@ -1189,8 +1222,9 @@ export default class VoxelManager { map.delete(index); } this.sourceVoxelManager.setAtIndex(index, v); - } - ); + }, + managerType: 'createHistoryVoxelManager', + }); voxelManager.map = map; voxelManager.scalarData = sourceVoxelManager.scalarData; voxelManager.sourceVoxelManager = sourceVoxelManager; @@ -1208,27 +1242,28 @@ export default class VoxelManager { ): VoxelManager { const { dimensions } = sourceVoxelManager; const map = new RLEVoxelMap(dimensions[0], dimensions[1], dimensions[2]); - const voxelManager = new VoxelManager( - dimensions, - (index) => map.get(index), - function (index, v) { + const voxelManager = new VoxelManager(dimensions, { + _get: (index) => map.get(index), + _set: function (index, v) { const originalV = map.get(index); if (originalV === undefined) { const oldV = this.sourceVoxelManager.getAtIndex(index); - if (oldV === v || oldV === undefined || v === null) { + if (oldV === v || (oldV === undefined && v === 0) || v === null) { // No-op return false; } - map.set(index, oldV); + map.set(index, oldV ?? 0); } else if (v === originalV || v === null) { map.delete(index); v = originalV; } this.sourceVoxelManager.setAtIndex(index, v); - } - ); - voxelManager.map = map; - voxelManager.scalarData = sourceVoxelManager.scalarData; + }, + _getScalarData: RLEVoxelMap.getScalarData, + _updateScalarData: map.updateScalarData, + managerType: 'createRLEHistoryVoxelManager', + map, + }); voxelManager.sourceVoxelManager = sourceVoxelManager; return voxelManager; } @@ -1249,10 +1284,10 @@ export default class VoxelManager { const [width, height] = dimensions; const planeSize = width * height; - const voxelManager = new VoxelManager( - dimensions, - (index) => map.get(Math.floor(index / planeSize))[index % planeSize], - (index, v) => { + const voxelManager = new VoxelManager(dimensions, { + _get: (index) => + map.get(Math.floor(index / planeSize))[index % planeSize], + _set: (index, v) => { const k = Math.floor(index / planeSize); let layer = map.get(k); if (!layer) { @@ -1261,8 +1296,9 @@ export default class VoxelManager { } layer[index % planeSize] = v; return true; - } - ); + }, + managerType: 'createLazyVoxelManager', + }); voxelManager.map = map; return voxelManager; } @@ -1279,17 +1315,20 @@ export default class VoxelManager { const [width, height, depth] = dimensions; const map = new RLEVoxelMap(width, height, depth); - const voxelManager = new VoxelManager( - dimensions, - (index) => map.get(index), - (index, v) => { + const voxelManager = new VoxelManager(dimensions, { + _get: (index) => map.get(index), + _set: (index, v) => { map.set(index, v); return true; - } - ); + }, + _getScalarData: RLEVoxelMap.getScalarData, + _updateScalarData: map.updateScalarData, + managerType: 'createRLEVolumeVoxelManager', + }); voxelManager.map = map; // @ts-ignore voxelManager.getPixelData = map.getPixelData.bind(map); + // @ts-ignore return voxelManager; } diff --git a/packages/core/test/utilities/RLEVoxelMap.jest.js b/packages/core/test/utilities/RLEVoxelMap.jest.js index 62462afa49..d93aa3f9f3 100644 --- a/packages/core/test/utilities/RLEVoxelMap.jest.js +++ b/packages/core/test/utilities/RLEVoxelMap.jest.js @@ -2,16 +2,17 @@ import { VoxelManager } from '../../src/utilities'; import RLEVoxelMap from '../../src/utilities/RLEVoxelMap'; import { describe, it, expect, beforeEach } from '@jest/globals'; -const dimension = [64, 128, 4]; +const size = [64, 128, 4]; const ijkPoint = [4, 2, 2]; const rleMap = new RLEVoxelMap(64, 128, 4); +const voxelMap = VoxelManager.createLazyVoxelManager(size); const j = 4; const baseIndex = j * 64; +const i = 2; -// @bill - fix this please -xdescribe('RLEVoxelMap', () => { +describe('RLEVoxelMap', () => { beforeEach(() => { rleMap.clear(); }); @@ -80,7 +81,7 @@ xdescribe('RLEVoxelMap', () => { describe('RLEVoxelManager', () => { it('sets', () => { - const map = VoxelManager.createRLEVolumeVoxelManager({ dimension }); + const map = VoxelManager.createRLEVoxelManager(size); map.setAtIJK(...ijkPoint, 15); expect(map.getAtIJK(...ijkPoint)).toBe(15); expect(map.getAtIJKPoint(ijkPoint)).toBe(15); diff --git a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts index 05727aa825..5c2e02be63 100644 --- a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts +++ b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts @@ -220,9 +220,7 @@ async function addSegmentationsToState() { // Create a segmentation of the same resolution as the source data await volumeLoader.createAndCacheDerivedLabelmapVolume(volumeId, { volumeId: segmentationId, - // The following doesn't quite work yet - // TODO, allow RLE to be used instead of scalars. - // voxelRepresentation: Enums.VoxelManagerEnum.RLE, + voxelRepresentation: Enums.VoxelManagerEnum.RLE, }); // Add the segmentations to state diff --git a/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts b/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts index faef99eb37..0469b8c1f2 100644 --- a/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts +++ b/packages/tools/src/tools/segmentation/strategies/compositions/setValue.ts @@ -1,6 +1,5 @@ import type { InitializedOperationData } from '../BrushStrategy'; import StrategyCallbacks from '../../../../enums/StrategyCallbacks'; -import { triggerEvent, eventTarget } from '@cornerstonejs/core'; /** * Creates a set value function which will apply the specified segmentIndex From 48de81b6d562af1a4c0461aec57d8d1be758dc1b Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 29 Nov 2024 09:30:12 -0500 Subject: [PATCH 08/16] fix: Undefined error on reject preview --- .../vtkClasses/vtkStreamingOpenGLTexture.js | 3 +-- packages/core/src/utilities/RLEVoxelMap.ts | 12 ++++++++---- packages/core/src/utilities/VoxelManager.ts | 6 ++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js index 20992a1c7a..f91e11d249 100644 --- a/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js +++ b/packages/core/src/RenderingEngine/vtkClasses/vtkStreamingOpenGLTexture.js @@ -122,8 +122,7 @@ function vtkStreamingOpenGLTexture(publicAPI, model) { continue; } - // Get a temporary copy - let data = image.voxelManager.getScalarData(true); + let data = image.voxelManager.getScalarData(); const gl = model.context; if (volume.dataType !== data.constructor.name) { diff --git a/packages/core/src/utilities/RLEVoxelMap.ts b/packages/core/src/utilities/RLEVoxelMap.ts index 713ce98018..6fc67e6cca 100644 --- a/packages/core/src/utilities/RLEVoxelMap.ts +++ b/packages/core/src/utilities/RLEVoxelMap.ts @@ -122,9 +122,12 @@ export default class RLEVoxelMap { this.kMultiple = this.jMultiple * height; } - /** This is a function on the voxel manager, to get the RLE scalar data. */ - public static getScalarData = function () { - const scalarData = new Float32Array(this.frameSize); + /** + * This is a function on the voxel manager, to get the RLE scalar data. + * @returns an array of the given type for the data. + */ + public static getScalarData = function (ArrayType = Uint8ClampedArray) { + const scalarData = new ArrayType(this.frameSize); this.map.updateScalarData(scalarData); return scalarData; }; @@ -291,7 +294,8 @@ export default class RLEVoxelMap { */ public set = (index: number, value: T) => { if (value === undefined) { - throw new Error(`Can't set undefined at ${index % this.width}`); + // Don't store undefined values + return; } const i = index % this.width; const j = (index - i) / this.width; diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index 5c26ce5e20..971ceaf8a2 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -446,10 +446,12 @@ export default class VoxelManager { * Otherwise, if the `_getScalarData` method is defined, it will be called to retrieve the scalar data. * If neither the scalar data nor the `_getScalarData` method is available, an error will be thrown. * + * @param storeScalarData - a parameter to allow storing the scalar data rather than throwing it away + * each time. * @returns The scalar data. * @throws {Error} If no scalar data is available. */ - public getScalarData(transient = null) { + public getScalarData(storeScalarData = false) { if (this.scalarData) { this._updateScalarData?.(this.scalarData); return this.scalarData; @@ -457,7 +459,7 @@ export default class VoxelManager { if (this._getScalarData) { const scalarData = this._getScalarData(); - if (!transient) { + if (storeScalarData) { console.log('Not transient, should store value', scalarData); } return scalarData; From dbc90f135d64c965d58c109c8282aeb43237c16e Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 10:42:53 -0500 Subject: [PATCH 09/16] wip --- .../helpers/setDefaultVolumeVOI.ts | 1 - packages/core/src/loaders/imageLoader.ts | 3 +- packages/core/src/utilities/RLEVoxelMap.ts | 10 +++-- packages/core/src/utilities/VoxelManager.ts | 43 ++++++++++++------- .../index.ts | 2 + 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index 3b2547d83b..0854095951 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -6,7 +6,6 @@ import type { } from '../../types'; import { loadAndCacheImage } from '../../loaders/imageLoader'; import * as metaData from '../../metaData'; -import getMinMax from '../../utilities/getMinMax'; import * as windowLevel from '../../utilities/windowLevel'; import { RequestType } from '../../enums'; import cache from '../../cache/cache'; diff --git a/packages/core/src/loaders/imageLoader.ts b/packages/core/src/loaders/imageLoader.ts index 4c30bcfd81..2151b08461 100644 --- a/packages/core/src/loaders/imageLoader.ts +++ b/packages/core/src/loaders/imageLoader.ts @@ -138,7 +138,8 @@ function ensureVoxelManager(image: IImage): void { }); image.voxelManager = voxelManager; - image.getPixelData = () => voxelManager.getScalarData(); + image.getPixelData = () => + voxelManager.getScalarData() as PixelDataTypedArray; delete image.imageFrame.pixelData; } } diff --git a/packages/core/src/utilities/RLEVoxelMap.ts b/packages/core/src/utilities/RLEVoxelMap.ts index 6fc67e6cca..b050207f90 100644 --- a/packages/core/src/utilities/RLEVoxelMap.ts +++ b/packages/core/src/utilities/RLEVoxelMap.ts @@ -1,6 +1,6 @@ import type Point3 from '../types/Point3'; import type BoundsIJK from '../types/BoundsIJK'; -import { PixelDataTypedArray } from '../types'; +import type { PixelDataTypedArray } from '../types'; /** * The RLERun specifies a contigous run of values for a row, @@ -132,8 +132,12 @@ export default class RLEVoxelMap { return scalarData; }; - public updateScalarData = function (scalarData: ArrayLike) { - scalarData.set(0); + /** + * Update the scalar data with the current RLE state + * @param scalarData - old scalar data to update + */ + public updateScalarData = function (scalarData: PixelDataTypedArray) { + scalarData.fill(0); const callback = (index, rle, row) => { const { start, end, value } = rle; for (let i = start; i < end; i++) { diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index 971ceaf8a2..54ccafce7a 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -94,7 +94,7 @@ export default class VoxelManager { this.managerType = options.managerType; this._getConstructor = options._getConstructor; this.numberOfComponents = this.numberOfComponents || 1; - this.scalarData = this.scalarData; + this.scalarData = options.scalarData as PixelDataTypedArray; this._getScalarData = options._getScalarData; this._updateScalarData = options._updateScalarData; } @@ -451,7 +451,7 @@ export default class VoxelManager { * @returns The scalar data. * @throws {Error} If no scalar data is available. */ - public getScalarData(storeScalarData = false) { + public getScalarData(storeScalarData = false): PixelDataTypedArray { if (this.scalarData) { this._updateScalarData?.(this.scalarData); return this.scalarData; @@ -462,7 +462,7 @@ export default class VoxelManager { if (storeScalarData) { console.log('Not transient, should store value', scalarData); } - return scalarData; + return scalarData as PixelDataTypedArray; } throw new Error('No scalar data available'); @@ -723,7 +723,11 @@ export default class VoxelManager { const voxels = new VoxelManager(dimensions, { _get: (index) => { index *= numberOfComponents; - return [scalarData[index++], scalarData[index++], scalarData[index++]]; + return [ + scalarData[index++], + scalarData[index++], + scalarData[index++], + ] as RGB; }, managerType: '_createRGBScalarVolumeVoxelManager', _set: (index, v) => { @@ -784,29 +788,31 @@ export default class VoxelManager { } function getVoxelValue(index) { - const { voxelManager, pixelIndex } = getPixelInfo(index); + const { voxelManager: imageVoxelManager, pixelIndex } = + getPixelInfo(index); - if (!voxelManager || pixelIndex === null) { + if (!imageVoxelManager || pixelIndex === null) { return null; } - return voxelManager.getAtIndex(pixelIndex) as Number; + return imageVoxelManager.getAtIndex(pixelIndex) as number | RGB; } function setVoxelValue(index, v) { - const { voxelManager, pixelIndex } = getPixelInfo(index); + const { voxelManager: imageVoxelManager, pixelIndex } = + getPixelInfo(index); - if (!voxelManager || pixelIndex === null) { + if (!imageVoxelManager || pixelIndex === null) { return false; } - const currentValue = voxelManager.getAtIndex(pixelIndex); + const currentValue = imageVoxelManager.getAtIndex(pixelIndex); const isChanged = !isEqual(v, currentValue); if (!isChanged) { return isChanged; } - voxelManager.setAtIndex(pixelIndex, v as number); + imageVoxelManager.setAtIndex(pixelIndex, v as number); return true; } @@ -819,7 +825,7 @@ export default class VoxelManager { return pixelInfo.pixelData.constructor; }; - const voxelManager = new VoxelManager(dimensions, { + const voxelManager = new VoxelManager(dimensions, { _get: getVoxelValue, _set: setVoxelValue, numberOfComponents, @@ -1054,6 +1060,7 @@ export default class VoxelManager { // Create a VoxelManager that will manage the active voxel group const voxelManager = new VoxelManager(dimensions, { _get: (index) => voxelGroups[timePoint]._get(index), + // @ts-ignore _set: (index, v) => voxelGroups[timePoint]._set(index, v), numberOfComponents, managerType: 'createScalarDynamicVolumeVoxelManager', @@ -1262,10 +1269,13 @@ export default class VoxelManager { this.sourceVoxelManager.setAtIndex(index, v); }, _getScalarData: RLEVoxelMap.getScalarData, - _updateScalarData: map.updateScalarData, + _updateScalarData: (scalarData) => { + map.updateScalarData(scalarData as PixelDataTypedArray); + return scalarData as PixelDataTypedArray; + }, managerType: 'createRLEHistoryVoxelManager', - map, }); + voxelManager.map = map; voxelManager.sourceVoxelManager = sourceVoxelManager; return voxelManager; } @@ -1324,7 +1334,10 @@ export default class VoxelManager { return true; }, _getScalarData: RLEVoxelMap.getScalarData, - _updateScalarData: map.updateScalarData, + _updateScalarData: (scalarData) => { + map.updateScalarData(scalarData as PixelDataTypedArray); + return scalarData as PixelDataTypedArray; + }, managerType: 'createRLEVolumeVoxelManager', }); voxelManager.map = map; diff --git a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts index 5c2e02be63..9b8f8d24b6 100644 --- a/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts +++ b/packages/tools/examples/labelmapSegmentationDynamicThreshold/index.ts @@ -220,6 +220,8 @@ async function addSegmentationsToState() { // Create a segmentation of the same resolution as the source data await volumeLoader.createAndCacheDerivedLabelmapVolume(volumeId, { volumeId: segmentationId, + // The following doesn't quite work yet + // TODO, allow RLE to be used instead of scalars. voxelRepresentation: Enums.VoxelManagerEnum.RLE, }); From 7d81953c3f68f616b55673b4d3e72be3fd9178ca Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 10:45:00 -0500 Subject: [PATCH 10/16] wip --- packages/core/src/utilities/VoxelManager.ts | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index 54ccafce7a..630d19a53c 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -48,7 +48,7 @@ export default class VoxelManager { // a limit on the number of slices to cache since it can grow indefinitely private _sliceDataCache = null as Map; - public readonly managerType: string; + public readonly _id: string; points: Set; width: number; @@ -77,7 +77,7 @@ export default class VoxelManager { _get: (index: number) => T; _set?: (index: number, v: T) => boolean; _getScalarData?: () => ArrayLike; - managerType: string; + _id?: string; _updateScalarData?: ( scalarData: ArrayLike ) => PixelDataTypedArray; @@ -91,7 +91,7 @@ export default class VoxelManager { this.frameSize = this.width * dimensions[1]; this._get = options._get; this._set = options._set; - this.managerType = options.managerType; + this._id = options._id || ''; this._getConstructor = options._getConstructor; this.numberOfComponents = this.numberOfComponents || 1; this.scalarData = options.scalarData as PixelDataTypedArray; @@ -729,7 +729,7 @@ export default class VoxelManager { scalarData[index++], ] as RGB; }, - managerType: '_createRGBScalarVolumeVoxelManager', + _id: '_createRGBScalarVolumeVoxelManager', _set: (index, v) => { index *= 3; const isChanged = !isEqual(scalarData[index], v); @@ -830,7 +830,7 @@ export default class VoxelManager { _set: setVoxelValue, numberOfComponents, _getConstructor, - managerType: 'createImageVolumeVoxelManager', + _id: 'createImageVolumeVoxelManager', }); voxelManager.getMiddleSliceData = () => { @@ -1063,7 +1063,7 @@ export default class VoxelManager { // @ts-ignore _set: (index, v) => voxelGroups[timePoint]._set(index, v), numberOfComponents, - managerType: 'createScalarDynamicVolumeVoxelManager', + _id: 'createScalarDynamicVolumeVoxelManager', }) as IVoxelManager | IVoxelManager; voxelManager.getScalarDataLength = () => { @@ -1170,7 +1170,7 @@ export default class VoxelManager { scalarData[index] = v; return isChanged; }, - managerType: '_createNumberVolumeVoxelManager', + _id: '_createNumberVolumeVoxelManager', }); voxels.scalarData = scalarData; @@ -1199,7 +1199,7 @@ export default class VoxelManager { const voxelManager = new VoxelManager(dimension, { _get: map.get.bind(map), _set: (index, v) => map.set(index, v) && true, - managerType: 'createMapVoxelManager', + _id: 'createMapVoxelManager', }); voxelManager.map = map; return voxelManager; @@ -1232,7 +1232,7 @@ export default class VoxelManager { } this.sourceVoxelManager.setAtIndex(index, v); }, - managerType: 'createHistoryVoxelManager', + _id: 'createHistoryVoxelManager', }); voxelManager.map = map; voxelManager.scalarData = sourceVoxelManager.scalarData; @@ -1273,7 +1273,7 @@ export default class VoxelManager { map.updateScalarData(scalarData as PixelDataTypedArray); return scalarData as PixelDataTypedArray; }, - managerType: 'createRLEHistoryVoxelManager', + _id: 'createRLEHistoryVoxelManager', }); voxelManager.map = map; voxelManager.sourceVoxelManager = sourceVoxelManager; @@ -1309,7 +1309,7 @@ export default class VoxelManager { layer[index % planeSize] = v; return true; }, - managerType: 'createLazyVoxelManager', + _id: 'createLazyVoxelManager', }); voxelManager.map = map; return voxelManager; @@ -1338,7 +1338,7 @@ export default class VoxelManager { map.updateScalarData(scalarData as PixelDataTypedArray); return scalarData as PixelDataTypedArray; }, - managerType: 'createRLEVolumeVoxelManager', + _id: 'createRLEVolumeVoxelManager', }); voxelManager.map = map; // @ts-ignore From 7a4463ca78a6a796c575d3372e9556faaaa3550b Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 11:12:14 -0500 Subject: [PATCH 11/16] refactoring --- common/reviews/api/core.api.md | 44 ++++- packages/core/src/utilities/VoxelManager.ts | 96 ++-------- .../src/utilities/createPositionCallback.ts | 76 ++++++++ .../src/utilities/pointInShapeCallback.ts | 171 ++++++++---------- 4 files changed, 196 insertions(+), 191 deletions(-) create mode 100644 packages/core/src/utilities/createPositionCallback.ts diff --git a/common/reviews/api/core.api.md b/common/reviews/api/core.api.md index 9b39c547f4..daab9ca544 100644 --- a/common/reviews/api/core.api.md +++ b/common/reviews/api/core.api.md @@ -3075,11 +3075,13 @@ class RLEVoxelMap { // (undocumented) clear(): void; // (undocumented) + static copyMap(destination: RLEVoxelMap, source: RLEVoxelMap): void; + // (undocumented) defaultValue: T; // (undocumented) delete(index: number): void; // (undocumented) - protected depth: number; + depth: number; // (undocumented) fillFrom(getter: (i: number, j: number, k: number) => T, boundsIJK: BoundsIJK): void; // (undocumented) @@ -3111,9 +3113,11 @@ class RLEVoxelMap { // (undocumented) getRun: (j: number, k: number) => RLERun[]; // (undocumented) + static getScalarData: (ArrayType?: Uint8ClampedArrayConstructor) => Uint8ClampedArray; + // (undocumented) has(index: number): boolean; // (undocumented) - protected height: number; + height: number; // (undocumented) protected jMultiple: number; // (undocumented) @@ -3135,7 +3139,9 @@ class RLEVoxelMap { // (undocumented) toIndex([i, j, k]: Point3): number; // (undocumented) - protected width: number; + updateScalarData: (scalarData: PixelDataTypedArray) => void; + // (undocumented) + width: number; } // @public (undocumented) @@ -4804,7 +4810,16 @@ type VolumeViewportProperties = ViewportProperties & { // @public (undocumented) class VoxelManager { - constructor(dimensions: any, _get: (index: number) => T, _set?: (index: number, v: T) => boolean); + constructor(dimensions: any, options: { + _get: (index: number) => T; + _set?: (index: number, v: T) => boolean; + _getScalarData?: () => ArrayLike; + _id?: string; + _updateScalarData?: (scalarData: ArrayLike) => PixelDataTypedArray; + numberOfComponents?: number; + scalarData?: ArrayLike; + _getConstructor?: () => new (length: number) => PixelDataTypedArray; + }); // (undocumented) static addBounds(bounds: BoundsIJK, point: Point3): void; // (undocumented) @@ -4881,7 +4896,7 @@ class VoxelManager { // (undocumented) frameSize: number; // (undocumented) - _get: (index: number) => T; + readonly _get: (index: number) => T; // (undocumented) getArrayOfModifiedSlices(): number[]; // (undocumented) @@ -4897,19 +4912,24 @@ class VoxelManager { // (undocumented) getConstructor(): new (length: number) => PixelDataTypedArray; // (undocumented) - _getConstructor?: () => new (length: number) => PixelDataTypedArray; + readonly _getConstructor?: () => new (length: number) => PixelDataTypedArray; // (undocumented) getDefaultBounds(): BoundsIJK; // (undocumented) getMiddleSliceData: () => PixelDataTypedArray; // (undocumented) + getMinMax(): { + min: any; + max: any; + }; + // (undocumented) getPoints(): Point3[]; // (undocumented) getRange: () => [number, number]; // (undocumented) - getScalarData(): PixelDataTypedArray; + getScalarData(storeScalarData?: boolean): PixelDataTypedArray; // (undocumented) - _getScalarData?: () => PixelDataTypedArray; + _getScalarData?: () => ArrayLike; // (undocumented) getScalarDataLength(): number; // (undocumented) @@ -4925,13 +4945,15 @@ class VoxelManager { slicePlane: number; }) => PixelDataTypedArray; // (undocumented) + readonly _id: string; + // (undocumented) isInObject: (pointLPS: any, pointIJK: any) => boolean; // (undocumented) map: Map | IRLEVoxelMap; // (undocumented) modifiedSlices: Set; // (undocumented) - numberOfComponents: number; + readonly numberOfComponents: any; // (undocumented) points: Set; // (undocumented) @@ -4939,7 +4961,7 @@ class VoxelManager { // (undocumented) rleForEach(callback: any, options?: any): void; // (undocumented) - _set: (index: number, v: T) => boolean; + readonly _set: (index: number, v: T) => boolean; // (undocumented) setAtIJK: (i: number, j: number, k: number, v: any) => boolean; // (undocumented) @@ -4961,6 +4983,8 @@ class VoxelManager { // (undocumented) toIndex(ijk: Point3): number; // (undocumented) + _updateScalarData?: (scalarData: ArrayLike) => PixelDataTypedArray; + // (undocumented) width: number; } diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index 630d19a53c..0c4392d96f 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -14,6 +14,7 @@ import type { import RLEVoxelMap from './RLEVoxelMap'; import isEqual from './isEqual'; import type vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import { iterateOverPointsInShapeVoxelManager } from './pointInShapeCallback'; /** * Have a default size for cached RLE encoded images. This is hard to guess @@ -263,95 +264,22 @@ export default class VoxelManager { const pointsInShape = []; if (useLPSTransform) { - const { imageData } = options; - const direction = imageData.getDirection(); - const rowCosines = direction.slice(0, 3); - const columnCosines = direction.slice(3, 6); - const scanAxisNormal = direction.slice(6, 9); - - const spacing = imageData.getSpacing(); - const [rowSpacing, columnSpacing, scanAxisSpacing] = spacing; - - // @ts-ignore will - - const start = vec3.fromValues(iMin, jMin, kMin); - - // @ts-ignore will be fixed in vtk-master - const worldPosStart = imageData.indexToWorld(start); - - const rowStep = vec3.fromValues( - rowCosines[0] * rowSpacing, - rowCosines[1] * rowSpacing, - rowCosines[2] * rowSpacing - ); - - const columnStep = vec3.fromValues( - columnCosines[0] * columnSpacing, - columnCosines[1] * columnSpacing, - columnCosines[2] * columnSpacing - ); - - const scanAxisStep = vec3.fromValues( - scanAxisNormal[0] * scanAxisSpacing, - scanAxisNormal[1] * scanAxisSpacing, - scanAxisNormal[2] * scanAxisSpacing - ); - - const currentPos = vec3.clone(worldPosStart) as Point3; - - for (let k = kMin; k <= kMax; k++) { - // Todo: There is an optimized version of this in the currently closed source - // code - it is quite noticeably faster as it pre-generates i,j,k arrays - // and adds the three numbers for each dimension. That version actually works - // with sparse data voxel managers, whereas this one doesn't - const startPosJ = vec3.clone(currentPos); - - for (let j = jMin; j <= jMax; j++) { - const startPosI = vec3.clone(currentPos); - - for (let i = iMin; i <= iMax; i++) { - const pointIJK = [i, j, k] as Point3; - - // The current world position (pointLPS) is now in currentPos - if (isInObject(currentPos, pointIJK)) { - const index = this.toIndex(pointIJK); - - const value = this._get(index); - - if (returnPoints) { - pointsInShape.push({ - value, - index, - pointIJK, - pointLPS: currentPos.slice(), - }); - } - - if (callback) { - callback({ value, index, pointIJK, pointLPS: currentPos }); - } - } - - // Increment currentPos by rowStep for the next iteration - vec3.add(currentPos, currentPos, rowStep); - } - - // Reset currentPos to the start of the next J line and increment by columnStep - vec3.copy(currentPos, startPosI); - vec3.add(currentPos, currentPos, columnStep); - } - - // Reset currentPos to the start of the next K slice and increment by scanAxisStep - vec3.copy(currentPos, startPosJ); - vec3.add(currentPos, currentPos, scanAxisStep); - } - + const pointsInShape = iterateOverPointsInShapeVoxelManager({ + voxelManager: this, + imageData: options.imageData, + bounds: [ + [iMin, iMax], + [jMin, jMax], + [kMin, kMax], + ], + pointInShapeFn: isInObject, + callback, + }); return pointsInShape; } // We don't need the complex LPS calculations and we can just iterate over the data // in the IJK coordinate system - if (this.map) { if (this.map instanceof RLEVoxelMap) { return this.rleForEach(callback, options); diff --git a/packages/core/src/utilities/createPositionCallback.ts b/packages/core/src/utilities/createPositionCallback.ts new file mode 100644 index 0000000000..bcea6a9ddf --- /dev/null +++ b/packages/core/src/utilities/createPositionCallback.ts @@ -0,0 +1,76 @@ +import * as vec3 from 'gl-matrix/vec3'; +import PointsManager from './PointsManager'; +import type { Point3 } from '../types'; + +/** + * Returns a function that takes an ijk position and efficiently returns + * the world position. Only works for integer ijk, AND values within the bounds. + * The position array is re-used, so don't preserve it/compare for different + * values, although you can provide an instance position to copy into. + * + * This function is safe to use out of order, and is stable in terms of calculations. + */ +export function createPositionCallback(imageData) { + const currentPos = vec3.create(); + const dimensions = imageData.getDimensions(); + const positionI = PointsManager.create3(dimensions[0]); + const positionJ = PointsManager.create3(dimensions[1]); + const positionK = PointsManager.create3(dimensions[2]); + + const direction = imageData.getDirection(); + const rowCosines = direction.slice(0, 3); + const columnCosines = direction.slice(3, 6); + const scanAxisNormal = direction.slice(6, 9); + + const spacing = imageData.getSpacing(); + const [rowSpacing, columnSpacing, scanAxisSpacing] = spacing; + + const worldPosStart = imageData.indexToWorld([0, 0, 0]); + + const rowStep = vec3.fromValues( + rowCosines[0] * rowSpacing, + rowCosines[1] * rowSpacing, + rowCosines[2] * rowSpacing + ); + + const columnStep = vec3.fromValues( + columnCosines[0] * columnSpacing, + columnCosines[1] * columnSpacing, + columnCosines[2] * columnSpacing + ); + + const scanAxisStep = vec3.fromValues( + scanAxisNormal[0] * scanAxisSpacing, + scanAxisNormal[1] * scanAxisSpacing, + scanAxisNormal[2] * scanAxisSpacing + ); + + const scaled = vec3.create(); + // Add the world position start to the I component so we don't need to add it + for (let i = 0; i < dimensions[0]; i++) { + positionI.push( + vec3.add(scaled, worldPosStart, vec3.scale(scaled, rowStep, i)) as Point3 + ); + } + for (let j = 0; j < dimensions[0]; j++) { + positionJ.push(vec3.scale(scaled, columnStep, j) as Point3); + } + for (let k = 0; k < dimensions[0]; k++) { + positionK.push(vec3.scale(scaled, scanAxisStep, k) as Point3); + } + + const dataI = positionI.getTypedArray(); + const dataJ = positionJ.getTypedArray(); + const dataK = positionK.getTypedArray(); + + return (ijk, destPoint = currentPos) => { + const [i, j, k] = ijk; + const offsetI = i * 3; + const offsetJ = j * 3; + const offsetK = k * 3; + destPoint[0] = dataI[offsetI] + dataJ[offsetJ] + dataK[offsetK]; + destPoint[1] = dataI[offsetI + 1] + dataJ[offsetJ + 1] + dataK[offsetK + 1]; + destPoint[2] = dataI[offsetI + 2] + dataJ[offsetJ + 2] + dataK[offsetK + 2]; + return destPoint as Point3; + }; +} diff --git a/packages/core/src/utilities/pointInShapeCallback.ts b/packages/core/src/utilities/pointInShapeCallback.ts index 281c391abd..f2be19c084 100644 --- a/packages/core/src/utilities/pointInShapeCallback.ts +++ b/packages/core/src/utilities/pointInShapeCallback.ts @@ -1,8 +1,8 @@ -import { vec3 } from 'gl-matrix'; +import type { vec3 } from 'gl-matrix'; import type { vtkImageData } from '@kitware/vtk.js/Common/DataModel/ImageData'; import type BoundsIJK from '../types/BoundsIJK'; import type { CPUImageData, Point3 } from '../types'; -import PointsManager from './PointsManager'; +import { createPositionCallback } from './createPositionCallback'; export type PointInShape = { value: number; @@ -44,80 +44,6 @@ export interface PointInShapeOptions { // minMaxGenerator?: (row: number) => { min: number, max: number }; } -/** - * Returns a function that takes an ijk position and efficiently returns - * the world position. Only works for integer ijk, AND values within the bounds. - * The position array is re-used, so don't preserve it/compare for different - * values, although you can provide an instance position to copy into. - * - * This function is safe to use out of order, and is stable in terms of calculations. - */ -export function createPositionCallback(imageData) { - const currentPos = vec3.create(); - const dimensions = imageData.getDimensions(); - const positionI = PointsManager.create3(dimensions[0]); - const positionJ = PointsManager.create3(dimensions[1]); - const positionK = PointsManager.create3(dimensions[2]); - - const direction = imageData.getDirection(); - const rowCosines = direction.slice(0, 3); - const columnCosines = direction.slice(3, 6); - const scanAxisNormal = direction.slice(6, 9); - - const spacing = imageData.getSpacing(); - const [rowSpacing, columnSpacing, scanAxisSpacing] = spacing; - - // @ts-ignore will be fixed in vtk-master - const worldPosStart = imageData.indexToWorld([0, 0, 0]); - - const rowStep = vec3.fromValues( - rowCosines[0] * rowSpacing, - rowCosines[1] * rowSpacing, - rowCosines[2] * rowSpacing - ); - - const columnStep = vec3.fromValues( - columnCosines[0] * columnSpacing, - columnCosines[1] * columnSpacing, - columnCosines[2] * columnSpacing - ); - - const scanAxisStep = vec3.fromValues( - scanAxisNormal[0] * scanAxisSpacing, - scanAxisNormal[1] * scanAxisSpacing, - scanAxisNormal[2] * scanAxisSpacing - ); - - const scaled = vec3.create(); - // Add the world position start to the I component so we don't need to add it - for (let i = 0; i < dimensions[0]; i++) { - positionI.push( - vec3.add(scaled, worldPosStart, vec3.scale(scaled, rowStep, i)) as Point3 - ); - } - for (let j = 0; j < dimensions[0]; j++) { - positionJ.push(vec3.scale(scaled, columnStep, j) as Point3); - } - for (let k = 0; k < dimensions[0]; k++) { - positionK.push(vec3.scale(scaled, scanAxisStep, k) as Point3); - } - - const dataI = positionI.getTypedArray(); - const dataJ = positionJ.getTypedArray(); - const dataK = positionK.getTypedArray(); - - return (ijk, destPoint = currentPos) => { - const [i, j, k] = ijk; - const offsetI = i * 3; - const offsetJ = j * 3; - const offsetK = k * 3; - destPoint[0] = dataI[offsetI] + dataJ[offsetJ] + dataK[offsetK]; - destPoint[1] = dataI[offsetI + 1] + dataJ[offsetJ + 1] + dataK[offsetK + 1]; - destPoint[2] = dataI[offsetI + 2] + dataJ[offsetJ + 2] + dataK[offsetK + 2]; - return destPoint as Point3; - }; -} - /** * For each point in the image (If boundsIJK is not provided, otherwise, for each * point in the provided bounding box), It runs the provided callback IF the point @@ -139,17 +65,9 @@ export function pointInShapeCallback( imageData: vtkImageData | CPUImageData, options: PointInShapeOptions ): Array | undefined { - const { - pointInShapeFn, - callback, - boundsIJK, - returnPoints = false, - // Destructure other options here as needed - } = options; - let iMin, iMax, jMin, jMax, kMin, kMax; + const { pointInShapeFn, callback, boundsIJK, returnPoints = false } = options; let scalarData; - const { numComps } = imageData as { numComps: number }; // if getScalarData is a method on imageData if ((imageData as CPUImageData).getScalarData) { @@ -163,16 +81,34 @@ export function pointInShapeCallback( const dimensions = imageData.getDimensions(); - if (!boundsIJK) { - iMin = 0; - iMax = dimensions[0]; - jMin = 0; - jMax = dimensions[1]; - kMin = 0; - kMax = dimensions[2]; - } else { - [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK; - } + const defaultBoundsIJK = [ + [0, dimensions[0]], + [0, dimensions[1]], + [0, dimensions[2]], + ]; + const bounds = boundsIJK || defaultBoundsIJK; + + const pointsInShape = iterateOverPointsInShape({ + imageData, + bounds, + scalarData, + pointInShapeFn, + callback, + }); + + return returnPoints ? pointsInShape : undefined; +} + +export function iterateOverPointsInShape({ + imageData, + bounds, + scalarData, + pointInShapeFn, + callback, +}) { + const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = bounds; + const { numComps } = imageData as { numComps: number }; + const dimensions = imageData.getDimensions(); const indexToWorld = createPositionCallback(imageData); const pointIJK = [0, 0, 0] as Point3; @@ -223,6 +159,47 @@ export function pointInShapeCallback( } } - // Modify the return statement - return returnPoints ? pointsInShape : undefined; + return pointsInShape; +} + +export function iterateOverPointsInShapeVoxelManager({ + voxelManager, + bounds, + imageData, + pointInShapeFn, + callback, +}) { + const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = bounds; + const indexToWorld = createPositionCallback(imageData); + const pointIJK = [0, 0, 0] as Point3; + const pointsInShape: Array = []; + + for (let k = kMin; k <= kMax; k++) { + pointIJK[2] = k; + + for (let j = jMin; j <= jMax; j++) { + pointIJK[1] = j; + + for (let i = iMin; i <= iMax; i++) { + pointIJK[0] = i; + const pointLPS = indexToWorld(pointIJK); + + if (pointInShapeFn(pointLPS, pointIJK)) { + const index = voxelManager.toIndex(pointIJK); + const value = voxelManager.getAtIndex(index); + + pointsInShape.push({ + value, + index, + pointIJK: [...pointIJK], + pointLPS: pointLPS.slice(), + }); + + callback?.({ value, index, pointIJK, pointLPS }); + } + } + } + } + + return pointsInShape; } From e1024e84c9e595f4eab9c813d9756c75b5a4f54a Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 11:18:07 -0500 Subject: [PATCH 12/16] fix --- packages/core/test/utilities/RLEVoxelMap.jest.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/test/utilities/RLEVoxelMap.jest.js b/packages/core/test/utilities/RLEVoxelMap.jest.js index d93aa3f9f3..f57482bbd9 100644 --- a/packages/core/test/utilities/RLEVoxelMap.jest.js +++ b/packages/core/test/utilities/RLEVoxelMap.jest.js @@ -6,7 +6,7 @@ const size = [64, 128, 4]; const ijkPoint = [4, 2, 2]; const rleMap = new RLEVoxelMap(64, 128, 4); -const voxelMap = VoxelManager.createLazyVoxelManager(size); +const voxelMap = VoxelManager.createLazyVoxelManager({ dimensions: size }); const j = 4; const baseIndex = j * 64; @@ -81,7 +81,9 @@ describe('RLEVoxelMap', () => { describe('RLEVoxelManager', () => { it('sets', () => { - const map = VoxelManager.createRLEVoxelManager(size); + const map = VoxelManager.createRLEVolumeVoxelManager({ + dimensions: size, + }); map.setAtIJK(...ijkPoint, 15); expect(map.getAtIJK(...ijkPoint)).toBe(15); expect(map.getAtIJKPoint(ijkPoint)).toBe(15); From 964701a14558231dd8a0282d037a662b850edba7 Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 11:59:13 -0500 Subject: [PATCH 13/16] Enhance VoxelManager and RenderingEngine: - Updated `getMinMax` method in `VoxelManager` to handle array inputs for min/max calculations. - Modified `setDefaultVolumeVOI` to correctly compute min/max values from voxel data. - Cleaned up and restructured tests in `volumeViewport_gpu_render_test.js`, restoring necessary imports and ensuring proper rendering tests for various volume types. --- .../helpers/setDefaultVolumeVOI.ts | 8 +- packages/core/src/utilities/VoxelManager.ts | 4 +- .../test/volumeViewport_gpu_render_test.js | 1805 ++++++++--------- 3 files changed, 905 insertions(+), 912 deletions(-) diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index 0854095951..e9a01ab10e 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -180,7 +180,13 @@ async function getVOIFromMiddleSliceMinMax( } // Get the min and max pixel values of the middle slice - const { min, max } = image.voxelManager.getMinMax(); + let { min, max } = image.voxelManager.getMinMax(); + + if (min.length > 1) { + min = Math.min(...min); + max = Math.max(...max); + } + return { lower: min, upper: max, diff --git a/packages/core/src/utilities/VoxelManager.ts b/packages/core/src/utilities/VoxelManager.ts index 0c4392d96f..034710e195 100644 --- a/packages/core/src/utilities/VoxelManager.ts +++ b/packages/core/src/utilities/VoxelManager.ts @@ -148,7 +148,7 @@ export default class VoxelManager { /** Gets the min/max pair - as array for RGB */ public getMinMax() { let min, max; - const callback = (v) => { + const callback = ({ value: v }) => { const isArray = Array.isArray(v); if (min === undefined) { min = isArray ? [...v] : v; @@ -164,7 +164,7 @@ export default class VoxelManager { max = Math.max(max, v); } }; - this.forEach(callback); + this.forEach(callback, { boundsIJK: this.getDefaultBounds() }); return { min, max }; } diff --git a/packages/core/test/volumeViewport_gpu_render_test.js b/packages/core/test/volumeViewport_gpu_render_test.js index 8c990eeeff..d366e63626 100644 --- a/packages/core/test/volumeViewport_gpu_render_test.js +++ b/packages/core/test/volumeViewport_gpu_render_test.js @@ -1,909 +1,896 @@ -// import * as cornerstone3D from '../src/index'; -// import * as testUtils from '../../../utils/test/testUtils'; -// import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; -// import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; -// import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; - -// // poly data -// import * as sphere_default_sagittal from './groundTruth/sphere_default_sagittal.png'; - -// // nearest neighbor interpolation -// import * as volumeURI_100_100_10_1_1_1_0_axial_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_nearest.png'; -// import * as volumeURI_100_100_10_1_1_1_0_sagittal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_nearest.png'; -// import * as volumeURI_100_100_10_1_1_1_0_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_nearest.png'; -// import * as volumeURI_100_100_10_1_1_1_1_color_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_nearest.png'; - -// // linear interpolation -// import * as volumeURI_100_100_10_1_1_1_0_axial_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_linear.png'; -// import * as volumeURI_100_100_10_1_1_1_0_sagittal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_linear.png'; -// import * as volumeURI_100_100_10_1_1_1_0_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_linear.png'; -// import * as volumeURI_100_100_10_1_1_1_1_color_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_linear.png'; -// import * as volumeURI_100_100_10_1_1_1_1_color_axial_linear from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_axial_linear.png'; - -// const { -// cache, -// RenderingEngine, -// imageLoader, -// metaData, -// Enums, -// volumeLoader, -// utilities, -// setVolumesForViewports, -// } = cornerstone3D; - -// const { ViewportType, Events } = Enums; - -// const { registerVolumeLoader } = volumeLoader; -// const { unregisterAllImageLoaders } = imageLoader; - -// const { fakeMetaDataProvider, compareImages, fakeVolumeLoader } = testUtils; - -// const renderingEngineId = utilities.uuidv4(); -// const viewportId = 'VIEWPORT'; - -// describe('Volume Viewport GPU -- ', () => { -// let renderingEngine; - -// beforeEach(function () { -// const testEnv = testUtils.setupTestEnvironment({ -// renderingEngineId, -// toolGroupIds: ['default'], -// }); -// renderingEngine = testEnv.renderingEngine; -// }); - -// afterEach(function () { -// testUtils.cleanupTestEnvironment({ -// renderingEngineId, -// toolGroupIds: ['default'], -// }); -// }); - -// describe('Volume Viewport Sagittal PolyData --- ', function () { -// it('should successfully render a sphere source', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.SAGITTAL, -// viewportType: ViewportType.VOLUME_3D, -// }); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// testUtils -// .compareImages( -// image, -// sphere_default_sagittal, -// 'sphere_default_sagittal' -// ) -// .then(done, done.fail); -// }); - -// try { -// const sphereSource = vtkSphereSource.newInstance({ -// center: [0, 0, 0], -// radius: 100, -// phiResolution: 10, -// thetaResolution: 10, -// }); -// const actor = vtkActor.newInstance(); -// const mapper = vtkMapper.newInstance(); - -// actor.getProperty().setEdgeVisibility(true); - -// mapper.setInputConnection(sphereSource.getOutputPort()); -// actor.setMapper(mapper); - -// const nonVolumeActors = []; -// nonVolumeActors.push({ uid: 'spherePolyData', actor }); - -// vp.setActors(nonVolumeActors); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Volume Viewport Axial Nearest Neighbor and Linear Interpolation --- ', function () { -// it('should successfully load a volume: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 11, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_axial_nearest, -// 'volumeURI_100_100_10_1_1_1_0_axial_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully load a volume: linear', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_axial_linear, -// 'volumeURI_100_100_10_1_1_1_0_axial_linear' -// ) -// .then(done, done.fail); -// }); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId }], -// [viewportId] -// ); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Volume Viewport Sagittal Nearest Neighbor and Linear Interpolation --- ', function () { -// it('should successfully load a volume: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.SAGITTAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_sagittal_nearest, -// 'volumeURI_100_100_10_1_1_1_0_sagittal_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully load a volume: linear', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.SAGITTAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_sagittal_linear, -// 'volumeURI_100_100_10_1_1_1_0_sagittal_linear' -// ) -// .then(done, done.fail); -// }); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Volume Viewport Sagittal Coronal Neighbor and Linear Interpolation --- ', function () { -// it('should successfully load a volume: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_coronal_nearest, -// 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// // we don't set imageIds as we are mocking the imageVolume to -// // return the volume immediately -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully load a volume: linear', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_coronal_linear, -// 'volumeURI_100_100_10_1_1_1_0_coronal_linear' -// ) -// .then(done, done.fail); -// }); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Rendering API', function () { -// it('should successfully use setVolumesForViewports API to load image', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_coronal_nearest, -// 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should be able to filter viewports based on volumeId', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const viewport = renderingEngine.getViewport(viewportId); -// const viewports = utilities.getViewportsWithVolumeId( -// volumeId, -// renderingEngine.id -// ); - -// expect(viewports.length).toBe(1); -// expect(viewports[0]).toBe(viewport); - -// done(); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// renderingEngine.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully use renderViewports API to load image', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const vp = renderingEngine.getViewport(viewportId); -// const canvas = vp.getCanvas(); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_coronal_nearest, -// 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully use renderViewport API to load image', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_coronal_nearest, -// 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully debug the offscreen canvas', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// const offScreen = renderingEngine._debugRender(); -// expect(offScreen).toEqual(image); -// done(); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully render frameOfReference', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_0_coronal_nearest, -// 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => -// volumeActor.getProperty().setInterpolationTypeToNearest(); - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ).then(() => { -// renderingEngine.renderFrameOfReference( -// 'Volume_Frame_Of_Reference' -// ); -// }); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { -// it('should successfully load a color volume: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// rgb: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_1_color_coronal_nearest, -// 'volumeURI_100_100_10_1_1_1_1_color_coronal_nearest' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => { -// volumeActor.getProperty().setInterpolationTypeToNearest(); -// }; - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully load a volume: linear', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// rgb: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_1_color_coronal_linear, -// 'volumeURI_100_100_10_1_1_1_1_color_coronal_linear' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => { -// volumeActor.getProperty().setInterpolationTypeToLinear(); -// }; - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('should successfully load a volume: linear', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// viewportType: ViewportType.ORTHOGRAPHIC, -// }); - -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 100, -// columns: 100, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// zSpacing: 1, -// rgb: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_100_100_10_1_1_1_1_color_axial_linear, -// 'volumeURI_100_100_10_1_1_1_1_color_axial_linear' -// ) -// .then(done, done.fail); -// }); - -// const callback = ({ volumeActor }) => { -// volumeActor.getProperty().setInterpolationTypeToLinear(); -// }; - -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId, callback }], -// [viewportId] -// ); -// vp.render(); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); -// }); +import * as cornerstone3D from '../src/index'; +import * as testUtils from '../../../utils/test/testUtils'; +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; + +// poly data +import * as sphere_default_sagittal from './groundTruth/sphere_default_sagittal.png'; + +// nearest neighbor interpolation +import * as volumeURI_100_100_10_1_1_1_0_axial_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_nearest.png'; +import * as volumeURI_100_100_10_1_1_1_0_sagittal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_nearest.png'; +import * as volumeURI_100_100_10_1_1_1_0_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_nearest.png'; +import * as volumeURI_100_100_10_1_1_1_1_color_coronal_nearest from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_nearest.png'; + +// linear interpolation +import * as volumeURI_100_100_10_1_1_1_0_axial_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_axial_linear.png'; +import * as volumeURI_100_100_10_1_1_1_0_sagittal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_sagittal_linear.png'; +import * as volumeURI_100_100_10_1_1_1_0_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_0_coronal_linear.png'; +import * as volumeURI_100_100_10_1_1_1_1_color_coronal_linear from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_linear.png'; +import * as volumeURI_100_100_10_1_1_1_1_color_axial_linear from './groundTruth/volumeURI_100_100_10_1_1_1_1_color_axial_linear.png'; + +const { imageLoader, Enums, volumeLoader, utilities, setVolumesForViewports } = + cornerstone3D; + +const { ViewportType, Events } = Enums; + +const renderingEngineId = utilities.uuidv4(); +const viewportId = 'VIEWPORT'; + +describe('Volume Viewport GPU -- ', () => { + let renderingEngine; + + beforeEach(function () { + const testEnv = testUtils.setupTestEnvironment({ + renderingEngineId, + toolGroupIds: ['default'], + }); + renderingEngine = testEnv.renderingEngine; + }); + + afterEach(function () { + testUtils.cleanupTestEnvironment({ + renderingEngineId, + toolGroupIds: ['default'], + }); + }); + + describe('Volume Viewport Sagittal PolyData --- ', function () { + it('should successfully render a sphere source', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.SAGITTAL, + viewportType: ViewportType.VOLUME_3D, + }); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + testUtils + .compareImages( + image, + sphere_default_sagittal, + 'sphere_default_sagittal' + ) + .then(done, done.fail); + }); + + try { + const sphereSource = vtkSphereSource.newInstance({ + center: [0, 0, 0], + radius: 100, + phiResolution: 10, + thetaResolution: 10, + }); + const actor = vtkActor.newInstance(); + const mapper = vtkMapper.newInstance(); + + actor.getProperty().setEdgeVisibility(true); + + mapper.setInputConnection(sphereSource.getOutputPort()); + actor.setMapper(mapper); + + const nonVolumeActors = []; + nonVolumeActors.push({ uid: 'spherePolyData', actor }); + + vp.setActors(nonVolumeActors); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Volume Viewport Axial Nearest Neighbor and Linear Interpolation --- ', function () { + it('should successfully load a volume: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 11, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_axial_nearest, + 'volumeURI_100_100_10_1_1_1_0_axial_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully load a volume: linear', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_axial_linear, + 'volumeURI_100_100_10_1_1_1_0_axial_linear' + ) + .then(done, done.fail); + }); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId }], + [viewportId] + ); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Volume Viewport Sagittal Nearest Neighbor and Linear Interpolation --- ', function () { + it('should successfully load a volume: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.SAGITTAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_sagittal_nearest, + 'volumeURI_100_100_10_1_1_1_0_sagittal_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully load a volume: linear', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.SAGITTAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_sagittal_linear, + 'volumeURI_100_100_10_1_1_1_0_sagittal_linear' + ) + .then(done, done.fail); + }); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Volume Viewport Sagittal Coronal Neighbor and Linear Interpolation --- ', function () { + it('should successfully load a volume: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + // we don't set imageIds as we are mocking the imageVolume to + // return the volume immediately + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully load a volume: linear', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_linear, + 'volumeURI_100_100_10_1_1_1_0_coronal_linear' + ) + .then(done, done.fail); + }); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Rendering API', function () { + it('should successfully use setVolumesForViewports API to load image', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should be able to filter viewports based on volumeId', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const viewport = renderingEngine.getViewport(viewportId); + const viewports = utilities.getViewportsWithVolumeId( + volumeId, + renderingEngine.id + ); + + expect(viewports.length).toBe(1); + expect(viewports[0]).toBe(viewport); + + done(); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + renderingEngine.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully use renderViewports API to load image', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const vp = renderingEngine.getViewport(viewportId); + const canvas = vp.getCanvas(); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully use renderViewport API to load image', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully debug the offscreen canvas', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + const offScreen = renderingEngine._debugRender(); + expect(offScreen).toEqual(image); + done(); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully render frameOfReference', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => + volumeActor.getProperty().setInterpolationTypeToNearest(); + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ).then(() => { + renderingEngine.renderFrameOfReference( + 'Volume_Frame_Of_Reference' + ); + }); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + fdescribe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { + it('should successfully load a color volume: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + rgb: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_1_color_coronal_nearest' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => { + volumeActor.getProperty().setInterpolationTypeToNearest(); + }; + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully load a volume: linear', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + rgb: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_coronal_linear, + 'volumeURI_100_100_10_1_1_1_1_color_coronal_linear' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => { + volumeActor.getProperty().setInterpolationTypeToLinear(); + }; + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + + it('should successfully load a volume: linear', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + viewportType: ViewportType.ORTHOGRAPHIC, + }); + + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 100, + columns: 100, + slices: 10, + xSpacing: 1, + ySpacing: 1, + zSpacing: 1, + rgb: 1, + }); + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_axial_linear, + 'volumeURI_100_100_10_1_1_1_1_color_axial_linear' + ) + .then(done, done.fail); + }); + + const callback = ({ volumeActor }) => { + volumeActor.getProperty().setInterpolationTypeToLinear(); + }; + + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId, callback }], + [viewportId] + ); + vp.render(); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); + }); +}); From 55c88f18b724f9e4d56bf1b7be8e3dbee339abb3 Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 12:13:46 -0500 Subject: [PATCH 14/16] update --- ..._100_100_10_1_1_1_1_color_axial_linear.png | Bin 4300 -> 7576 bytes ...00_100_10_1_1_1_1_color_coronal_linear.png | Bin 4585 -> 7874 bytes ...0_100_10_1_1_1_1_color_coronal_nearest.png | Bin 4196 -> 7595 bytes .../test/volumeViewport_gpu_render_test.js | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/groundTruth/volumeURI_100_100_10_1_1_1_1_color_axial_linear.png b/packages/core/test/groundTruth/volumeURI_100_100_10_1_1_1_1_color_axial_linear.png index c4713851f5d9cc2546af34f8535c9218c0a166cb..50596a97833b32d6d2e7463a0d0b10b643b5fbf7 100644 GIT binary patch literal 7576 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 literal 4300 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 diff --git a/packages/core/test/groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_nearest.png b/packages/core/test/groundTruth/volumeURI_100_100_10_1_1_1_1_color_coronal_nearest.png index 60529ad5b10e80af0fce0573c8cca875929b8861..8877cc1d4c2b8298a9eb5884eb91a151597f6125 100644 GIT binary patch literal 7595 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 literal 4196 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL9Be?5hW%z|fD~hKkh>GZx^prw85jiQJY5_^ zD&pSWd6*mGDbjLro)1TAM@z^wpA#uETr(?FjTSsxS)n)g?_S%t)p_3YQ>5+f&Od+t zQJ#^Bg+t(ncVm0QpQSVY&t(Ae|8a11v^z8anG+itTyppX6&xBG8Y-B93Kax6IXsR$ z69DPr=rA8uG#Uh>sbDlCjFttX#o++f2nV7WE9cC=xD;Gd9oR2eF)#mlX=Cz;L&JgV0y_d7#rfMgh%>RIMJuZ(+M z+IeiYJ+t5h?`;o2x&wp+CJ1fG2C_D&70hp_S$A<(J5cd$rt|&9arf8H{J1wik@JLU z^ao8~4XFXFAUSWD0$CBl0u#3Of|aT^-e!4H^GkQlrE{^)Kqq9g*v)%hv;W}wk6b5A ze}4h#TLIM9d&?Ebx&gFqZSAZ!pcST#yIG!moIk(u%CvV%zyOX>`0(ei&H8xrACesn zF&lj49f5`|YG@GI80!jTsWLJ~-dYw3a;NnFK@~JYc0Tdvp!2r z#Ua7h!Qq-&mK@N#Q$P;8v}YYq7bx2H{M21@`|M*-#4$8ZYG|0U034X6KwsZpQ3-Mx zQbZ{zWJ{a!1AXTO^k-8UFOYQ&=$hQc&p@v6ba3#x05bQ$tcHd)xmRp(#vCJ~rsHl< zTx2sbZQ2(2SwO*Ifr^5HYQ}XS3&pK0OIY3lLxE!rFn!%}I}3{EQAZ->fzgm0O_QS; kXS4_!Ehp(-FVdQ&MBb@003mP1poj5 diff --git a/packages/core/test/volumeViewport_gpu_render_test.js b/packages/core/test/volumeViewport_gpu_render_test.js index d366e63626..3934493cc4 100644 --- a/packages/core/test/volumeViewport_gpu_render_test.js +++ b/packages/core/test/volumeViewport_gpu_render_test.js @@ -729,7 +729,7 @@ describe('Volume Viewport GPU -- ', () => { }); }); - fdescribe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { + describe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { it('should successfully load a color volume: nearest', function (done) { const element = testUtils.createViewports(renderingEngine, { viewportId, From bbb2e8e478fa9b5c09a3ace7f5dde100a1fca89e Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 13:14:39 -0500 Subject: [PATCH 15/16] bring back tests --- loggg.txt | 67 + .../helpers/setDefaultVolumeVOI.ts | 14 +- ...ageURI_100_100_0_10_1_1_1_linear_color.png | Bin 14056 -> 14059 bytes ...geURI_100_100_0_10_1_1_1_nearest_color.png | Bin 9008 -> 9010 bytes .../groundTruth/imageURI_11_11_4_1_1_1_0.png | Bin 6157 -> 6135 bytes ...geURI_256_256_100_100_1_1_0_CT_nearest.png | Bin 6303 -> 6303 bytes ...imageURI_256_256_100_100_1_1_0_nearest.png | Bin 6303 -> 6303 bytes .../imageURI_256_256_50_10_1_1_0.png | Bin 4909 -> 4910 bytes .../imageURI_64_64_0_10_5_5_0_nearest.png | Bin 4812 -> 4812 bytes .../imageURI_64_64_20_5_1_1_0_nearest.png | Bin 5713 -> 5714 bytes ...imageURI_64_64_20_5_1_1_0_nearestFlipH.png | Bin 5820 -> 5819 bytes ..._64_64_20_5_1_1_0_nearestFlipHRotate90.png | Bin 4391 -> 4394 bytes .../imageURI_64_64_30_10_5_5_0_nearest.png | Bin 5878 -> 5877 bytes .../imageURI_64_64_54_10_5_5_0_nearest.png | Bin 4915 -> 4915 bytes .../groundTruth/sphere_default_sagittal.png | Bin 48305 -> 67117 bytes .../test/stackViewport_cpu_render_test.js | 1170 +++++----- .../test/stackViewport_gpu_render_test.js | 2062 ++++++++--------- .../test/volumeViewport_gpu_render_test.js | 240 +- .../volumeViewport_gpu_setProperties_test.js | 156 +- 19 files changed, 1906 insertions(+), 1803 deletions(-) create mode 100644 loggg.txt diff --git a/loggg.txt b/loggg.txt new file mode 100644 index 0000000000..8002c1ca80 --- /dev/null +++ b/loggg.txt @@ -0,0 +1,67 @@ +yarn run v1.22.22 +$ karma start +Webpack bundling... +Webpack starts watching... +asset commons.js 40.4 MiB [emitted] (name: commons) (id hint: commons) +asset 17dd54813d5acc10bf8f.wasm 5.38 MiB [emitted] [immutable] [from: node_modules/@icr/polyseg-wasm/dist/ICRPolySeg.wasm] (auxiliary name: commons) (auxiliary id hint: commons) +asset polySeg.js 10.3 KiB [emitted] (name: polySeg) +asset runtime.js 9.61 KiB [emitted] (name: runtime) +asset FrameOfReferenceSpecificToolStateManager_test.4220545566.js 1.06 KiB [emitted] (name: FrameOfReferenceSpecificToolStateManager_test.4220545566) +asset segmentationSegmentIndexController_test.1135915510.js 1.05 KiB [emitted] (name: segmentationSegmentIndexController_test.1135915510) +asset segmentationVisibilityController_test.2298580792.js 1.04 KiB [emitted] (name: segmentationVisibilityController_test.2298580792) +asset stackContextPrefetch_test.4025630150.js 1.04 KiB [emitted] (name: stackContextPrefetch_test.4025630150) +asset volumeViewport_gpu_setProperties_test.1323015147.js 1.04 KiB [emitted] (name: volumeViewport_gpu_setProperties_test.1323015147) +asset segmentationRectangleScissor_test.2231863985.js 1.04 KiB [emitted] (name: segmentationRectangleScissor_test.2231863985) +asset segmentationSphereScissor_test.3397904729.js 1.03 KiB [emitted] (name: segmentationSphereScissor_test.3397904729) +asset volumeViewport_gpu_render_test.1271609346.js 1.03 KiB [emitted] (name: volumeViewport_gpu_render_test.1271609346) +asset stackViewport_cpu_render_test.3882391571.js 1.03 KiB [emitted] (name: stackViewport_cpu_render_test.3882391571) +asset stackViewport_gpu_render_test.1562760794.js 1.03 KiB [emitted] (name: stackViewport_gpu_render_test.1562760794) ++ 38 assets +webpack 5.81.0 compiled successfully in 10347 ms +29 11 2024 13:12:28.963:WARN [karma]: No captured browser, open http://localhost:9876/ +29 11 2024 13:12:28.970:INFO [karma-server]: Karma v6.4.4 server started at http://localhost:9876/ +29 11 2024 13:12:28.970:INFO [launcher]: Launching browsers ChromeHeadlessNoSandbox with concurrency 1 +29 11 2024 13:12:28.973:INFO [launcher]: Starting browser ChromeHeadless +29 11 2024 13:12:29.301:INFO [Chrome Headless 101.0.4950.0 (Mac OS 10.15.7)]: Connected on socket YeSAipLnJqmpHzacAAAB with id 36288736 +LOG LOG: 'CornerstoneRender: using GPU rendering' +WARN LOG: 'Warning: Set value to model directly dataType, Uint8Array' +WARN LOG: 'Warning: Set value to model directly voxelManager, [object Object]' +WARN LOG: 'Warning: Set value to model directly id, fakeVolumeLoader:%7B%22loader%22%3A%22fakeVolumeLoader%22%2C%22name%22%3A%22volumeURI%22%2C%22rows%22%3A100%2C%22columns%22%3A100%2C%22slices%22%3A10%2C%22xSpacing%22%3A1%2C%22ySpacing%22%3A1%2C%22zSpacing%22%3A1%2C%22rgb%22%3A1%7D' +WARN LOG: 'Warning: Set value to model directly numberOfComponents, 3' +WARN LOG: 'Warning: Set value to model directly hasScalarVolume, false' +WARN LOG: 'Warning: Set value to model directly viewportId, VIEWPORT' +LOG LOG: '****************************, ', 0, 255 +DEBUG LOG: '[Update Baseline]' +DEBUG LOG: 'volumeURI_100_100_10_1_1_1_1_color_coronal_nearest: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAYAAACAvzbMAAAAAXNSR0IArs4c6QAAD9tJREFUeF7t1dFtVTEURNGbykJpoTJCZQ9BB4wyYI9WvuPzrtextN9ez+v1+CNAgAABAn8p8CYgfynm3wkQIEDgj4CAeAgECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAnEAfl8Pp+fz8/o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' +DEBUG LOG: 'running cleanupTestEnvironment' + + Volume Viewport GPU -- + Volume Viewport Color images Neighbor and Linear Interpolation --- +  PASS: should successfully load a color volume: nearest +WARN LOG: 'Warning: Set value to model directly dataType, Uint8Array' +WARN LOG: 'Warning: Set value to model directly voxelManager, [object Object]' +WARN LOG: 'Warning: Set value to model directly id, fakeVolumeLoader:%7B%22loader%22%3A%22fakeVolumeLoader%22%2C%22name%22%3A%22volumeURI%22%2C%22rows%22%3A100%2C%22columns%22%3A100%2C%22slices%22%3A10%2C%22xSpacing%22%3A1%2C%22ySpacing%22%3A1%2C%22zSpacing%22%3A1%2C%22rgb%22%3A1%7D' +WARN LOG: 'Warning: Set value to model directly numberOfComponents, 3' +WARN LOG: 'Warning: Set value to model directly hasScalarVolume, false' +WARN LOG: 'Warning: Set value to model directly viewportId, VIEWPORT' +LOG LOG: '****************************, ', 0, 255 +DEBUG LOG: '[Update Baseline]' +DEBUG LOG: 'volumeURI_100_100_10_1_1_1_1_color_coronal_linear: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAYAAACAvzbMAAAAAXNSR0IArs4c6QAAD9tJREFUeF7t1dFtVTEURNGbykJpoTJCZQ9BB4wyYI9WvuPzrtextN9ez+v1+CNAgAABAn8p8CYgfynm3wkQIEDgj4CAeAgECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAkISMTmEAECBAgIiDdAgAABApGAgERsDhEgQICAgHgDBAgQIBAJCEjE5hABAgQICIg3QIAAAQKRgIBEbA4RIECAgIB4AwQIECAQCQhIxOYQAQIECAiIN0CAAAECkYCARGwOESBAgICAeAMECBAgEAnEAfl8Pp+fz8/o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' +DEBUG LOG: 'running cleanupTestEnvironment' +  PASS: should successfully load a volume: linear +WARN LOG: 'Warning: Set value to model directly dataType, Uint8Array' +WARN LOG: 'Warning: Set value to model directly voxelManager, [object Object]' +WARN LOG: 'Warning: Set value to model directly id, fakeVolumeLoader:%7B%22loader%22%3A%22fakeVolumeLoader%22%2C%22name%22%3A%22volumeURI%22%2C%22rows%22%3A100%2C%22columns%22%3A100%2C%22slices%22%3A10%2C%22xSpacing%22%3A1%2C%22ySpacing%22%3A1%2C%22zSpacing%22%3A1%2C%22rgb%22%3A1%7D' +WARN LOG: 'Warning: Set value to model directly numberOfComponents, 3' +WARN LOG: 'Warning: Set value to model directly hasScalarVolume, false' +WARN LOG: 'Warning: Set value to model directly viewportId, VIEWPORT' +LOG LOG: '****************************, ', 0, 255 +DEBUG LOG: '[Update Baseline]' +DEBUG LOG: 'volumeURI_100_100_10_1_1_1_1_color_axial_linear: data:image/png;base64,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' +DEBUG LOG: 'running cleanupTestEnvironment' +  PASS: should successfully load a volume: linear + +Webpack stopped watching. +29 11 2024 13:12:41.483:WARN [Chrome Headless 101.0.4950.0 (Mac OS 10.15.7)]: Disconnected (0 times) Client disconnected from CONNECTED state (transport close) +Chrome Headless 101.0.4950.0 (Mac OS 10.15.7) ERROR + Disconnected Client disconnected from CONNECTED state (transport close) diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index e9a01ab10e..adf83f88a1 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -9,6 +9,7 @@ import * as metaData from '../../metaData'; import * as windowLevel from '../../utilities/windowLevel'; import { RequestType } from '../../enums'; import cache from '../../cache/cache'; +import { getMinMax } from '../../utilities'; const PRIORITY = 0; const REQUEST_TYPE = RequestType.Prefetch; @@ -180,12 +181,15 @@ async function getVOIFromMiddleSliceMinMax( } // Get the min and max pixel values of the middle slice - let { min, max } = image.voxelManager.getMinMax(); + // let { min, max } = image.voxelManager.getMinMax(); - if (min.length > 1) { - min = Math.min(...min); - max = Math.max(...max); - } + // if (min.length > 1) { + // min = Math.min(...min); + // max = Math.max(...max); + // } + + const { min, max } = getMinMax(image.voxelManager.getScalarData()); + console.log('****************************, ', min, max); return { lower: min, diff --git a/packages/core/test/groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png b/packages/core/test/groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png index 29f5645c22c047b0c1c0a2720788ce844b6b046f..4a58feac4b34645a1198e78fbe3c3115ea057cb2 100644 GIT binary patch literal 14059 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 MT9kP<+Mj{pDw diff --git a/packages/core/test/groundTruth/imageURI_100_100_0_10_1_1_1_nearest_color.png b/packages/core/test/groundTruth/imageURI_100_100_0_10_1_1_1_nearest_color.png index bb37faad6392651b31c4b6f1eb4b725653222282..42335106b1a0af1635cc718eaf09b3acfd731102 100644 GIT binary patch delta 708 zcmdnsw#jWmWc?$Z|ipX)nI`G_gNVZbOXhKk_TA184j?zf1fNG;K0Z#!octzsz+Rhfk9m6u74Ov zIDmm+4>Q;(u9yuB3^4^I_K_grdIv^^4}2gixHJk5GBFe!y!bZ-C=S%aaKM@xHsH1( zkGuB)>!G^c+2qN(?^TAe>L~aZGWERm-qYEAT0m?*~264^Z&e<&-c*Z z@6XNS^*p|NU*pW$}NP3uX4#{eAy`y}C`c)x3GI#Kq814%Ndg#?Zjs_fB0KB&@-} za1U%0BdcBn1B0H$uK8;~!u1On7%JF-Rsbb;Brr1Uc<`cb6Hpwehv9)NHEh6b!FJ{Y zpX=}UKc3xR9{uIx*SmFJA0K`Ex9DP|S;{W;|TiDGNe^bU#@$+-xo;~-c|9*J5a^m^APqW)M+dtg%{C^2Yecavi_CF8v zA6)MC^nGSURU&B;?z+PXOfhD|BhMmFWF5`li zbqo$dat!~htr-{`iYEN8(_k?92^8jI@GWOxXwctR`H$&>j2c8OM38|YBkz6pd@hEw z8Vnao*c+AswK6c6Y-eX^zRRc}b@V^;gcf$7NW+sa`~EBhYVTnJ8dlGkv5T33L7Bm! zxcVQH!3|l4MS*`<6*&1pnoqv`$$cQ<8#BYAPzD9g%^R6pWj0S>vSj2r`L%x~0|V1N zPZ!6K%^z8v8KeHQGt8^^Ikxv``@Y{F#r!kpeSB5>dFS>YSK|A&Z?EwA-1Gjv>HO+u z|JvVodF6y`AKkUxZ~XrK_qmpR;TIpp|KFJ34-5=@+2bEyU;qE_yzRrAmp^_fx%b#M ml5ujb()!7^O5Ng(_KZK7tlIgf-vXv01_n=8KbLh*2~7Z$^eI*V diff --git a/packages/core/test/groundTruth/imageURI_11_11_4_1_1_1_0.png b/packages/core/test/groundTruth/imageURI_11_11_4_1_1_1_0.png index df3bf56da445cc78a1fa07f75e2bde599fd3ec3a..128f1136e4758707e7252ca33a980f5405e91529 100644 GIT binary patch literal 6135 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL9Be?5hW%z|fD~hKkh>GZx^prw85qPjdb&7< zRK&f#d-j~l@m{xwc@Dcm@BZJXdQ;*G!_$vxGJ-8Xzh^A&o#80Aud_LT@#o(^kNfTa zeLQTh&H3lg&(Fv6_y7I&`1}3({lC6@zuiCo=X>rSuh;Ld`}6bpeEYh;zeIn%znuQ> z@Av!Kn-eow{s}YLb3U;@X!3c#;)MV4ek?}Dzn66>Iw%;*MBBAGa4bnJFpX_vXq*yx zw0^%J6Hravt2q{(42?dwo;}oIaZ+fX_;J4Shk6cy6OSAOI2v{O_FrV?5b$6)vaVl2 zfa&F|*xKs~4hp8uz2x=Vf9EO6J#$1)98s<@~~Apfxi>=kYr!uoTUc66N^8>%vy|=ZB*L z%Oh8Z2dxd#C%E@M+X^)Jtn!+_DNHPr_B5P%%-P~#^!fRle=HnJ{`0L@2fAt}U%wN} zzvuPMzpE9hiyahLHktob>jyehP-M1UGtkqSC0k=#7#c+)UEHLBrb+HBekQ}jVw67p z_++3+W zn;bY)R4e#oof{mEt@%_V01S}NS5`P2$X76W@V@cSkFVVhdpJ1u2ox|^F1)_y8gGlk zEcb}tCxM1lGR{0M%+Z+g?EKBYoB|&IW#jyTUM!N8YvuS+t+3k|z7%h3<5$wV|TD6OkU19>!%v9(!8%jD5Ad9+L(?TirF#T_j!MvIHl;$pP87%eVF j2Z+cU4x?L-?;qz{+4fh08X_6Mk$eVES3j3^P6X6`qS7bj-`_&f*_oj=!kd(K_$iy!Gd4JvX5Kf3r&xY3PUOtF1-li*ccNLx-@gu zA6;DvuLe%!BLTNAVeqw39C0W19-GbdRcn<8>?>vtRu?PV_!$6O4bo$_6N4LrL<-gJ*rg%D)ZV;Ua4_sZ^=!FJ?EF7aI3mo>}VL>F3gCzm^2-EVg=Q$T%Z#*6+mdk7@NyBE?yxc`pZb4EK zq=4rh3am(GLQuDJe6i_IZlw%Co@{ToQAZ2>zATh$= z^uQ|u_EtW;pTWWK_;ohZco0wRH3yeXYSQ+V~CoS9=j zsJ|pBr?8)$M>{np_L~EK#w+JA#8~E!*^2R@nBNq zUQmlLivVshGQU{L)G=EX=eu=mGJ4#)Z61^%#;Png3d&%cD)a~TG)bSH3R4-WHFaXt za>MZIxg4nWo+pAA2=oI|)$a)fyu3b9=?AGQ464`)BE!6A=1X;x{ z353wtNZfal<@#McNz1x1+?QR)ti%RZ4zeDz+5%+TTc^_0==|t@F*K5o(Ao{i9eM6~ z7YPV7Mdg-^4T>!sJ6HZjzbc?@iED*aiyb%gC>ujKV z9!)$A^qWp*8qt03Qlz%_vTAMHVTpV7X#f z>s4s&rUJiu=Nxd$+p^R{3kBPAz}N?K$0Kck=qM=-hQ*51ZNt1=gt_-1e4!&Cn07V& z8A##m^@r5CV5;O~q&fBmEo(w(wax{T^bUswYqG4!IG`wA#=YRE985 zHPjDgVELjpAQK14sJfXE4sNe3+B+Q&2Ibpq2mEtrR6aui9n%`n?+)?32= zNo`L_46YSl83n^3*kT{#(X(BjW)!lF=D1z*H!6osNaX^JXq_g6RxY4stCE%hnSE$g zk=VJ(ORnfiqtd8jy0arf3@?)9UbF_wZP(N)TX1v3Ny&je9`X%1L+T-s4KeRT(}Bme z7p|BSEA2L!UBVw+>g3$uRHW}5E`?)jC_~bYg21oPy9DOI;koRI)B_N7dqqvg+>J8D z2Cn{hnbI23rc4MeDr1_&f;FQKhfSjV|4uh1Q8tP4-;W@ZD8Hpa+a${W=1-VuQZ`M> zrb*c($|h0%hr1rrr2Mw-0Zo&#X;OY?ld{3JhnL^iZrS(+-0veX3t}TG!&c|~1*m@= A!TEND=j^{5n_L(&_Xi47eE^>Gx{buvK<1=cn z|5TPc^8CC1`}gzzI5IG@2sk`2=d2N%a9`!wdgTTNCXPSO4i0|=CeA6GxL+hOPH4jX z|Gzp_nD+ZVc>DJ4c5^n4KT01yFaKX`_sP5B_iNMt-|S>2#DDzp?R~y{W%pZW&YJ)K z{w~!2{-$5($ET;K-R(c#_AtWS>1+yBbek@tS^x_+PCx3_Zt44Ll#`~5aqzT(5Lx9hk2*Z+Hee!9Sq zKmT6-{i_=PNf79IhDLWmfeG#}zeRw&Qp&>eq*SMt6X>rB5k|(!DS7>_Ku^ZGJ2=E0 ozcd}6@_G{0Q(L(d$o2pDJ&igZOk1^n4g(N)y85}Sb4q9e0GI^S6aWAK delta 484 zcmbPlINxxBwcpmz*P^V68w44@b9V67T;R9zke7YY!N{`b#16?LjHUj`>o(foI%9h+ zzI1`z{`#u=|4;atI20NfnCd+qY;O3YXjm`H$RgnI;6Dq?5q`#ra|`_UO!YZ>3ikhhZr*?F(?*tke_!8DzW?r_yz+;?_wU<(Yh+*l|JU#J{_+1lKUHM` zd!YX0^cohhlLJ{;o&@f)6$5fAWEmMNWxeB_7#LaPTpb+bmflVlbYN(_E+{bJ`o=G+ oARppTUQeQWYAcrpxxVo~qo-BJgI6aPOl1H9Pgg&ebxsLQ00KeA&;S4c diff --git a/packages/core/test/groundTruth/imageURI_256_256_100_100_1_1_0_nearest.png b/packages/core/test/groundTruth/imageURI_256_256_100_100_1_1_0_nearest.png index c5c31c1e6365193dd3afdd9d01516f8267eefad1..5c9a612fd276d6e9af916e15fe46eb8d42108055 100644 GIT binary patch delta 482 zcmbPlINxxBwcpgxOflBP4Hua%Ioy14>END=j^{5n_L(&_Xi47eE^>Gx{buvK<1=cn z|5TPc^8CC1`}gzzI5IG@2sk`2=d2N%a9`!wdgTTNCXPSO4i0|=CeA6GxL+hOPH4jX z|Gzp_nD+ZVc>DJ4c5^n4KT01yFaKX`_sP5B_iNMt-|S>2#DDzp?R~y{W%pZW&YJ)K z{w~!2{-$5($ET;K-R(c#_AtWS>1+yBbek@tS^x_+PCx3_Zt44Ll#`~5aqzT(5Lx9hk2*Z+Hee!9Sq zKmT6-{i_=PNf79IhDLWmfeG#}zeRw&Qp&>eq*SMt6X>rB5k|(!DS7>_Ku^ZGJ2=E0 ozcd}6@_G{0Q(L(d$o2pDJ&igZOk1^n4g(N)y85}Sb4q9e0GI^S6aWAK delta 484 zcmbPlINxxBwcpmz*P^V68w44@b9V67T;R9zke7YY!N{`b#16?LjHUj`>o(foI%9h+ zzI1`z{`#u=|4;atI20NfnCd+qY;O3YXjm`H$RgnI;6Dq?5q`#ra|`_UO!YZ>3ikhhZr*?F(?*tke_!8DzW?r_yz+;?_wU<(Yh+*l|JU#J{_+1lKUHM` zd!YX0^cohhlLJ{;o&@f)6$5fAWEmMNWxeB_7#LaPTpb+bmflVlbYN(_E+{bJ`o=G+ oARppTUQeQWYAcrpxxVo~qo-BJgI6aPOl1H9Pgg&ebxsLQ00KeA&;S4c diff --git a/packages/core/test/groundTruth/imageURI_256_256_50_10_1_1_0.png b/packages/core/test/groundTruth/imageURI_256_256_50_10_1_1_0.png index f94b0ec10e73fcda270599c6aade398e61ca2295..63cee1d8d553c45351572cef42f022cf5ff5fc42 100644 GIT binary patch delta 392 zcmZ3hwoYw=Vd4u<7srr_xVKj~_J)Qtusv*N(PYU<=`4{hkXAEkb&w5l+pEp>5Y zjA#kjFip3e<6CUJa{s^nDdp$>r9bof{NP)$82YyBm)!EBk75g{Ob$69PIvm5j?qGpyJ1ehfi;A zPM_}HFBg0E`RC{7r|ZY>i>W;aGW$U_OP>ARy7%hefBmnQov{DW$6wR0fB%)7ZC~~4 z%gd)vPfwpdd-m+r^Zz|NJNxwS@9)>!JqKC&L6>QJ{hheDy|txWDvXw_kFVDn-{QLV0L~`FFs+r{eIa) z_5YXK)&2SL>CtuZ>FWOT?)<-CzrW`D?cJx_`Q_Ik?3$maf4;uHj%x{f`SjhpgY$|w knI_*7vYnhF)GqdqWrFx+8ENn3O$FVdQ&MBb@05%=4O8@`> delta 391 zcmV;20eJqdCaorrF=6LPL_t(|UhUPfja63w2H}SUp+Q1KmPnTZ+y*g?Y#nq#2ZV&g zPH;v6ScFut$d*F{`>wB%NB>94J7@nhbESE3ce%T}_=f-j2q1vKl>{CL$d$zW903Fn zK;Ulz0^&oF&`%g3AieZiYX^XBsY{rjuWIe-8H2z-yg&7U`y+qbu;KoXEsFn;?I0r?N% zx*7ol4iONLL)a32{viSR`ON)A1Q7T>0Rf48A+v!6kPNe+1knRS{&xX+``gA~IQ?(USE lT!H|TffARKWfCkFfv=Dw(;RIvzAOL$002ovPDHLkV1ig|ys-cP diff --git a/packages/core/test/groundTruth/imageURI_64_64_0_10_5_5_0_nearest.png b/packages/core/test/groundTruth/imageURI_64_64_0_10_5_5_0_nearest.png index fec55f33bcbf1dc79eb0fd66123f1a9703d7eebd..5430eae2e76d13d4103346210e417a2520b68547 100644 GIT binary patch delta 435 zcmX@3dPa4EjfadOgKJ>Ev9qKn^OcRP0S=cKGSgmrNCzBS@WlMo@u+iWx#U&;N!Zt< zEvdFVUddt Yza;E=+rrilQVc-g>FVdQ&MBb@0A}>4OaK4? delta 436 zcmX@3dPa4Ejfb=&gX_gY<1$H4#xD<{ur5Q=T}*GVH)##-Db_e*F0R$bS3n zx83(IeB}_h5%}O!ndI~9pC_%pFF#>^a*w?CHu+x@UVr^{;s5;lJ_bf6W2WcZKUeIt zJ#X{txG~cwaW}@uPqmgU42|r<0uvsza%^^F>F3>Sz{bs7f8am=iS2*R{d{nG{q&m4 z3XDuF|F#}@`qgM&oxSc%OP~{{&3PgKa^wv!2ZwD{clEp*7#d5Nm^PieGZx^prw85qQPJY5_^ zD&pSWIkY2EXxGFd_Ff>kTVA9+e z=>k&3bwrkGLiIZw_Wk?u_3{1spXNLN3pbvhZ(sj?a?Q_APj~Zg(H0^e+{y3MxKzxoCkwCO(3sFQ%haEd^-+?*4%Gz?c~5PtqF9YiWfwEOW)V5D z%cF=rNMYyAnR6Um9U4QftobNB&Dx=%;eW?FzAu~=jb9>fbviUPh}{n6RM7 zbAhKrqlx3KXOEp5cd)YLacWG6uI!%|*4Xpu)|>-YjI5jVCf#Wi@yIJvQx6afP!KiE zo(BxPqY4Gy4vlwCRe#ySl``e}GnQ*WjinQx@GWsD+H7pu*ww(aaz*50b)9lxVE`;E7-=m1~dnK2KfOzu(T}IX-?e`ZiYfT7@n=Krn zzQEy{>m=(5OefEtd%~i{#Icvi!1Ze^Vq6*%S{E+qTjCHj+1R`h=!um( zBfkm+D6C^`jAr6`5_WI#@=%4vd(6}~_&GGL=~gLMV4XC3?n%xN77-8Ev$K{rFclhd z+z|>;*#1e*b}f_S)2)_`rA%BYu_`;6H7DG8Ic@G{un91n4w1kJ~@QfYCl0oTI@xnmR`_`)JuYT6T_BMWZ!4 z&PL*BLus_#Ioj?VZN`uGibgw`qn*srKKtkp2#(RTfodonm>;JQU;ig+_MKIwO!oi( zK7Vg_+5X?p{`ddP|2i_U)QSK2GT&c*{{OE#TOYUQ%irI(^-YQ}6AOoc1#pPa@95hN zK;{eK0EOCVWl9bW2Q+~rVp@6astyef^BcgUUBJOFKj6qwaG|vhkogNZE;WDYTn+(+ z0xyTgIbF9Cf#awbfCFd0blw67PaA`QgIXbX_`HEkJK*pgXjG4dV-9fqYWbtG2q5zd z&?wN@6maZyN&}On==Q@x3JweW9l*nnz_Himz>&(8J7lK;nYF<2xAT|$(4G22zbt3{ V11^1eciGZx^prw85qR4JzX3_ zD&pSWIk<7zQC`*y?hOzvvHTPI@$7uQUEQxQ!QAak znTcoax->pXOD|#)Wn>Ks-26;)fkO-r({>h-6KnUl`>%FztIYLFnA^YEzjSPgt~o!M7;wY_LP)4hfb# zpuXtJ{&``IJ)dsPIbg-ex=C-+okkIlyfQWQ0KotSQPb={El>Fb6du^Cl(7F|+2im_ z=dFu^;{k3J#ibJ@MLj$Mj;UNwS>P~BA@?yz2}g`@fWmbld7Jg(CluDL<k?zNXpQ{>q9^IOCzzOJegDF=gig;udJyQz+dcxFvwHy=Ik*x{^ z-VTj-PE~)|!j&@R`ZJbmz%VPF_=InXL(yhq%f_w-rj;uqKLfp~$JQ9l#PuZX-s0t< z3XAucsc-OeXk62+Qm(){Y4+TcoFObC9)X2;n`9Bn9#wmV1L zouke8(O%JLCv&uuIof9*9Rk5Inl?}kr62RfPvpn{51V~wRVkDG-_OhS?Jl4H|LbS> zf8Y8E42}C)_WgZ)yZ`{0PcxeCt diff --git a/packages/core/test/groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipH.png b/packages/core/test/groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipH.png index 809cd2db552f624e0484a806c24ffd6d999e3387..d254762b3eb5e82f41e0c3cbc3563f88e8b94677 100644 GIT binary patch delta 633 zcmdm^yIXgHVSTBmi(^Pd+}k_5H>w;LWWCTRxrDKSv7kAdz2p*KK)Y-3h#jGSU6-jH6|#Vb#n+P)H^sd?qCK=DHNzIaOk_CqXg2! z`iM^eWXQw@Cd;JF9Uwg-9oFckd1y~qZ(qOEv%z8Ef$943`+mQR-~X%p|J{rK{iXkX zc|SkS{@?547cO7sdU9QV|Gs}ePRlPm``3_>sgh~`-*3_P4*z>HxlZ%v)9LmVCe!uZQXQ0B}U{m=f|DMmpb>!&eMD`M^2Vbi+1+G8nY%rgo zUUf}KLT`aX8+(M!J|<1R1K(J*SVcUR{`<36^u!;YxeW(40$sms^9Qh&hRp{B7+ELZ z6_MF|mUSLqeR*bM&EKD|U-$3-chUKf2#cP`kK_0Mf4uAe_qBcf-*?rwKKxhy@btU> zdinoflV3cvuc@y6Ef)2#pA#G|ufGNWW8{Z=OjZ4L*9os3Homws^2h}8#ov_7(Lc> z+B~CIETz=x-<8C_q2bqdetmT5qTuJV*RNk+um4|A!J&bnalrv|E)|Vu=Q#xw93Ip& zaUF353oA3S79B|wWMpFD&=4qZU;r}uSVT?;&UR(t5U5vJ;BW^dC14>Gpx~Y(1~RIF zsgRumWQd1Dqs+!g7myyVBeLkGEfGDTfB&!3WQInkM*aAGe_rnX|EatFd%e{!`7ICc z|NFfC`u6|-%)Wft!u0dyY5(>0f1k!LJp0#>k*Si&{{Nridxz_uPu8mY@v!~=&*}fx z>^|^)f9?NYpQ7K?JBfIF4>{}ppqxu%!hO!C=l`t-y2x-cA6JQG#ojosBc>Hf4|tz+ z&fCh;#;q~oA){FTdWSCN4YrM5jI2Q`?d$VdjqIJ(6$*3~IP|^LsR3)5?8suWc>=Sc z(B=)S;f(dMQ40I(zQ2Awzy5DJKWDJOj(`Wp_y7N~eEzTH^?$xBf1i2S-tEEZ{P?*0 z|9;=TaCraEU%!5tZaE|`3=V|rua`Q+Fq0Q*;5ev8i3AGb05e9B4Ev#fk_3P^@Ykz-y>wL2QRzuDIZ-2jjyV+3V zabI3o!6Cur!R!9@Y$;2)4L5&eEasTpz&5e|L5V}$|GK?#`M{`+_Pz8~H8W(xMs z7yml~15TQ8a~Q82^X3UWWsD|%>`&fK-+oe^dM_}-Nt&$Y5XP!7eQ@rs%Q>g0Gsf%y(Tw(wMPgg&ebxsLQ08TInIRF3v delta 702 zcmZ3bv|MR|Vf{l-7srr_xVKjWdt+aUwp`4XkYi92U}AjmYzl+HEJgtqwF?Ys2_;KB zD(AG@ZQAnsvX1@tkMY%4r@lIU{Pp9-migyk?Fs%fkGVz zP*~xE$AJc$q|bsNMG^x0!D=g15**|<{_FrMWZ{`Ox6JBCD)Xn`_I34p|9|tF0upUv zy8rLTx2oSizpZ?-KgXeN&)KP|r;9rJ@Ym6V1INYnR zuHRczmvv4FO#l;i8~XqN diff --git a/packages/core/test/groundTruth/imageURI_64_64_30_10_5_5_0_nearest.png b/packages/core/test/groundTruth/imageURI_64_64_30_10_5_5_0_nearest.png index 4094a1a9f2f5de20aa9bfcb04ed6bb49d020daa8..85aa05e04862e71cdaff47b2017ab095920ab8fe 100644 GIT binary patch delta 642 zcmeyS`&D;>Vf}hf7srr_xVKl&3NAAjbbVMTA#gx|q0mR>-eZe|_ZDp?91QFXi9Dfe zwj?Zmmc6=uUb*hq>fbS;bLZF9{QUU1o1c+Iz=5IBo5^1AgsS0jK?ep#mVe$24mK=c zaREjq&ctF)kfH{MJrK3XV)fVri4=PfrLh0+$L;$6{~WfT=)6F*G5`L)t*4|pfB*gc z{e)@xG{GO2_y7O$^!5IIbt$j28*Bc5{;psD|MPwOPsPpxKlXC``Sp6e@BZuS=ly;D ze13i1w|x6|&JSMi-(UCh>2G~`P9FvNgBc#BOe{fr6@S+0GqHTCp7?FPAqz)}&s4FW z_M8GI{&QaVS_|aIKR5e#pM~Sd>B;{^9&c_CNMc1HVwvl0#82ec|IsaXzAX5|pfUd6 zr_=4~*QCE+-~V@OoJQvQ9dHLk7)Dp|Elw;QOWrjMbqrf;by)yyWs=ntauZhh+?gzcNMhye&ZUHx3vIVCg! E0Oq6vYXATM delta 629 zcmeyW`%QO(Vf_YA7srr_xVKl&3g(&%x;}hvpuk|waOpr{Mc?CwosTP)I5Ma)SogS# zD)Y`*u`~DU`SN2y^Y3P_&Z_9=vONMDJ$DbdM``3PQJ5=}gmub=2 zxv4C5KmNAs-~a#Zulu%rVkhkXe>$%p|Nqg#x|V&4^%9Nobw8i_^8dcRTK?~s%l_-@ z-tO<^sQLfzt`jKC%xXCAHT2m_X!ne0S5*~mM}qu38AIYVvH;TKV%r0 ze#&@H28uU^vT&q?-ks(ORAJ}d;9z(Bk_wA}LxLzHQ)KA&R6(GM^FWQ~H=1w)HAd9C yHaLV`&5Z;qc~H#4@uXPC*NIwY6Jd0t*nfr~x%Cg2F0nph00K`}KbLh*2~7Z4JMm5c diff --git a/packages/core/test/groundTruth/imageURI_64_64_54_10_5_5_0_nearest.png b/packages/core/test/groundTruth/imageURI_64_64_54_10_5_5_0_nearest.png index 7809bf5730e3a4d4a6aef2e0082374ec3118d764..520d1ac8310819fd09538939505fe52e9ea8794e 100644 GIT binary patch delta 57 zcmV-90LK5bCbK4xQUSq{Qt_0S^)sEVWsrOpBTE P00000NkvXXu0mjfVc8ou delta 57 zcmdn2wpndL5ToJ5;3*v6`|r!=@83Id;(ICf-(TK-{CfHNW&uWiArTAP$R$B@{6ZLj Nz|+;wWt~$(6955J7(oC4 diff --git a/packages/core/test/groundTruth/sphere_default_sagittal.png b/packages/core/test/groundTruth/sphere_default_sagittal.png index 4ac0880049be6c02f3f979c42ce09021ce839cf9..5abc12406fd0c7fa9bcea3ed50a14de3b67f5ae5 100644 GIT binary patch literal 67117 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

u^V~ya5$e1_G6pPt@pGaRbo-?f!V1q|cz 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 diff --git a/packages/core/test/stackViewport_cpu_render_test.js b/packages/core/test/stackViewport_cpu_render_test.js index 9c7d591c6f..40c4528fcf 100644 --- a/packages/core/test/stackViewport_cpu_render_test.js +++ b/packages/core/test/stackViewport_cpu_render_test.js @@ -1,585 +1,585 @@ -// import * as cornerstone3D from '../src/index'; -// import * as testUtils from '../../../utils/test/testUtils'; - -// import * as cpu_imageURI_64_64_20_5_1_1_0 from './groundTruth/cpu_imageURI_64_64_20_5_1_1_0.png'; -// import * as cpu_imageURI_64_33_20_5_1_1_0 from './groundTruth/cpu_imageURI_64_33_20_5_1_1_0.png'; -// import * as cpu_imageURI_64_64_30_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_30_10_5_5_0.png'; -// import * as cpu_imageURI_64_64_0_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_0_10_5_5_0.png'; -// import * as cpu_imageURI_64_64_54_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_54_10_5_5_0.png'; -// import * as cpu_imageURI_256_256_100_100_1_1_0_voi from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0_voi.png'; -// import * as cpu_imageURI_256_256_100_100_1_1_0 from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0.png'; -// import * as cpu_imageURI_256_256_50_10_1_1_0 from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0.png'; -// import * as cpu_imageURI_256_256_50_10_1_1_0_invert from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0_invert.png'; -// import * as cpu_imageURI_256_256_50_10_1_1_0_rotate from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0_rotate.png'; -// import * as cpu_imageURI_256_256_100_100_1_1_0_hotIron from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0_hotIron.png'; - -// const { -// cache, -// RenderingEngine, -// utilities, -// imageLoader, -// metaData, -// Enums, -// setUseCPURendering, -// resetUseCPURendering, -// CONSTANTS, -// } = cornerstone3D; - -// const { Events, ViewportType } = Enums; -// const { CPU_COLORMAPS } = CONSTANTS; -// const { fakeImageLoader, fakeMetaDataProvider, compareImages } = testUtils; - -// const renderingEngineId = utilities.uuidv4(); -// const viewportId = 'VIEWPORT'; - -// describe('StackViewport CPU -- ', () => { -// let renderingEngine; - -// beforeEach(() => { -// setUseCPURendering(true); -// const testEnv = testUtils.setupTestEnvironment({ -// renderingEngineId, -// toolGroupIds: ['default'], -// }); -// renderingEngine = testEnv.renderingEngine; -// }); - -// afterEach(() => { -// setUseCPURendering(true, false); -// testUtils.cleanupTestEnvironment({ -// renderingEngineId, -// toolGroupIds: ['default'], -// }); -// }); - -// describe('Basic Rendering --- ', function () { -// it('Should render one cpu stack viewport of square size properly', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// cpu_imageURI_64_64_20_5_1_1_0, -// 'cpu_imageURI_64_64_20_5_1_1_0' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one cpu stack viewport of rectangle size properly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 33, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// cpu_imageURI_64_33_20_5_1_1_0, -// 'cpu_imageURI_64_33_20_5_1_1_0' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should use enableElement API to render one cpu stack viewport of square size and 5mm spacing properly: nearest', function (done) { -// const element = document.createElement('div'); -// element.style.width = `256px`; -// element.style.height = `256px`; -// document.body.appendChild(element); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 30, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// renderingEngine.enableElement({ -// viewportId: viewportId, -// type: ViewportType.STACK, -// element: element, -// defaultOptions: { -// background: [1, 0, 1], // pinkish background -// }, -// }); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// testUtils -// .compareImages( -// image, -// cpu_imageURI_64_64_30_10_5_5_0, -// 'cpu_imageURI_64_64_30_10_5_5_0' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one cpu stack viewport, first slice correctly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 0, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 10, -// barWidth: 20, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 1, -// }; -// const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); - -// const imageInfo3 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 30, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 2, -// }; -// const imageId3 = testUtils.encodeImageIdInfo(imageInfo3); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// testUtils -// .compareImages( -// image, -// cpu_imageURI_64_64_0_10_5_5_0, -// 'cpu_imageURI_64_64_0_10_5_5_0' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId1, imageId2, imageId3], 0); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one cpu stack viewport, last slice correctly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 0, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 10, -// barWidth: 20, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 1, -// }; -// const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); - -// const imageInfo3 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 54, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 2, -// }; -// const imageId3 = testUtils.encodeImageIdInfo(imageInfo3); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// testUtils -// .compareImages( -// image, -// cpu_imageURI_64_64_54_10_5_5_0, -// 'cpu_imageURI_64_64_54_10_5_5_0' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId1, imageId2, imageId3], 2); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('setProperties cpu', function () { -// it('Should render one cpu stack viewport with voi presets correctly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 256, -// columns: 256, -// barStart: 100, -// barWidth: 100, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// testUtils -// .compareImages( -// image, -// cpu_imageURI_256_256_100_100_1_1_0_voi, -// 'cpu_imageURI_256_256_100_100_1_1_0_voi' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ -// voiRange: { lower: 0, upper: 440 }, -// }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one cpu stack viewport with multiple imageIds of different size and different spacing: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 256, -// columns: 256, -// barStart: 100, -// barWidth: 100, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 30, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// testUtils -// .compareImages( -// image, -// cpu_imageURI_256_256_100_100_1_1_0, -// 'cpu_imageURI_256_256_100_100_1_1_0' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId1, imageId2], 0); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one cpu stack viewport with multiple images with linear interpolation correctly', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 11, -// columns: 11, -// barStart: 4, -// barWidth: 1, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 256, -// columns: 256, -// barStart: 50, -// barWidth: 10, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// cpu_imageURI_256_256_50_10_1_1_0, -// 'cpu_imageURI_256_256_50_10_1_1_0' -// ) -// .then(done, done.fail); -// }); -// try { -// vp.setStack([imageId1, imageId2], 1); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one cpu stack viewport with invert', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// cpu_imageURI_256_256_50_10_1_1_0_invert, -// 'cpu_imageURI_256_256_50_10_1_1_0_invert' -// ) -// .then(done, done.fail); -// }); -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ invert: true }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one cpu stack viewport with rotation', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// cpu_imageURI_256_256_50_10_1_1_0_rotate, -// 'cpu_imageURI_256_256_50_10_1_1_0_rotate' -// ) -// .then(done, done.fail); -// }); -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setViewPresentation({ rotation: 90 }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// // Uncomment and adapt the following block if needed -// // describe('false colormap cpu', function () { -// // it('Should render one cpu stack viewport with presets correctly', function (done) { -// // const element = testUtils.createViewports(renderingEngine, { -// // viewportId, -// // }); - -// // const imageId = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0'; - -// // const vp = renderingEngine.getViewport(viewportId); - -// // element.addEventListener(Events.IMAGE_RENDERED, () => { -// // const canvas = vp.getCanvas(); -// // const image = canvas.toDataURL('image/png'); - -// // testUtils.compareImages( -// // image, -// // cpu_imageURI_256_256_100_100_1_1_0_hotIron, -// // 'cpu_imageURI_256_256_100_100_1_1_0_hotIron' -// // ).then(done, done.fail); -// // }); - -// // try { -// // vp.setStack([imageId], 0).then(() => { -// // vp.setColormap(CPU_COLORMAPS.hotIron); -// // vp.render(); -// // }); -// // } catch (e) { -// // done.fail(e); -// // } -// // }); -// // }); -// }); +import * as cornerstone3D from '../src/index'; +import * as testUtils from '../../../utils/test/testUtils'; + +import * as cpu_imageURI_64_64_20_5_1_1_0 from './groundTruth/cpu_imageURI_64_64_20_5_1_1_0.png'; +import * as cpu_imageURI_64_33_20_5_1_1_0 from './groundTruth/cpu_imageURI_64_33_20_5_1_1_0.png'; +import * as cpu_imageURI_64_64_30_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_30_10_5_5_0.png'; +import * as cpu_imageURI_64_64_0_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_0_10_5_5_0.png'; +import * as cpu_imageURI_64_64_54_10_5_5_0 from './groundTruth/cpu_imageURI_64_64_54_10_5_5_0.png'; +import * as cpu_imageURI_256_256_100_100_1_1_0_voi from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0_voi.png'; +import * as cpu_imageURI_256_256_100_100_1_1_0 from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0.png'; +import * as cpu_imageURI_256_256_50_10_1_1_0 from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0.png'; +import * as cpu_imageURI_256_256_50_10_1_1_0_invert from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0_invert.png'; +import * as cpu_imageURI_256_256_50_10_1_1_0_rotate from './groundTruth/cpu_imageURI_256_256_50_10_1_1_0_rotate.png'; +import * as cpu_imageURI_256_256_100_100_1_1_0_hotIron from './groundTruth/cpu_imageURI_256_256_100_100_1_1_0_hotIron.png'; + +const { + cache, + RenderingEngine, + utilities, + imageLoader, + metaData, + Enums, + setUseCPURendering, + resetUseCPURendering, + CONSTANTS, +} = cornerstone3D; + +const { Events, ViewportType } = Enums; +const { CPU_COLORMAPS } = CONSTANTS; +const { fakeImageLoader, fakeMetaDataProvider, compareImages } = testUtils; + +const renderingEngineId = utilities.uuidv4(); +const viewportId = 'VIEWPORT'; + +describe('StackViewport CPU -- ', () => { + let renderingEngine; + + beforeEach(() => { + setUseCPURendering(true); + const testEnv = testUtils.setupTestEnvironment({ + renderingEngineId, + toolGroupIds: ['default'], + }); + renderingEngine = testEnv.renderingEngine; + }); + + afterEach(() => { + setUseCPURendering(true, false); + testUtils.cleanupTestEnvironment({ + renderingEngineId, + toolGroupIds: ['default'], + }); + }); + + describe('Basic Rendering --- ', function () { + it('Should render one cpu stack viewport of square size properly', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + cpu_imageURI_64_64_20_5_1_1_0, + 'cpu_imageURI_64_64_20_5_1_1_0' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport of rectangle size properly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 33, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + cpu_imageURI_64_33_20_5_1_1_0, + 'cpu_imageURI_64_33_20_5_1_1_0' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should use enableElement API to render one cpu stack viewport of square size and 5mm spacing properly: nearest', function (done) { + const element = document.createElement('div'); + element.style.width = `256px`; + element.style.height = `256px`; + document.body.appendChild(element); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 30, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + renderingEngine.enableElement({ + viewportId: viewportId, + type: ViewportType.STACK, + element: element, + defaultOptions: { + background: [1, 0, 1], // pinkish background + }, + }); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + testUtils + .compareImages( + image, + cpu_imageURI_64_64_30_10_5_5_0, + 'cpu_imageURI_64_64_30_10_5_5_0' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport, first slice correctly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 0, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 10, + barWidth: 20, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 1, + }; + const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); + + const imageInfo3 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 30, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 2, + }; + const imageId3 = testUtils.encodeImageIdInfo(imageInfo3); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + testUtils + .compareImages( + image, + cpu_imageURI_64_64_0_10_5_5_0, + 'cpu_imageURI_64_64_0_10_5_5_0' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2, imageId3], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport, last slice correctly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 0, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 10, + barWidth: 20, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 1, + }; + const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); + + const imageInfo3 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 54, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 2, + }; + const imageId3 = testUtils.encodeImageIdInfo(imageInfo3); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + testUtils + .compareImages( + image, + cpu_imageURI_64_64_54_10_5_5_0, + 'cpu_imageURI_64_64_54_10_5_5_0' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2, imageId3], 2); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('setProperties cpu', function () { + it('Should render one cpu stack viewport with voi presets correctly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 256, + columns: 256, + barStart: 100, + barWidth: 100, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + testUtils + .compareImages( + image, + cpu_imageURI_256_256_100_100_1_1_0_voi, + 'cpu_imageURI_256_256_100_100_1_1_0_voi' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + voiRange: { lower: 0, upper: 440 }, + }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with multiple imageIds of different size and different spacing: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 256, + columns: 256, + barStart: 100, + barWidth: 100, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 30, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + testUtils + .compareImages( + image, + cpu_imageURI_256_256_100_100_1_1_0, + 'cpu_imageURI_256_256_100_100_1_1_0' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with multiple images with linear interpolation correctly', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 11, + columns: 11, + barStart: 4, + barWidth: 1, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId1 = testUtils.encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 256, + columns: 256, + barStart: 50, + barWidth: 10, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId2 = testUtils.encodeImageIdInfo(imageInfo2); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + cpu_imageURI_256_256_50_10_1_1_0, + 'cpu_imageURI_256_256_50_10_1_1_0' + ) + .then(done, done.fail); + }); + try { + vp.setStack([imageId1, imageId2], 1); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with invert', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + cpu_imageURI_256_256_50_10_1_1_0_invert, + 'cpu_imageURI_256_256_50_10_1_1_0_invert' + ) + .then(done, done.fail); + }); + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ invert: true }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one cpu stack viewport with rotation', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + cpu_imageURI_256_256_50_10_1_1_0_rotate, + 'cpu_imageURI_256_256_50_10_1_1_0_rotate' + ) + .then(done, done.fail); + }); + try { + vp.setStack([imageId], 0).then(() => { + vp.setViewPresentation({ rotation: 90 }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + // Uncomment and adapt the following block if needed + // describe('false colormap cpu', function () { + // it('Should render one cpu stack viewport with presets correctly', function (done) { + // const element = testUtils.createViewports(renderingEngine, { + // viewportId, + // }); + + // const imageId = 'fakeImageLoader:imageURI_256_256_100_100_1_1_0'; + + // const vp = renderingEngine.getViewport(viewportId); + + // element.addEventListener(Events.IMAGE_RENDERED, () => { + // const canvas = vp.getCanvas(); + // const image = canvas.toDataURL('image/png'); + + // testUtils.compareImages( + // image, + // cpu_imageURI_256_256_100_100_1_1_0_hotIron, + // 'cpu_imageURI_256_256_100_100_1_1_0_hotIron' + // ).then(done, done.fail); + // }); + + // try { + // vp.setStack([imageId], 0).then(() => { + // vp.setColormap(CPU_COLORMAPS.hotIron); + // vp.render(); + // }); + // } catch (e) { + // done.fail(e); + // } + // }); + // }); +}); diff --git a/packages/core/test/stackViewport_gpu_render_test.js b/packages/core/test/stackViewport_gpu_render_test.js index 6118744760..3db4eefb7f 100644 --- a/packages/core/test/stackViewport_gpu_render_test.js +++ b/packages/core/test/stackViewport_gpu_render_test.js @@ -1,1031 +1,1031 @@ -// import * as cornerstone3D from '../src/index'; -// import * as csTools3d from '../../tools/src/index'; -// import * as testUtils from '../../../utils/test/testUtils'; -// import { encodeImageIdInfo } from '../../../utils/test/testUtils'; - -// // nearest neighbor interpolation -// import * as imageURI_64_33_20_5_1_1_0_nearest from './groundTruth/imageURI_64_33_20_5_1_1_0_nearest.png'; -// import * as imageURI_64_64_20_5_1_1_0_nearest from './groundTruth/imageURI_64_64_20_5_1_1_0_nearest.png'; -// import * as imageURI_64_64_30_10_5_5_0_nearest from './groundTruth/imageURI_64_64_30_10_5_5_0_nearest.png'; -// import * as imageURI_256_256_100_100_1_1_0_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_nearest.png'; -// import * as imageURI_256_256_100_100_1_1_0_CT_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_CT_nearest.png'; -// import * as imageURI_64_64_54_10_5_5_0_nearest from './groundTruth/imageURI_64_64_54_10_5_5_0_nearest.png'; -// import * as imageURI_64_64_0_10_5_5_0_nearest from './groundTruth/imageURI_64_64_0_10_5_5_0_nearest.png'; -// import * as imageURI_100_100_0_10_1_1_1_nearest_color from './groundTruth/imageURI_100_100_0_10_1_1_1_nearest_color.png'; -// import * as imageURI_11_11_4_1_1_1_0_nearest_invert_90deg from './groundTruth/imageURI_11_11_4_1_1_1_0_nearest_invert_90deg.png'; -// import * as imageURI_64_64_20_5_1_1_0_nearestFlipH from './groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipH.png'; -// import * as imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90 from './groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90.png'; - -// // linear interpolation -// import * as imageURI_11_11_4_1_1_1_0 from './groundTruth/imageURI_11_11_4_1_1_1_0.png'; -// import * as imageURI_256_256_50_10_1_1_0 from './groundTruth/imageURI_256_256_50_10_1_1_0.png'; -// import * as imageURI_100_100_0_10_1_1_1_linear_color from './groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png'; -// import * as calibrated_1_5_imageURI_11_11_4_1_1_1_0_1 from './groundTruth/calibrated_1_5_imageURI_11_11_4_1_1_1_0_1.png'; - -// const { cache, RenderingEngine, utilities, imageLoader, metaData, Enums } = -// cornerstone3D; - -// const { Events, ViewportType, InterpolationType } = Enums; -// const { calibratedPixelSpacingMetadataProvider } = utilities; - -// const { fakeImageLoader, fakeMetaDataProvider, compareImages } = testUtils; - -// const renderingEngineId = utilities.uuidv4(); - -// const viewportId = 'VIEWPORT'; - -// const AXIAL = 'AXIAL'; -// const SAGITTAL = 'SAGITTAL'; -// const CORONAL = 'CORONAL'; - -// describe('renderingCore -- Stack', () => { -// let renderingEngine; - -// beforeEach(function () { -// const testEnv = testUtils.setupTestEnvironment({ -// renderingEngineId, -// toolGroupIds: ['default'], -// }); -// renderingEngine = testEnv.renderingEngine; -// }); - -// afterEach(function () { -// testUtils.cleanupTestEnvironment({ -// renderingEngineId, -// toolGroupIds: ['default'], -// }); -// }); - -// describe('Stack Viewport Nearest Neighbor Interpolation --- ', function () { -// it('Should render one stack viewport of square size properly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// compareImages( -// image, -// imageURI_64_64_20_5_1_1_0_nearest, -// 'imageURI_64_64_20_5_1_1_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport of rectangle size properly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 33, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// compareImages( -// image, -// imageURI_64_33_20_5_1_1_0_nearest, -// 'imageURI_64_33_20_5_1_1_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 30, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// compareImages( -// image, -// imageURI_64_64_30_10_5_5_0_nearest, -// 'imageURI_64_64_30_10_5_5_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should use enableElement API to render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 30, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// renderingEngine.enableElement({ -// viewportId: viewportId, -// type: ViewportType.STACK, -// element: element, -// defaultOptions: { -// background: [1, 0, 1], // pinkish background -// }, -// }); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// compareImages( -// image, -// imageURI_64_64_30_10_5_5_0_nearest, -// 'imageURI_64_64_30_10_5_5_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport, first slice correctly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 0, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId1 = encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 10, -// barWidth: 20, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 1, -// }; -// const imageId2 = encodeImageIdInfo(imageInfo2); - -// const imageInfo3 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 30, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 2, -// }; -// const imageId3 = encodeImageIdInfo(imageInfo3); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// compareImages( -// image, -// imageURI_64_64_0_10_5_5_0_nearest, -// 'imageURI_64_64_0_10_5_5_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId1, imageId2, imageId3], 0).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport, last slice correctly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 0, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 0, -// }; -// const imageId1 = encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 10, -// barWidth: 20, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 1, -// }; -// const imageId2 = encodeImageIdInfo(imageInfo2); - -// const imageInfo3 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 54, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 2, -// }; -// const imageId3 = encodeImageIdInfo(imageInfo3); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// compareImages( -// image, -// imageURI_64_64_54_10_5_5_0_nearest, -// 'imageURI_64_64_54_10_5_5_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId1, imageId2, imageId3], 2).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport with CT presets correctly: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 256, -// columns: 256, -// barStart: 100, -// barWidth: 100, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// compareImages( -// image, -// imageURI_256_256_100_100_1_1_0_CT_nearest, -// 'imageURI_256_256_100_100_1_1_0_CT_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ -// voiRange: { lower: -160, upper: 240 }, -// interpolationType: InterpolationType.NEAREST, -// }); -// }); - -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport with multiple imageIds of different size and different spacing: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 256, -// columns: 256, -// barStart: 100, -// barWidth: 100, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId1 = encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 30, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 1, -// }; -// const imageId2 = encodeImageIdInfo(imageInfo2); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// compareImages( -// image, -// imageURI_256_256_100_100_1_1_0_nearest, -// 'imageURI_256_256_100_100_1_1_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId1, imageId2], 0).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport with multiple imageIds of different size and different spacing, second slice: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 256, -// columns: 256, -// barStart: 100, -// barWidth: 100, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId1 = encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 30, -// barWidth: 10, -// xSpacing: 5, -// ySpacing: 5, -// sliceIndex: 1, -// }; -// const imageId2 = encodeImageIdInfo(imageInfo2); - -// const vp = renderingEngine.getViewport(viewportId); - -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// compareImages( -// image, -// imageURI_64_64_30_10_5_5_0_nearest, -// 'imageURI_64_64_30_10_5_5_0_nearest' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId1, imageId2], 1).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Stack Viewport Linear Interpolation --- ', () => { -// it('Should render one stack viewport with linear interpolation correctly', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 11, -// columns: 11, -// barStart: 4, -// barWidth: 1, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// compareImages( -// image, -// imageURI_11_11_4_1_1_1_0, -// 'imageURI_11_11_4_1_1_1_0' -// ).then(done, done.fail); -// }); -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ voiRange: { lower: -160, upper: 240 } }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render one stack viewport with multiple images with linear interpolation correctly', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo1 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 11, -// columns: 11, -// barStart: 4, -// barWidth: 1, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId1 = encodeImageIdInfo(imageInfo1); - -// const imageInfo2 = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 256, -// columns: 256, -// barStart: 50, -// barWidth: 10, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 1, -// }; -// const imageId2 = encodeImageIdInfo(imageInfo2); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// compareImages( -// image, -// imageURI_256_256_50_10_1_1_0, -// 'imageURI_256_256_50_10_1_1_0' -// ).then(done, done.fail); -// }); -// try { -// vp.setStack([imageId1, imageId2], 1).then(() => { -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Color Stack Images', () => { -// it('Should render color images: linear', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.SAGITTAL, -// width: 512, -// height: 512, -// }); - -// // color image generation with 10 strips of different colors -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 100, -// columns: 100, -// barStart: 0, -// barWidth: 10, -// xSpacing: 1, -// ySpacing: 1, -// rgb: 1, -// pt: 0, -// sliceIndex: 0, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// compareImages( -// image, -// imageURI_100_100_0_10_1_1_1_linear_color, -// 'imageURI_100_100_0_10_1_1_1_linear_color' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should render color images: nearest', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// width: 512, -// height: 512, -// }); - -// // color image generation with 10 strips of different colors -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 100, -// columns: 100, -// barStart: 0, -// barWidth: 10, -// xSpacing: 1, -// ySpacing: 1, -// rgb: 1, -// pt: 0, -// sliceIndex: 0, -// }; -// const imageId = encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// compareImages( -// image, -// imageURI_100_100_0_10_1_1_1_nearest_color, -// 'imageURI_100_100_0_10_1_1_1_nearest_color' -// ).then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ interpolationType: InterpolationType.NEAREST }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Stack Viewport Calibration and Scaling --- ', () => { -// it('Should be able to render a stack viewport with PET modality scaling', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 11, -// columns: 11, -// barStart: 4, -// barWidth: 1, -// xSpacing: 1, -// ySpacing: 1, -// PT: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// expect(vp.scaling.PT).toEqual({ -// suvbwToSuvlbm: 1, -// suvbwToSuvbsa: 1, -// }); -// done(); -// }); -// try { -// vp.setStack([imageId], 0); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should be able to calibrate the pixel spacing', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 11, -// columns: 11, -// barStart: 4, -// barWidth: 1, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// const imageRenderedCallback = () => { -// calibratedPixelSpacingMetadataProvider.add(imageId, { -// scale: 0.5, -// }); - -// vp.calibrateSpacing(imageId); -// element.removeEventListener( -// Events.IMAGE_RENDERED, -// imageRenderedCallback -// ); -// element.addEventListener( -// Events.IMAGE_RENDERED, -// secondImageRenderedCallbackAfterCalibration -// ); -// }; - -// const secondImageRenderedCallbackAfterCalibration = () => { -// done(); -// }; - -// element.addEventListener(Events.IMAGE_RENDERED, imageRenderedCallback); - -// element.addEventListener(Events.IMAGE_SPACING_CALIBRATED, (evt) => { -// const { calibration } = evt.detail; -// expect(calibration?.scale).toBe(0.5); -// }); - -// try { -// vp.setStack([imageId], 0); -// vp.render(); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Stack Viewport setProperties API --- ', () => { -// it('Should be able to use setProperties API', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 11, -// columns: 11, -// barStart: 4, -// barWidth: 1, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// const subscribeToImageRendered = () => { -// element.addEventListener(Events.IMAGE_RENDERED, (evt) => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); - -// let props = vp.getProperties(); -// const rotation = vp.getViewPresentation().rotation; -// expect(rotation).toBe(90); -// expect(props.interpolationType).toBe(InterpolationType.NEAREST); -// expect(props.invert).toBe(true); - -// testUtils -// .compareImages( -// image, -// imageURI_11_11_4_1_1_1_0_nearest_invert_90deg, -// 'imageURI_11_11_4_1_1_1_0_nearest_invert_90deg' -// ) -// .then(done, done.fail); -// }); -// }; - -// try { -// vp.setStack([imageId], 0).then(() => { -// subscribeToImageRendered(); -// vp.setProperties({ -// interpolationType: InterpolationType.NEAREST, -// voiRange: { lower: -260, upper: 140 }, -// invert: true, -// }); -// vp.setViewPresentation({ rotation: 90 }); - -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should be able to resetProperties API', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 11, -// columns: 11, -// barStart: 4, -// barWidth: 1, -// xSpacing: 1, -// ySpacing: 1, -// sliceIndex: 0, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); - -// const firstImageRenderedCallback = () => { -// element.removeEventListener( -// Events.IMAGE_RENDERED, -// firstImageRenderedCallback -// ); - -// let props = vp.getProperties(); -// const rotation = vp.getViewPresentation().rotation; -// expect(rotation).toBe(90); -// expect(props.interpolationType).toBe(InterpolationType.NEAREST); -// expect(props.invert).toBe(true); - -// setTimeout(() => { -// console.log('reseting properties'); -// vp.resetProperties(); -// }); - -// element.addEventListener( -// Events.IMAGE_RENDERED, -// secondImageRenderedCallback -// ); -// }; - -// const secondImageRenderedCallback = () => { -// console.log('resetProperties callback'); -// const props = vp.getProperties(); -// expect(props.interpolationType).toBe(InterpolationType.LINEAR); -// expect(props.invert).toBe(false); - -// done(); -// console.log('done'); -// }; - -// element.addEventListener( -// Events.IMAGE_RENDERED, -// firstImageRenderedCallback -// ); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ -// interpolationType: InterpolationType.NEAREST, -// voiRange: { lower: -260, upper: 140 }, -// invert: true, -// }); -// vp.setRotation(90); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); - -// describe('Flipping', function () { -// it('Should be able to flip a stack viewport horizontally', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// imageURI_64_64_20_5_1_1_0_nearestFlipH, -// 'imageURI_64_64_20_5_1_1_0_nearestFlipH' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ -// interpolationType: InterpolationType.NEAREST, -// }); - -// vp.setCamera({ flipHorizontal: true }); - -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); - -// it('Should be able to flip a stack viewport vertically and rotate it', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.AXIAL, -// }); - -// const imageInfo = { -// loader: 'fakeImageLoader', -// name: 'imageURI', -// rows: 64, -// columns: 64, -// barStart: 20, -// barWidth: 5, -// xSpacing: 1, -// ySpacing: 1, -// }; -// const imageId = testUtils.encodeImageIdInfo(imageInfo); - -// const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90, -// 'imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90' -// ) -// .then(done, done.fail); -// }); - -// try { -// vp.setStack([imageId], 0).then(() => { -// vp.setProperties({ -// interpolationType: InterpolationType.NEAREST, -// }); - -// vp.setRotation(90); -// vp.setCamera({ flipVertical: true }); -// vp.render(); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); -// }); +import * as cornerstone3D from '../src/index'; +import * as csTools3d from '../../tools/src/index'; +import * as testUtils from '../../../utils/test/testUtils'; +import { encodeImageIdInfo } from '../../../utils/test/testUtils'; + +// nearest neighbor interpolation +import * as imageURI_64_33_20_5_1_1_0_nearest from './groundTruth/imageURI_64_33_20_5_1_1_0_nearest.png'; +import * as imageURI_64_64_20_5_1_1_0_nearest from './groundTruth/imageURI_64_64_20_5_1_1_0_nearest.png'; +import * as imageURI_64_64_30_10_5_5_0_nearest from './groundTruth/imageURI_64_64_30_10_5_5_0_nearest.png'; +import * as imageURI_256_256_100_100_1_1_0_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_nearest.png'; +import * as imageURI_256_256_100_100_1_1_0_CT_nearest from './groundTruth/imageURI_256_256_100_100_1_1_0_CT_nearest.png'; +import * as imageURI_64_64_54_10_5_5_0_nearest from './groundTruth/imageURI_64_64_54_10_5_5_0_nearest.png'; +import * as imageURI_64_64_0_10_5_5_0_nearest from './groundTruth/imageURI_64_64_0_10_5_5_0_nearest.png'; +import * as imageURI_100_100_0_10_1_1_1_nearest_color from './groundTruth/imageURI_100_100_0_10_1_1_1_nearest_color.png'; +import * as imageURI_11_11_4_1_1_1_0_nearest_invert_90deg from './groundTruth/imageURI_11_11_4_1_1_1_0_nearest_invert_90deg.png'; +import * as imageURI_64_64_20_5_1_1_0_nearestFlipH from './groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipH.png'; +import * as imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90 from './groundTruth/imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90.png'; + +// linear interpolation +import * as imageURI_11_11_4_1_1_1_0 from './groundTruth/imageURI_11_11_4_1_1_1_0.png'; +import * as imageURI_256_256_50_10_1_1_0 from './groundTruth/imageURI_256_256_50_10_1_1_0.png'; +import * as imageURI_100_100_0_10_1_1_1_linear_color from './groundTruth/imageURI_100_100_0_10_1_1_1_linear_color.png'; +import * as calibrated_1_5_imageURI_11_11_4_1_1_1_0_1 from './groundTruth/calibrated_1_5_imageURI_11_11_4_1_1_1_0_1.png'; + +const { cache, RenderingEngine, utilities, imageLoader, metaData, Enums } = + cornerstone3D; + +const { Events, ViewportType, InterpolationType } = Enums; +const { calibratedPixelSpacingMetadataProvider } = utilities; + +const { fakeImageLoader, fakeMetaDataProvider, compareImages } = testUtils; + +const renderingEngineId = utilities.uuidv4(); + +const viewportId = 'VIEWPORT'; + +const AXIAL = 'AXIAL'; +const SAGITTAL = 'SAGITTAL'; +const CORONAL = 'CORONAL'; + +describe('renderingCore -- Stack', () => { + let renderingEngine; + + beforeEach(function () { + const testEnv = testUtils.setupTestEnvironment({ + renderingEngineId, + toolGroupIds: ['default'], + }); + renderingEngine = testEnv.renderingEngine; + }); + + afterEach(function () { + testUtils.cleanupTestEnvironment({ + renderingEngineId, + toolGroupIds: ['default'], + }); + }); + + describe('Stack Viewport Nearest Neighbor Interpolation --- ', function () { + it('Should render one stack viewport of square size properly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + }; + const imageId = encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + compareImages( + image, + imageURI_64_64_20_5_1_1_0_nearest, + 'imageURI_64_64_20_5_1_1_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport of rectangle size properly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 33, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + compareImages( + image, + imageURI_64_33_20_5_1_1_0_nearest, + 'imageURI_64_33_20_5_1_1_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 30, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId = encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + imageURI_64_64_30_10_5_5_0_nearest, + 'imageURI_64_64_30_10_5_5_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should use enableElement API to render one stack viewport of square size and 5mm spacing properly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 30, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId = encodeImageIdInfo(imageInfo); + + renderingEngine.enableElement({ + viewportId: viewportId, + type: ViewportType.STACK, + element: element, + defaultOptions: { + background: [1, 0, 1], // pinkish background + }, + }); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + imageURI_64_64_30_10_5_5_0_nearest, + 'imageURI_64_64_30_10_5_5_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport, first slice correctly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 0, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId1 = encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 10, + barWidth: 20, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 1, + }; + const imageId2 = encodeImageIdInfo(imageInfo2); + + const imageInfo3 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 30, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 2, + }; + const imageId3 = encodeImageIdInfo(imageInfo3); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + imageURI_64_64_0_10_5_5_0_nearest, + 'imageURI_64_64_0_10_5_5_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2, imageId3], 0).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport, last slice correctly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 0, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 0, + }; + const imageId1 = encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 10, + barWidth: 20, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 1, + }; + const imageId2 = encodeImageIdInfo(imageInfo2); + + const imageInfo3 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 54, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 2, + }; + const imageId3 = encodeImageIdInfo(imageInfo3); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + imageURI_64_64_54_10_5_5_0_nearest, + 'imageURI_64_64_54_10_5_5_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2, imageId3], 2).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport with CT presets correctly: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 256, + columns: 256, + barStart: 100, + barWidth: 100, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + imageURI_256_256_100_100_1_1_0_CT_nearest, + 'imageURI_256_256_100_100_1_1_0_CT_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + voiRange: { lower: -160, upper: 240 }, + interpolationType: InterpolationType.NEAREST, + }); + }); + + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport with multiple imageIds of different size and different spacing: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 256, + columns: 256, + barStart: 100, + barWidth: 100, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId1 = encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 30, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 1, + }; + const imageId2 = encodeImageIdInfo(imageInfo2); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + imageURI_256_256_100_100_1_1_0_nearest, + 'imageURI_256_256_100_100_1_1_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2], 0).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport with multiple imageIds of different size and different spacing, second slice: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 256, + columns: 256, + barStart: 100, + barWidth: 100, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId1 = encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 30, + barWidth: 10, + xSpacing: 5, + ySpacing: 5, + sliceIndex: 1, + }; + const imageId2 = encodeImageIdInfo(imageInfo2); + + const vp = renderingEngine.getViewport(viewportId); + + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + compareImages( + image, + imageURI_64_64_30_10_5_5_0_nearest, + 'imageURI_64_64_30_10_5_5_0_nearest' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId1, imageId2], 1).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Stack Viewport Linear Interpolation --- ', () => { + it('Should render one stack viewport with linear interpolation correctly', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 11, + columns: 11, + barStart: 4, + barWidth: 1, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + compareImages( + image, + imageURI_11_11_4_1_1_1_0, + 'imageURI_11_11_4_1_1_1_0' + ).then(done, done.fail); + }); + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ voiRange: { lower: -160, upper: 240 } }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render one stack viewport with multiple images with linear interpolation correctly', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo1 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 11, + columns: 11, + barStart: 4, + barWidth: 1, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId1 = encodeImageIdInfo(imageInfo1); + + const imageInfo2 = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 256, + columns: 256, + barStart: 50, + barWidth: 10, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 1, + }; + const imageId2 = encodeImageIdInfo(imageInfo2); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + compareImages( + image, + imageURI_256_256_50_10_1_1_0, + 'imageURI_256_256_50_10_1_1_0' + ).then(done, done.fail); + }); + try { + vp.setStack([imageId1, imageId2], 1).then(() => { + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Color Stack Images', () => { + it('Should render color images: linear', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.SAGITTAL, + width: 512, + height: 512, + }); + + // color image generation with 10 strips of different colors + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 100, + columns: 100, + barStart: 0, + barWidth: 10, + xSpacing: 1, + ySpacing: 1, + rgb: 1, + pt: 0, + sliceIndex: 0, + }; + const imageId = encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + compareImages( + image, + imageURI_100_100_0_10_1_1_1_linear_color, + 'imageURI_100_100_0_10_1_1_1_linear_color' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should render color images: nearest', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + width: 512, + height: 512, + }); + + // color image generation with 10 strips of different colors + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 100, + columns: 100, + barStart: 0, + barWidth: 10, + xSpacing: 1, + ySpacing: 1, + rgb: 1, + pt: 0, + sliceIndex: 0, + }; + const imageId = encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + compareImages( + image, + imageURI_100_100_0_10_1_1_1_nearest_color, + 'imageURI_100_100_0_10_1_1_1_nearest_color' + ).then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ interpolationType: InterpolationType.NEAREST }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Stack Viewport Calibration and Scaling --- ', () => { + it('Should be able to render a stack viewport with PET modality scaling', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 11, + columns: 11, + barStart: 4, + barWidth: 1, + xSpacing: 1, + ySpacing: 1, + PT: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + expect(vp.scaling.PT).toEqual({ + suvbwToSuvlbm: 1, + suvbwToSuvbsa: 1, + }); + done(); + }); + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + + it('Should be able to calibrate the pixel spacing', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 11, + columns: 11, + barStart: 4, + barWidth: 1, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + const imageRenderedCallback = () => { + calibratedPixelSpacingMetadataProvider.add(imageId, { + scale: 0.5, + }); + + vp.calibrateSpacing(imageId); + element.removeEventListener( + Events.IMAGE_RENDERED, + imageRenderedCallback + ); + element.addEventListener( + Events.IMAGE_RENDERED, + secondImageRenderedCallbackAfterCalibration + ); + }; + + const secondImageRenderedCallbackAfterCalibration = () => { + done(); + }; + + element.addEventListener(Events.IMAGE_RENDERED, imageRenderedCallback); + + element.addEventListener(Events.IMAGE_SPACING_CALIBRATED, (evt) => { + const { calibration } = evt.detail; + expect(calibration?.scale).toBe(0.5); + }); + + try { + vp.setStack([imageId], 0); + vp.render(); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Stack Viewport setProperties API --- ', () => { + it('Should be able to use setProperties API', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 11, + columns: 11, + barStart: 4, + barWidth: 1, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + const subscribeToImageRendered = () => { + element.addEventListener(Events.IMAGE_RENDERED, (evt) => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + + let props = vp.getProperties(); + const rotation = vp.getViewPresentation().rotation; + expect(rotation).toBe(90); + expect(props.interpolationType).toBe(InterpolationType.NEAREST); + expect(props.invert).toBe(true); + + testUtils + .compareImages( + image, + imageURI_11_11_4_1_1_1_0_nearest_invert_90deg, + 'imageURI_11_11_4_1_1_1_0_nearest_invert_90deg' + ) + .then(done, done.fail); + }); + }; + + try { + vp.setStack([imageId], 0).then(() => { + subscribeToImageRendered(); + vp.setProperties({ + interpolationType: InterpolationType.NEAREST, + voiRange: { lower: -260, upper: 140 }, + invert: true, + }); + vp.setViewPresentation({ rotation: 90 }); + + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should be able to resetProperties API', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 11, + columns: 11, + barStart: 4, + barWidth: 1, + xSpacing: 1, + ySpacing: 1, + sliceIndex: 0, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + + const firstImageRenderedCallback = () => { + element.removeEventListener( + Events.IMAGE_RENDERED, + firstImageRenderedCallback + ); + + let props = vp.getProperties(); + const rotation = vp.getViewPresentation().rotation; + expect(rotation).toBe(90); + expect(props.interpolationType).toBe(InterpolationType.NEAREST); + expect(props.invert).toBe(true); + + setTimeout(() => { + console.log('reseting properties'); + vp.resetProperties(); + }); + + element.addEventListener( + Events.IMAGE_RENDERED, + secondImageRenderedCallback + ); + }; + + const secondImageRenderedCallback = () => { + console.log('resetProperties callback'); + const props = vp.getProperties(); + expect(props.interpolationType).toBe(InterpolationType.LINEAR); + expect(props.invert).toBe(false); + + done(); + console.log('done'); + }; + + element.addEventListener( + Events.IMAGE_RENDERED, + firstImageRenderedCallback + ); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + interpolationType: InterpolationType.NEAREST, + voiRange: { lower: -260, upper: 140 }, + invert: true, + }); + vp.setRotation(90); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); + + describe('Flipping', function () { + it('Should be able to flip a stack viewport horizontally', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + imageURI_64_64_20_5_1_1_0_nearestFlipH, + 'imageURI_64_64_20_5_1_1_0_nearestFlipH' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + interpolationType: InterpolationType.NEAREST, + }); + + vp.setCamera({ flipHorizontal: true }); + + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + + it('Should be able to flip a stack viewport vertically and rotate it', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.AXIAL, + }); + + const imageInfo = { + loader: 'fakeImageLoader', + name: 'imageURI', + rows: 64, + columns: 64, + barStart: 20, + barWidth: 5, + xSpacing: 1, + ySpacing: 1, + }; + const imageId = testUtils.encodeImageIdInfo(imageInfo); + + const vp = renderingEngine.getViewport(viewportId); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90, + 'imageURI_64_64_20_5_1_1_0_nearestFlipHRotate90' + ) + .then(done, done.fail); + }); + + try { + vp.setStack([imageId], 0).then(() => { + vp.setProperties({ + interpolationType: InterpolationType.NEAREST, + }); + + vp.setRotation(90); + vp.setCamera({ flipVertical: true }); + vp.render(); + }); + } catch (e) { + done.fail(e); + } + }); + }); +}); diff --git a/packages/core/test/volumeViewport_gpu_render_test.js b/packages/core/test/volumeViewport_gpu_render_test.js index 3934493cc4..3ef238bebf 100644 --- a/packages/core/test/volumeViewport_gpu_render_test.js +++ b/packages/core/test/volumeViewport_gpu_render_test.js @@ -52,6 +52,8 @@ describe('Volume Viewport GPU -- ', () => { viewportId, orientation: Enums.OrientationAxis.SAGITTAL, viewportType: ViewportType.VOLUME_3D, + width: 500, + height: 500, }); const vp = renderingEngine.getViewport(viewportId); @@ -60,13 +62,15 @@ describe('Volume Viewport GPU -- ', () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - sphere_default_sagittal, - 'sphere_default_sagittal' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + sphere_default_sagittal, + 'sphere_default_sagittal' + ) + .then(done, done.fail); + }, 200); }); try { @@ -95,12 +99,14 @@ describe('Volume Viewport GPU -- ', () => { }); }); - describe('Volume Viewport Axial Nearest Neighbor and Linear Interpolation --- ', function () { + xdescribe('Volume Viewport Axial Nearest Neighbor and Linear Interpolation --- ', function () { it('should successfully load a volume: nearest', function (done) { const element = testUtils.createViewports(renderingEngine, { viewportId, orientation: Enums.OrientationAxis.AXIAL, viewportType: ViewportType.ORTHOGRAPHIC, + width: 500, + height: 500, }); const volumeId = testUtils.encodeVolumeIdInfo({ @@ -118,13 +124,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_axial_nearest, - 'volumeURI_100_100_10_1_1_1_0_axial_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_axial_nearest, + 'volumeURI_100_100_10_1_1_1_0_axial_nearest' + ) + .then(done, done.fail); + }, 1000); }); const callback = ({ volumeActor }) => @@ -171,13 +179,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_axial_linear, - 'volumeURI_100_100_10_1_1_1_0_axial_linear' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_axial_linear, + 'volumeURI_100_100_10_1_1_1_0_axial_linear' + ) + .then(done, done.fail); + }, 200); }); try { @@ -195,9 +205,9 @@ describe('Volume Viewport GPU -- ', () => { done.fail(e); } }); - }); + }, 2000); - describe('Volume Viewport Sagittal Nearest Neighbor and Linear Interpolation --- ', function () { + xdescribe('Volume Viewport Sagittal Nearest Neighbor and Linear Interpolation --- ', function () { it('should successfully load a volume: nearest', function (done) { const element = testUtils.createViewports(renderingEngine, { viewportId, @@ -220,13 +230,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_sagittal_nearest, - 'volumeURI_100_100_10_1_1_1_0_sagittal_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_sagittal_nearest, + 'volumeURI_100_100_10_1_1_1_0_sagittal_nearest' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => @@ -273,13 +285,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_sagittal_linear, - 'volumeURI_100_100_10_1_1_1_0_sagittal_linear' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_sagittal_linear, + 'volumeURI_100_100_10_1_1_1_0_sagittal_linear' + ) + .then(done, done.fail); + }, 200); }); try { @@ -302,7 +316,7 @@ describe('Volume Viewport GPU -- ', () => { }); }); - describe('Volume Viewport Sagittal Coronal Neighbor and Linear Interpolation --- ', function () { + xdescribe('Volume Viewport Sagittal Coronal Neighbor and Linear Interpolation --- ', function () { it('should successfully load a volume: nearest', function (done) { const element = testUtils.createViewports(renderingEngine, { viewportId, @@ -325,13 +339,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => @@ -380,13 +396,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_linear, - 'volumeURI_100_100_10_1_1_1_0_coronal_linear' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_linear, + 'volumeURI_100_100_10_1_1_1_0_coronal_linear' + ) + .then(done, done.fail); + }, 200); }); try { @@ -409,7 +427,7 @@ describe('Volume Viewport GPU -- ', () => { }); }); - describe('Rendering API', function () { + xdescribe('Rendering API', function () { it('should successfully use setVolumesForViewports API to load image', function (done) { const element = testUtils.createViewports(renderingEngine, { viewportId, @@ -432,13 +450,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => @@ -539,13 +559,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => @@ -592,13 +614,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => @@ -694,13 +718,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_0_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_0_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_0_coronal_nearest' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => @@ -729,7 +755,7 @@ describe('Volume Viewport GPU -- ', () => { }); }); - describe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { + xdescribe('Volume Viewport Color images Neighbor and Linear Interpolation --- ', function () { it('should successfully load a color volume: nearest', function (done) { const element = testUtils.createViewports(renderingEngine, { viewportId, @@ -753,13 +779,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_1_color_coronal_nearest, - 'volumeURI_100_100_10_1_1_1_1_color_coronal_nearest' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_coronal_nearest, + 'volumeURI_100_100_10_1_1_1_1_color_coronal_nearest' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => { @@ -807,13 +835,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_1_color_coronal_linear, - 'volumeURI_100_100_10_1_1_1_1_color_coronal_linear' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_coronal_linear, + 'volumeURI_100_100_10_1_1_1_1_color_coronal_linear' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => { @@ -861,13 +891,15 @@ describe('Volume Viewport GPU -- ', () => { element.addEventListener(Events.IMAGE_RENDERED, () => { const canvas = vp.getCanvas(); const image = canvas.toDataURL('image/png'); - testUtils - .compareImages( - image, - volumeURI_100_100_10_1_1_1_1_color_axial_linear, - 'volumeURI_100_100_10_1_1_1_1_color_axial_linear' - ) - .then(done, done.fail); + setTimeout(() => { + testUtils + .compareImages( + image, + volumeURI_100_100_10_1_1_1_1_color_axial_linear, + 'volumeURI_100_100_10_1_1_1_1_color_axial_linear' + ) + .then(done, done.fail); + }, 200); }); const callback = ({ volumeActor }) => { diff --git a/packages/core/test/volumeViewport_gpu_setProperties_test.js b/packages/core/test/volumeViewport_gpu_setProperties_test.js index 8085411c80..6365d08d76 100644 --- a/packages/core/test/volumeViewport_gpu_setProperties_test.js +++ b/packages/core/test/volumeViewport_gpu_setProperties_test.js @@ -1,89 +1,89 @@ -// import * as cornerstone3D from '../src/index'; -// import * as testUtils from '../../../utils/test/testUtils'; +import * as cornerstone3D from '../src/index'; +import * as testUtils from '../../../utils/test/testUtils'; -// // linear interpolation -// import * as volumeURI_32_32_10_1_1_1_0 from './groundTruth/volumeURI_32_32_10_1_1_1_0.png'; +// linear interpolation +import * as volumeURI_32_32_10_1_1_1_0 from './groundTruth/volumeURI_32_32_10_1_1_1_0.png'; -// const { cache, RenderingEngine, Enums, volumeLoader, setVolumesForViewports } = -// cornerstone3D; +const { cache, RenderingEngine, Enums, volumeLoader, setVolumesForViewports } = + cornerstone3D; -// const { Events } = Enums; +const { Events } = Enums; -// const viewportId = 'VIEWPORT'; +const viewportId = 'VIEWPORT'; -// fdescribe('Volume Viewport SetProperties -- ', () => { -// let renderingEngine; +xdescribe('Volume Viewport SetProperties -- ', () => { + let renderingEngine; -// beforeEach(function () { -// const testEnv = testUtils.setupTestEnvironment({ -// toolGroupIds: ['default'], -// viewportIds: [viewportId], -// }); -// renderingEngine = testEnv.renderingEngine; -// }); + beforeEach(function () { + const testEnv = testUtils.setupTestEnvironment({ + toolGroupIds: ['default'], + viewportIds: [viewportId], + }); + renderingEngine = testEnv.renderingEngine; + }); -// afterEach(function () { -// testUtils.cleanupTestEnvironment({ -// renderingEngineId: renderingEngine.id, -// toolGroupIds: ['default'], -// }); -// }); + afterEach(function () { + testUtils.cleanupTestEnvironment({ + renderingEngineId: renderingEngine.id, + toolGroupIds: ['default'], + }); + }); -// it('should successfully modify the viewport with invert and setVOI', function (done) { -// const element = testUtils.createViewports(renderingEngine, { -// viewportId, -// orientation: Enums.OrientationAxis.CORONAL, -// viewportType: Enums.ViewportType.ORTHOGRAPHIC, -// }); + it('should successfully modify the viewport with invert and setVOI', function (done) { + const element = testUtils.createViewports(renderingEngine, { + viewportId, + orientation: Enums.OrientationAxis.CORONAL, + viewportType: Enums.ViewportType.ORTHOGRAPHIC, + }); -// const volumeId = testUtils.encodeVolumeIdInfo({ -// loader: 'fakeVolumeLoader', -// name: 'volumeURI', -// rows: 32, -// columns: 32, -// slices: 10, -// xSpacing: 1, -// ySpacing: 1, -// rgb: 1, -// }); -// const vp = renderingEngine.getViewport(viewportId); + const volumeId = testUtils.encodeVolumeIdInfo({ + loader: 'fakeVolumeLoader', + name: 'volumeURI', + rows: 32, + columns: 32, + slices: 10, + xSpacing: 1, + ySpacing: 1, + rgb: 1, + }); + const vp = renderingEngine.getViewport(viewportId); -// element.addEventListener(Events.IMAGE_RENDERED, () => { -// const canvas = vp.getCanvas(); -// const image = canvas.toDataURL('image/png'); -// testUtils -// .compareImages( -// image, -// volumeURI_32_32_10_1_1_1_0, -// 'volumeURI_32_32_10_1_1_1_0' -// ) -// .then(done, done.fail); -// }); + element.addEventListener(Events.IMAGE_RENDERED, () => { + const canvas = vp.getCanvas(); + const image = canvas.toDataURL('image/png'); + testUtils + .compareImages( + image, + volumeURI_32_32_10_1_1_1_0, + 'volumeURI_32_32_10_1_1_1_0' + ) + .then(done, done.fail); + }); -// try { -// volumeLoader -// .createAndCacheVolume(volumeId, { imageIds: [] }) -// .then(() => { -// setVolumesForViewports( -// renderingEngine, -// [{ volumeId: volumeId }], -// [viewportId] -// ).then(() => { -// vp.setProperties({ -// voiRange: { -// lower: 50, -// upper: 100, -// }, -// invert: true, -// }); -// vp.render(); -// }); -// }) -// .catch((e) => { -// done(e); -// }); -// } catch (e) { -// done.fail(e); -// } -// }); -// }); + try { + volumeLoader + .createAndCacheVolume(volumeId, { imageIds: [] }) + .then(() => { + setVolumesForViewports( + renderingEngine, + [{ volumeId: volumeId }], + [viewportId] + ).then(() => { + vp.setProperties({ + voiRange: { + lower: 50, + upper: 100, + }, + invert: true, + }); + vp.render(); + }); + }) + .catch((e) => { + done(e); + }); + } catch (e) { + done.fail(e); + } + }); +}); From 1f2d674cc15ec82fc6b6a1fc3aa16d62ad3e09d6 Mon Sep 17 00:00:00 2001 From: sedghi Date: Fri, 29 Nov 2024 13:21:22 -0500 Subject: [PATCH 16/16] update --- .../RenderingEngine/helpers/setDefaultVolumeVOI.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts index adf83f88a1..e9a01ab10e 100644 --- a/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts +++ b/packages/core/src/RenderingEngine/helpers/setDefaultVolumeVOI.ts @@ -9,7 +9,6 @@ import * as metaData from '../../metaData'; import * as windowLevel from '../../utilities/windowLevel'; import { RequestType } from '../../enums'; import cache from '../../cache/cache'; -import { getMinMax } from '../../utilities'; const PRIORITY = 0; const REQUEST_TYPE = RequestType.Prefetch; @@ -181,15 +180,12 @@ async function getVOIFromMiddleSliceMinMax( } // Get the min and max pixel values of the middle slice - // let { min, max } = image.voxelManager.getMinMax(); + let { min, max } = image.voxelManager.getMinMax(); - // if (min.length > 1) { - // min = Math.min(...min); - // max = Math.max(...max); - // } - - const { min, max } = getMinMax(image.voxelManager.getScalarData()); - console.log('****************************, ', min, max); + if (min.length > 1) { + min = Math.min(...min); + max = Math.max(...max); + } return { lower: min,