| theme | the-unnamed | |
|---|---|---|
| background | https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=2072 | |
| title | Appose Workshop | |
| info | ## Appose: Interprocess Cooperation with Shared Memory I2K 2025 Workshop Teaching participants how to use Appose with Fiji for Python integration | |
| class | text-center | |
| drawings |
|
|
| transition | slide-left | |
| mdc | true | |
| duration | 120min |
Curtis Rueden @ UW-Madison LOCI
"Halfway to I2K" 2025
SLIDES: https://fiji.github.io/i2k-2025-appose/
VIDEO: https://youtu.be/9Rf4ynSDozc
- π Interprocess - Multiple processes connected via communication protocol
- π€ Cooperation - Build environments, start services, run tasks
- πΎ Shared Memory - Cross-platform, cross-language memory buffers
Python Mode: Runs Python in the same process as Java
- Powered by PyImageJ (imglyb, scyjava, jgo, jpype) and enabled by Jaunch
- Memory can be shared directly
- Java objects wrapped as Python objects via JPype
- Write true Python (CPython, not Jython) scripts in Fiji's Script Editor
Multiple isolated environments, each with their own dependencies, running simultaneously.
Multiple processes (programs) connected via communication protocol
- Fiji (Java) β Python program
- napari (Python) β Java program
- Java β Java
- Python β Python
- Python β R
::right::
- Called program doesn't need to "know about" caller
- Clean separation of concerns
- Use best tool for each job
- Each process has own memory space
- No direct memory sharing (traditionally)
How Appose facilitates
interprocess cooperation:
graph TD
Env[Environment] -->|contains| S1[Service/Worker 1]
Env -->|contains| S2[Service/Worker 2]
Env -->|contains| S3[Service/Worker ...]
S1 -->|executes| T1[Task 1]
S1 -->|executes| T2[Task 2]
S1 -->|executes| T3[Task ...]
S2 -->|executes| T4[Task 1]
S2 -->|executes| T5[Task 2]
style Env fill:#4a9eff,stroke:#333,stroke-width:3px,color:#fff
style S1 fill:#6bc95f,stroke:#333,stroke-width:2px,color:#fff
style S2 fill:#6bc95f,stroke:#333,stroke-width:2px,color:#fff
style S3 fill:#6bc95f,stroke:#333,stroke-width:2px,color:#fff
style T1 fill:#ffa726,stroke:#333,stroke-width:1px,color:#fff
style T2 fill:#ffa726,stroke:#333,stroke-width:1px,color:#fff
style T3 fill:#ffa726,stroke:#333,stroke-width:1px,color:#fff
style T4 fill:#ffa726,stroke:#333,stroke-width:1px,color:#fff
style T5 fill:#ffa726,stroke:#333,stroke-width:1px,color:#fff
- Build Environment
- Using pixi, uv, or micromamba
- No pre-installation needed
- Everything downloaded on demand
- Start Service
- Worker process running in that environment
- Stays alive for multiple tasks
- Run Tasks
- Feed inputs, receive outputs
- Via JSON serialization
- Through the service/worker
Cross-platform, cross-language support for named shared memory buffers
from multiprocessing.shared_memory import SharedMemoryfrom appose import SharedMemoryimport org.apposed.appose.SharedMemory;New class modeled closely after Python's implementation
Result: Efficient sharing of large data (images!) without serialization overhead
Shared memory with structural metadata (images!)
NDArray = SharedMemory + dtype + shape
import appose
# Create shared memory NDArray
data = appose.NDArray("uint16", [2, 20, 25])
# Access as NumPy array
numpy_array = data.ndarray()π€ How does that work under the hood?
numpy.ndarray(
prod(data.shape), dtype=data.dtype, buffer=data.shm.buf
).reshape(data.shape)import org.apposed.appose.NDArray;
import net.imglib2.appose.*; // ShmImg, NDArrays
// Receive NDArray from Python (via task.outputs)
ShmImg<FloatType> img = new ShmImg<>(ndarray);
// Or create and send to Python (as a task input)
NDArray ndarray = NDArrays.ndArray(new FloatType(), 4, 3, 2);
Img<FloatType> img = NDArrays.asArrayImg(ndarray, new FloatType());
// Or copy to ShmImg from existing (presumably non-shm) Img
Img<FloatType> sharedCopy = ShmImg.copyOf(someOtherImg);
// See also net.imglib2.util.ImgUtil.copy(srcImg, destImg)Real-world Appose integrations in action
One-click live segmentation
- Segment Anything Model integration
- Python AI model called from Java
- Interactive segmentation UI
https://github.com/segment-anything-models-java/SAMJ-IJ
Help βΊ Update... βΊ Manage Update Sites βΊ SAMJ
Deep learning spot detectors
- v9-appose branch
- Python-based detection in Java application
- Real-time particle tracking
https://github.com/trackmate-sc/TrackMate/tree/v9-appose
Run fiji/plugin/trackmate/TrackMatePlugIn.java in IDE
Large-scale tracking capabilities
- Cell detection and tracking with Python deep learning models
- Powered by Appose + Cellpose3 + TrackAstra
- Proof-of-concept stage: best to use Linux
https://github.com/mastodon-sc/mastodon-deep-lineage/
Help βΊ Update... βΊ Manage Update Sites βΊ Mastodon + Mastodon-DeepLineage
Using TGMM mini example dataset
Download from:
https://github.com/mastodon-sc/mastodon-example-data/tree/master/tgmm-mini
Files needed:
datasethdf5.h5- Image datadatasethdf5.xml- BDV metadata
Create new Mastodon project based on these two files
Install the required Python environments via Appose
Menu: Plugins βΊ Tracking βΊ Python environments for detectors/linkers
- Update/Install cellpose3 - Cell segmentation model
- Update/Install trackastra - Deep learning-based tracking
error libmamba Could not solve for environment specs
The following packages are incompatible
ββ pytorch-cuda =* * is not installable because there are no viable options
ββ pytorch-cuda 11.6 would require
β ββ cuda =11.6 *, which does not exist (perhaps a missing channel);
ββ pytorch-cuda 11.7 would require
β ββ cuda =11.7 *, which does not exist (perhaps a missing channel);
ββ pytorch-cuda 11.8 would require
ββ cuda =11.8 *, which does not exist (perhaps a missing channel).
critical libmamba Could not solve for environment specs
[ERROR] Installation failed for cellpose3
Menu: Plugins βΊ Tracking βΊ Detection βΊ Cellpose3
Settings to adjust:
| Parameter | Value | Notes |
|---|---|---|
| Estimated diameter | 25 | Cell size in pixels |
| GPU usage | 1.0 | Use full GPU (if available) |
Run detection
- Takes 3-5 minutes on laptop, or less than 1 minute on workstation
- Cellpose runs in Python via Appose, results sent back to Mastodon
Menu: Plugins βΊ Tracking βΊ Linking βΊ TrackAstra
Settings:
- Leave default parameters
- TrackAstra uses deep learning for tracking
Run linking:
- Links detected cells across timepoints
- Creates complete cell lineage tracks
- All computation happens in Python via Appose
Platform note: Tested on Linux and Windows. Detection works on both; linking works best on Linux.
Build your own Appose-powered tool in Fiji
Fiji plugin (Groovy script) that calls UNSEG for nucleus/cell segmentation
UNSEG: Unsupervised segmentation of cells and their nuclei in tissue
https://github.com/uttamLab/UNSEG
- Setting up Python environments with pixi
- Adapting Python code for Appose
- Writing Fiji scripts that call Python
- Debugging cross-language integration
Required:
- Fiji installed
Recommended:
- bash terminal
- pixi
- git
- vim (or your favorite text editor)
- claude (or your favorite AI assistant)
Can follow along without codingβjust watch!
Follow along or just watch
We'll go through all the steps together!
π‘ Stuck? Clone reference repo:
git clone https://github.com/ctrueden/unseg-fijiUse tags to jump to any step:
git checkout step03Or to view changes for a particular step:
git show step04Create a dedicated folder for this project:
mkdir ~/Desktop/unseg-fiji
cd ~/Desktop/unseg-fijiInitialize an empty pixi project skeleton:
pixi init
π‘ Install pixi from: https://pixi.sh/latest/installation/
This creates pixi.toml: the environment specification
π― Checkpoint: git init && git add . && git commit -m 'Add initial project skeleton'
Get the UNSEG repository from GitHub:
git clone https://github.com/uttamLab/UNSEG
cp UNSEG/unseg.py .
unzip UNSEG/image.zipWhat's in here?
unseg.py- The segmentation algorithmrun_unseg.ipynb- Jupyter notebook showing how to use itrequirements.txt- Python dependenciesimage.zip- Sample image data
π― Checkpoint: git add unseg.py && git commit -m 'Add unseg script'
Extract code from the Jupyter notebook into a Python script:
Manual approach:
- Go to https://github.com/uttamLab/UNSEG in browser
- Click into
run_unseg.ipynb - Copy code blocks into text editor
- Save as
run.py
Or use your AI assistant! π€
π― Checkpoint: git add run.py && git commit -m 'Add test script'
Import dependencies into pixi project:
pixi import UNSEG/requirements.txt --format pypi-txt --environment defaultThis updates pixi.toml environment specification.
π― Checkpoint: git commit -a -m 'Add unseg dependencies from requirements.txt'
1. Add needed dependencies: pixi add python=3.9 appose==0.7.2
2. Move packages from pypi-dependencies to dependencies
3. Test it: pixi run python run.py
Common issues to fix:
opencv-pythonfails β move back topypi-dependencies- Missing images β run
unzip UNSEG/image.zip - Qt errors β change
opencv-pythontoopencv-python-headless
π― Checkpoint: git commit -a -m 'Update dependencies to first working version'
Make the library "listenable" by adding a callback mechanism:
Add at the top of unseg.py:
report = print
def listen(callback):
global report
report = callbackThen replace all print( with report(.
π€ Why? Allows calling code to capture progress updates via callback
π― Checkpoint: git commit -m 'Change print statements to callback invocations' unseg.py
Paste the contents of run.py at the bottom of unseg.py:
Now we have a single file that:
- Defines the library functions
- Includes example usage code
- Can be called as a script
π‘ This makes it easy to test the code standalone before integrating with Appose
Test it again:
pixi run python unseg.pyπ― Checkpoint: git rm -f run.py && git commit -a -m 'Unify all code into one script'
- Simplify
open_imgto only open the image - Move intensity channel selection to main script
π€ Why? Prepares for images coming from Appose
π― Checkpoint:
git commit \
-m 'Split out channel selection logic' \
unseg.py::right::
def open_img(path_to_img):
"""Returns the RGB image (img)"""
return io.imread(path_to_img, plugin="tifffile")
def plot_img(img, tlt='', cmp='gray'):
...
# Path to image
path_to_img = './image/Gallbladder_Normal_Tissue.tif'
# Open and plot the original image
img = open_img(path_to_img)
plot_img(img, tlt='Image')
# Select intensity channels for processing: two channels
# with nuclei (DAPI) and cell membrane (Na+K+ATPase) markers
h = img.shape[0]
w = img.shape[1]
intensity = np.zeros((h,w,2), dtype='float64')
intensity[:,:,0] = img[:,:,2] # Nuclei Marker
intensity[:,:,1] = img[:,:,0] # Cell Membrane MarkerAdd Appose task handling to beginning of the main section:
appose_mode = 'task' in globals()
if appose_mode:
listen(task.update)
else:
from appose.python_worker import Task
task = Task()What this does:
- Checks if running in Appose context (task exists) or standalone
- Connects our
listencallback to Appose'stask.updatemethod - Creates a dummy task object when running outside Appose
π― Checkpoint: git commit -m 'Make script Appose-aware' unseg.py
Modify unseg.py to work in "Appose mode":
-
Get input image from task instead of file:
if appose_mode: image = task.inputs['image'].ndarray() else: image = open_img(path_to_img)
-
Comment out plot calls (or make conditional)
-
Set outputs:
if appose_mode: task.outputs['nuclei'] = mask_nuclei task.outputs['cells'] = mask_cells
π― Checkpoint: git commit -m 'Add case logic for appose mode' unseg.py
Now the exciting part: call Python from Java! π
Launch Fiji β File βΊ New βΊ Script... β Language βΊ Groovy
π€ Why Groovy?
- Full Java compatibility
- Works seamlessly with Fiji
- Simpler syntax than pure Java
Declare inputs & outputs as SciJava script parameters:
#@ Img image
#@output Img nuclei
#@output Img cells
println(image)Save the script as Unseg_Fiji.groovy
π― Checkpoint: git add Unseg_Fiji.groovy && git commit -m 'Start writing the Groovy script'
Embed the pixi.toml configuration:
import org.apposed.appose.Appose
println("== BUILDING ENVIRONMENT ==")
pixiToml = """
# ... paste entire pixi.toml contents here ...
"""
env = Appose.pixi().content(pixiToml).logDebug().build()
println("Environment build complete: ${env.base()}")π― Checkpoint: git commit -m 'Build the Appose environment' Unseg_Fiji.groovy
Load the adapted unseg.py:
// Read in the Python script (TODO: load as resource instead of hardcoding path)
unsegPath = System.getProperty("user.home") + "/Desktop/unseg-fiji/unseg.py"
unsegScript = new File(unsegPath).text
println("Loaded unseg script of length ${unsegScript.length()}")π‘ Later: You could embed the Python code directly in the Groovy script, or package it as a resource
π― Checkpoint: git commit -m 'Load the unseg Python script' Unseg_Fiji.groovy
For copying an ImgLib2 Img to Appose NDArray:
import net.imglib2.appose.ShmImg
imgToAppose = { img ->
ndArray = ShmImg.copyOf(image).ndArray()
println("Copied image into shared memory: ${ndArray.shape()}")
return ndArray
}For wrapping an Appose NDArray as ImgLib2 Img:
import net.imglib2.appose.NDArrays
apposeToImg = { ndarray ->
NDArrays.asArrayImg(ndarray)
}π― Checkpoint: git commit -m 'Add NDArray utility functions' Unseg_Fiji.groovy
Invoke the Python code via an Appose task:
println("== STARTING PYTHON SERVICE ==")
try (python = env.python()) {
inputs = ["ndarray": imgToAppose(image)]
task = python.task(unsegScript, inputs)
.listen { if (it.message) println("[UNSEG] ${it.message}") }
.waitFor()
println("TASK FINISHED: ${task.status}")
if (task.error) println(task.error)
nuclei = apposeToImg(task.outputs["nuclei"])
cells = apposeToImg(task.outputs["cells"])
}
finally {
println("== TERMINATING PYTHON SERVICE ==")
}π― Checkpoint: git commit -m 'Call unseg code via Appose' Unseg_Fiji.groovy
TASK FINISHED: FAILED
Traceback (most recent call last):
File "/home/curtis/.local/share/appose/unseg-fiji/.pixi/envs/default/lib/python3.9/site-packages/appose/python_worker.py", line 145, in _run
exec(compile(block, "<string>", mode="exec"), _globals, binding)
File "<string>", line 1644, in <module>
AttributeError: 'Task' object has no attribute 'inputs'
π‘ HINTS:
- Task inputs are available as variables directly, not
task.inputs - The Groovy script passes a var named
ndarray, notimage - Use
appose.NDArray.ndarray()to obtain anumpy.ndarray - To transform input & output images, these functions are needed β‘οΈ
def flip_img(img):
"""Flips a NumPy array between Java (F_ORDER) and NumPy-friendly (C_ORDER)"""
return np.transpose(img, tuple(reversed(range(img.ndim))))
def share_as_ndarray(img):
"""Copies a NumPy array into a same-sized newly allocated block of shared memory"""
from appose import NDArray
shared = NDArray(str(img.dtype), img.shape)
shared.ndarray()[:] = img
return sharedπ― Checkpoint: git commit -m 'Fix Appose-related bugs in Python code' unseg.py
Common issues:
- Environment build fails β Test build on command line; use
logDebugfunction - Service won't start or Tasks won't run β Use
Service#debugfunction - Python script errors β Test standalone first with
pixi run python unseg.py - Input/output types β Verify Appose can serialize your data types
Debug strategy:
- Test Python code standalone
- Test environment with simple script
- Add Appose integration incrementally
After this workshop:
- Explore Appose examples: https://github.com/apposed/appose
- Try other Python libraries in Fiji
- Integrate Appose into your own projects
- Join the discussion: https://forum.image.sc (tag: appose)
- Contribute back! PRs welcome
Ideas for experimentation:
- Different deep learning models
- R-based statistical analysis
- Custom Python algorithms
- Bidirectional workflows (Python calling Java)
Thank you for participating! π



