Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
8edcbf3
Fix camera orbit after loading a state file
psavery Aug 10, 2026
5f2e6b4
Refresh animation dialog ranges on execution
psavery Aug 10, 2026
79a4bb2
Fix phantom left click from macOS trackpad
psavery Aug 13, 2026
9af65e5
Serialize HDF5 access with a process-wide mutex
psavery Aug 11, 2026
9e0e4c7
Add volume lighting presets
psavery Aug 17, 2026
9a7d39f
Fix animation progress for nonzero time ranges
psavery Aug 20, 2026
b6f1ee6
Restore time series playback
psavery Aug 20, 2026
7811204
Add movie export dialog with MP4 support
psavery Aug 20, 2026
c2b2730
Add Gentle lighting preset and a shadows toggle
psavery Aug 20, 2026
70c17b0
Key the scattering guard on voxel count, not input MTime
psavery Aug 20, 2026
a9ef5fa
Do not time the first frame of a scattering configuration
psavery Aug 20, 2026
28d0bb3
Fire the scattering safety valve on a measurement, not an extrapolation
psavery Aug 20, 2026
b44fe91
Raise the still-frame budget to one second
psavery Aug 20, 2026
7a82f00
Give Full the same shadow reach as Soft
psavery Aug 20, 2026
e490f65
Correct why the sharpening cap exists
psavery Aug 20, 2026
fa80075
Say so when the guard switches shadows off
psavery Aug 20, 2026
072f2a3
Animate clip planes, opacity and camera viewpoints
psavery Aug 20, 2026
7a62053
Save the animation setup with the state file
psavery Aug 20, 2026
8660c5f
Show camera viewpoints as thumbnails
psavery Aug 20, 2026
0503fa5
Run a visualization animation over one leg of the camera path
psavery Aug 20, 2026
30222fe
Add a Label Map visualization type
psavery Aug 20, 2026
643373f
Carry the render estimate across a changed opacity curve
psavery Aug 20, 2026
54bc6e9
Animate a volume between two opacity curves
psavery Aug 20, 2026
93fe67e
Hold one render quality across an exported morph
psavery Aug 20, 2026
2756fbd
Let Rotate keep the original dimensions
psavery Aug 20, 2026
1d97e46
Overhaul the Animation Helper
psavery Aug 20, 2026
0c6254e
Keep label maps out of the animation surface
psavery Aug 20, 2026
8d560aa
Give an animation enough frames to play
psavery Aug 20, 2026
5dfd493
Load a captured opacity curve back into the editor
psavery Aug 20, 2026
0a4eae4
Save the rest of the animation and label state
psavery Aug 20, 2026
ec10b3e
Point the pyxrf-utils fallback at the HXN shared deployment
psavery Aug 20, 2026
23520cd
Trim the animation header comments
psavery Aug 20, 2026
74f9bed
Add periodic execution for schema-v2 Python nodes
alesgenova Aug 21, 2026
93d3110
Merge pull request #31 from alesgenova/periodic-execution
psavery Aug 24, 2026
51c024a
Use the tomviz-pipeline library
alesgenova Aug 26, 2026
67c4f5f
Remove Python bridge code orphaned by the numpy dataset switch
alesgenova Aug 26, 2026
33a8674
Support Python 3.14 and 3.15
alesgenova Aug 26, 2026
fd504b1
Let Python nodes update their own parameters
alesgenova Aug 27, 2026
9e2e090
Merge pull request #32 from alesgenova/use-tomviz-pipeline-library
psavery Aug 28, 2026
45d93aa
Validate external Python environments in the node editor
alesgenova Aug 28, 2026
5a9b640
Merge pull request #33 from alesgenova/verify-external-env
psavery Aug 31, 2026
21c2c5f
Refine the external environment version check
psavery Sep 3, 2026
7be6641
Bring back the manual manipulation and registration operators
psavery Sep 3, 2026
dca6fa6
Read .nxs files as HDF5
psavery Sep 3, 2026
5ab2aec
Fly through viewpoints and hand opacity back after playback
psavery Sep 3, 2026
c1af35e
Accept file drops anywhere over the main window
psavery Sep 3, 2026
4b0724d
Use an exact quarter-turn fast path in the rotate operator
psavery Sep 3, 2026
9c9427e
Add a Combine Datasets operator
psavery Sep 3, 2026
4c28783
Auto-pad differing scan shapes in the pyxrf tomo assembly
psavery Sep 3, 2026
70b3f21
Populate the PyXRF scan table from files already present
psavery Sep 3, 2026
f5e7ef2
Support periodic execution in the ptycho and pyxrf sources
psavery Sep 3, 2026
f1ad35a
Add acquisition simulators for ptycho and pyxrf
psavery Sep 3, 2026
483744b
Run pyxrf-utils through pixi with --as-is
psavery Sep 3, 2026
7e6615a
Scan the ptycho directory in a background thread
psavery Sep 3, 2026
33add5b
Unify the ptycho and XRF scan list files
psavery Sep 3, 2026
a2c6e2e
Make PtychoSource self-contained for external execution
psavery Sep 3, 2026
22c2c7f
Keep the ITK and real-time tomography operators internal-only
psavery Sep 3, 2026
65ec8a8
Restore the tomviz.external_dataset import path
psavery Sep 3, 2026
2a1d79d
Add Fourier filtering, peak mask, and image math operators
psavery Sep 3, 2026
b263071
Guard the Python node backend and editor against concurrent apply
psavery Sep 3, 2026
a0a014d
Let slice and clip views be linked together
psavery Sep 3, 2026
e81ea22
Bring back the image viewer mode
psavery Sep 3, 2026
c0a7950
Add auto contrast over a selected region
psavery Sep 3, 2026
9390a92
Add cut-out rendering to the volume sink
psavery Sep 3, 2026
eb359fe
Absorb new scans during periodic execution
psavery Sep 3, 2026
28469a5
Show the voxel under the cursor again
psavery Sep 3, 2026
b7b1d78
Clone the selected dataset, keeping its port type
psavery Sep 3, 2026
d77aaf0
Move the volume cut-out controls into a group below Lighting
psavery Sep 3, 2026
8c431b4
Let Image Math resample a second dataset of a different shape
psavery Sep 3, 2026
5caf909
Link slice and clip views by physical position and custom plane
psavery Sep 3, 2026
e5ff162
Add Save Scan List buttons to the ptycho and PyXRF dialogs
psavery Sep 3, 2026
9ac24d7
Add a Fourier Mask operator driven by a mask volume
psavery Sep 3, 2026
7048f4b
Give Binary Threshold sliders that span the real data range
psavery Sep 3, 2026
7d7232d
Add opacity presets to the histogram editor
psavery Sep 3, 2026
4699c5b
Stop offering a slice index animation for Custom planes
psavery Sep 3, 2026
f43632c
Record module state with camera viewpoints
psavery Sep 3, 2026
06c9eaa
Make Clear Subvolume work in the new pipeline
psavery Sep 3, 2026
084ae13
Trim or pad near-equal shapes in Image Math instead of stretching
psavery Sep 3, 2026
c8c1c41
Hide the box selector when its editor is hidden or another node is ac…
psavery Sep 4, 2026
05289a8
Add an exploded view to the volume sink
psavery Sep 11, 2026
13f2ecc
Let users save and reload their own lighting presets
psavery Sep 11, 2026
7793a1b
Give viewpoints a caption shown in the view
psavery Sep 11, 2026
e323bf5
Link Binary Threshold sliders to the Threshold visualization
psavery Sep 14, 2026
137c44c
Auto-detect peaks in Fourier Peak Mask
psavery Sep 14, 2026
298ac4e
Describe the FFT branch instead of a clone in Fourier Mask
psavery Sep 14, 2026
4f8be5b
Show Custom in the slice panel when the plane is grabbed
psavery Sep 14, 2026
e10be20
Start threshold ranges at the 80th percentile of the data
psavery Sep 14, 2026
7a59609
Threshold the visualization per voxel, not per point cell
psavery Sep 14, 2026
2cb0850
Round fractional thresholds inward for integer data
psavery Sep 14, 2026
c6fd1a6
Render label maps as surfaces by default
psavery Sep 14, 2026
31deed6
Sample label map volumes finely and lift their ambient light
psavery Sep 14, 2026
99023e3
Hold a newly inserted node until its dialog applies it
psavery Sep 14, 2026
faab98d
Fly two-viewpoint paths along a real line
psavery Sep 14, 2026
5e5d739
Treat modules a viewpoint never saw as hidden there
psavery Sep 14, 2026
dd4b1b0
List the state recorded with viewpoints as animations
psavery Sep 14, 2026
83d2696
Show the recorded opacity curves in the keyframe list
psavery Sep 14, 2026
e0f7422
Show a selected animation row in the authoring controls
psavery Sep 14, 2026
a5b75f8
Collapse unchecked Cut Out and Exploded View groups to their title
psavery Sep 14, 2026
ea45f31
Record slice and clip planes and iso values with viewpoints
psavery Sep 14, 2026
34efc89
Move the Shadows switch under Advanced and warn on every path that tu…
psavery Sep 15, 2026
54268a3
Seed tilt axis preview slices from the axis the orientation indexes
psavery Sep 16, 2026
d00f222
lazy load beamline dependency stack
alesgenova Sep 17, 2026
322a8ec
Improve view image mode so it uses the existing 2D/3D view toggle
alesgenova Sep 17, 2026
59dadd6
restore multiple volume support in the same view
alesgenova Sep 17, 2026
d3ea13f
Merge pull request #35 from alesgenova/various-improvements
psavery Sep 17, 2026
c89b96f
Threshold and label connected components with NumPy and SciPy
psavery Sep 17, 2026
6a02083
Seed threshold visualizations from the brightest voxels
psavery Sep 17, 2026
ab8f528
Add a Remove Labels operator linked to the Label Map visualization
psavery Sep 17, 2026
24548ae
Animate the exploded view from the Animation Helper
psavery Sep 17, 2026
bd29252
Let saved lighting presets be renamed
psavery Sep 17, 2026
ff71a41
Animate and record thresholds, cut-outs, label visibility and more
psavery Sep 17, 2026
f58ae34
Seed thresholds at the 95th percentile of the non-background voxels
psavery Sep 17, 2026
d952c76
Run binary morphology and Unsharp Mask on SciPy instead of ITK
psavery Sep 17, 2026
003ccc3
Tidy the new animations and bindings after review
psavery Sep 17, 2026
cb6afac
Keep enumeration parameters on the option the user chose
psavery Sep 17, 2026
61febe6
Let the exploded view cut along any direction
psavery Sep 17, 2026
fab6a1c
Let the exploded view's cuts be offset by a number of voxels
psavery Sep 17, 2026
e5cb738
Follow exploded view changes in the Animation Helper's range
psavery Sep 17, 2026
1958221
Keep exploded slab planes off the shared multi-volume mapper
psavery Sep 17, 2026
4f21aad
Print correctly if tomviz-pipeline is too old
psavery Sep 17, 2026
ebdebd7
Re-select label faces instead of re-extracting them
psavery Sep 18, 2026
d72b041
Keep the Label Map responsive with tens of thousands of labels
psavery Sep 18, 2026
e261d6c
Give animation its own menu
psavery Sep 18, 2026
33d39d9
Let the viewpoint captions be placed anywhere in the view
psavery Sep 18, 2026
0e624d6
Select label faces through the public cell API
psavery Sep 18, 2026
950810a
Smooth label surfaces without shrinking them
psavery Sep 18, 2026
9724d2a
Record label points on detached color maps too
psavery Sep 18, 2026
65b9a54
Rename a lambda local that shadowed the constructor's
psavery Sep 18, 2026
71989e3
Skip the label render test before any view exists without OpenGL
psavery Sep 21, 2026
f4e3e5f
Let a viewpoint orbit the focal point before the camera moves on
psavery Sep 21, 2026
dcee578
Let the duration and position boxes take a whole number
psavery Sep 21, 2026
67de714
Keep the animations working whatever the user does mid-play
psavery Sep 21, 2026
d17acfa
Probe OpenGL for the label render test by initializing a window
psavery Sep 21, 2026
ea8f8c5
Commit a viewpoint rename after the list is done with it
psavery Sep 21, 2026
fb215ba
Skip the label render test on what VTK says creating a window
psavery Sep 21, 2026
f44f12e
Test a module animation added while the scene plays
psavery Sep 21, 2026
d7902a3
Manage custom transforms in the user directory: create/clone/edit/delete
alesgenova Sep 23, 2026
4248083
Merge pull request #36 from alesgenova/custom-transform-manager
psavery Sep 23, 2026
9a4f155
Make the opening orbit when Play is pressed, not when data loads
psavery Sep 21, 2026
6598635
Give each leg and orbit its own frame count
psavery Sep 23, 2026
5428fc9
Keep scanning ~/.tomviz for custom transforms
psavery Sep 23, 2026
feba142
Add a Simulated Live Acquisition sample source
psavery Sep 23, 2026
d5890ff
Add a reproducible batch-processing example
psavery Sep 23, 2026
97b0c0f
Keep the rename editor open in the Animation Helper
psavery Sep 23, 2026
e5a5e6d
Keep the components of a replaced multi-component array
psavery Sep 23, 2026
564fb92
Leave the camera alone when animations change the exploded view
psavery Sep 23, 2026
8ea9a8e
Notice a ptycho scan whose config file lands last
psavery Sep 23, 2026
a6e10ec
Keep the clipping range fitted to what tomviz draws
psavery Sep 24, 2026
5924d9f
Name the FFT operator so a search for Fourier finds it
psavery Sep 24, 2026
5438d64
Take output colors from the primary input, never a label map
psavery Sep 24, 2026
da89169
Color labels by their value, not the order they were found
psavery Sep 24, 2026
b6bb198
Save any output that holds data, transient or not
psavery Sep 24, 2026
cab8199
Avoid ParaView's broken camera link connection
psavery Sep 24, 2026
0316dfc
Rescale the current opacity curve, not a stale copy
psavery Sep 24, 2026
83cf5c4
Keep the camera's side and nearest up in the 2D image viewer
psavery Sep 24, 2026
1b0de9b
Animate more of the scene between viewpoints
psavery Sep 24, 2026
cf08a55
Tune the Animation Helper's defaults
psavery Sep 24, 2026
3b00914
Bump the fallback version to 3.1.0
psavery Sep 24, 2026
6090f0f
Keep a clip's arrow hidden while its plane is
psavery Sep 24, 2026
df24b07
Mention solidity in the record-state tooltip
psavery Sep 25, 2026
11ac60b
Open the tomviz movie dialog from the Export Movie menus
psavery Sep 25, 2026
a4d917b
Test Python 3.14 in CI
psavery Sep 25, 2026
d0c9879
Hide a string parameter's label along with its field
psavery Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.11, 3.12, 3.13]
python-version: [3.11, 3.12, 3.13, 3.14]
config:
- {
name: Linux,
Expand Down Expand Up @@ -77,6 +77,10 @@ jobs:
- name: Install Test Requirements
run: |
conda install -y --override-channels -c conda-forge --file tomviz/tests/python/requirements-dev.txt
# The pipeline engine is the standalone tomviz-pipeline package
# from PyPI; the compatible range comes from the one place it is
# defined, and its dependencies come from runtime_requirements.txt.
pip install --no-deps -U "$(python tomviz/tomviz/python/tomviz/_pipeline_requirement.py)"
pip install --no-build-isolation --no-deps -U tomviz/tomviz/python
pip install --no-build-isolation --no-deps -U tomviz/acquisition
# These do not have to be in the same environment as Tomviz, but the dependencies
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/runtime_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ pygments
pystackreg
h5py
click
tqdm
scikit-image
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.cache
*.egg-info
__pycache__
tomviz/python/build
21 changes: 21 additions & 0 deletions BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,35 @@ Tomviz python library itself.

```bash
conda install -y --override-channels -c conda-forge --file tomviz/.github/workflows/runtime_requirements.txt
pip install --no-deps -U "$(python tomviz/tomviz/python/tomviz/_pipeline_requirement.py)"
pip install --no-build-isolation --no-deps -U tomviz/tomviz/python
```

The pipeline engine is the separately released
[tomviz-pipeline](https://pypi.org/project/tomviz-pipeline/) package;
`tomviz/python` is the application-side layer on top of it. The
compatible tomviz-pipeline range is defined once, in
`tomviz/python/tomviz/_pipeline_requirement.py`, which prints it when
run as a script — hence the `$(python ...)` above. Both installs use
`--no-deps` so pip never replaces the conda-managed packages. If your
environment predates that split it still has the old in-tree package
installed under the same name and the two will shadow each other — run
`pip uninstall tomviz-pipeline tomviz-app` and delete
`tomviz/tomviz/python/build/` before the two `pip install` lines above.

Tomviz may then be ran as follows:

```bash
./tomviz-build/bin/tomviz
```

To run the test suite (`ctest` in the build directory), also install the
test requirements:

```bash
conda install -y --override-channels -c conda-forge --file tomviz/tests/python/requirements-dev.txt
```

All of the build instructions above put into a single script are as follows:

```bash
Expand All @@ -67,5 +87,6 @@ bash tomviz/.github/workflows/scripts/build_tomviz.sh

# Install runtime dependencies
conda install -y --override-channels -c conda-forge --file tomviz/.github/workflows/runtime_requirements.txt
pip install --no-deps -U "$(python tomviz/tomviz/python/tomviz/_pipeline_requirement.py)"
pip install --no-build-isolation --no-deps -U tomviz/tomviz/python
```
21 changes: 18 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ include(Git)
determine_version("${tomviz_SOURCE_DIR}" "${GIT_EXECUTABLE}" "tomviz")
# The version being developed here. Authoritative, because "git
# describe" keeps reporting the previous release until the matching tag
# is created, and a 3.0 build must not label itself 2.3.1. Also what
# is created, and a 3.1 build must not label itself 3.0.0. Also what
# source tarballs (no git at all) fall back on.
set(tomviz_version_major 3)
set(tomviz_version_minor 0)
set(tomviz_version_minor 1)
set(tomviz_version_patch 0)
set(tomviz_version_extra)
set(tomviz_version
Expand All @@ -118,7 +118,7 @@ set(tomviz_version
# git describe refines that in two ways: it supplies the commit-count
# and sha suffix that distinguishes a development build from the release
# (empty when sitting exactly on the tag), and once a tag newer than the
# version above exists it takes over the number too, so tagging 3.0.1
# version above exists it takes over the number too, so tagging 3.1.1
# needs no edit here. Packaging that builds outside a git checkout can
# pass -DTOMVIZ_GIT_DESCRIBE=<version> to drive both.
if(tomviz_VERSION_FULL)
Expand Down Expand Up @@ -166,6 +166,21 @@ find_package(ITK 4.9)
set(PYBIND11_CPP_STANDARD "-std=c++17" CACHE STRING "")
add_subdirectory(${PROJECT_SOURCE_DIR}/thirdparty/pybind11)

# Route pybind11's GIL management through CPython's PyGILState_Ensure /
# PyGILState_Release (see pybind11/gil_simple.h) instead of its own
# thread-state caching. Tomviz embeds Python alongside VTK/ParaView,
# which pervasively use PyGILState_Ensure (e.g. vtkSmartPyObject's
# destructor). Mixing the two GIL bookkeeping schemes across threads —
# a pipeline worker running a Python transform via pybind while the
# main thread tears down a Python-bearing widget via VTK — corrupts the
# worker's thread state (PyThreadState with a NULL interpreter), a
# SIGSEGV in the next Python allocation. Defined here as an INTERFACE
# definition so every translation unit that includes pybind11/gil.h
# agrees on the gil_scoped_* type (it changes a type alias; a per-target
# definition would be an ODR violation).
target_compile_definitions(pybind11::pybind11
INTERFACE PYBIND11_SIMPLE_GIL_MANAGEMENT)

option(tomviz_ENABLE_LOOKING_GLASS "Enable the looking glass plugin" OFF)
if (tomviz_ENABLE_LOOKING_GLASS)
if (NOT DEFINED tomviz_LOOKING_GLASS_PLUGIN_PATH)
Expand Down
1 change: 1 addition & 0 deletions docker/tomviz-builder/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN echo "#!/bin/bash" > build.sh && \
echo "pip2 install -q -r ../tomviz/tests/python/requirements-dev.txt" >> build.sh && \
echo "pip3 install -q -r ../tomviz/tests/python/requirements-dev.txt" >> build.sh && \
echo "pip2 install -q -r ../tomviz/acquisition/requirements-dev.txt" >> build.sh && \
echo "pip3 install -q \"tomviz-pipeline>=3.1.3\"" >> build.sh && \
echo "pip3 install -q ../tomviz/tomviz/python/" >> build.sh && \
echo "ctest -VV -S /tomviz/cmake/CircleContinuous.cmake" >> build.sh && \
chmod u+x build.sh
Expand Down
10 changes: 5 additions & 5 deletions docker/tomviz-pipeline/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
FROM library/python:3.7-slim
FROM library/python:3.12-slim
MAINTAINER Chris Harris <chris.harris@kitware.com>
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
ENV PYTHONUNBUFFERED=ON

COPY tomviz/python/ /tmp/python/

RUN pip install /tmp/python/ && \
rm -rf /tmp/python/
# The pipeline runtime is the standalone tomviz-pipeline package. scipy
# is not a dependency of the engine but is widely used by operator
# scripts.
RUN pip install "tomviz-pipeline>=3.1.3" scipy

ENTRYPOINT ["tomviz-pipeline"]

Expand Down
8 changes: 4 additions & 4 deletions docker/tomviz-tomopy-pipeline/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ ENV LANG=C.UTF-8
# Install tomopy
RUN conda install -c conda-forge tomopy

COPY tomviz/python/ /tmp/python/

RUN pip install /tmp/python/ && \
rm -rf /tmp/python/
# The pipeline runtime is the standalone tomviz-pipeline package. scipy
# is not a dependency of the engine but is widely used by operator
# scripts.
RUN pip install "tomviz-pipeline>=3.1.3" scipy

ENTRYPOINT ["tomviz-pipeline"]

Expand Down
50 changes: 50 additions & 0 deletions examples/batch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Batch processing example

One pipeline, many datasets, no GUI. This directory builds a small
reproducible case for the `tomviz-pipeline` command line tool:

* `make_example.py` writes four volumes with a different number of
spheres in each (`example/inputs/particles_0N.emd`) and a pipeline
`example/particles.tvsm` that blurs, thresholds and labels the spheres.
The pipeline is assembled from the operator scripts in `tomviz/python`,
the same thing you get by saving it from the tomviz GUI.
* `run.sh` runs that pipeline over every input in one call.

```sh
pip install "tomviz-pipeline>=3.1.7" # 3.1.7 added the label map writer
./run.sh
```

`tomviz-pipeline` runs the state file once per matched input and writes
each run's leaf outputs (here, the label map from Connected Components)
to its own directory:

```
example/results/
run_0/4_Connected_Components__volume.emd
run_0/state.tvsm
run_1/4_Connected_Components__volume.emd
run_1/state.tvsm
run_2/...
run_3/...
```

Each `state.tvsm` is the pipeline as it ran, with that run's input pinned;
it can be opened in tomviz or passed back to `tomviz-pipeline -s`.

Open a label map in tomviz, or read it from Python:

```python
from tomviz_pipeline.io import load_dataset

labels = load_dataset('example/results/run_2/4_Connected_Components__volume.emd')
print(labels.active_scalars.max(), 'spheres') # 4
```

To apply your own pipeline the same way, save it from the GUI
(`File > Save State`) and point `tomviz-pipeline -s` at it; the `--input`
override works with any state file whose only source is a file reader.
The full set of options, including multi-source pipelines and running in
an external Python environment, is in the
[External Pipelines](https://tomviz.readthedocs.io/en/latest/pipelines.html)
documentation.
127 changes: 127 additions & 0 deletions examples/batch/make_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python
"""Build the inputs and the pipeline for the batch-processing example.

python make_example.py [DIRECTORY] [--count N] [--size S]

writes DIRECTORY/inputs/particles_00.emd ... particles_0N.emd, each a
noisy volume with a different number of spheres in it, and
DIRECTORY/particles.tvsm, a pipeline that blurs, thresholds and labels
the spheres. The pipeline is assembled from the operator scripts shipped
in tomviz/python, so it is the same thing saving it from the GUI gives
you, and the example never drifts from the operators.

Needs the tomviz-pipeline package (pip install tomviz-pipeline).
"""
import argparse
import json
from pathlib import Path

import numpy as np

from tomviz_pipeline import Dataset
from tomviz_pipeline.writers.emd import write_emd

OPERATORS = Path(__file__).resolve().parents[2] / 'tomviz' / 'python'


def make_volume(index, size, rng):
"""A volume with ``2 + index`` non-overlapping spheres in noise.

The spheres sit on a jittered 2 x 2 x 2 lattice, so they never touch
and Connected Components finds exactly as many as were placed."""
spheres = 2 + index
volume = rng.normal(0.0, 0.15, (size, size, size)).astype(np.float32)
lattice = [(i, j, k) for i in (0.25, 0.75) for j in (0.25, 0.75)
for k in (0.25, 0.75)]
rng.shuffle(lattice)
x, y, z = np.mgrid[:size, :size, :size]
for site in lattice[:spheres]:
center = np.array(site) * size + rng.uniform(-size / 16, size / 16, 3)
radius = rng.uniform(size / 12, size / 8)
inside = ((x - center[0])**2 + (y - center[1])**2 +
(z - center[2])**2) <= radius**2
volume[inside] = 1.0
dataset = Dataset({'Scalars': np.asfortranarray(volume)}, 'Scalars')
dataset.spacing = (1.0, 1.0, 1.0)
return dataset, spheres


def operator_node(node_id, name, label, **arguments):
"""A Python operator node the way a state file stores it: the
description and the script travel inside the file."""
return {
'id': node_id,
'type': 'transform.legacyPython',
'label': label,
'description': (OPERATORS / f'{name}.json').read_text(),
'script': (OPERATORS / f'{name}.py').read_text(),
'arguments': arguments,
}


def make_state(first_input):
nodes = [
{'id': 1, 'type': 'source.reader', 'label': 'Reader',
'fileNames': [str(first_input)]},
operator_node(2, 'GaussianFilter', 'Gaussian Blur', sigma=1.0),
operator_node(3, 'BinaryThreshold', 'Binary Threshold',
lower_threshold=0.5, upper_threshold=10.0),
operator_node(4, 'ConnectedComponents', 'Connected Components',
background_value=0, connectivity=3, min_size=10),
]
# Binary Threshold delivers its label map on a port named after its
# child dataset, which is where Connected Components reads from.
links = [
{'from': {'node': 1, 'port': 'volume'},
'to': {'node': 2, 'port': 'volume'}},
{'from': {'node': 2, 'port': 'volume'},
'to': {'node': 3, 'port': 'volume'}},
{'from': {'node': 3, 'port': 'thresholded_segmentation'},
'to': {'node': 4, 'port': 'volume'}},
]
return {
'schemaVersion': 2,
'pipeline': {'nextNodeId': 5, 'nodes': nodes, 'links': links},
}


def build(directory, count=4, size=64, seed=0):
"""Write the inputs and the state file. Returns the state file path,
the input paths and how many spheres each input holds."""
directory = Path(directory)
inputs_dir = directory / 'inputs'
inputs_dir.mkdir(parents=True, exist_ok=True)
rng = np.random.default_rng(seed)

inputs, spheres = [], []
for index in range(count):
dataset, n = make_volume(index, size, rng)
path = inputs_dir / f'particles_{index:02d}.emd'
write_emd(dataset, path)
inputs.append(path)
spheres.append(n)

state_path = directory / 'particles.tvsm'
state_path.write_text(json.dumps(make_state(inputs[0]), indent=2))
return state_path, inputs, spheres


def main():
parser = argparse.ArgumentParser(description=__doc__.split('\n')[0])
parser.add_argument('directory', nargs='?', default='example')
parser.add_argument('--count', type=int, default=4,
help='how many input volumes to write')
parser.add_argument('--size', type=int, default=64,
help='edge length of each volume in voxels')
args = parser.parse_args()

state_path, inputs, spheres = build(args.directory, args.count,
args.size)
for path, n in zip(inputs, spheres):
print(f'{path}: {n} spheres')
print(f'{state_path}: Gaussian Blur -> Binary Threshold -> '
'Connected Components')


if __name__ == '__main__':
main()
13 changes: 13 additions & 0 deletions examples/batch/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# Run the batch-processing example end to end: make the inputs and the
# pipeline, then process every input with one tomviz-pipeline call.
set -e
cd "$(dirname "$0")"

python make_example.py example
tomviz-pipeline -s example/particles.tvsm -o example/results \
--input 'example/inputs/*.emd'

echo
echo "Results:"
find example/results -type f | sort
Loading
Loading