Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
219 changes: 115 additions & 104 deletions packages/adapters/src/adapters/Cornerstone/Segmentation_4X.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ export function generateSegmentation(
return fillSegmentation(segmentation, inputLabelmaps3D, userOptions);
}

/**
* The segment index of a voxel value of one labelmap3D. A value that is not in
* `labelmap3D.labelToSegmentIndex` is its own segment index.
*/
export function getSegmentIndexForLabel(labelmap3D, labelValue) {
return labelmap3D?.labelToSegmentIndex?.[labelValue] ?? labelValue;
}

/**
* The set of non-zero segment values actually present in a frame's pixel data,
* collected in a single pass. `segmentsOnLabelmap` can be stale, so presence is
Expand Down Expand Up @@ -98,6 +106,12 @@ function segmentsPresentInFrame(pixelData) {
*
* @param segmentation - The segmentation object to be filled.
* @param inputLabelmaps3D - An array of 3D labelmaps, or a single 3D labelmap.
* The voxels of a labelmap3D hold label values. `labelToSegmentIndex` maps a
* label value to its segment index, for a layer that stores a segment under
* another label value; a value that is not in the map is its own segment
* index. `segmentsOnLabelmap` of each labelmap2D lists label values. Two
* label values with the same segment index, in one labelmap3D or in two,
* give one SEG segment.
* @param userOptions - Optional configuration settings. Will override the default options.
* - `transferSyntaxUid` — the output transfer syntax. Defaults to RLE Lossless
* (`1.2.840.10008.1.2.5`); Explicit VR Little Endian
Expand Down Expand Up @@ -128,8 +142,8 @@ export function fillSegmentation(
? inputLabelmaps3D
: [inputLabelmaps3D];

const referencedFramesPerLabelmap = [];
const frameDescriptors = [];
const entriesBySegmentIndex = new Map();

for (
let labelmapIndex = 0;
Expand All @@ -139,138 +153,97 @@ export function fillSegmentation(
const labelmap3D = labelmaps3D[labelmapIndex];
const { labelmaps2D, metadata: segmentMetadata } = labelmap3D;

const referencedFramesPerSegment = [];

for (let i = 1; i < segmentMetadata.length; i++) {
if (segmentMetadata[i]) {
referencedFramesPerSegment[i] = [];
}
}

for (let i = 0; i < labelmaps2D.length; i++) {
const labelmap2D = labelmaps2D[i];

if (!labelmap2D?.pixelData) {
if (!labelmap2D) {
continue;
}

const { segmentsOnLabelmap } = labelmap2D;
const presentSegments = segmentsPresentInFrame(labelmap2D.pixelData);

segmentsOnLabelmap.forEach((segmentIndex) => {
segmentsPresentInFrame(labelmap2D.pixelData).forEach((labelValue) => {
const segmentIndex = getSegmentIndexForLabel(labelmap3D, labelValue);

if (
!segmentMetadata[segmentIndex] ||
!segmentsOnLabelmap.includes(labelValue)
) {
return;
}

if (!entriesBySegmentIndex.has(segmentIndex)) {
entriesBySegmentIndex.set(segmentIndex, {
segmentIndex,
metadata: segmentMetadata[segmentIndex],
sources: [],
frameIndices: new Set(),
});
}
const entry = entriesBySegmentIndex.get(segmentIndex);
if (
segmentIndex !== 0 &&
segmentMetadata[segmentIndex] &&
referencedFramesPerSegment[segmentIndex] &&
presentSegments.has(segmentIndex)
!entry.sources.some(
(source) =>
source.labelmap3D === labelmap3D &&
source.labelValue === labelValue
)
) {
referencedFramesPerSegment[segmentIndex].push(i);
entry.sources.push({ labelmap3D, labelValue });
}
entry.frameIndices.add(i);
});
}

referencedFramesPerLabelmap[labelmapIndex] = referencedFramesPerSegment;
}

let numberOfFrames = 0;

for (
let labelmapIndex = 0;
labelmapIndex < labelmaps3D.length;
labelmapIndex++
) {
const referencedFramesPerSegment =
referencedFramesPerLabelmap[labelmapIndex];
const labelmap3D = labelmaps3D[labelmapIndex];
const { metadata: segmentMetadata } = labelmap3D;

for (
let segmentIndex = 1;
segmentIndex < referencedFramesPerSegment.length;
segmentIndex++
) {
const referencedFrameIndicies = referencedFramesPerSegment[segmentIndex];

if (!referencedFrameIndicies?.length || !segmentMetadata[segmentIndex]) {
continue;
}

const labelmaps = _getLabelmapsFromReferencedFrameIndicies(
labelmap3D,
referencedFrameIndicies
);

if (
!labelmaps.length ||
!labelmaps.some((frame) => frame?.length && frame.some((v) => v !== 0))
) {
continue;
}
// dcmjs numbers the segments in the order they are added.
const segmentEntries = [...entriesBySegmentIndex.values()].sort(
(a, b) => a.segmentIndex - b.segmentIndex
);

numberOfFrames += referencedFrameIndicies.length;
}
}
const numberOfFrames = segmentEntries.reduce(
(total, entry) => total + entry.frameIndices.size,
0
);

if (numberOfFrames === 0) {
throw new Error('No non-empty segmentation frames found for SEG export');
}

segmentation.setNumberOfFrames(numberOfFrames);

for (
let labelmapIndex = 0;
labelmapIndex < labelmaps3D.length;
labelmapIndex++
) {
const referencedFramesPerSegment =
referencedFramesPerLabelmap[labelmapIndex];

const labelmap3D = labelmaps3D[labelmapIndex];
const { metadata: segmentMetadata } = labelmap3D;
const firstSegmentNumber =
(segmentation.dataset.SegmentSequence?.length ?? 0) + 1;

for (
let segmentIndex = 1;
segmentIndex < referencedFramesPerSegment.length;
segmentIndex++
) {
const referencedFrameIndicies = referencedFramesPerSegment[segmentIndex];

if (!referencedFrameIndicies?.length || !segmentMetadata[segmentIndex]) {
continue;
}
segmentEntries.forEach((entry, entryIndex) => {
const referencedFrameIndicies = [...entry.frameIndices].sort(
(a, b) => a - b
);
const { labelmaps, labelValue } = _getSegmentLabelmaps(
entry.sources,
referencedFrameIndicies
);

const labelmaps = _getLabelmapsFromReferencedFrameIndicies(
labelmap3D,
referencedFrameIndicies
);
// Frame numbers start from 1.
const referencedFrameNumbers = referencedFrameIndicies.map(
(element) => element + 1
);

if (
!labelmaps.length ||
!labelmaps.some((frame) => frame?.length && frame.some((v) => v !== 0))
) {
continue;
}
segmentation.addSegmentFromLabelmap(
entry.metadata,
labelmaps,
labelValue,
referencedFrameNumbers
);

// Frame numbers start from 1.
const referencedFrameNumbers = referencedFrameIndicies.map(
(element) => element + 1
);
const referencedSegmentNumber = firstSegmentNumber + entryIndex;

referencedFrameNumbers.forEach((frameNumber) => {
frameDescriptors.push({
referencedSegmentNumber: segmentIndex,
sourceFrameIndex: frameNumber - 1,
});
referencedFrameNumbers.forEach((frameNumber) => {
frameDescriptors.push({
referencedSegmentNumber,
sourceFrameIndex: frameNumber - 1,
});

segmentation.addSegmentFromLabelmap(
segmentMetadata[segmentIndex],
labelmaps,
segmentIndex,
referencedFrameNumbers
);
}
}
});
});

if (frameDescriptors.length && images?.length && metadata) {
// Every SEG frame must reference a resolvable source SOP Instance UID.
Expand Down Expand Up @@ -380,6 +353,44 @@ export function fillSegmentation(
return segmentation;
}

/**
* The frames of one SEG segment and the value that marks the segment in them.
* A segment with one source uses the frames of its labelmap3D. A segment with
* more sources gets new binary frames, with 1 where any source has its label
* value.
*/
function _getSegmentLabelmaps(sources, referencedFrameIndicies) {
if (sources.length === 1) {
const [{ labelmap3D, labelValue }] = sources;
return {
labelmaps: _getLabelmapsFromReferencedFrameIndicies(
labelmap3D,
referencedFrameIndicies
),
labelValue,
};
}

const labelmaps = referencedFrameIndicies.map((frameIndex) => {
let merged;
sources.forEach(({ labelmap3D, labelValue }) => {
const pixelData = labelmap3D.labelmaps2D[frameIndex]?.pixelData;
if (!pixelData) {
return;
}
merged ||= new Uint8Array(pixelData.length);
for (let i = 0; i < pixelData.length; i++) {
if (pixelData[i] === labelValue) {
merged[i] = 1;
}
}
});
return merged;
});

return { labelmaps, labelValue: 1 };
}

