Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0f2ad10
Fix overlapping segmentation render issues
rodrigobasilio2022 Jul 1, 2025
a90f60a
Merge branch 'main' into fix/OverlappingSegmentationBug
rodrigobasilio2022 Jul 1, 2025
061fbd8
fix labelmapImageIds variable redefinition
rodrigobasilio2022 Jul 1, 2025
429051a
Refactor as copilot suggestions
rodrigobasilio2022 Jul 1, 2025
5bb835d
Refactor to maintain 1d array use
rodrigobasilio2022 Jul 1, 2025
f1b72a4
Refactor as copilot suggestions
rodrigobasilio2022 Jul 1, 2025
4d058fe
Minor change
rodrigobasilio2022 Jul 1, 2025
fa43ba6
Fix ci error
rodrigobasilio2022 Jul 2, 2025
8aec325
refactor references to imageIds as 2d array
rodrigobasilio2022 Jul 2, 2025
593de5f
Merge branch 'main' into fix/OverlappingSegmentationBug
rodrigobasilio2022 Jul 3, 2025
95b6d92
Refactor as reviewer suggestions
rodrigobasilio2022 Jul 4, 2025
b4a3f5a
update documentation
rodrigobasilio2022 Jul 4, 2025
6057088
mark as deprecated
rodrigobasilio2022 Jul 4, 2025
c6c6630
remove volumeIds
rodrigobasilio2022 Jul 7, 2025
56e87b2
refactor code
rodrigobasilio2022 Jul 7, 2025
dcca5d1
fix example bugs
rodrigobasilio2022 Jul 7, 2025
0833447
Merge branch 'main' into fix/OverlappingSegmentationBug
rodrigobasilio2022 Jul 8, 2025
0261ed5
fix bug in segmentation volume creation
rodrigobasilio2022 Jul 8, 2025
a867d98
Merge branch into fix/OverlappingSegmentationBug
rodrigobasilio2022 Jul 9, 2025
066f0ae
Merge branch 'main' into fix/OverlappingSegmentationBug
rodrigobasilio2022 Jul 9, 2025
b54196d
Merge branch 'main' of https://github.com/rodrigobasilio2022/cornerst…
rodrigobasilio2022 Jul 10, 2025
77b33d4
Merge branch 'main' into pr/rodrigobasilio2022/2170
rodrigobasilio2022 Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/types/IActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type VolumeActor = vtkVolume;
export type ImageActor = vtkImageSlice;

export type ICanvasActor = CanvasActor;

