Skip to content

Commit

Permalink
feat: allow clipping of volumetric renderings
Browse files Browse the repository at this point in the history
  • Loading branch information
maartenbreddels committed Jun 27, 2023
1 parent f1bf877 commit 817f3ed
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 11 deletions.
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,5 +386,6 @@
'examples/popup': 'examples/screenshot/ipyvolume-popup-legend-iris.gif',
'examples/lighting': 'examples/screenshot/volume-rendering-specular.gif',
'examples/slice': 'examples/screenshot/ipyvolume-slice-head.gif',
'examples/volume-clipping': 'examples/screenshot/volume-clip.gif',
}
exclude_patterns = ['**.ipynb_checkpoints']
2 changes: 2 additions & 0 deletions docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Examples
examples/lighting
examples/popup
examples/slice
examples/slice
examples/volume-clipping

Feel free to contribute new examples:

Expand Down
Binary file added docs/source/examples/screenshot/volume-clip.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
90 changes: 90 additions & 0 deletions docs/source/examples/volume-clipping.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "580c6579",
"metadata": {},
"source": [
"# Clipping of volume\n",
"\n",
"In order to inspect volumetric renderings, it may be useful to remove parts of it. Using the `clip_x_min`, `clip_x_max`, `clip_y_min`, `clip_y_max`, `clip_z_min` and `clip_z_max` traits, we can control which part of the volume is rendered. In the example below you can use the sliders labels 'xmin' and 'xmax' to inspect regions inside the volume."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c7e47337",
"metadata": {},
"outputs": [],
"source": [
"import ipyvolume as ipv\n",
"fig = ipv.figure()\n",
"volume = ipv.examples.head(show=False, description=\"Patient X\")\n",
"ipv.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "34ea1395",
"metadata": {},
"outputs": [],
"source": [
"import ipywidgets as w\n",
"clip_min = w.IntSlider(description=\"xmin\", min=0, max=128, value=50)\n",
"clip_max = w.IntSlider(description=\"xmax\", min=0, max=128, value=100)\n",
"w.jslink((clip_min, 'value'), (volume, 'clip_x_min'))\n",
"w.jslink((clip_max, 'value'), (volume, 'clip_x_max'))\n",
"container = ipv.gcc()\n",
"container.children = container.children + [clip_min, clip_max]\n"
]
},
{
"cell_type": "markdown",
"id": "81702c3d",
"metadata": {},
"source": [
"Note that you can also link the instance the `slice_x` trait of the figure to the `clip_x_max` trait. Now we can hold the shift key and the clip plane will follow the mouse cursor."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68b54115",
"metadata": {},
"outputs": [],
"source": [
"w.jslink((fig, 'slice_x'), (volume, 'clip_x_max'));"
]
},
{
"cell_type": "markdown",
"id": "22f48b7f",
"metadata": {},
"source": [
"[screencapture](screenshot/volume-clip.gif)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.16"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
7 changes: 7 additions & 0 deletions ipyvolume/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ class Volume(widgets.Widget, LegendData):
extent = traitlets.Any().tag(sync=True)
extent_original = traitlets.Any()

clip_x_min = traitlets.CFloat(None, allow_none=True).tag(sync=True)
clip_x_max = traitlets.CFloat(None, allow_none=True).tag(sync=True)
clip_y_min = traitlets.CFloat(None, allow_none=True).tag(sync=True)
clip_y_max = traitlets.CFloat(None, allow_none=True).tag(sync=True)
clip_z_min = traitlets.CFloat(None, allow_none=True).tag(sync=True)
clip_z_max = traitlets.CFloat(None, allow_none=True).tag(sync=True)

def __init__(self, **kwargs):
super(Volume, self).__init__(**kwargs)
self._update_data()
Expand Down
41 changes: 30 additions & 11 deletions js/src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ class VolumeView extends widgets.WidgetView {
this.renderer.rebuild_multivolume_rendering_material();
this.renderer.update();
});
this.model.on("change:clip_x_min change:clip_x_max change:clip_y_min change:clip_y_max change:clip_z_min change:clip_z_max", () => {
this.renderer.update();
});

(window as any).last_volume = this; // for debugging purposes

Expand Down Expand Up @@ -168,6 +171,7 @@ class VolumeModel extends widgets.WidgetModel {
};

volume: any;
scales?: any;
texture_volume: THREE.DataTexture;
uniform_volumes_values: {
data_range?: any,
Expand Down Expand Up @@ -321,27 +325,42 @@ class VolumeModel extends widgets.WidgetModel {
return this.get("rendering_method") === "NORMAL";
}
set_scales(scales) {
const sx = createD3Scale(scales.x).range([0, 1]);
const sy = createD3Scale(scales.y).range([0, 1]);
const sz = createD3Scale(scales.z).range([0, 1]);
this.scales = scales;
this.update_geometry();
}
update_geometry() {
const sx = createD3Scale(this.scales.x).range([0, 1]);
const sy = createD3Scale(this.scales.y).range([0, 1]);
const sz = createD3Scale(this.scales.z).range([0, 1]);

const extent = this.get("extent");

// normalized coordinates of the corners of the box
// return v, of the clipped value
const or_clip = (v, name) => {
const clip_value = this.get("clip_" + name);
if ((clip_value === undefined) || (clip_value === null)) {
return v;
}
return clip_value;
}

// normalized coordinates of the corners of the box
const x0n = sx(extent[0][0]);
const x1n = sx(extent[0][1]);
const y0n = sy(extent[1][0]);
const y1n = sy(extent[1][1]);
const z0n = sz(extent[2][0]);
const z1n = sz(extent[2][1]);

// clipped coordinates
const cx0 = Math.max(x0n, 0);
const cx1 = Math.min(x1n, 1);
const cy0 = Math.max(y0n, 0);
const cy1 = Math.min(y1n, 1);
const cz0 = Math.max(z0n, 0);
const cz1 = Math.min(z1n, 1);
// normalized coordinates of the corners of the box
// including the custom clipping, and viewport clipping
const cx0 = Math.max(sx(or_clip(extent[0][0], "x_min")), 0);
const cx1 = Math.min(sx(or_clip(extent[0][1], "x_max")), 1);
const cy0 = Math.max(sy(or_clip(extent[1][0], "y_min")), 0);
const cy1 = Math.min(sy(or_clip(extent[1][1], "y_max")), 1);
const cz0 = Math.max(sz(or_clip(extent[2][0], "z_min")), 0);
const cz1 = Math.min(sz(or_clip(extent[2][1], "z_max")), 1);


// the clipped coordinates back to world space, then normalized to extend
// these are example calculations, the transform goes into scale and offset uniforms below
Expand Down

0 comments on commit 817f3ed

Please sign in to comment.