Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
14 changes: 12 additions & 2 deletions packages/core/src/cache/classes/ImageVolume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ export class ImageVolume {
dataType?: PixelDataTypedArrayString;

// @deprecated
numTimePoints? = null as number;
get numTimePoints(): number {
// @ts-expect-error
return typeof this.numDimensionGroups === 'number'
// @ts-expect-error
? this.numDimensionGroups
: 1;
}
numFrames = null as number;
suppressWarnings: boolean;

Expand Down Expand Up @@ -217,7 +223,11 @@ export class ImageVolume {

/** return true if it is a 4D volume or false if it is 3D volume */
public isDynamicVolume(): boolean {
return this.numTimePoints > 1;
if (this.numTimePoints) {
return this.numTimePoints > 1;
}

return false;
}

/**
Expand Down
123 changes: 122 additions & 1 deletion packages/core/src/utilities/splitImageIdsBy4DTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,123 @@ import * as metaData from '../metaData';
// (2001,1003) Philips Diffusion B-factor [OK]
// (0019,100c) Siemens Diffusion B Value [Implemented, not tested]
// (0043,1039) GE Diffusion B Value [OK]
//
// Multiframe 4D Support (NM Multi-frame Module):
// (0054,0070) TimeSlotVector [OK]
// (0054,0080) SliceVector [Used for ordering within time slots]

interface MappedIPP {
imageId: string;
imagePositionPatient;
}

interface MultiframeSplitResult {
imageIdGroups: string[][];
splittingTag: string;
}

/**
* Generates frame-specific imageIds for a multiframe image.
* Replaces the frame number in the imageId with the specified frame number (1-based).
*/
function generateFrameImageId(
baseImageId: string,
frameNumber: number
): string {
return baseImageId.replace(/\/frames\/\d+/, `/frames/${frameNumber}`);
}

/**
* Handles multiframe 4D splitting using TimeSlotVector (0054,0070).
* For NM Multi-frame images where frames are indexed by time slot and slice.
*
* @param imageIds - Array containing the base imageId (typically just one for multiframe)
* @returns Split result if multiframe 4D is detected, null otherwise
*/
function handleMultiframe4D(imageIds: string[]): MultiframeSplitResult | null {
if (!imageIds || imageIds.length === 0) {
return null;
}

const baseImageId = imageIds[0];
const instance = metaData.get('instance', baseImageId);

if (!instance) {
return null;
}

const numberOfFrames = instance.NumberOfFrames;
if (!numberOfFrames || numberOfFrames <= 1) {
return null;
}

const timeSlotVector = instance.TimeSlotVector;
if (!timeSlotVector || !Array.isArray(timeSlotVector)) {
return null;
}

const sliceVector = instance.SliceVector;
const numberOfSlices = instance.NumberOfSlices;

if (timeSlotVector.length !== numberOfFrames) {
console.warn(
'TimeSlotVector length does not match NumberOfFrames:',
timeSlotVector.length,
'vs',
numberOfFrames
);
return null;
}

const timeSlotGroups: Map<
number,
Array<{ frameIndex: number; sliceIndex: number }>
> = new Map();

for (let frameIndex = 0; frameIndex < numberOfFrames; frameIndex++) {
const timeSlot = timeSlotVector[frameIndex];
const sliceIndex = sliceVector ? sliceVector[frameIndex] : frameIndex;

if (!timeSlotGroups.has(timeSlot)) {
timeSlotGroups.set(timeSlot, []);
}

timeSlotGroups.get(timeSlot).push({ frameIndex, sliceIndex });
}

const sortedTimeSlots = Array.from(timeSlotGroups.keys()).sort(
(a, b) => a - b
);

const imageIdGroups: string[][] = sortedTimeSlots.map((timeSlot) => {
const frames = timeSlotGroups.get(timeSlot);

frames.sort((a, b) => a.sliceIndex - b.sliceIndex);

return frames.map((frame) =>
generateFrameImageId(baseImageId, frame.frameIndex + 1)
);
});

const expectedSlicesPerTimeSlot = numberOfSlices || imageIdGroups[0]?.length;
const allGroupsHaveSameLength = imageIdGroups.every(
(group) => group.length === expectedSlicesPerTimeSlot
);

if (!allGroupsHaveSameLength) {
console.warn(
'Multiframe 4D split resulted in uneven time slot groups. Expected',
expectedSlicesPerTimeSlot,
'slices per time slot.'
);
}

return {
imageIdGroups,
splittingTag: 'TimeSlotVector',
};
}

const groupBy = (array, key) => {
return array.reduce((rv, x) => {
(rv[x[key]] = rv[x[key]] || []).push(x);
Expand Down Expand Up @@ -173,16 +284,26 @@ function getPetFrameReferenceTime(imageId) {
/**
* Split the imageIds array by 4D tags into groups. Each group must have the
* same number of imageIds or the same imageIds array passed in is returned.
*
* For multiframe images (NumberOfFrames > 1), this function checks for
* TimeSlotVector (0054,0070) which is common in NM (Nuclear Medicine) gated
* SPECT/PET images. The TimeSlotVector indicates which time slot each frame
* belongs to, and SliceVector (0054,0080) indicates the slice position.
*
* @param imageIds - array of imageIds
* @returns imageIds grouped by 4D tags
*/
function splitImageIdsBy4DTags(imageIds: string[]): {
imageIdGroups: string[][];
splittingTag: string | null;
} {
const multiframeResult = handleMultiframe4D(imageIds);
if (multiframeResult) {
return multiframeResult;
}

const positionGroups = getIPPGroups(imageIds);
if (!positionGroups) {
// When no position groups are found, return the original array wrapped and indicate no tag was used
return { imageIdGroups: [imageIds], splittingTag: null };
}

Expand Down
Loading