pyimagej
pyimagej copied to clipboard
ImageJ Ops threshold results do not work with `3D Object Counter`
While working on some workflows using the 3D Object Counter
I discovered a bug where 8-bit binary images created from ImageJ Ops (i.e. the output from a threshold op) and converted to ImagePlus
type (needed for the plugin) fail to produce any object detections. If the threshold is applied via the original ImageJ thresholder the plugin works as expected.
As I see it there are at least two potential sources of this bug.
- The
3D Object Counter
plugin is looking/doing something unusual with theImagePlus
passed to it that the convertedImagePlus
from ImageJ Ops is missing. - 8-bit binary images are not converting properly to
ImagePlus
type (i.e. something larger is wrong).
If option (1) is at play here, then I’d like to know what the 3D Object Counter
is looking for. I’ve viewed the source a few times and I can’t see anything obviously unusual about what its doing. This plugin is super useful and it would be great to get it to work correctly with ImageJ Ops outputs.
If its option (2), the bug could be impacting other operations that use ImagePlus
thresholds. This makes using ImageJ Ops to apply thresholds tricky if some plugins act like the 3D Object Counter
with converted ImagePlus
images. I haven’t tried yet, but the next thing to do is perhaps compare the ImagePlus
object produced by both approaches.
Also I’m not exactly sure if this repository is the best place for this issue, it might better belong in SciJava
where the ConvertService
lives.
Minimal example
Here is a minimal example demonstrating this behavior. The process_imagej_thres
method works as expected, producing a results table and several output images. The process_op_thres
fails with an empty table and empty image outputs (of the correct shape).
You can download the sample data for this minimal example from my sample-data repository here: sample data
You can probably use other data, but this workflow was intended for zstacks of dots/puncta.
import imagej
import code
# initialize PyImageJ
ij = imagej.init("sc.fiji:fiji", mode="interactive")
print(f"ImageJ version: {ij.getVersion()}")
# load data
dataset = ij.io().open("data/vif_zstack_100x.tif")
def pre_process(dataset: "net.imagej.Dataset"):
dataset = ij.op().convert().int32(dataset)
dataset_blur = ij.op().run("filter.gauss", dataset, 3)
return dataset - dataset_blur
def process_imagej_thres(dataset: "net.imagej.Dataset"):
# apply triangle threshold and fill holes
imp = ij.py.to_imageplus(dataset)
ij.IJ.setAutoThreshold(imp, "Triangle")
ij.IJ.run(imp, "Convert to Mask", "method=Triangle background=Light calculate black")
imp = ij.WindowManager.getCurrentImage()
imp.setTitle("mask-imagej")
ij.IJ.run(imp, "Fill Holes", "stack")
# reorder stack and run plugin
ij.IJ.run("Re-order Hyperstack ...", "channels=[Slices (z)] slices=[Channels (c)] frames=[Frames (t)]")
ij.IJ.run(ij.WindowManager.getCurrentImage(), "3D Objects Counter", "threshold=128 slice=30 min.=25 max.=15250000 exclude_objects_on_edges objects surfaces statistics")
def process_ops_thres(dataset: "net.imagej.Dataset"):
# apply triangle threshold and fill holes
dataset_mask = ij.op().run("threshold.triangle", dataset)
dataset_mask = ij.op().run("morphology.fillHoles", dataset_mask)
dataset_mask_i = ij.dataset().create(dataset_mask)
ij.op().run("image.invert", dataset_mask_i, dataset_mask)
# convert to ImagePlus and activate image
imp = ij.py.to_imageplus(dataset_mask_i)
imp.show()
imp = ij.WindowManager.getCurrentImage()
imp.setTitle("mask-imglib")
# set threshold on imp
ip = imp.getProcessor()
ip.setAutoThreshold("Triangle", True, ip.RED_LUT)
imp.updateAndDraw()
# reorder stack and run plugin
ij.IJ.run("Re-order Hyperstack ...", "channels=[Slices (z)] slices=[Channels (c)] frames=[Frames (t)]")
ij.IJ.run(ij.WindowManager.getCurrentImage(), "3D Objects Counter", "threshold=128 slice=30 min.=25 max.=15250000 exclude_objects_on_edges objects surfaces statistics")
# launch
image = pre_process(dataset)
process_imagej_thres(image)
process_ops_thres(image)
# return REPL
code.interact(local=locals())
The output of the Ops commands is a virtual stack, isn't it? Maybe that's a notable difference. Did you try if a run("Duplicate...", "")
or imp.duplicate()
or similar helps when inserted after the conversion to ImagePlus
?