export function _getLabelmapsFromReferencedFrameIndicies(
labelmap3D,
referencedFrameIndicies
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { normalizers, derivations } from 'dcmjs';
import { Enums } from '@cornerstonejs/core';
import { fillSegmentation as fillBitmapSegmentation } from '../../Cornerstone/Segmentation_4X';
import {
fillSegmentation as fillBitmapSegmentation,
getSegmentIndexForLabel,
} from '../../Cornerstone/Segmentation_4X';
import {
encodeFramesToTransferSyntax,
EXPLICIT_VR_LITTLE_ENDIAN_TRANSFER_SYNTAX_UID,
Expand Down Expand Up @@ -137,8 +140,11 @@ function maxSegmentValue(labelmap3DArray, frameIndices: number[]): number {
return;
}
for (let i = 0; i < pixelData.length; i++) {
if (pixelData[i] > max) {
max = pixelData[i];
const segmentIndex = pixelData[i]
? getSegmentIndexForLabel(labelmap3D, pixelData[i])
: 0;
if (segmentIndex > max) {
max = segmentIndex;
}
}
});
Expand Down Expand Up @@ -271,10 +277,10 @@ function fillLabelmapSegmentation(
}
const len = Math.min(source.length, frameLength);
for (let i = 0; i < len; i++) {
const value = source[i];
if (value === 0) {
if (source[i] === 0) {
continue;
}
const value = getSegmentIndexForLabel(labelmap3D, source[i]);
if (frame[i] !== 0 && frame[i] !== value && !overlapWarned) {
cs3dLogger.warn(
'generateSegmentation: overlapping labelmap segments detected on the ' +
Expand Down Expand Up @@ -417,6 +423,9 @@ function fillLabelmapSegmentation(
*
* @param images - An array of the cornerstone image objects, which includes imageId and metadata
* @param labelmaps - An array of the 3D Volumes that contain the segmentation data.
* Each labelmap3D can have `labelToSegmentIndex`, the map from a voxel value
* to the segment index, for a layer that stores a segment under another label
* value. The export applies the map and does not copy the voxels.
* @param options.sopClassUID - Output SEG SOP Class UID. Defaults to BINARY
* Segmentation (`1.2.840.10008.5.1.4.1.1.66.4`) for broad PACS/viewer
* compatibility. Pass the Label Map Segmentation UID
Expand Down
Loading
Loading