# ImageSlice and cropping planes widget

**URL:** https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918
**Category:** Web
**Created:** [July 10, 2022, 7:55pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918 "2022-07-10T19:55:41Z")
**Posts on this page:** 10
**Page:** 1

<div class="post-metadata">

### Author: ![PaulHax](https://discourse.vtk.org/user_avatar/discourse.vtk.org/paulhax/32/2748_2.png) [@PaulHax](https://discourse.vtk.org/u/PaulHax)
#### Post date: [July 10, 2022, 7:55pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/1 "2022-07-10T19:55:41Z")

</div>

I’ed like to use the ImageCroppingWidget to clip/crop ImageSlices/ImageMappers. I modified the ImageCroppingWidget example to includes ImageSlices:

[https://codesandbox.io/s/volume-rendering-bounds-forked-e8433n?file=/src/index.js](https://codesandbox.io/s/volume-rendering-bounds-forked-e8433n?file=/src/index.js)

If I transform the slice plane origins with `imageData.worldToIndex` (rather than `imageData.indexToWorld` as with the volume), thing look good…

But if I give a non-zero origin such as  
`image.setOrigin([100, 0, 0]);`

and move the widget handles, the ImageSlices don’t clip right. What am I missing?

---

<div class="post-metadata">

### Author: ![Forrest](https://discourse.vtk.org/user_avatar/discourse.vtk.org/forrest/32/300_2.png) [@Forrest](https://discourse.vtk.org/u/Forrest)
#### Post date: [July 11, 2022, 8:35pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/2 "2022-07-11T20:35:06Z")

</div>

This looks like an issue with the ImageMapper’s clipping planes and a non-zeroed image origin. I did a quick test with a clipping plane (normal: [1, 0, 0], origin: [100, 0, 0]) on the ImageMapper example with an image origin of [1, 0, 0], and nothing is clipping. FYI @thewtex

---

<div class="post-metadata">

### Author: ![PaulHax](https://discourse.vtk.org/user_avatar/discourse.vtk.org/paulhax/32/2748_2.png) [@PaulHax](https://discourse.vtk.org/u/PaulHax)
#### Post date: [July 12, 2022, 9:53pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/3 "2022-07-12T21:53:54Z")

</div>

This is the fix, methinks:

> <https://github.com/Kitware/vtk-js/pull/2512>
>
> \### Context
> ImageMapper cropping planes are not correctly positioned. This cod…esandbox illustrates the issue:
> https://codesandbox.io/s/volume-rendering-bounds-forked-e8433n?file=/src/index.js
> 
> \### Results
> \<!--
> Describe or illustrate the effects of your contribution. Please include:
> \- comparisons of the behavior before vs after
> \- screenshots of new or changed visualizations if applicable
> \--\>
> 
> \### Changes
> Need to transform cropping plane normals by \`transpose(inverse(worldToIndex))\` rather than just \`worldToIndex\`
> 
> \### PR and Code Checklist
> \<!--
> NOTE: We will not merge if the following steps have not been completed!
> \--\>
> \- \[x\] \[semantic-release\](https://github.com/semantic-release/semantic-release) commit messages
> \- \[x\] Run \`npm run reformat\` to have correctly formatted code
> 
> \### Testing
> Can test with this modified ImageCroppingWidget example that has ImageSlices
> \`\`\`
> import { vec3, quat, mat4 } from 'gl-matrix';
> 
> import 'vtk.js/Sources/favicon';
> 
> // Load the rendering pieces we want to use (for both WebGL and WebGPU)
> import 'vtk.js/Sources/Rendering/Profiles/Geometry';
> import 'vtk.js/Sources/Rendering/Profiles/Volume';
> import 'vtk.js/Sources/Rendering/Profiles/Glyph';
> 
> import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow';
> import vtkWidgetManager from 'vtk.js/Sources/Widgets/Core/WidgetManager';
> import vtkHttpDataSetReader from 'vtk.js/Sources/IO/Core/HttpDataSetReader';
> import vtkImageCroppingWidget from 'vtk.js/Sources/Widgets/Widgets3D/ImageCroppingWidget';
> import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction';
> import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction';
> import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
> import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
> import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane';
> import vtkImageMapper from 'vtk.js/Sources/Rendering/Core/ImageMapper';
> import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice';
> 
> // Force the loading of HttpDataAccessHelper to support gzip decompression
> import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper';
> 
> import controlPanel from './controlPanel.html';
> 
> // ----------------------------------------------------------------------------
> // Standard rendering code setup
> // ----------------------------------------------------------------------------
> 
> const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
> background: \[1, 1, 1\],
> });
> const renderer = fullScreenRenderer.getRenderer();
> const renderWindow = fullScreenRenderer.getRenderWindow();
> const apiRenderWindow = fullScreenRenderer.getApiSpecificRenderWindow();
> 
> global.renderer = renderer;
> global.renderWindow = renderWindow;
> 
> // ----------------------------------------------------------------------------
> // 2D overlay rendering
> // ----------------------------------------------------------------------------
> 
> const overlaySize = 15;
> const overlayBorder = 2;
> const overlay = document.createElement('div');
> overlay.style.position = 'absolute';
> overlay.style.width = \`${overlaySize}px\`;
> overlay.style.height = \`${overlaySize}px\`;
> overlay.style.border = \`solid ${overlayBorder}px red\`;
> overlay.style.borderRadius = '50%';
> overlay.style.left = '-100px';
> overlay.style.pointerEvents = 'none';
> document.querySelector('body').appendChild(overlay);
> 
> // ----------------------------------------------------------------------------
> // Widget manager
> // ----------------------------------------------------------------------------
> 
> const widgetManager = vtkWidgetManager.newInstance();
> widgetManager.setRenderer(renderer);
> 
> const widget = vtkImageCroppingWidget.newInstance();
> 
> function widgetRegistration(e) {
> const action = e ? e.currentTarget.dataset.action : 'addWidget';
> const viewWidget = widgetManager\[action\](widget);
> if (viewWidget) {
> viewWidget.setDisplayCallback((coords) =\> {
> overlay.style.left = '-100px';
> if (coords) {
> const \[w, h\] = apiRenderWindow.getSize();
> overlay.style.left = \`${Math.round(
> (coords\[0\]\[0\] / w) \* window.innerWidth -
> overlaySize \* 0.5 -
> overlayBorder
> )}px\`;
> overlay.style.top = \`${Math.round(
> ((h - coords\[0\]\[1\]) / h) \* window.innerHeight -
> overlaySize \* 0.5 -
> overlayBorder
> )}px\`;
> }
> });
> 
> renderer.resetCamera();
> renderer.resetCameraClippingRange();
> }
> widgetManager.enablePicking();
> renderWindow.render();
> }
> 
> // Initial widget register
> widgetRegistration();
> 
> // ----------------------------------------------------------------------------
> // Volume rendering
> // ----------------------------------------------------------------------------
> 
> const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true });
> 
> const actor = vtkVolume.newInstance();
> const volumeMapper = vtkVolumeMapper.newInstance();
> volumeMapper.setSampleDistance(1.1);
> actor.setMapper(volumeMapper);
> 
> // create color and opacity transfer functions
> const ctfun = vtkColorTransferFunction.newInstance();
> ctfun.addRGBPoint(0, 85 / 255.0, 0, 0);
> ctfun.addRGBPoint(95, 1.0, 1.0, 1.0);
> ctfun.addRGBPoint(225, 0.66, 0.66, 0.5);
> ctfun.addRGBPoint(255, 0.3, 1.0, 0.5);
> const ofun = vtkPiecewiseFunction.newInstance();
> ofun.addPoint(0.0, 0.0);
> ofun.addPoint(255.0, 1.0);
> actor.getProperty().setRGBTransferFunction(0, ctfun);
> actor.getProperty().setScalarOpacity(0, ofun);
> actor.getProperty().setScalarOpacityUnitDistance(0, 3.0);
> actor.getProperty().setInterpolationTypeToLinear();
> actor.getProperty().setUseGradientOpacity(0, true);
> actor.getProperty().setGradientOpacityMinimumValue(0, 2);
> actor.getProperty().setGradientOpacityMinimumOpacity(0, 0.0);
> actor.getProperty().setGradientOpacityMaximumValue(0, 20);
> actor.getProperty().setGradientOpacityMaximumOpacity(0, 1.0);
> actor.getProperty().setShade(true);
> actor.getProperty().setAmbient(0.2);
> actor.getProperty().setDiffuse(0.7);
> actor.getProperty().setSpecular(0.3);
> actor.getProperty().setSpecularPower(8.0);
> 
> volumeMapper.setInputConnection(reader.getOutputPort());
> 
> // -----------------------------------------------------------
> // Get data
> // -----------------------------------------------------------
> 
> function getCroppingPlanes(imageData, ijkPlanes) {
> const rotation = quat.create();
> mat4.getRotation(rotation, imageData.getIndexToWorld());
> 
> const rotateVec = (vec) =\> {
> const out = \[0, 0, 0\];
> vec3.transformQuat(out, vec, rotation);
> return out;
> };
> 
> const \[iMin, iMax, jMin, jMax, kMin, kMax\] = ijkPlanes;
> const origin = imageData.indexToWorld(\[iMin, jMin, kMin\]);
> // opposite corner from origin
> const corner = imageData.indexToWorld(\[iMax, jMax, kMax\]);
> return \[
> // X min/max
> vtkPlane.newInstance({ normal: rotateVec(\[1, 0, 0\]), origin }),
> vtkPlane.newInstance({ normal: rotateVec(\[-1, 0, 0\]), origin: corner }),
> // Y min/max
> vtkPlane.newInstance({ normal: rotateVec(\[0, 1, 0\]), origin }),
> vtkPlane.newInstance({ normal: rotateVec(\[0, -1, 0\]), origin: corner }),
> // X min/max
> vtkPlane.newInstance({ normal: rotateVec(\[0, 0, 1\]), origin }),
> vtkPlane.newInstance({ normal: rotateVec(\[0, 0, -1\]), origin: corner }),
> \];
> }
> 
> reader.setUrl(\`${\_\_BASE\_PATH\_\_}/data/volume/headsq.vti\`).then(() =\> {
> reader.loadData().then(() =\> {
> const image = reader.getOutputData();
> image.setOrigin(\[100, 30, 0\]);
> // image.setSpacing(\[1, 1, 1\]);
> // image.setDirection(\[1, 0, 0, 0, 1, 0, 0, 0, 1\]);
> 
> const imageActorI = vtkImageSlice.newInstance();
> const imageActorJ = vtkImageSlice.newInstance();
> const imageActorK = vtkImageSlice.newInstance();
> 
> renderer.addActor(imageActorK);
> renderer.addActor(imageActorJ);
> renderer.addActor(imageActorI);
> 
> const imageMapperK = vtkImageMapper.newInstance();
> imageMapperK.setInputData(image);
> imageMapperK.setKSlice(10);
> imageActorK.setMapper(imageMapperK);
> 
> const imageMapperJ = vtkImageMapper.newInstance();
> imageMapperJ.setInputData(image);
> imageMapperJ.setJSlice(10);
> imageActorJ.setMapper(imageMapperJ);
> 
> const imageMapperI = vtkImageMapper.newInstance();
> imageMapperI.setInputData(image);
> imageMapperI.setISlice(10);
> imageActorI.setMapper(imageMapperI);
> 
> // Need to add clipping plane at start or else clip planes not included in ImageMapper shader
> const p = vtkPlane.newInstance({
> normal: \[1, 0, 0\],
> origin: \[50, 0, 0\],
> });
> imageMapperK.addClippingPlane(p);
> imageMapperJ.addClippingPlane(p);
> imageMapperI.addClippingPlane(p);
> 
> // update crop widget
> widget.copyImageDataDescription(image);
> const cropState = widget.getWidgetState().getCroppingPlanes();
> cropState.onModified(() =\> {
> const cropPlanes = getCroppingPlanes(image, cropState.getPlanes());
> 
> \[volumeMapper, imageMapperI, imageMapperJ, imageMapperK\].forEach(
> (mapper) =\> {
> mapper.removeAllClippingPlanes();
> cropPlanes.forEach((plane) =\> {
> mapper.addClippingPlane(plane);
> });
> mapper.modified();
> }
> );
> 
> renderWindow.render();
> });
> 
> // add volume to renderer
> renderer.addVolume(actor);
> renderer.resetCamera();
> renderer.resetCameraClippingRange();
> renderWindow.render();
> });
> });
> 
> // -----------------------------------------------------------
> // UI control handling
> // -----------------------------------------------------------
> 
> fullScreenRenderer.addController(controlPanel);
> 
> function updateFlag(e) {
> const value = !!e.target.checked;
> const name = e.currentTarget.dataset.name;
> widget.set({ \[name\]: value }); // can be called on either viewWidget or parentWidget
> 
> widgetManager.enablePicking();
> renderWindow.render();
> }
> 
> const elems = document.querySelectorAll('.flag');
> for (let i = 0; i \< elems.length; i++) {
> elems\[i\].addEventListener('change', updateFlag);
> }
> 
> const buttons = document.querySelectorAll('button');
> for (let i = 0; i \< buttons.length; i++) {
> buttons\[i\].addEventListener('click', widgetRegistration);
> }
> \`\`\`
> \- \[\] This change adds or fixes unit tests 
> \- \[x\] Tested environment:
> - \*\*vtk.js\*\*: master
> - \*\*OS\*\*: Ubuntu
> - \*\*Browser\*\*: Chrome 110
> 
> \<!--
> Edit and uncomment the section below if relevant
> 
> \### Funding
> This contribution is funded by \[Example\](https://example.com).
> 
> --\>

---

<div class="post-metadata">

### Author: ![yuxi-ovo](https://discourse.vtk.org/user_avatar/discourse.vtk.org/yuxi-ovo/32/10150_2.png) [@yuxi-ovo](https://discourse.vtk.org/u/yuxi-ovo)
#### Post date: [July 28, 2025, 5:47pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/4 "2025-07-28T17:47:23Z")

</div>

Hello, can you package and send me a complete implementation of this feature? I’m working on it but encountered some issues Thank you

---

<div class="post-metadata">

### Author: ![finetjul](https://discourse.vtk.org/user_avatar/discourse.vtk.org/finetjul/32/154_2.png) [@finetjul](https://discourse.vtk.org/u/finetjul)
#### Post date: [July 28, 2025, 6:08pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/5 "2025-07-28T18:08:12Z")

</div>

The fix has been merged into VTK.js master. Feel free to download/use the latest version.

---

<div class="post-metadata">

### Author: ![yuxi-ovo](https://discourse.vtk.org/user_avatar/discourse.vtk.org/yuxi-ovo/32/10150_2.png) [@yuxi-ovo](https://discourse.vtk.org/u/yuxi-ovo)
#### Post date: [July 28, 2025, 6:22pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/6 "2025-07-28T18:22:28Z")

</div>

I looked at his example. What I want is different from his. I want to display an image combined with 3 slices, and then I can drag the control points to adjust the slice of the corresponding slice. It should look like this

 ![MultiSliceImageMapper Control Points](https://discourse.vtk.org/uploads/default/original/2X/d/de19b560b0396fb22591f5428d2ecc5c9492574f.jpeg)  
I am just missing this one feature now, please help me

---

<div class="post-metadata">

### Author: ![yuxi-ovo](https://discourse.vtk.org/user_avatar/discourse.vtk.org/yuxi-ovo/32/10150_2.png) [@yuxi-ovo](https://discourse.vtk.org/u/yuxi-ovo)
#### Post date: [July 28, 2025, 7:15pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/7 "2025-07-28T19:15:40Z")

</div>

I implemented a basic version, but there are still some issues [https://codesandbox.io/p/sandbox/volume-rendering-bounds-forked-llhrpt](https://codesandbox.io/p/sandbox/volume-rendering-bounds-forked-llhrpt)

1. The control point dragging has issues, not accurate enough
2. The control points of each slice should be mutually independent, not affecting each other when dragging
3. The control points of each slice can only move in the direction of this slice  
Can you help me?

---

<div class="post-metadata">

### Author: ![finetjul](https://discourse.vtk.org/user_avatar/discourse.vtk.org/finetjul/32/154_2.png) [@finetjul](https://discourse.vtk.org/u/finetjul)
#### Post date: [July 28, 2025, 8:05pm UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/8 "2025-07-28T20:05:00Z")

</div>

It’s strange that the spheres should move the current slice. I would expect the control handles to be ON the slice instead.

---

<div class="post-metadata">

### Author: ![yuxi-ovo](https://discourse.vtk.org/user_avatar/discourse.vtk.org/yuxi-ovo/32/10150_2.png) [@yuxi-ovo](https://discourse.vtk.org/u/yuxi-ovo)
#### Post date: [July 29, 2025, 4:28am UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/9 "2025-07-29T04:28:57Z")

</div>

What should I do? I also made some modifications based on your example

---

<div class="post-metadata">

### Author: ![finetjul](https://discourse.vtk.org/user_avatar/discourse.vtk.org/finetjul/32/154_2.png) [@finetjul](https://discourse.vtk.org/u/finetjul)
#### Post date: [July 29, 2025, 6:46am UTC](https://discourse.vtk.org/t/imageslice-and-cropping-planes-widget/8918/10 "2025-07-29T06:46:49Z")

</div>

From a user perspective, I think I would prefer if the “planes” are draggable instead of the handles.

I would create a vtkPlaneWidget. The user can click whereever on a plane to scroll it.  
The widget could be transparent but have a rectangle border with a red, green or blue color.

That’s a bit of work though. (We can help if you have funding).

Alternatively, you could rely on the soon to be merged [vtkTransformControlsWidget](https://github.com/Kitware/vtk-js/pull/3280)
