Skip to content

Instantly share code, notes, and snippets.

@ctrueden
Last active February 10, 2026 19:29
Show Gist options
  • Select an option

  • Save ctrueden/9a8f7a03b75fb1ac29e38b6c390be196 to your computer and use it in GitHub Desktop.

Select an option

Save ctrueden/9a8f7a03b75fb1ac29e38b6c390be196 to your computer and use it in GitHub Desktop.
Run StarDist 2D in Fiji via Appose using cellcast.
#@ Img image
#@ Double (value=1.0, description="minimum percentile value for normalization") pmin
#@ Double (value=99.8, description="maximum percentile value for normalization") pmax
#@ Double (value=0.479, description="Polygon probability threshold") prob_threshold
#@ Double (value=0.3, description="Non-Maximum Suppression threshold") nms_threshold
#@ Boolean (value=true, description="Set True for GPU inference via WebGPU, False for CPU inference") gpu
#@output Img labels
import org.apposed.appose.Appose
cellcastScript = """
def flip_img(img):
""\"Flips a NumPy array between Java (F_ORDER) and NumPy-friendly (C_ORDER)""\"
import numpy as np
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
import cellcast.models as ccm
labels = ccm.stardist_2d_versatile_fluo.predict(
flip_img(image.ndarray()),
pmin,
pmax,
prob_threshold,
nms_threshold,
gpu,
)
share_as_ndarray(flip_img(labels))
"""
println("== BUILDING ENVIRONMENT ==")
env = Appose.uv().include("cellcast").name("cellcast").logDebug().build()
println("Environment build complete: ${env.base()}")
// Conversion functions: ImgLib2 Img <-> Appose NDArray
imgToAppose = { img ->
ndArray = net.imglib2.appose.ShmImg.copyOf(image).ndArray()
println("Copied image into shared memory: ${ndArray.shape()} DType{${ndArray.dType()}}")
return ndArray
}
apposeToImg = { ndarray ->
net.imglib2.appose.NDArrays.asArrayImg(ndarray)
}
copyImg = { img ->
// Note: We use PlanarImg because the original ImageJ likes them best.
copy = new net.imglib2.img.planar.PlanarImgFactory(img.getType()).create(img.dimensionsAsLongArray())
net.imglib2.util.ImgUtil.copy(img, copy)
return copy
}
// Run the script as an Appose task
println("== STARTING PYTHON SERVICE ==")
try (python = env.python()) {
inputs = [
"image": imgToAppose(image),
"pmin": pmin,
"pmax": pmax,
"prob_threshold": prob_threshold,
"nms_threshold": nms_threshold,
"gpu": gpu,
]
task = python.task(cellcastScript, inputs)
.listen { if (it.message) println("[CELLCAST] ${it.message}") }
.waitFor()
println("TASK FINISHED: ${task.status}")
if (task.error) println(task.error)
labels = copyImg(apposeToImg(task.result()))
// Without the copyImg, imglib2-ij fails to wrap such ArrayImgs to ImagePlus,
// due to ImageProcessorUtils expecting a backing Java primitive array type.
//
// pixels = labels.update( null ).getCurrentStorageArray()
// ^ Pixels is a DirectShortBufferU here -- makes sense; it's shared memory
//
// But then... how the heck does the unseg_fiji plugin work?!
// So for the moment, we just copy it into a non-shm Img. :'-(
}
finally {
println("== TERMINATING PYTHON SERVICE ==")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment