I would like to know how to implement a functionality where the MPR viewer updates its displayed slides as the user interacts with a slider. Specifically, I am using the vtlResliceCursorWidget and seeking guidance on how to synchronize the slider with the MPR viewer, ensuring that the selected slide changes correspondingly as the slider is moved. Any insights or code examples regarding this integration would be greatly appreciated. Thank you in advance for your assistance!
@finetjul @Sebastien_Jourdain @Forrest
The CPR example uses a reslice cursor widget and a slider to control the angle of the reslicing.
The angle can be changed using the green line too.
Here is the live demo.
The source code is in Sources\Rendering\Core\ImageCPRMapper\example\index.js
.
Thank you so much for your response. I am using the example provided at vtk.js as a reference.
My goal is to display axial, coronal, and sagittal views. I would like to be able to change the slice using the mouse scroll for all MPR scenes and also use the UI scroll bar available on all MPR scenes for this purpose. Additionally, I want to implement a crosshair functionality. Moreover, if I adjust the strength and level on one view, it should affect the other views as well.
I have already managed to change the slice using the mouse scroll, but I am unsure of how to achieve the same functionality using the UI scroll bar.
Could you please help me with implementing the desired features?
@cory.quammen @pieper @will.schroeder do any of you have a solution for this issue?
I suggest you use cornerstone3D rather than reimplementing this from scratch.
Thank you for recommending cornerstone3D, which I did consider. However, I have specific requirements that I need to meet, and I believe it would be more beneficial for me to stick with a single framework. After reviewing the cornerstone3D documentation, I noticed a lack of available resources, which concerns me. If I were to encounter any issues while combining vtk.js with cornerstone3D, it might be challenging to find solutions due to the limited community support. As a beginner, I find it difficult to understand how to proceed in this situation. Despite this, I truly appreciate your input! @pieper
Hi Raja, to scroll within the vtkRelisceCursorWidget, all you have to do is modify the “center” of the widget. Your slider would modify one coordinate of the center (the one of the specific axis).
e.g. widget.setCenter(center[0], center[1] + step, center[2])
if you scroll along the Y axis
@finetjul, I truly appreciate your response. Could you kindly provide additional context for better understanding?
resliceCursorWidget = vtlResliceCursorWidget.newInstance(); // see ResliceCursorWidget example for more context
...
bounds = widget.getWidgetState().getImage().getBounds();
const ySlider= document.getElementById('ySlider');
ySlider.min = bounds[2];
ySlider.max =bounds[3];
ySlider.addEventListener('change', (ev) => {
center = resliceCursorWidget.getWidgetState().getCenter();
resliceCursorWidget.setCenter(center[0], ev.target.value, center[1]);
}
const bounds = resliceCursorWidget.getWidgetState().getImage().getBounds();
const ySlider = document.getElementById('slicingScale');
ySlider.min = bounds[2];
ySlider.max = bounds[3];
ySlider.addEventListener('input', (ev) => {
let center = resliceCursorWidget.getWidgetState().getCenter();
resliceCursorWidget.setCenter([center[0], ev.target.value, center[2]]);
});
Based on your suggestion, I have implemented the code you provided. However, while adjusting the slider, the crosshair’s position changes horizontally in the sagittal view. The image slice remains unchanged.
@finetjul
Interaction events are not triggered when changing the center programmatically.
You might have to call updateViews()
(from the example) manually…
I want to change the slices, but here crosshair is moving
please share your code…
import '@kitware/vtk.js/favicon';
import '@kitware/vtk.js/Rendering/Profiles/All';
import vtkGenericRenderWindow from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow';
import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader';
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
import vtkInteractorStyleImage from '@kitware/vtk.js/Interaction/Style/InteractorStyleImage';
import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper';
import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice';
import ImageConstants from '@kitware/vtk.js/Rendering/Core/ImageMapper/Constants';
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager';
import { xyzToViewType } from '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget/Constants';
import vtkImageReslice from '@kitware/vtk.js/Imaging/Core/ImageReslice';
import { SlabMode } from '@kitware/vtk.js/Imaging/Core/ImageReslice/Constants';
import vtkResliceCursorWidget from '@kitware/vtk.js/Widgets/Widgets3D/ResliceCursorWidget';
import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource';
import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
import { CaptureOn } from '@kitware/vtk.js/Widgets/Core/WidgetManager/Constants';
import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader';
import httpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';
const viewEleX = document.querySelector('#xView');
const viewEleY = document.querySelector('#yView');
const viewEleZ = document.querySelector('#zView');
const mprContainer = [viewEleX, viewEleY, viewEleZ];
const viewAttributes = [];
const resliceCursorWidget = vtkResliceCursorWidget.newInstance();
const widgetState = resliceCursorWidget.getWidgetState();
widgetState
.getStatesWithLabel('rotation')
.forEach((handle) => {
handle.setVisible(false);
});
for (let i = 0; i < 3; i++) {
const obj = {};
const genericRenderWindow = vtkGenericRenderWindow.newInstance();
genericRenderWindow.setContainer(mprContainer[i]);
genericRenderWindow.getOpenGLRenderWindow().setSize(mprContainer[i].clientWidth, mprContainer[i].clientHeight);
genericRenderWindow.resize();
obj.genericRenderWindow = genericRenderWindow;
const renderer = genericRenderWindow.getRenderer();
renderer.getActiveCamera().setParallelProjection(true);
obj.renderer = renderer;
const renderWindow = genericRenderWindow.getRenderWindow();
const interactor = vtkInteractorStyleImage.newInstance();
renderWindow.getInteractor().setInteractorStyle(interactor);
obj.renderWindow = renderWindow;
const widgetManager = vtkWidgetManager.newInstance();
widgetManager.setRenderer(renderer);
widgetManager.enablePicking();
widgetManager.setCaptureOn(CaptureOn.MOUSE_MOVE);
obj.widgetManager = widgetManager;
const widgetInstance = widgetManager.addWidget(resliceCursorWidget, xyzToViewType[i]);
widgetInstance.setScaleInPixels(false);
widgetInstance.setKeepOrthogonality(true);
obj.widgetInstance = widgetInstance;
const reslice = vtkImageReslice.newInstance();
reslice.setSlabNumberOfSlices(1);
reslice.setTransformInputSampling(false);
reslice.setAutoCropOutput(true);
reslice.setOutputDimensionality(2);
obj.reslice = reslice;
const resliceMapper = vtkImageMapper.newInstance();
resliceMapper.setInputConnection(reslice.getOutputPort());
obj.resliceMapper = resliceMapper;
const resliceActor = vtkImageSlice.newInstance();
resliceActor.setMapper(resliceMapper);
obj.resliceActor = resliceActor;
viewAttributes.push(obj);
}
function updateReslice(
interactionContext = {
viewType: '',
reslice: null,
actor: null,
renderer: null,
resetFocalPoint: false,
keepFocalPointPosition: false,
computeFocalPointOffset: false,
spheres: null,
}
) {
const modified = resliceCursorWidget.updateReslicePlane(
interactionContext.reslice,
interactionContext.viewType
);
if (modified) {
const resliceAxes = interactionContext.reslice.getResliceAxes();
interactionContext.actor.setUserMatrix(resliceAxes);
}
resliceCursorWidget.updateCameraPoints(
interactionContext.renderer,
interactionContext.viewType,
interactionContext.resetFocalPoint,
interactionContext.keepFocalPointPosition,
interactionContext.computeFocalPointOffset
);
return modified;
}
function setupResliceViews(vtkImage) {
resliceCursorWidget.setImage(vtkImage);
const bounds = resliceCursorWidget.getWidgetState().getImage().getBounds();
const ySlider = document.getElementById('slicingScale');
ySlider.min = bounds[2];
ySlider.max = bounds[3];
ySlider.addEventListener('input', (ev) => {
let center = resliceCursorWidget.getWidgetState().getCenter();
resliceCursorWidget.setCenter([center[0], ev.target.value, center[2]]);
updateReslice({
viewType: xyzToViewType[0],
reslice: viewAttributes[0].reslice,
actor: viewAttributes[0].resliceActor,
renderer: viewAttributes[0].renderer,
resetFocalPoint: true,
keepFocalPointPosition: false,
computeFocalPointOffset: true,
});
viewAttributes[0].renderWindow.render();
});
for (let i = 0; i < 3; i++) {
const obj = viewAttributes[i];
obj.reslice.setInputData(vtkImage);
obj.renderer.addActor(obj.resliceActor);
const viewType = xyzToViewType[i];
obj.widgetInstance.onInteractionEvent(
({ computeFocalPointOffset, canUpdateFocalPoint }) => {
const activeViewType = resliceCursorWidget.getWidgetState().getActiveViewType();
const keepFocalPointPosition = activeViewType !== viewType && canUpdateFocalPoint;
updateReslice({
viewType: viewType,
reslice: obj.reslice,
actor: obj.resliceActor,
renderer: obj.renderer,
resetFocalPoint: false,
keepFocalPointPosition,
computeFocalPointOffset,
});
});
updateReslice({
viewType: viewType,
reslice: obj.reslice,
actor: obj.resliceActor,
renderer: obj.renderer,
resetFocalPoint: true,
keepFocalPointPosition: false,
computeFocalPointOffset: true,
});
obj.renderWindow.getInteractor().render();
}
}
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true });
reader
.setUrl('https://kitware.github.io/vtk-js/data/volume/LIDC2.vti')
.then(() => reader.loadData())
.then(() => {
const image = reader.getOutputData();
setupResliceViews(image);
});
In ySlider.addEventListener(‘input’, (ev) => {
I have already added it inside setupResliceViews
function in my code.
My email got truncated, let me re-post it here properly:
In ySlider.addEventListener('input', (ev) => {
, you should call updateView()
for the Y (i.e.1) view and not the X (i.e. 0) view.
It’s working. Thank you so much. However, I’m encountering another issue. Let me address it.
Here is the revised code for changing the image slice using the slider.
const bounds = resliceCursorWidget.getWidgetState().getImage().getBounds();
const ySlider = document.getElementById('slicingScale');
ySlider.min = bounds[2];
ySlider.max = bounds[3];
ySlider.addEventListener('input', (ev) => {
let center = resliceCursorWidget.getWidgetState().getCenter();
resliceCursorWidget.setCenter([center[0], ev.target.value, center[2]]);
updateReslice({
viewType: xyzToViewType[1],
reslice: viewAttributes[1].reslice,
actor: viewAttributes[1].resliceActor,
renderer: viewAttributes[1].renderer,
resetFocalPoint: true,
keepFocalPointPosition: false,
computeFocalPointOffset: true,
});
viewAttributes[1].renderWindow.render();
});
Hi @finetjul, while using the above code, when I change the image slice using the slider and then scroll, the images in all three viewports disappear.
I am printing the value of getCenter when scrolling, and I’ve noticed something unusual in the displayed output. Please refer to the image. @finetjul