diff --git a/.gitignore b/.gitignore index d1a3e9b..ae177d7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.project /.settings/ /target +/.vscode/ diff --git a/src/main/java/org/scijava/plugins/scripting/python/Main.java b/src/main/java/org/scijava/plugins/scripting/python/Main.java index a3c067d..10f1598 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/Main.java +++ b/src/main/java/org/scijava/plugins/scripting/python/Main.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -26,6 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * #L% */ + package org.scijava.plugins.scripting.python; import org.scijava.script.ScriptREPL; diff --git a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java index cc65a15..4a747dc 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java +++ b/src/main/java/org/scijava/plugins/scripting/python/OptionsPython.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,6 +29,14 @@ package org.scijava.plugins.scripting.python; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.StringJoiner; + import org.scijava.app.AppService; import org.scijava.command.CommandService; import org.scijava.launcher.Config; @@ -38,27 +46,20 @@ import org.scijava.plugin.Menu; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; +import org.scijava.ui.DialogPrompt; +import org.scijava.ui.UIService; import org.scijava.widget.Button; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.LinkedHashMap; -import java.util.Map; +import org.scijava.widget.TextWidget; /** * Options for configuring the Python environment. - * + * * @author Curtis Rueden */ -@Plugin(type = OptionsPlugin.class, menu = { - @Menu(label = MenuConstants.EDIT_LABEL, - weight = MenuConstants.EDIT_WEIGHT, - mnemonic = MenuConstants.EDIT_MNEMONIC), - @Menu(label = "Options", mnemonic = 'o'), - @Menu(label = "Python...", weight = 10), -}) +@Plugin(type = OptionsPlugin.class, menu = { @Menu( + label = MenuConstants.EDIT_LABEL, weight = MenuConstants.EDIT_WEIGHT, + mnemonic = MenuConstants.EDIT_MNEMONIC), @Menu(label = "Options", + mnemonic = 'o'), @Menu(label = "Python...", weight = 10), }) public class OptionsPython extends OptionsPlugin { @Parameter @@ -73,12 +74,28 @@ public class OptionsPython extends OptionsPlugin { @Parameter(label = "Python environment directory", persist = false) private File pythonDir; - @Parameter(label = "Rebuild Python environment", callback = "rebuildEnv") + @Parameter(label = "Conda dependencies", style = TextWidget.AREA_STYLE, + persist = false) + private String condaDependencies; + + @Parameter(label = "Pip dependencies", style = TextWidget.AREA_STYLE, + persist = false) + private String pipDependencies; + + @Parameter(label = "Build Python environment", callback = "rebuildEnv") private Button rebuildEnvironment; - @Parameter(label = "Launch in Python mode", callback = "updatePythonConfig", persist = false) + @Parameter(label = "Launch in Python mode", callback = "updatePythonConfig", + persist = false) private boolean pythonMode; + @Parameter(required = false) + private UIService uiService; + + private boolean initialPythonMode = false; + private String initialCondaDependencies; + private String initialPipDependencies; + // -- OptionsPython methods -- public File getPythonDir() { @@ -124,28 +141,100 @@ public void load() { } if (pythonDir == null) { - // For the default Python directory, try to match the platform string used for Java installations. - final String javaPlatform = System.getProperty("scijava.app.java-platform"); - final String platform = javaPlatform != null ? javaPlatform : - System.getProperty("os.name") + "-" + System.getProperty("os.arch"); - final Path pythonPath = appService.getApp().getBaseDirectory().toPath().resolve("python").resolve(platform); + // For the default Python directory, try to match the platform + // string used for Java installations. + final String javaPlatform = System.getProperty( + "scijava.app.java-platform"); + final String platform = javaPlatform != null ? javaPlatform : System + .getProperty("os.name") + "-" + System.getProperty("os.arch"); + final Path pythonPath = appService.getApp().getBaseDirectory().toPath() + .resolve("python").resolve(platform); pythonDir = pythonPath.toFile(); } + + // Store the initial value of pythonMode for later comparison + initialPythonMode = pythonMode; + + // Populate condaDependencies and pipDependencies from environment.yml + condaDependencies = ""; + pipDependencies = ""; + java.util.Set pipBlacklist = new java.util.HashSet<>(); + pipBlacklist.add("appose-python"); + pipBlacklist.add("pyimagej"); + File envFile = getEnvironmentYamlFile(); + if (envFile.exists()) { + try { + java.util.List lines = java.nio.file.Files.readAllLines(envFile + .toPath()); + boolean inDeps = false, inPip = false; + StringJoiner condaDeps = new StringJoiner("\n"); + StringJoiner pipDeps = new StringJoiner("\n"); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.startsWith("#") || trimmed.isEmpty()) { + // Ignore empty and comment lines + continue; + } + if (trimmed.startsWith("dependencies:")) { + inDeps = true; + continue; + } + if (inDeps && trimmed.startsWith("- pip")) { + inPip = true; + continue; + } + if (inDeps && trimmed.startsWith("- ") && !inPip) { + String dep = trimmed.substring(2).trim(); + if (!dep.equals("pip")) condaDeps.add(dep); + continue; + } + if (inPip && trimmed.startsWith("- ")) { + String pipDep = trimmed.substring(2).trim(); + boolean blacklisted = false; + for (String bad : pipBlacklist) { + if (pipDep.contains(bad)) { + blacklisted = true; + break; + } + } + if (!blacklisted) pipDeps.add(pipDep); + continue; + } + if (inDeps && !trimmed.startsWith("- ") && !trimmed.isEmpty()) + inDeps = false; + if (inPip && (!trimmed.startsWith("- ") || trimmed.isEmpty())) inPip = + false; + } + condaDependencies = condaDeps.toString().trim(); + pipDependencies = pipDeps.toString().trim(); + initialCondaDependencies = condaDependencies; + initialPipDependencies = pipDependencies; + } + catch (Exception e) { + log.debug("Could not read environment.yml: " + e.getMessage()); + } + } } public void rebuildEnv() { - // Use scijava.app.python-env-file system property if present. + File environmentYaml = writeEnvironmentYaml(); + commandService.run(RebuildEnvironment.class, true, "environmentYaml", + environmentYaml, "targetDir", pythonDir); + } + + /** + * Returns the File for the environment.yml, using the system property if set. + */ + private File getEnvironmentYamlFile() { final Path appPath = appService.getApp().getBaseDirectory().toPath(); - File environmentYaml = appPath.resolve("config").resolve("environment.yml").toFile(); - final String pythonEnvFileProp = System.getProperty("scijava.app.python-env-file"); + File environmentYaml = appPath.resolve("config").resolve("environment.yml") + .toFile(); + final String pythonEnvFileProp = System.getProperty( + "scijava.app.python-env-file"); if (pythonEnvFileProp != null) { - environmentYaml = OptionsPython.stringToFile(appPath, pythonEnvFileProp); + environmentYaml = stringToFile(appPath, pythonEnvFileProp); } - - commandService.run(RebuildEnvironment.class, true, - "environmentYaml", environmentYaml, - "targetDir", pythonDir - ); + return environmentYaml; } @Override @@ -175,6 +264,66 @@ public void save() { // Proceed gracefully if config file cannot be written. log.debug(exc); } + + if (pythonMode && (pythonDir == null || !pythonDir.exists())) { + rebuildEnv(); + } + else { + writeEnvironmentYaml(); + } + // Warn the user if pythonMode was just enabled and wasn't before + if (!initialPythonMode && pythonMode && uiService != null) { + String msg = + "You have just enabled Python mode. Please restart for these changes to take effect! (after your python environment initializes, if needed)\n\n" + + "If Fiji fails to start, try deleting your configuration file and restarting.\n\nConfiguration file: " + + configFile; + uiService.showDialog(msg, "Python Mode Enabled", + DialogPrompt.MessageType.WARNING_MESSAGE); + } + } + + private File writeEnvironmentYaml() { + File envFile = getEnvironmentYamlFile(); + + // skip writing if nothing has changed + if (initialCondaDependencies.equals(condaDependencies) && + initialPipDependencies.equals(pipDependencies)) return envFile; + + // Update initial dependencies to detect future changes + initialCondaDependencies = condaDependencies; + initialPipDependencies = pipDependencies; + + // Write environment.yml from condaDependencies and pipDependencies + try { + String name = "fiji"; + String[] channels = { "conda-forge" }; + String pyimagej = "pyimagej>=1.7.0"; + String apposePython = + "git+https://github.com/apposed/appose-python.git@efe6dadb2242ca45820fcbb7aeea2096f99f9cb2"; + StringBuilder yml = new StringBuilder(); + yml.append("name: ").append(name).append("\nchannels:\n"); + for (String ch : channels) + yml.append(" - ").append(ch).append("\n"); + yml.append("dependencies:\n"); + for (String dep : condaDependencies.split("\n")) { + String trimmed = dep.trim(); + if (!trimmed.isEmpty()) yml.append(" - ").append(trimmed).append("\n"); + } + yml.append(" - pip\n"); + yml.append(" - pip:\n"); + for (String dep : pipDependencies.split("\n")) { + String trimmed = dep.trim(); + if (!trimmed.isEmpty()) yml.append(" - ").append(trimmed).append( + "\n"); + } + yml.append(" - ").append(pyimagej).append("\n"); + yml.append(" - ").append(apposePython).append("\n"); + java.nio.file.Files.write(envFile.toPath(), yml.toString().getBytes()); + } + catch (Exception e) { + log.debug("Could not write environment.yml: " + e.getMessage()); + } + return envFile; } // -- Utility methods -- @@ -195,8 +344,8 @@ static File stringToFile(Path baseDir, String value) { */ static String fileToString(Path baseDir, File file) { Path filePath = file.toPath(); - Path relPath = filePath.startsWith(baseDir) ? - baseDir.relativize(filePath) : filePath.toAbsolutePath(); + Path relPath = filePath.startsWith(baseDir) ? baseDir.relativize(filePath) + : filePath.toAbsolutePath(); return relPath.toString(); } } diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java index 751bdee..8abb24b 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptEngine.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -44,6 +44,7 @@ import javax.script.ScriptException; import org.scijava.Context; +import org.scijava.command.CommandService; import org.scijava.log.LogService; import org.scijava.object.ObjectService; import org.scijava.plugin.Parameter; @@ -61,10 +62,13 @@ public class PythonScriptEngine extends AbstractScriptEngine { @Parameter private ObjectService objectService; - + @Parameter private LogService logService; - + + @Parameter + private CommandService commandService; + public PythonScriptEngine(final Context context) { context.inject(this); setLogService(logService); @@ -78,24 +82,29 @@ public Object eval(final String script) throws ScriptException { .filter(obj -> "PythonScriptRunner".equals(objectService.getName(obj)))// .findFirst(); if (!pythonScriptRunner.isPresent()) { + // Try to help user by running OptionsPython plugin + if (commandService != null) { + commandService.run(OptionsPython.class, true); + } throw new IllegalStateException(// - "The PythonScriptRunner could not be found in the ObjectService. To use the\n" + - "Python script engine, you must call scyjava.enable_scijava_scripting(context)\n" + - "with this script engine's associated SciJava context before using it."); + "The PythonScriptRunner could not be found.\n" + + "To use the Python script engine, you must launch your application in Python mode."); } - return pythonScriptRunner.get().apply(new Args(script, engineScopeBindings, scriptContext)); + return pythonScriptRunner.get().apply(new Args(script, engineScopeBindings, + scriptContext)); } @Override public Object eval(Reader reader) throws ScriptException { StringBuilder buf = new StringBuilder(); - char [] cbuf = new char [65536]; + char[] cbuf = new char[65536]; while (true) { try { int nChars = reader.read(cbuf); if (nChars <= 0) break; buf.append(cbuf, 0, nChars); - } catch (IOException e) { + } + catch (IOException e) { throw new ScriptException(e); } } @@ -106,8 +115,8 @@ public Object eval(Reader reader) throws ScriptException { public Bindings createBindings() { return new ScriptBindings(); } - - //Somehow just type casting did not work... + + // Somehow just type casting did not work... private static class ScriptBindings implements Bindings { private Map bindingsMap; @@ -178,11 +187,14 @@ public Object remove(Object key) { } private static class Args { + public final String script; public final Map vars; public final ScriptContext scriptContext; - public Args(final String script, final Map vars, final ScriptContext scriptContext) { + public Args(final String script, final Map vars, + final ScriptContext scriptContext) + { this.script = script; this.vars = vars; this.scriptContext = scriptContext; diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java index 3896bb6..f1c4417 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptLanguage.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -31,6 +31,7 @@ import java.util.Arrays; import java.util.List; + import javax.script.ScriptEngine; import org.scijava.Priority; @@ -39,13 +40,14 @@ import org.scijava.script.ScriptLanguage; /** - * An adapter for Python (scyjava) to the SciJava scripting interface. + * An adapter for Python (pyimagej) to the SciJava scripting interface. * * @author Curtis Rueden * @author Karl Duderstadt * @see ScriptEngine */ -@Plugin(type = ScriptLanguage.class, name = "Python (scyjava)", priority = Priority.VERY_LOW) +@Plugin(type = ScriptLanguage.class, name = "Python (pyimagej)", + priority = Priority.VERY_LOW) public class PythonScriptLanguage extends AbstractScriptLanguage { @Override @@ -62,5 +64,5 @@ public List getExtensions() { public ScriptEngine getScriptEngine() { return new PythonScriptEngine(getContext()); } - + } diff --git a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java index ee7e413..d9452df 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java +++ b/src/main/java/org/scijava/plugins/scripting/python/PythonScriptSyntaxHighlighter.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -38,9 +38,9 @@ * * @author Karl Duderstadt */ -@Plugin(type = SyntaxHighlighter.class, name = "python-(scyjava)") +@Plugin(type = SyntaxHighlighter.class, name = "python-(pyimagej)") public class PythonScriptSyntaxHighlighter extends PythonTokenMaker implements -SyntaxHighlighter + SyntaxHighlighter { // Everything implemented in PythonTokenMaker } diff --git a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java index 6a61a63..ebc3147 100644 --- a/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java +++ b/src/main/java/org/scijava/plugins/scripting/python/RebuildEnvironment.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -29,6 +29,13 @@ package org.scijava.plugins.scripting.python; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + import org.apposed.appose.Appose; import org.apposed.appose.Builder; import org.scijava.app.AppService; @@ -37,17 +44,12 @@ import org.scijava.log.Logger; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.stream.Stream; +import org.scijava.ui.DialogPrompt; +import org.scijava.ui.UIService; /** * SciJava command wrapper to build a Python environment. - * + * * @author Curtis Rueden */ @Plugin(type = Command.class, label = "Rebuild Python environment") @@ -65,13 +67,31 @@ public class RebuildEnvironment implements Command { @Parameter(label = "Target directory") private File targetDir; + @Parameter(required = false) + private UIService uiService; + // -- OptionsPython methods -- @Override public void run() { final File backupDir = new File(targetDir.getPath() + ".old"); + if (targetDir.exists()) { + boolean confirmed = true; + if (uiService != null) { + String msg = + "The environment directory already exists. If you continue, it will be renamed to '" + + backupDir.getName() + + "' (and any previous backup will be deleted). Continue?"; + DialogPrompt.Result result = uiService.showDialog(msg, + "Confirm Environment Rebuild", + DialogPrompt.MessageType.QUESTION_MESSAGE, + DialogPrompt.OptionType.YES_NO_OPTION); + confirmed = result == DialogPrompt.Result.YES_OPTION; + } + if (!confirmed) return; + } + // Delete the previous backup environment recursively. if (backupDir.exists()) { - // Delete the previous backup environment recursively. try (Stream x = Files.walk(backupDir.toPath())) { x.sorted(Comparator.reverseOrder()).forEach(p -> { try { @@ -90,22 +110,35 @@ public void run() { if (targetDir.exists()) targetDir.renameTo(backupDir); // Build the new environment. try { - Builder builder = Appose - .file(environmentYaml, "environment.yml") - .subscribeOutput(this::report) - .subscribeError(this::report) - .subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / max)); - System.err.println("Building Python environment"); // HACK: stderr stream triggers console window show. + Builder builder = Appose.file(environmentYaml, "environment.yml") + .subscribeOutput(this::reportMsg).subscribeError(this::reportErr) + .subscribeProgress((msg, cur, max) -> Splash.update(msg, (double) cur / + max)); + + // HACK: stderr stream triggers console window show. + System.err.println(); + log.info("Building Python environment"); Splash.show(); builder.build(targetDir); + // Notify user of success + if (uiService != null) { + uiService.showDialog( + "Python environment setup was successful and is ready to use!", + "Environment Ready", DialogPrompt.MessageType.INFORMATION_MESSAGE); + } } catch (IOException exc) { log.error("Failed to build Python environment", exc); } } - private void report(String s) { + private void reportErr(String s) { + if (s.isEmpty()) System.err.print("."); + else log.error(s); + } + + private void reportMsg(String s) { if (s.isEmpty()) System.err.print("."); - else System.err.print(s); + else log.info(s); } } diff --git a/src/main/resources/script_templates/PyImageJ/CellposeStarDistSegmentation.py b/src/main/resources/script_templates/PyImageJ/CellposeStarDistSegmentation.py new file mode 100644 index 0000000..09e8cc7 --- /dev/null +++ b/src/main/resources/script_templates/PyImageJ/CellposeStarDistSegmentation.py @@ -0,0 +1,140 @@ +#@ ImageJ ij + +''' +Note that this script requires a Python environment that includes StarDist and Cellpose +StarDist currently only supports NumPy 1.x, which necessitates using TensorFlow 2.15 or earlier +TensorFlow 2.15 itself requires python 3.11 or earlier + +You can rebuild your Python environment by using: +Edit > Options > Python… + +The following configuration was used to develop this script: + +--Conda dependencies-- +python=3.11 +numpy=1.26.4 + +--Pip dependencies-- +tensorflow==2.15 +cellpose==4.0.6 +stardist==0.9.0 +csbdeep==0.8.0 +''' + +import sys +import imagej.convert as convert +import numpy as np +import matplotlib.pyplot as plt +from cellpose import models +from csbdeep.utils import normalize +from stardist.models import StarDist2D +import scyjava as sj + +def filter_index_image(narr:np.ndarray, min_size:int, max_size:int): + """ + Filter an index image's labels with a pixel size range. + """ + unique = np.unique(narr) + for label in unique: + if label == 0: + # skip the background + continue + + # create a crop for each label + bbox = get_bounding_box(np.where(narr == label)) + bbox_crop = narr[bbox[0]:bbox[2] + 1, bbox[1]:bbox[3] + 1].copy() + bbox_crop[bbox_crop != label] = 0 + + # get the number of pixels in label + bbox_crop = bbox_crop.astype(bool) + label_size = np.sum(bbox_crop) + + if not min_size <= label_size <= max_size: + narr[narr == label] = 0 + + return narr + +def get_bounding_box(indices: np.ndarray): + """ + Get the bounding box coordinates from a the label indices. + """ + # get min and max bounds of indices array + min_row = np.min(indices[0]) + min_col = np.min(indices[1]) + max_row = np.max(indices[0]) + max_col = np.max(indices[1]) + + return (min_row, min_col, max_row, max_col) + +# open image data and convert to Python from Java +#TODO does this connection need to be closed? +data = ij.io().open('https://media.imagej.net/pyimagej/3d/hela_a3g.tif') +xdata = ij.py.from_java(data) + +# show the first channel +ij.ui().show("nucleus", ij.py.to_java(xdata[:, :, 0])) + +# show the second channel +ij.ui().show("cytoplasm", ij.py.to_java(xdata[:, :, 1] * 125)) + +# run StarDist on nuclei channel +model = StarDist2D.from_pretrained('2D_versatile_fluo') +nuc_labels, _ = model.predict_instances(normalize(xdata[:, :, 0])) + +# run Cellpose on cytoplasm (grayscale) +model = models.CellposeModel(gpu=False, model_type='cyto') +ch = [0, 0] +cyto_labels = model.eval(xdata[:, :, 1].data, channels=ch, diameter=72.1) + +# show the stardist results +ij.ui().show("StarDist results", ij.py.to_java(nuc_labels)) +ij.IJ.run("mpl-viridis", ""); + +# show the second channel +ij.ui().show("Cellpose results", ij.py.to_java(cyto_labels[0])) +ij.IJ.run("mpl-viridis", ""); + +# filter the stardist results and display +filter_index_image(nuc_labels, 500, 10000) +ij.ui().show("StarDist filtered", ij.py.to_java(nuc_labels)) +ij.IJ.run("mpl-viridis", ""); + +# ensure ROI Manager exists +rm = ij.RoiManager.getInstance() +if rm is None: + ij.IJ.run("ROI Manager...") + rm = ij.RoiManager.getInstance() + +# Reset the ROI manager +rm.reset() + +# convert to ImgLib2 ROI in a ROITree +nuc_roi_tree = convert.index_img_to_roi_tree(ij, nuc_labels) +cyto_roi_tree = convert.index_img_to_roi_tree(ij, cyto_labels[0]) + +# print the contents of the ROITree (nuclear ROIs) +len(nuc_roi_tree.children()) +for i in range(len(nuc_roi_tree.children())): + print(nuc_roi_tree.children().get(i).data()) + +# display the input data, select channel 2 and enhance the contrast +data_title = "hela_a3g.tif" +ij.ui().show(data_title, data) +imp = ij.WindowManager.getImage(data_title) +imp.setC(2) +ij.IJ.run(imp, "Enhance Contrast", "saturated=0.35") + +# convert a single ImgLib2 roi to a legacy ImageJ ROI with the ConvertService. +imglib_polygon_roi = nuc_roi_tree.children().get(0).data() +ij_polygon_roi = ij.convert().convert(imglib_polygon_roi, sj.jimport('ij.gui.PolygonRoi')) +print(type(ij_polygon_roi)) + +# convert index images to ImageJ ROI in RoiManager +#TODO any way to color the selections? We can use Colors... but it appears to be global and the last one run wins +#ij.IJ.run(imp, "Colors...", "foreground=blue background=black selection=red"); +convert.index_img_to_roi_manager(ij, nuc_labels) +convert.index_img_to_roi_manager(ij, cyto_labels[0]) + +#TODO this pops an unnecessary display at the end but if I don't make it the last line the ROIs don't show +rm.moveRoisToOverlay(imp) +rm.runCommand(imp, "Show All") \ No newline at end of file