Add GPU-accelerated 3D z-stack renderer#1096
Conversation
Implements a standalone vispy-based 3D volume renderer with full Cell_ACDC integration: multi-channel overlay, segmentation mask volumes, anisotropic voxel scaling, plane/ISO/MIP depiction modes, smooth-ISO pre-filter, percentile auto-contrast, opacity/contrast controls, keyboard shortcuts, QSettings persistence, and screenshot export. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new GPU-accelerated 3D z-stack renderer (VisPy-based) and integrates it into the main GUI as a new z-projection option, alongside a large smoke-test suite intended to validate renderer behavior without requiring an active OpenGL window.
Changes:
- Introduces
cellacdc.renderer3dwith a Qt window, controls, overlay volumes, voxel anisotropy scaling, and screenshot export. - Integrates the renderer into
gui.pyvia a new z-projection dropdown entry and adapter-based wiring for feeding current volumes/overlays. - Adds
renderer3doptional dependency group (VisPy + PyOpenGL) and a newtests/test_renderer3d.pytest module.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| tests/test_renderer3d.py | Adds renderer3d smoke/API/tests, including vispy-compat checks and GUI integration expectations. |
| pyproject.toml | Adds renderer3d optional dependencies and includes them in all. |
| cellacdc/renderer3d.py | New VisPy/Qt 3D volume renderer window, controls, overlays, voxel scaling, settings persistence, screenshot export. |
| cellacdc/gui.py | Adds “3D z-render” mode, launches/updates/hides renderer window, and supplies current z-stack + overlays + voxel sizes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| def test_interpolation_modes_valid_in_vispy(): | ||
| """Every INTERPOLATION_MODES id must be a valid vispy Volume interpolation.""" | ||
| from cellacdc import renderer3d |
| from cellacdc import renderer3d | ||
| import vispy | ||
| from qtpy import API_NAME | ||
| vispy.use(API_NAME) | ||
| from vispy.scene.visuals import Volume |
| def test_depiction_plane_configs_valid_in_vispy(): | ||
| """_PLANE_CONFIGS normals must be unit vectors; vispy supports 'plane' mode.""" | ||
| import vispy; from qtpy import API_NAME; vispy.use(API_NAME) | ||
| from vispy.scene.visuals import Volume | ||
| from cellacdc import renderer3d |
| """VolumeRenderer3DWindow must expose all expected public methods.""" | ||
| from cellacdc.renderer3d import VolumeRenderer3DWindow | ||
| required = { | ||
| 'update_volume', 'set_rendering_mode', 'set_colormap', 'set_clim', | ||
| 'set_gamma', 'set_opacity', 'set_iso_threshold', 'set_attenuation', | ||
| 'set_interpolation', 'set_step_size', 'set_smooth_iso', | ||
| 'set_depiction', 'set_zplane_position', 'set_plane_thickness', | ||
| 'set_voxel_scale', 'reset_view', 'save_screenshot', | ||
| 'auto_contrast_percentile', | ||
| } |
| s = QSettings(TEST_ORG, TEST_APP) | ||
| assert s.value('mode_idx', type=int) == 3 | ||
| assert s.value('colormap', type=str) == 'viridis' | ||
| assert s.value('interp_idx', type=int) == 1 | ||
| assert abs(s.value('clim_min', type=float) - 0.05) < 1e-6 |
| COLORMAPS: list[str] = [ | ||
| 'grays', 'viridis', 'hot', 'coolwarm', 'blues', 'reds', | ||
| 'greens', 'plasma', 'inferno', 'magma', | ||
| ] | ||
|
|
| except ImportError: | ||
| pass # scipy not available — skip smoothing silently | ||
| self._gpu_data_is_smoothed = smoothed | ||
| current_cmap = 'grays' |
| s.setValue('clim_max', c._clim_max.value()) | ||
| s.setValue('gamma', c._gamma_spin.value()) | ||
| s.setValue('step_size', c._step_spin.value()) | ||
| s.setValue('smooth_iso', c._smooth_iso_cb.isChecked()) |
| ol_img = img.mean(axis=0) | ||
| elif zProjHow == 'median z-proj.': | ||
| ol_img = np.median(img, axis=0) | ||
| else: | ||
| ol_img = img[z].copy() |
|
@codex review |
|
Hi @keejkrej, Thank you for this contribution, it looks awesome!! 🚀 Before we merge, do you plan to look at the Copilot comments, or should I also have a look? Also, the tests are failing because the Thanks! |
|
Hi @ElpadoCan , it would be great if you can have a look at the copilot comments and make some changes. I think one has to make several important design choices to make the 3d renderer truly useful for segmentation (it's more than just model integrations). |
What do you mean by this? You mean displaying segmentation masks in the 3D renderer as well? Because editing segmentation masks in 3D sounds hard, I would not do it for now. What do you think? |
|
Hi @keejkrej, For now, I will remove the |
|
I mean whether to show 3d render in separate window or in side by side view in the gui. Do we need real time feedbacks for the segmentation? It is currently implemented as a post-validation of segmentation. |
|
I actually like it in a separate window, what do you think @Teranis? |
I don't think we need the ability to edit the segmentation masks in the 3d renderer. The only thing I would add is to display the segmentation masks overlaid with colours. Can we do this by displaying an image for the masks as well? |
|
Cool feature that would be nice to have:
This way, one can visualise volumes where the classic z-anisotropy is rendered as isotropic |
|
Hi, this looks very cool! I like the idea that it's in a separate window since it's really just a visualisation. Along these lines, I would move the entry point to a dedicated button or menu option in the top ribbon, and if possible, make it so one could use the feature outside of the GUI as a utility. |
Yes, I agree with everything. It would be cool if this was an easy to use python API as well. Regarding UI, I'm working on some improvements. For the missing packages, I will add automatic installation. I'm also planning to add a Cell ID selector to show only a specific cell (if segm is available of course). Stay tuned 🚀 |
Note tha this is already possible like this renderer = VolumeRenderer3DWindow()
renderer.update_volume(zstack_array) # (Z, Y, X) numpy array
renderer.show()We can add |
vispy 3d rendering of z-stacks, supports overlay channel and segmentation channel
don't merge yet, haven't thoroughly tested
feel free to checkout and test