export type ViewportActor = Actor | VolumeActor | ImageActor | ICanvasActor;
/**
* Cornerstone Actor Entry including actor uid, actual Actor, and
* slabThickness for the actor. ActorEntry is the object that
Expand All @@ -19,7 +19,7 @@ export interface ActorEntry {
/** actor UID */
uid: string;
/** actual actor object */
actor: Actor | VolumeActor | ImageActor | ICanvasActor;
actor: ViewportActor;
/** the id of the referenced object (e.g., volume) from which this actor is derived or created*/
referencedId?: string;
/** slab thickness for the actor */
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
ActorEntry,
ImageActor,
ICanvasActor,
ViewportActor,
} from './IActor';
import type {
IImageLoadObject,
Expand Down Expand Up @@ -211,6 +212,7 @@ export type {
VolumeActor,
Actor,
ActorEntry,
ViewportActor,
ImageActor,
ICanvasActor,
IImageLoadObject,
Expand Down
57 changes: 57 additions & 0 deletions packages/core/src/utilities/getNumberOfReferenceImageIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import cache from '../cache/cache';
/**
* Returns the number of unique reference image IDs from a list of image IDs.
* For each imageId, if the cached image has a referencedImageId, it will use that;
* otherwise, it will use the image's own imageId. If cache.getImage(imageId) returns undefined,
* that imageId is skipped. The function returns the count of unique reference image IDs found.
*
* @param {string[]} imageIds - An array of image IDs to check.
* @returns {number} The number of unique reference image IDs.
*/
export function getNumberOfReferenceImageIds(imageIds: string[]): number {
if (!imageIds || imageIds.length === 0) {
return 0;
}

const uniqueReferenceImageIds = getReferenceImageIds(imageIds);
return uniqueReferenceImageIds.length;
}

/**
* Returns an array of unique reference image IDs from a list of image IDs.
* For each imageId, if the cached image has a referencedImageId, it will return that;
* otherwise, it will return the image's own imageId. If cache.getImage(imageId) returns undefined,
* that imageId is skipped. The returned array contains only unique reference image IDs.
*
* @param {string[]} imageIds - An array of image IDs to check.
* @returns {string[]} An array of unique reference image IDs.
*/
export function getReferenceImageIds(imageIds: string[]): string[] {
if (!imageIds || imageIds.length === 0) {
return [];
}

const referenceImageIds = imageIds
.map((imageId) => {
if (!imageId || typeof imageId !== 'string') {
return null; // Skip invalid imageIds
}

try {
const image = cache.getImage(imageId);
if (!image) {
return null; // Skip if image is undefined
}
return image.referencedImageId
? image.referencedImageId
: image.imageId;
} catch (error) {
console.warn(`Failed to get image for imageId: ${imageId}`, error);
return null; // Skip if cache.getImage throws an error
}
})
.filter((id): id is string => !!id); // Filter out nulls and falsy values

const uniqueReferenceImageIds = new Set(referenceImageIds);
return Array.from(uniqueReferenceImageIds);
}
6 changes: 6 additions & 0 deletions packages/core/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ import calculateSpacingBetweenImageIds from './calculateSpacingBetweenImageIds';
export * as logger from './logger';
import { calculateNeighborhoodStats } from './calculateNeighborhoodStats';
import getPixelSpacingInformation from './getPixelSpacingInformation';
import {
getNumberOfReferenceImageIds,
getReferenceImageIds,
} from './getNumberOfReferenceImageIds';
import { asArray } from './asArray';

const getViewportModality = (viewport: IViewport, volumeId?: string) =>
Expand Down Expand Up @@ -204,5 +208,7 @@ export {
buildMetadata,
calculateNeighborhoodStats,
getPixelSpacingInformation,
getNumberOfReferenceImageIds,
getReferenceImageIds,
asArray,
};
178 changes: 111 additions & 67 deletions packages/core/src/utilities/isValidVolume.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,138 @@
import * as metaData from '../metaData';
import { getNumberOfReferenceImageIds } from './getNumberOfReferenceImageIds';
import isEqual from './isEqual';

/**
* Checks if the given imageIds form a valid volume. A volume is considered valid if all imageIds
* have the same series instance UID, modality, columns, rows, image orientation patient, and pixel
* spacing.
* Checks if the given imageIds form valid volume(s). Accepts a single array of imageIds (for single or multi-volume segmentations).
*
* @param imageIds - The imageIds to check.
* @returns true if the imageIds form a valid volume, false otherwise.
* The function determines the number of images per volume by creating a set of reference image IDs for the given imageIds.
* For each imageId, it uses cache.getImage(imageId).referencedImageId if available, otherwise falls back to the imageId itself.
* If cache.getImage(imageId) returns undefined, that imageId is skipped.
* The number of unique reference image IDs is used to split the input into groups (volumes), and each group is validated independently.
*
* A volume is considered valid if all imageIds in the group have the same series instance UID, modality, columns,
* rows, image orientation patient, and pixel spacing.
*
* @param imageIds - The imageIds to check (flat string[] for single or multi-volume).
* @returns true if all imageId groups form valid volumes, false otherwise.
*/
function isValidVolume(imageIds: string[]): boolean {
if (imageIds.length <= 1) {
return false;
}
/**
* Validates metadata for a single image and returns it if valid.
* @param imageId - The image ID to validate
* @returns The metadata object if valid, null if invalid
*/
function getValidatedMetadata(imageId: string) {
try {
const generalSeries = metaData.get('generalSeriesModule', imageId);
const imagePlane = metaData.get('imagePlaneModule', imageId);

const imageId0 = imageIds[0];
if (!generalSeries || !imagePlane) {
return null;
}

const { modality, seriesInstanceUID } = metaData.get(
'generalSeriesModule',
imageId0
);
const { modality, seriesInstanceUID } = generalSeries;
const {
imageOrientationPatient,
pixelSpacing,
frameOfReferenceUID,
columns,
rows,
usingDefaultValues,
} = imagePlane;

// Check for required properties and default values
if (
usingDefaultValues ||
!modality ||
!seriesInstanceUID ||
!imageOrientationPatient ||
!pixelSpacing ||
columns == null ||
rows == null
) {
return null;
}

const {
imageOrientationPatient,
pixelSpacing,
frameOfReferenceUID,
columns,
rows,
usingDefaultValues,
} = metaData.get('imagePlaneModule', imageId0);
return {
modality,
seriesInstanceUID,
imageOrientationPatient,
pixelSpacing,
frameOfReferenceUID,
columns,
rows,
};
} catch (error) {
console.warn(`Failed to get metadata for imageId: ${imageId}`, error);
return null;
}
}

if (usingDefaultValues) {
/**
* Validates that all images in a group have consistent metadata.
* @param idsToCheck - Array of image IDs to validate
* @returns true if all images have consistent metadata, false otherwise
*/
function validateImageGroup(idsToCheck: string[]): boolean {
if (!idsToCheck || idsToCheck.length <= 1) {
return false;
}

const baseMetadata = {
modality,
imageOrientationPatient,
pixelSpacing,
frameOfReferenceUID,
columns,
rows,
seriesInstanceUID,
};

let validVolume = true;

for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const { modality, seriesInstanceUID } = metaData.get(
'generalSeriesModule',
imageId
);
const { imageOrientationPatient, pixelSpacing, columns, rows } =
metaData.get('imagePlaneModule', imageId);

if (seriesInstanceUID !== baseMetadata.seriesInstanceUID) {
validVolume = false;
break;
}
// Get baseline metadata from first image
const baseMetadata = getValidatedMetadata(idsToCheck[0]);
if (!baseMetadata) {
return false;
}

if (modality !== baseMetadata.modality) {
validVolume = false;
break;
// Validate all other images against baseline
for (let i = 1; i < idsToCheck.length; i++) {
const currentMetadata = getValidatedMetadata(idsToCheck[i]);
if (!currentMetadata) {
return false;
}

if (columns !== baseMetadata.columns) {
validVolume = false;
break;
// Check each property for consistency
if (
currentMetadata.seriesInstanceUID !== baseMetadata.seriesInstanceUID ||
currentMetadata.modality !== baseMetadata.modality ||
currentMetadata.columns !== baseMetadata.columns ||
currentMetadata.rows !== baseMetadata.rows ||
!isEqual(
currentMetadata.imageOrientationPatient,
baseMetadata.imageOrientationPatient
) ||
!isEqual(currentMetadata.pixelSpacing, baseMetadata.pixelSpacing)
) {
return false;
}
}

if (rows !== baseMetadata.rows) {
validVolume = false;
break;
}
return true;
}

if (
!isEqual(imageOrientationPatient, baseMetadata.imageOrientationPatient)
) {
validVolume = false;
break;
function isValidVolume(imageIds: string[]): boolean {
if (!imageIds || imageIds.length === 0) {
return false;
}
let groups: string[][] = [imageIds];
const numberOfImages = getNumberOfReferenceImageIds(imageIds);
// If numberOfImages is defined, split the imageIds into groups of that size.
if (numberOfImages && imageIds.length >= numberOfImages) {
const numVolumes = Math.floor(imageIds.length / numberOfImages);
groups = [];
for (let i = 0; i < numVolumes; i++) {
groups.push(imageIds.slice(i * numberOfImages, (i + 1) * numberOfImages));
}
}

if (!isEqual(pixelSpacing, baseMetadata.pixelSpacing)) {
validVolume = false;
break;
// Validate each group
for (const group of groups) {
if (!validateImageGroup(group)) {
return false;
}
}

return validVolume;
return true;
}

export { isValidVolume };
Loading
Loading