Skip to content

Commit 2811ec1

Browse files
committed
chore(segmentation): split data-modified render perf changes into #2797
1 parent b9d6365 commit 2811ec1

3 files changed

Lines changed: 13 additions & 172 deletions

File tree

packages/tools/src/eventListeners/segmentation/segmentationDataModifiedEventListener.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { SegmentationDataModifiedEventType } from '../../types/EventTypes';
2-
import { triggerSegmentationRenderForModified } from '../../stateManagement/segmentation/SegmentationRenderingEngine';
2+
import { triggerSegmentationRenderBySegmentationId } from '../../stateManagement/segmentation/SegmentationRenderingEngine';
33
import onLabelmapSegmentationDataModified from './labelmap/onLabelmapSegmentationDataModified';
44
import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation';
55

@@ -16,10 +16,7 @@ const onSegmentationDataModified = function (
1616
onLabelmapSegmentationDataModified(evt);
1717
}
1818

19-
// Data modifications stream in at brush-stroke frequency; this trigger
20-
// renders cheap viewports live and defers projection-heavy ones (labelmap
21-
// over MIP) until the stream goes quiet.
22-
triggerSegmentationRenderForModified(segmentationId);
19+
triggerSegmentationRenderBySegmentationId(segmentationId);
2320
};
2421

2522
export default onSegmentationDataModified;

packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -299,108 +299,9 @@ function triggerSegmentationRenderBySegmentationId(
299299
segmentationRenderingEngine.renderSegmentation(segmentationId);
300300
}
301301

302-
// Trailing delay before re-rendering a projection-heavy viewport after a
303-
// segmentation-data modification (brush edits fire many events per second).
304-
const DEFERRED_SEGMENTATION_RENDER_DELAY_MS = 240;
305-
const deferredSegmentationRenderTimers = new Map<
306-
string,
307-
ReturnType<typeof setTimeout>
308-
>();
309-
310-
/**
311-
* Whether re-rendering this viewport's segmentations means re-marching a slab
312-
* per fragment (labelmap over MIP style projections), which is too expensive
313-
* to repeat for every segmentation-data-modified event.
314-
*/
315-
function isProjectionHeavySegmentationViewport(
316-
viewport: Types.IViewport
317-
): boolean {
318-
// Legacy volume viewports render labelmap-over-MIP by switching the whole
319-
// viewport to the edge-projection blend mode.
320-
const blendMode = (
321-
viewport as Partial<{ getBlendMode: () => Enums.BlendModes }>
322-
).getBlendMode?.();
323-
324-
if (blendMode === Enums.BlendModes.LABELMAP_EDGE_PROJECTION_BLEND) {
325-
return true;
326-
}
327-
328-
// Generic planar viewports are projection-heavy when any of their display
329-
// sets renders across a slab (e.g. a MIP source with a projected labelmap
330-
// overlay, which inherits the source's slab thickness).
331-
const planarViewport = viewport as Partial<{
332-
getActors: () => Types.ActorEntry[];
333-
getSourceDataId: () => string | undefined;
334-
getDisplaySetPresentation: (
335-
dataId: string
336-
) => { slabThickness?: number } | undefined;
337-
}>;
338-
339-
if (typeof planarViewport.getDisplaySetPresentation !== 'function') {
340-
return false;
341-
}
342-
343-
const dataIds: string[] = [];
344-
const sourceDataId = planarViewport.getSourceDataId?.();
345-
346-
if (sourceDataId) {
347-
dataIds.push(sourceDataId);
348-
}
349-
350-
planarViewport.getActors?.().forEach((actorEntry) => {
351-
if (actorEntry.representationUID) {
352-
dataIds.push(String(actorEntry.representationUID));
353-
}
354-
});
355-
356-
return dataIds.some(
357-
(dataId) =>
358-
(planarViewport.getDisplaySetPresentation(dataId)?.slabThickness ?? 0) > 0
359-
);
360-
}
361-
362-
/**
363-
* Segmentation render trigger for high-frequency data modifications (brush
364-
* strokes and the like): viewports that are cheap to repaint render live,
365-
* while projection-heavy viewports (labelmap over MIP) are re-rendered once
366-
* the modification stream goes quiet.
367-
*/
368-
function triggerSegmentationRenderForModified(segmentationId?: string): void {
369-
const viewportIds =
370-
segmentationRenderingEngine._getViewportIdsForSegmentation(segmentationId);
371-
372-
viewportIds.forEach((viewportId) => {
373-
const { viewport } = getEnabledElementByViewportId(viewportId) || {};
374-
375-
if (!viewport) {
376-
return;
377-
}
378-
379-
if (!isProjectionHeavySegmentationViewport(viewport)) {
380-
segmentationRenderingEngine.renderSegmentationsForViewport(viewportId);
381-
return;
382-
}
383-
384-
const existingTimer = deferredSegmentationRenderTimers.get(viewportId);
385-
386-
if (existingTimer !== undefined) {
387-
clearTimeout(existingTimer);
388-
}
389-
390-
deferredSegmentationRenderTimers.set(
391-
viewportId,
392-
setTimeout(() => {
393-
deferredSegmentationRenderTimers.delete(viewportId);
394-
segmentationRenderingEngine.renderSegmentationsForViewport(viewportId);
395-
}, DEFERRED_SEGMENTATION_RENDER_DELAY_MS)
396-
);
397-
});
398-
}
399-
400302
const segmentationRenderingEngine = new SegmentationRenderingEngine();
401303
export {
402304
triggerSegmentationRender,
403305
triggerSegmentationRenderBySegmentationId,
404-
triggerSegmentationRenderForModified,
405306
segmentationRenderingEngine,
406307
};

packages/tools/src/tools/displayTools/Labelmap/addVolumesAsIndependentComponents.ts

Lines changed: 11 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
convertMapperToNotSharedMapper,
55
volumeLoader,
66
eventTarget,
7+
createVolumeActor,
78
type Types,
89
} from '@cornerstonejs/core';
910
import { Events, SegmentationRepresentations } from '../../../enums';
@@ -112,14 +113,6 @@ export async function addVolumesAsIndependentComponents({
112113

113114
viewport.removeActors([uid]);
114115
const oldMapper = actor.getMapper();
115-
// convertMapperToNotSharedMapper reuses the old mapper's vtkImageData and
116-
// replaces its point-data scalars with a CPU array, which this function then
117-
// rewrites as a 2-component (base + seg) array. The shared streaming mapper
118-
// renders from the GPU texture and misconfigures its shader if that injected
119-
// array is still active, so capture the original scalars (usually none) to
120-
// undo the mutation on restore.
121-
const sharedImageData = (oldMapper as vtkVolumeMapper).getInputData();
122-
const originalScalars = sharedImageData.getPointData().getScalars() ?? null;
123116
const mapper = convertMapperToNotSharedMapper(oldMapper as vtkVolumeMapper);
124117
actor.setMapper(mapper);
125118

@@ -144,29 +137,9 @@ export async function addVolumesAsIndependentComponents({
144137
.getIndependentComponents();
145138
actor.getProperty().setIndependentComponents(true);
146139

147-
// While the segmentation is mounted, setLabelmapColorAndOpacity treats this
148-
// combined actor as the labelmap actor and enables the label-outline shader
149-
// path on its property. Capture the pre-mount outline state so the restore
150-
// can undo it - otherwise the plain base volume keeps rendering through the
151-
// label-outline shader after the representation is removed.
152-
const oldUseLabelOutline = actor.getProperty().getUseLabelOutline();
153-
// @ts-ignore - fix type in vtk
154-
const oldLabelOutlineOpacity = actor.getProperty().getLabelOutlineOpacity();
155-
const oldLabelOutlineThickness = actor
156-
.getProperty()
157-
.getLabelOutlineThickness();
158-
159-
// Reuse the representationUID computed by the render plan (which includes
160-
// the labelmapId suffix). The plan's reconcile step compares actor UIDs
161-
// against that format; a mismatch makes it tear down this combined actor —
162-
// which is also the viewport's base volume actor — on the next render.
163-
const representationUID =
164-
(volumeInputArray[0].representationUID as string) ??
165-
`${segmentationId}-${SegmentationRepresentations.Labelmap}`;
166-
167140
viewport.addActor({
168141
...defaultActor,
169-
representationUID,
142+
representationUID: `${segmentationId}-${SegmentationRepresentations.Labelmap}`,
170143
});
171144

172145
internalCache.set(uid, {
@@ -182,7 +155,7 @@ export async function addVolumesAsIndependentComponents({
182155

183156
function onSegmentationDataModified(evt) {
184157
// update the second component of the array with the new segmentation data
185-
const { segmentationId, modifiedSlicesToUse } = evt.detail;
158+
const { segmentationId } = evt.detail;
186159
const { representationData } = getSegmentation(segmentationId);
187160
const { volumeId: segVolumeId } =
188161
representationData.Labelmap as LabelmapSegmentationDataVolume;
@@ -196,37 +169,24 @@ export async function addVolumesAsIndependentComponents({
196169

197170
const imageData = mapper.getInputData();
198171
const array = imageData.getPointData().getArray(0);
199-
const combinedData = array.getData();
172+
const baseData = array.getData();
200173
const newComp = 2;
201174
const dims = segImageData.getDimensions();
202-
const sliceSize = dims[0] * dims[1];
203175

204-
// Brush strokes report which slices they touched; merging only those
205-
// keeps the CPU cost proportional to the edit instead of the volume.
206-
const slices: number[] = modifiedSlicesToUse?.length
207-
? modifiedSlicesToUse
208-
: Array.from({ length: dims[2] }, (_, i) => i);
176+
const slices = Array.from({ length: dims[2] }, (_, i) => i);
209177

210178
for (const z of slices) {
211-
const sliceStart = z * sliceSize;
212-
const sliceImage = cache.getImage(segmentationVolume.imageIds?.[z]);
213-
const sliceData = sliceImage?.voxelManager?.getScalarData?.();
214-
215-
if (sliceData?.length === sliceSize) {
216-
for (let i = 0; i < sliceSize; ++i) {
217-
combinedData[(sliceStart + i) * newComp + 1] = sliceData[i];
218-
}
219-
} else {
220-
for (let i = 0; i < sliceSize; ++i) {
221-
const iTuple = sliceStart + i;
222-
combinedData[iTuple * newComp + 1] = segVoxelManager.getAtIndex(
179+
for (let y = 0; y < dims[1]; ++y) {
180+
for (let x = 0; x < dims[0]; ++x) {
181+
const iTuple = x + dims[0] * (y + dims[1] * z);
182+
baseData[iTuple * newComp + 1] = segVoxelManager.getAtIndex(
223183
iTuple
224184
) as number;
225185
}
226186
}
227187
}
228188

229-
array.setData(combinedData);
189+
array.setData(baseData);
230190

231191
imageData.modified();
232192
viewport.render();
@@ -243,10 +203,7 @@ export async function addVolumesAsIndependentComponents({
243203
return;
244204
}
245205

246-
// The data-modified handler was registered debounced; the plain
247-
// removeEventListener would leave the debounce wrapper attached forever,
248-
// still merging into this detached imageData on every edit.
249-
eventTarget.removeEventListenerDebounced(
206+
eventTarget.removeEventListener(
250207
Events.SEGMENTATION_DATA_MODIFIED,
251208
onSegmentationDataModified
252209
);
@@ -269,25 +226,11 @@ export async function addVolumesAsIndependentComponents({
269226

270227
// Restore the original actor and add it back to the viewport.
271228
actor.setMapper(oldMapper);
272-
273-
const pointData = sharedImageData.getPointData();
274-
275-
if (originalScalars) {
276-
pointData.setScalars(originalScalars);
277-
} else {
278-
pointData.removeArray('Pixels');
279-
}
280-
sharedImageData.modified();
281-
282229
actor.getProperty().setColorMixPreset(oldColorMixPreset);
283230
actor
284231
.getProperty()
285232
.setForceNearestInterpolation(1, oldForceNearestInterpolation);
286233
actor.getProperty().setIndependentComponents(oldIndependentComponents);
287-
actor.getProperty().setUseLabelOutline(oldUseLabelOutline);
288-
// @ts-ignore - fix type in vtk
289-
actor.getProperty().setLabelOutlineOpacity(oldLabelOutlineOpacity);
290-
actor.getProperty().setLabelOutlineThickness(oldLabelOutlineThickness);
291234

292235
viewport.addActor({
293236
...defaultActor,

0 commit comments

Comments
 (0)