promptList) {
107 | SwingUtilities.invokeLater(() -> promptList.stream()
108 | .map(rect -> new Rectangle(rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]))
109 | .forEach(roi -> MACRO_CONSUMER.deleteRectRoi(roi)));
110 | }
111 |
112 | };
113 |
114 | static
115 | {
116 | // IMPORTANT: Set the DEFAULT DIR WHERE MODELS ARE DOWNLOADED TO THE WANTED DIR.
117 | // IF NOT IN MACS IT WILL DEFAULT TO "/" AND RAISE AN ERROR
118 | SamEnvManagerAbstract.DEFAULT_DIR = Constants.FIJI_FOLDER + File.separator + "appose_"
119 | + ((!PlatformDetection.isMacOS() || !PlatformDetection.isUsingRosseta()) ? PlatformDetection.getArch()
120 | : PlatformDetection.ARCH_ARM64 );
121 | }
122 |
123 | // TODO I (Carlos) don't know how to develop in IJ2 @Parameter
124 | //private LogService logService = new LogService();
125 |
126 | /**
127 | * Run the plugin
128 | * @throws InterruptedException if there is any thread interruption error
129 | * @throws IOException if there is any file error
130 | */
131 | public void run() throws IOException, InterruptedException {
132 | if (Recorder.record)
133 | Recorder.recordString(MACRO_RECORD_COMMENT);
134 |
135 | // TODO I (Carlos) don't know how to develop in IJ2 final Logger log = logService.subLogger("SAMJ");
136 | try {
137 |
138 | // TODO I (Carlos) don't know how to develop in IJ2 Logger guiSublogger = log.subLogger("PromptsResults window");
139 | SAMJLogger guilogger = new SAMJLogger() {
140 | @Override
141 | public void info(String text) {System.out.println(text);}
142 | @Override
143 | public void warn(String text) {System.out.println(text);}
144 | @Override
145 | public void error(String text) {System.out.println(text);}
146 | };
147 |
148 | // TODO I (Carlos) don't know how to develop in IJ2 Logger networkSublogger = log.subLogger("Networks window");
149 | SAMJLogger networkLogger = new SAMJLogger() {
150 | @Override
151 | public void info(String text) {System.out.println("network -- " + text);}
152 | @Override
153 | public void warn(String text) {System.out.println("network -- " + text);}
154 | @Override
155 | public void error(String text) {System.out.println("network -- " + text);}
156 | };
157 |
158 | SwingUtilities.invokeLater(() -> {
159 | MainGUI samjDialog = new MainGUI(new Consumer());
160 | GUI.center(samjDialog);
161 | });
162 | } catch (RuntimeException e) {
163 | e.printStackTrace();
164 | }
165 | }
166 |
167 | /**
168 | * method for tesitng during development
169 | * @param args
170 | * nothing
171 | * @throws InterruptedException if there is any thread related error
172 | * @throws IOException if there is any file related error
173 | */
174 | public static void main(String[] args) throws IOException, InterruptedException {
175 | ImageJ ij = new ImageJ();
176 | new SAMJ_Annotator().run();
177 | }
178 |
179 |
180 | @Override
181 | public void run(String arg) {
182 | boolean isMacro = IJ.isMacro();
183 | boolean isHeadless = GraphicsEnvironment.isHeadless();
184 | try {
185 | if (isMacro) {
186 | runMacro();
187 | } else if (isHeadless) {
188 | } else {
189 | run();
190 | }
191 | } catch (IOException | InterruptedException e) {
192 | e.printStackTrace();
193 | }
194 | }
195 |
196 | /**
197 | * Processes an image using SAM2 Tiny to generate segmentation masks based on provided prompts.
198 | * This method is suitable for scripting within ImageJ/Fiji environments.
199 | * If you want to use another SAM variant use {@link #samJReturnContours(SAMModel, RandomAccessibleInterval, List, List)}
200 | *
201 | * The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval}
202 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating
203 | * regions or points of interest.
204 | *
205 | * @param
206 | * the ImgLib2 data type of the input image
207 | * @param rai
208 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C)
209 | * @param pointPrompts
210 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos}
211 | * @param rectPrompts
212 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height}
213 | * @return
214 | * a {@link RandomAccessibleInterval} of type {@link UnsignedShortType} containing segmentation masks corresponding to each provided prompt
215 | * @throws IOException
216 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly
217 | * @throws RuntimeException
218 | * if an error occurs during the segmentation process
219 | * @throws InterruptedException
220 | * if the segmentation process is unexpectedly interrupted
221 | */
222 | public static < T extends RealType< T > & NativeType< T > >
223 | RandomAccessibleInterval samJReturnMask(RandomAccessibleInterval rai,
224 | List pointPrompts,
225 | List rectPrompts) throws IOException, RuntimeException, InterruptedException {
226 | return samJReturnMask(new SAM2Tiny(), rai, pointPrompts, rectPrompts);
227 | }
228 |
229 | /**
230 | * Processes an image using a Segment Anything Model (SAM) variant to generate segmentation masks based on provided prompts.
231 | * This method is suitable for scripting within ImageJ/Fiji environments.
232 | *
233 | * The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval}
234 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating
235 | * regions or points of interest.
236 | *
237 | * @param
238 | * the ImgLib2 data type of the input image
239 | * @param model
240 | * the SAM model instance used for segmentation (e.g., SAM2Tiny, SAM2Small, SAM2Large, EfficientSAM).
241 | * Example instantiation:
242 | * {@code
243 | * import ai.nets.samj.communication.model.SAM2Tiny;
244 | * SAMModel model = new SAM2Tiny();
245 | * }
246 | * @param rai
247 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C)
248 | * @param pointPrompts
249 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos}
250 | * @param rectPrompts
251 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height}
252 | * @return
253 | * a {@link RandomAccessibleInterval} of type {@link UnsignedShortType} containing segmentation masks corresponding to each provided prompt
254 | * @throws IOException
255 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly
256 | * @throws RuntimeException
257 | * if an error occurs during the segmentation process
258 | * @throws InterruptedException
259 | * if the segmentation process is unexpectedly interrupted
260 | */
261 | public static < T extends RealType< T > & NativeType< T > >
262 | RandomAccessibleInterval samJReturnMask(SAMModel model, RandomAccessibleInterval rai,
263 | List pointPrompts,
264 | List rectPrompts) throws IOException, RuntimeException, InterruptedException {
265 | List masks = samJReturnContours(model, rai, pointPrompts, rectPrompts);
266 | return Mask.getMask(rai.dimensionsAsLongArray()[0], rai.dimensionsAsLongArray()[1], masks);
267 | }
268 |
269 | /**
270 | * Processes an image using a SAM2 Tiny to generate segmentation contours based on provided prompts.
271 | * This method is suitable for scripting within ImageJ/Fiji environments.
272 | * If you want to use another SAM variant use {@link #samJReturnContours(SAMModel, RandomAccessibleInterval, List, List)}
273 | *
274 | * The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval}
275 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating
276 | * regions or points of interest.
277 | *
278 | * @param
279 | * the ImgLib2 data type of the input image
280 | * @param rai
281 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C)
282 | * @param pointPrompts
283 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos}
284 | * @param rectPrompts
285 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height}
286 | * @return
287 | * a List of {@link Mask} that contain the polygons that define the contour of each of the segmented objects.
288 | * @throws IOException
289 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly
290 | * @throws RuntimeException
291 | * if an error occurs during the segmentation process
292 | * @throws InterruptedException
293 | * if the segmentation process is unexpectedly interrupted
294 | */
295 | public static < T extends RealType< T > & NativeType< T > >
296 | List samJReturnContours(RandomAccessibleInterval rai, List pointPrompts, List rectPrompts) throws IOException, RuntimeException, InterruptedException {
297 | return samJReturnContours(new SAM2Tiny(), rai, pointPrompts, rectPrompts);
298 | }
299 |
300 | /**
301 | * Processes an image using a Segment Anything Model (SAM) variant to generate segmentation contours based on provided prompts.
302 | * This method is suitable for scripting within ImageJ/Fiji environments.
303 | *
304 | * The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval}
305 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating
306 | * regions or points of interest.
307 | *
308 | * @param
309 | * the ImgLib2 data type of the input image
310 | * @param model
311 | * the SAM model instance used for segmentation (e.g., SAM2Tiny, SAM2Small, SAM2Large, EfficientSAM).
312 | * Example instantiation:
313 | * {@code
314 | * import ai.nets.samj.communication.model.SAM2Tiny;
315 | * SAMModel model = new SAM2Tiny();
316 | * }
317 | * @param rai
318 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C)
319 | * @param pointPrompts
320 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos}
321 | * @param rectPrompts
322 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height}
323 | * @return
324 | * a List of {@link Mask} that contain the polygons that define the contour of each of the segmented objects.
325 | * @throws IOException
326 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly
327 | * @throws RuntimeException
328 | * if an error occurs during the segmentation process
329 | * @throws InterruptedException
330 | * if the segmentation process is unexpectedly interrupted
331 | */
332 | public static < T extends RealType< T > & NativeType< T > >
333 | List samJReturnContours(SAMModel model, RandomAccessibleInterval rai, List pointPrompts, List rectPrompts) throws IOException, RuntimeException, InterruptedException {
334 | if ((pointPrompts == null || pointPrompts.size() == 0) && (rectPrompts == null || rectPrompts.size() == 0))
335 | throw new IllegalArgumentException("Please provide at least one point prompt or rectangular prompt.");
336 | if (MACRO_CONSUMER == null)
337 | MACRO_CONSUMER = new Consumer();
338 | SAMModel selected = MainGUI.DEFAULT_MODEL_LIST.stream()
339 | .filter(mm -> mm.getName().equals(model.getName())).findFirst().orElse(null);
340 | if (selected == null)
341 | throw new IllegalArgumentException("Specified model does not exist. Please, for more info visit: "
342 | + MACRO_INFO);
343 | selected.setImage(rai, null);
344 | selected.setReturnOnlyBiggest(true);
345 | RandomAccessibleInterval maskRai = null;
346 | List callbackedContours = new ArrayList();
347 | BatchCallback callback = new BatchCallback() {
348 | @Override
349 | public void setTotalNumberOfRois(int nRois) {}
350 | @Override
351 | public void updateProgress(int n) {}
352 | @Override
353 | public void drawRoi(List masks) {
354 | callbackedContours.addAll(masks);
355 | }
356 | @Override
357 | public void deletePointPrompt(List promptList) {}
358 | @Override
359 | public void deleteRectPrompt(List promptList) {}
360 |
361 | };
362 | List contours = selected.processBatchOfPrompts(pointPrompts, rectPrompts, maskRai, callback);
363 | callbackedContours.addAll(contours);
364 |
365 |
366 | selected.closeProcess();
367 | return callbackedContours;
368 | }
369 |
370 | private void runMacro() throws IOException, RuntimeException, InterruptedException {
371 | if (Macro.getOptions() == null)
372 | return;
373 | parseCommand();
374 |
375 | if (macroModel == null && macroExport.equals("true")) {
376 | macroExport();
377 | } else if (macroModel != null && macroExport.equals("true")) {
378 | macroRunSAMJ();
379 | macroExport();
380 | } else if (macroModel != null ) {
381 | macroRunSAMJ();
382 | }
383 | }
384 |
385 | private void macroExport() {
386 | if (MACRO_CONSUMER == null)
387 | throw new IllegalArgumentException("In order to be able to export annotations to mask, "
388 | + "some annotations with the SAMJ Macro should have been done first.");
389 | MACRO_CONSUMER.exportImageLabeling();
390 | }
391 |
392 | private < T extends RealType< T > & NativeType< T > >
393 | void macroRunSAMJ() throws IOException, RuntimeException, InterruptedException {
394 | if (MACRO_CONSUMER == null)
395 | MACRO_CONSUMER = new Consumer();
396 | MACRO_CONSUMER.setFocusedImage(MACRO_CONSUMER.getFocusedImage());
397 | SAMModel selected = MainGUI.DEFAULT_MODEL_LIST.stream()
398 | .filter(mm -> mm.getName().equals(macroModel)).findFirst().orElse(null);
399 | if (selected == null)
400 | throw new IllegalArgumentException("Specified model does not exist. Please, for more info visit: "
401 | + MACRO_INFO);
402 | MACRO_CONSUMER.setModel(selected);
403 | RandomAccessibleInterval rai = MACRO_CONSUMER.getFocusedImageAsRai();
404 | selected.setImage(rai, null);
405 | selected.setReturnOnlyBiggest(true);
406 | List pointPrompts = MACRO_CONSUMER.getPointRoisOnFocusImage();
407 | List rectPrompts = MACRO_CONSUMER.getRectRoisOnFocusImage();
408 | RandomAccessibleInterval maskRai = null;
409 | if (macroMaskPrompt != null)
410 | maskRai = null;
411 | selected.processBatchOfPrompts(pointPrompts, rectPrompts, maskRai, MACRO_CALLBACK);
412 | pointPrompts.stream().forEach(pp -> MACRO_CONSUMER.deletePointRoi(pp));
413 | rectPrompts.stream().forEach(pp -> MACRO_CONSUMER.deleteRectRoi(pp));
414 |
415 | selected.closeProcess();
416 | }
417 |
418 | private void parseCommand() {
419 | String macroArg = Macro.getOptions();
420 |
421 | macroModel = parseArg(macroArg, macroOptionalKeys[0], false);
422 | macroMaskPrompt = parseArg(macroArg, macroOptionalKeys[1], false);
423 | macroExport = parseArg(macroArg, macroOptionalKeys[2], false);
424 | if (macroModel == null && macroExport == null)
425 | throw new IllegalArgumentException("SAMJ macro requires the parameter 'model' to be"
426 | + " specified to make annotations or the parameter 'export' if the "
427 | + "user wants to export already annotated masks. More info at: " + MACRO_INFO);
428 | if (macroExport == null)
429 | macroExport = "false";
430 | macroExport = macroExport.toLowerCase().equals("false") ? "false" : macroExport;
431 | macroExport = macroExport.toLowerCase().equals("true") ? "true" : macroExport;
432 | if (!macroExport.equals("false") && !macroExport.equals("true"))
433 | throw new IllegalArgumentException("The SAMJ macro argument 'export' can only be true or false."
434 | + " For more info: " + MACRO_INFO);
435 | }
436 |
437 | private static String parseArg(String macroArg, String arg, boolean required) {
438 | String value = Macro.getValue(macroArg, arg, null);
439 | if (value != null && value.equals(""))
440 | value = null;
441 | if (value == null && required)
442 | throw new IllegalArgumentException("SAMJ macro requires to the variable '" + arg + "'. "
443 | + "For more info, please visit: " + MACRO_INFO);
444 | return value;
445 | }
446 | }
447 |
--------------------------------------------------------------------------------
/src/main/java/ai/nets/samj/ij/ui/Consumer.java:
--------------------------------------------------------------------------------
1 | package ai.nets.samj.ij.ui;
2 |
3 | import java.awt.Color;
4 | import java.awt.Component;
5 | import java.awt.Polygon;
6 | import java.awt.Rectangle;
7 | import java.awt.event.KeyEvent;
8 | import java.awt.event.KeyListener;
9 | import java.awt.event.MouseEvent;
10 | import java.awt.event.MouseListener;
11 | import java.awt.event.WindowEvent;
12 | import java.awt.event.WindowListener;
13 | import java.lang.reflect.InvocationTargetException;
14 | import java.util.ArrayList;
15 | import java.util.Arrays;
16 | import java.util.Enumeration;
17 | import java.util.Iterator;
18 | import java.util.List;
19 | import java.util.Stack;
20 | import java.util.stream.Collectors;
21 | import java.util.stream.IntStream;
22 |
23 | import javax.swing.DefaultListModel;
24 | import javax.swing.JList;
25 | import javax.swing.JScrollPane;
26 | import javax.swing.JViewport;
27 | import javax.swing.event.ListDataEvent;
28 | import javax.swing.event.ListDataListener;
29 |
30 | import ai.nets.samj.annotation.Mask;
31 | import ai.nets.samj.gui.components.ComboBoxItem;
32 | import ai.nets.samj.ij.ui.commands.AddRoiCommand;
33 | import ai.nets.samj.ij.ui.commands.Command;
34 | import ai.nets.samj.ij.ui.commands.DeleteRoiCommand;
35 | import ai.nets.samj.ij.utils.RoiManagerPrivateViolator;
36 | import ai.nets.samj.models.AbstractSamJ;
37 | import ai.nets.samj.ui.ConsumerInterface;
38 | import ij.IJ;
39 | import ij.IJEventListener;
40 | import ij.ImageListener;
41 | import ij.ImagePlus;
42 | import ij.Prefs;
43 | import ij.WindowManager;
44 | import ij.gui.ImageCanvas;
45 | import ij.gui.ImageWindow;
46 | import ij.gui.Overlay;
47 | import ij.gui.PointRoi;
48 | import ij.gui.PolygonRoi;
49 | import ij.gui.Roi;
50 | import ij.gui.Toolbar;
51 | import ij.plugin.CompositeConverter;
52 | import ij.plugin.OverlayLabels;
53 | import ij.plugin.frame.Recorder;
54 | import ij.plugin.frame.RoiManager;
55 | import io.bioimage.modelrunner.system.PlatformDetection;
56 | import net.imglib2.FinalInterval;
57 | import net.imglib2.Interval;
58 | import net.imglib2.Localizable;
59 | import net.imglib2.Point;
60 | import net.imglib2.RandomAccessibleInterval;
61 | import net.imglib2.img.Img;
62 | import net.imglib2.img.display.imagej.ImageJFunctions;
63 | import net.imglib2.type.NativeType;
64 | import net.imglib2.type.numeric.RealType;
65 | import net.imglib2.type.numeric.integer.UnsignedShortType;
66 |
67 | /**
68 | *
69 | * @author Carlos Garcia Lopez de Haro
70 | */
71 | public class Consumer extends ConsumerInterface implements MouseListener, KeyListener, WindowListener, IJEventListener, ListDataListener {
72 | /**
73 | * The image being processed
74 | */
75 | private ImagePlus activeImage;
76 | /**
77 | * Canvas of the image selected. Used to record the prompts drawn by the user
78 | */
79 | private ImageCanvas activeCanvas;
80 | /**
81 | * Window of the selected image. Used to record the prompts drawn by the user
82 | */
83 | private ImageWindow activeWindow;
84 | /**
85 | * Instance of the ROI manager to save the ROIs created
86 | */
87 | private RoiManager roiManager;
88 | /**
89 | * Instance of the list displayed in the ROI manager
90 | */
91 | private DefaultListModel listModel;
92 | /**
93 | * Whether to add the ROIs created to the ROI manager or not
94 | */
95 | private boolean isAddingToRoiManager = true;
96 | /**
97 | * Counter of the ROIs created
98 | */
99 | private int promptsCreatedCnt = 0;
100 | /**
101 | * A list to save several ROIs that are being created for the same prompt.
102 | * Whenever the prompt is sent to the model, this list is emptied
103 | */
104 | private List temporalROIs = new ArrayList();
105 | /**
106 | * A list to save several ROIs that are being created from the same prompt.
107 | * This list saves only the "negative" ROIs, those that are not part of the instance of interest,
108 | * but part of the background.
109 | * Whenever the prompt is sent to the model, this list is emptied.
110 | */
111 | private List temporalNegROIs = new ArrayList();
112 | /**
113 | * For the point prompts, whether if hte user is collecting several prompts (pressing the ctrl key)
114 | * or just one
115 | */
116 | private boolean isCollectingPoints = false;
117 | /**
118 | * All the points being collected that reference the instance of interest
119 | */
120 | private List collectedPoints = new ArrayList();
121 | /**
122 | * All the points being collected that reference the background (ctrl + alt)
123 | */
124 | private List collecteNegPoints = new ArrayList();
125 | /**
126 | * List of the annotated masks on an image
127 | */
128 | private Stack annotatedMask = new Stack();
129 | /**
130 | * List that keeps track of the annotated masks
131 | */
132 | private Stack redoAnnotatedMask = new Stack();
133 | /**
134 | * Tracks if Ctrl+Z has already been handled
135 | */
136 | private boolean undoPressed = false;
137 | /**
138 | * Tracks if Ctrl+Y has already been handled
139 | */
140 | private boolean redoPressed = false;
141 | /**
142 | * Whether the SAMJ specific listeners are registered or not.
143 | */
144 | private boolean registered = false;
145 | /**
146 | * Whether the delete operation comes from ctrl+Z or ctrl+Y or from the roi manager
147 | */
148 | private boolean isCommand = false;
149 |
150 | public Consumer() {
151 | IJ.addEventListener(this);
152 | ImagePlus.addImageListener(new ImageListener() {
153 |
154 | @Override
155 | public void imageOpened(ImagePlus imp) {}
156 | @Override
157 | public void imageUpdated(ImagePlus imp) {}
158 |
159 | @Override
160 | public void imageClosed(ImagePlus imp) {
161 | if (guiCallback == null)
162 | return;
163 | if (imp != Consumer.this.activeImage)
164 | return;
165 | Consumer.this.deactivateListeners();
166 | Consumer.this.guiCallback.run();
167 | }
168 |
169 | });
170 | }
171 |
172 | @Override
173 | /**
174 | * {@inheritDoc}
175 | *
176 | * GEt the list of open images in ImageJ
177 | */
178 | public List getListOfOpenImages() {
179 | return Arrays.stream(WindowManager.getImageTitles())
180 | .map(title -> new IJComboBoxItem((Object) WindowManager.getImage(title)))
181 | .collect(Collectors.toList());
182 | }
183 |
184 | @Override
185 | public void addPolygonsFromGUI(List masks) {
186 | // TODO improve the naming
187 | this.addToRoiManager(masks, "batch");
188 | }
189 |
190 | @Override
191 | public List getPolygonsFromRoiManager() {
192 | return Arrays.stream(roiManager.getRoisAsArray()).map(i -> i.getPolygon()).collect(Collectors.toList());
193 | }
194 |
195 | @Override
196 | public void enableAddingToRoiManager(boolean shouldBeAdding) {
197 | this.isAddingToRoiManager = shouldBeAdding;
198 | }
199 |
200 | @Override
201 | public void exportImageLabeling() {
202 | if (Recorder.record)
203 | Recorder.recordString("run(\"SAMJ Annotator\", \"export=true\");" + System.lineSeparator());
204 | int width = activeImage.getWidth();
205 | int height = activeImage.getHeight();
206 | List masks = new ArrayList();
207 | List doNotInclude = new ArrayList();
208 | for (int i = this.annotatedMask.size() - 1; i >= 0; i --) {
209 | Command maskList = annotatedMask.get(i);
210 | if (maskList instanceof DeleteRoiCommand) {
211 | for (Mask mm: maskList.getMasks())
212 | doNotInclude.add(mm.getName());
213 | } else if (maskList instanceof AddRoiCommand) {
214 | for (Mask mm : maskList.getMasks()) {
215 | if (doNotInclude.contains(mm.getName()))
216 | continue;
217 | masks.add(mm);
218 | }
219 | }
220 | }
221 | RandomAccessibleInterval raiMask = Mask.getMask(width, height, masks);
222 | ImagePlus impMask = ImageJFunctions.show(raiMask);
223 | impMask.setTitle(activeImage.getTitle() + "-labeling");
224 | impMask.getProcessor().setMinAndMax(0, annotatedMask.size());
225 | }
226 |
227 | @Override
228 | public void activateListeners() {
229 | if (registered) return;
230 | activeCanvas.addMouseListener(this);
231 | activeCanvas.addKeyListener(this);
232 | activeWindow.addWindowListener(this);
233 | activeWindow.addKeyListener(this);
234 |
235 | activeCanvas.removeKeyListener(IJ.getInstance());
236 | activeWindow.removeKeyListener(IJ.getInstance());
237 | registered = true;
238 | }
239 |
240 | @Override
241 | public void deactivateListeners() {
242 | if (!registered) return;
243 | activeCanvas.removeMouseListener(this);
244 | activeCanvas.removeKeyListener(this);
245 | activeWindow.removeWindowListener(this);
246 | activeWindow.removeKeyListener(this);
247 |
248 | activeWindow.addKeyListener(IJ.getInstance());
249 | activeCanvas.addKeyListener(IJ.getInstance());
250 | registered = false;
251 | }
252 |
253 | @Override
254 | public boolean isValidPromptSelected() {
255 | return Toolbar.getToolName().equals("rectangle")
256 | || Toolbar.getToolName().equals("point")
257 | || Toolbar.getToolName().equals("multipoint");
258 | }
259 |
260 | @Override
261 | public void setFocusedImage(Object image) {
262 | boolean changed = activeImage != (ImagePlus) image;
263 | if (!changed) {
264 | WindowManager.setCurrentWindow(activeWindow);
265 | return;
266 | }
267 | activeImage = (ImagePlus) image;
268 | this.activeCanvas = this.activeImage.getCanvas();
269 | this.activeWindow = this.activeImage.getWindow();
270 | if (this.isAddingToRoiManager)
271 | this.roiManager = startRoiManager();
272 | }
273 |
274 | @Override
275 | public void deselectImage() {
276 | activeImage = null;
277 | this.activeCanvas = null;
278 | this.activeWindow = null;
279 | }
280 |
281 | @Override
282 | public Object getFocusedImage() {
283 | return WindowManager.getCurrentImage();
284 | }
285 |
286 | @Override
287 | public String getFocusedImageName() {
288 | return WindowManager.getCurrentImage().getTitle();
289 | }
290 |
291 | @Override
292 | public & NativeType> RandomAccessibleInterval getFocusedImageAsRai() {
293 | ImagePlus imp = WindowManager.getCurrentImage();
294 | boolean isColorRGB = imp.getType() == ImagePlus.COLOR_RGB;
295 | Img image = ImageJFunctions.wrap(isColorRGB ? CompositeConverter.makeComposite(imp) : imp);
296 | return image;
297 | }
298 |
299 | @Override
300 | public List getRectRoisOnFocusImage() {
301 | Roi roi = WindowManager.getCurrentImage().getRoi();
302 | List list = getRectRoisFromRoiManager();
303 | if (roi == null)
304 | return list;
305 | if (roi.getType() != Roi.RECTANGLE)
306 | return list;
307 | if (list.stream().anyMatch(a -> a.equals(roi.getBounds())))
308 | return list;
309 | list.add(roi.getBounds());
310 | return list;
311 | }
312 |
313 | @Override
314 | public List getPointRoisOnFocusImage() {
315 | Roi roi = WindowManager.getCurrentImage().getRoi();
316 | List list = getPointRoisFromRoiManager();
317 | if (roi == null)
318 | return list;
319 | if (roi.getType() != Roi.POINT)
320 | return list;
321 | Iterator it = roi.iterator();
322 | while (it.hasNext()) {
323 | java.awt.Point p = it.next();
324 | int[] arr = new int[] {(int) p.getX(), (int) p.getY()};
325 | if (list.stream().anyMatch(a -> Arrays.equals(a, arr)))
326 | continue;
327 | list.add(arr);
328 | }
329 | return list;
330 | }
331 |
332 | @Override
333 | public void deletePointRoi(int[] pp) {
334 | Roi[] roiManagerRois = RoiManager.getInstance().getRoisAsArray();
335 | int ii = -1;
336 | if (roiManagerRois != null) {
337 | ii ++;
338 | for (Roi managerRoi : roiManagerRois) {
339 | if (managerRoi.getType() != Roi.POINT)
340 | continue;
341 | PointRoi pRoi = (PointRoi) managerRoi;
342 | Iterator iter = pRoi.iterator();
343 | while (iter.hasNext()) {
344 | java.awt.Point point = iter.next();
345 | if (point.x == pp[0] && point.y == pp[1]) {
346 | pRoi.deleteHandle(pp[0], pp[1]);
347 | if (!iter.hasNext()) {
348 | try {
349 | RoiManagerPrivateViolator.deleteRoiAtPosition(RoiManager.getInstance(), ii);
350 | } catch (NoSuchFieldException | SecurityException | NoSuchMethodException
351 | | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
352 | e.printStackTrace();
353 | }
354 | }
355 | return;
356 | }
357 | }
358 | }
359 | }
360 | Roi roi = WindowManager.getCurrentImage().getRoi();
361 | if (roi != null && roi.getType() == Roi.POINT) {
362 | PointRoi pRoi = (PointRoi) roi;
363 | Iterator iter = roi.iterator();
364 | while (iter.hasNext()) {
365 | java.awt.Point point = iter.next();
366 | if (point.x == pp[0] && point.y == pp[1]) {
367 | pRoi.deleteHandle(pp[0], pp[1]);
368 | return;
369 | }
370 | }
371 | }
372 | }
373 |
374 | @Override
375 | public void deleteRectRoi(Rectangle rect) {
376 | Roi[] roiManagerRois = RoiManager.getInstance().getRoisAsArray();
377 | if (roiManagerRois != null) {
378 | for (int i = 0; i < roiManagerRois.length; i ++) {
379 | Roi managerRoi = roiManagerRois[i];
380 | if (managerRoi.getType() != Roi.RECTANGLE)
381 | continue;
382 | if (managerRoi.getBounds().equals(rect)) {
383 | try {
384 | RoiManagerPrivateViolator.deleteRoiAtPosition(RoiManager.getInstance(), i);
385 | } catch (NoSuchFieldException | SecurityException | NoSuchMethodException | IllegalAccessException
386 | | IllegalArgumentException | InvocationTargetException e) {
387 | e.printStackTrace();
388 | }
389 | return;
390 | }
391 | }
392 | }
393 | Roi roi = WindowManager.getCurrentImage().getRoi();
394 | if (roi != null && roi.getType() == Roi.RECTANGLE) {
395 | if (roi.getBounds().equals(rect)) {
396 | activeImage.deleteRoi();
397 | return;
398 | }
399 | }
400 | }
401 |
402 | private List getRectRoisFromRoiManager() {
403 | List list = new ArrayList();
404 | Roi[] rois = RoiManager.getInstance().getRoisAsArray();
405 | if (rois.length == 0)
406 | return list;
407 | list = Arrays.stream(rois).filter(rr -> rr.getType() == Roi.RECTANGLE)
408 | .map(rr -> {
409 | rr.setImage(activeImage);
410 | return rr.getBounds();
411 | }).collect(Collectors.toList());
412 | return list;
413 | }
414 |
415 | private List getPointRoisFromRoiManager() {
416 | List list = new ArrayList();
417 | Roi[] rois = RoiManager.getInstance().getRoisAsArray();
418 | if (rois.length == 0)
419 | return list;
420 | List roiList = Arrays.stream(rois).filter(rr -> rr.getType() == Roi.POINT).collect(Collectors.toList());
421 | for (Roi rr : roiList) {
422 | Iterator it = rr.iterator();
423 | while (it.hasNext()) {
424 | java.awt.Point p = it.next();
425 | list.add(new int[] {(int) p.getX(), (int) p.getY()});
426 | }
427 | rr.setImage(activeImage);
428 | }
429 | return list;
430 | }
431 |
432 | @Override
433 | /**
434 | * when the plugin is closed, close everything
435 | */
436 | public void windowClosed(WindowEvent e) {
437 | roiManager.close();
438 | this.selectedModel.closeProcess();
439 | this.selectedModel = null;
440 | this.deactivateListeners();
441 | this.activeImage = null;
442 | this.activeCanvas = null;
443 | this.activeWindow = null;
444 | }
445 |
446 |
447 | @Override
448 | public void keyPressed(KeyEvent e) {
449 | if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_Z && this.annotatedMask.size() != 0 && !redoPressed) {
450 | redoPressed = true;
451 | isCommand = true;
452 | Command command = annotatedMask.pop();
453 | command.undo();
454 | this.redoAnnotatedMask.push(command);
455 | } else if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_Y && this.redoAnnotatedMask.size() != 0 && !undoPressed) {
456 | undoPressed = true;
457 | isCommand = true;
458 | Command command = redoAnnotatedMask.pop();
459 | command.execute();
460 | this.annotatedMask.push(command);
461 | }
462 | e.consume();
463 | }
464 |
465 | @Override
466 | /**
467 | * Monitor when the control key is being released for the point prompts.
468 | * Whenever it is released and the point prompt is selected, the points that have already been drawn
469 | * are sent to SAMJ
470 | */
471 | public void keyReleased(KeyEvent e) {
472 | if ((e.getKeyCode() == KeyEvent.VK_CONTROL && !PlatformDetection.isMacOS())
473 | || (e.getKeyCode() == KeyEvent.VK_META && PlatformDetection.isMacOS())) {
474 | submitAndClearPoints();
475 | }
476 | if (e.getKeyCode() == KeyEvent.VK_Z) {
477 | redoPressed = false;
478 | }
479 | if (e.getKeyCode() == KeyEvent.VK_Y) {
480 | undoPressed = false;
481 | }
482 | }
483 |
484 | @Override
485 | public void mouseReleased(MouseEvent e) {
486 | if (activeImage.getRoi() == null)
487 | return;
488 | if (Toolbar.getToolName().equals("rectangle")) {
489 | annotateRect();
490 | } else if (Toolbar.getToolName().equals("point") || Toolbar.getToolName().equals("multipoint")) {
491 | annotatePoints(e);
492 | } else if (Toolbar.getToolName().equals("freeline")) {
493 | annotateBrush(e);
494 | } else {
495 | return;
496 | }
497 | if (!isCollectingPoints) activeImage.deleteRoi();
498 | }
499 |
500 | private void annotateRect() {
501 | final Roi roi = activeImage.getRoi();
502 | final Rectangle rectBounds = roi.getBounds();
503 | final Interval rectInterval = new FinalInterval(
504 | new long[] { rectBounds.x, rectBounds.y },
505 | new long[] { rectBounds.x+rectBounds.width-1, rectBounds.y+rectBounds.height-1 } );
506 | submitRectPrompt(rectInterval);
507 | }
508 |
509 | private void submitRectPrompt(Interval rectInterval) {
510 | try {
511 | addToRoiManager(this.selectedModel.fetch2dSegmentation(rectInterval), "rect");
512 | } catch (Exception ex) {
513 | ex.printStackTrace();;
514 | }
515 | }
516 |
517 | private void annotatePoints(MouseEvent e) {
518 | final Roi roi = activeImage.getRoi();
519 | // TODO think what to do with negative points
520 | if (e.isControlDown() && e.isAltDown() && false) {
521 | roi.setFillColor(Color.red);
522 | //add point to the list only
523 | isCollectingPoints = true;
524 | Iterator iterator = roi.iterator();
525 | java.awt.Point p = iterator.next();
526 | while (iterator.hasNext()) p = iterator.next();
527 | collecteNegPoints.add( new Point(p.x,p.y) ); //NB: add ImgLib2 Point
528 | //TODO log.info("Image window: collecting points..., already we have: "+collectedPoints.size());
529 | } else if ((e.isControlDown() && !PlatformDetection.isMacOS()) || (e.isMetaDown() && PlatformDetection.isMacOS())) {
530 | //add point to the list only
531 | isCollectingPoints = true;
532 | Iterator iterator = roi.iterator();
533 | java.awt.Point p = iterator.next();
534 | while (iterator.hasNext()) p = iterator.next();
535 | collectedPoints.add( new Point(p.x,p.y) ); //NB: add ImgLib2 Point
536 | //TODO log.info("Image window: collecting points..., already we have: "+collectedPoints.size());
537 | } else {
538 | isCollectingPoints = false;
539 | //collect this last one
540 | Iterator iterator = roi.iterator();
541 | java.awt.Point p = iterator.next();
542 | while (iterator.hasNext()) p = iterator.next();
543 | collectedPoints.add( new Point(p.x,p.y) );
544 | submitAndClearPoints();
545 | }
546 | }
547 |
548 | /**
549 | * Send the point prompts to SAM and clear the lists collecting them
550 | */
551 | private void submitAndClearPoints() {
552 | if (this.selectedModel == null) return;
553 | if (collectedPoints.size() == 0) return;
554 |
555 | //TODO log.info("Image window: Processing now points, this count: "+collectedPoints.size());
556 | isCollectingPoints = false;
557 | activeImage.deleteRoi();
558 | Rectangle zoomedRectangle = this.activeCanvas.getSrcRect();
559 | try {
560 | if (activeImage.getWidth() * activeImage.getHeight() > Math.pow(AbstractSamJ.MAX_ENCODED_AREA_RS, 2)
561 | || activeImage.getWidth() > AbstractSamJ.MAX_ENCODED_SIDE || activeImage.getHeight() > AbstractSamJ.MAX_ENCODED_SIDE)
562 | addToRoiManager(selectedModel.fetch2dSegmentation(collectedPoints, collecteNegPoints, zoomedRectangle),
563 | (collectedPoints.size() > 1 ? "points" : "point"));
564 | else
565 | addToRoiManager(selectedModel.fetch2dSegmentation(collectedPoints, collecteNegPoints),
566 | (collectedPoints.size() > 1 ? "points" : "point"));
567 | } catch (Exception ex) {
568 | ex.printStackTrace();
569 | }
570 | collectedPoints = new ArrayList();
571 | collecteNegPoints = new ArrayList();
572 | temporalROIs = new ArrayList();
573 | temporalNegROIs = new ArrayList();
574 | }
575 |
576 | private void annotateBrush(MouseEvent e) {
577 | final Roi roi = activeImage.getRoi();
578 | // TODO this is not a real mask prompt, it is just taking
579 | // TODO all the points in a line and using them, modify it for a true mask
580 | if (e.isControlDown() && e.isAltDown()) {
581 | temporalNegROIs.add(roi);
582 | roi.setStrokeColor(Color.red);
583 | isCollectingPoints = true;
584 | Iterator it = roi.iterator();
585 | while (it.hasNext()) {
586 | java.awt.Point p = it.next();
587 | collecteNegPoints.add(new Point(p.x,p.y));
588 | }
589 | addTemporalRois();
590 | } else if (e.isControlDown()) {
591 | temporalROIs.add(roi);
592 | isCollectingPoints = true;
593 | Iterator it = roi.iterator();
594 | while (it.hasNext()) {
595 | java.awt.Point p = it.next();
596 | collectedPoints.add(new Point(p.x,p.y));
597 | }
598 | addTemporalRois();
599 | } else {
600 | isCollectingPoints = false;
601 | Rectangle rect = roi.getBounds();
602 | if (rect.height == 1) {
603 | for (int i = 0; i < rect.width; i ++) {
604 | collectedPoints.add(new Point(rect.x + i, rect.y));
605 | }
606 | } else if (rect.width == 1) {
607 | for (int i = 0; i < rect.height; i ++) {
608 | collectedPoints.add(new Point(rect.x, rect.y + i));
609 | }
610 | } else {
611 | Iterator it = roi.iterator();
612 | while (it.hasNext()) {
613 | java.awt.Point p = it.next();
614 | collectedPoints.add(new Point(p.x,p.y));
615 | }
616 | }
617 | // TODO move this logic to SAMJ into the masks option
618 | if (collectedPoints.size() > 1 && collectedPoints.size() < 6)
619 | collectedPoints = Arrays.asList(new Localizable[] {collectedPoints.get(1)});
620 | else if (collectedPoints.size() > 1 && collectedPoints.size() < 50) {
621 | List newCollectedPoints = new ArrayList();
622 | while (newCollectedPoints.size() == 0) {
623 | for (Localizable pp : collectedPoints) {
624 | if (Math.random() < 0.2) newCollectedPoints.add(pp);
625 | }
626 | }
627 | collectedPoints = newCollectedPoints;
628 | } else if (collectedPoints.size() > 50) {
629 | List newCollectedPoints = new ArrayList();
630 | while (newCollectedPoints.size() < 10) {
631 | for (Localizable pp : collectedPoints) {
632 | if (Math.random() < Math.min(0.1, 50.0 / collectedPoints.size())) newCollectedPoints.add(pp);
633 | }
634 | }
635 | collectedPoints = newCollectedPoints;
636 | }
637 | submitAndClearPoints();
638 | }
639 | }
640 |
641 | private RoiManager startRoiManager() {
642 | RoiManager roiManager = RoiManager.getInstance();
643 | if (roiManager == null) {
644 | roiManager = new RoiManager();
645 | for (Component comp :roiManager.getComponents()) {
646 | if (comp instanceof JScrollPane) {
647 | for (Component comp2 : ((JScrollPane) comp).getComponents()) {
648 | if (comp2 instanceof JViewport) {
649 | for (Component comp3 : ((JViewport) comp2).getComponents()) {
650 | if (comp3 instanceof JList) {
651 | listModel = (DefaultListModel) ((JList) comp3).getModel();
652 | listModel.addListDataListener(this);
653 | break;
654 | }
655 | }
656 | }
657 | }
658 | }
659 | }
660 | }
661 | // TODO what to do? roiManager.reset();
662 | roiManager.setVisible(true);
663 | roiManager.setTitle("SAM Roi Manager");
664 | Prefs.useNamesAsLabels = true;
665 | Roi imRoi = activeImage.getRoi();
666 | deleteOtherImageRois();
667 | roiManager.setEditMode(activeImage, true);
668 | activeImage.setRoi(imRoi);
669 | return roiManager;
670 | }
671 |
672 | private void deleteOtherImageRois() {
673 | try {
674 | int n = RoiManager.getInstance().getCount() - 1;
675 | int originalSize = this.annotatedMask.size();
676 | for (int i = 0; i < originalSize; i ++) {
677 | List maskList = annotatedMask.pop().getMasks();
678 | for (int j = maskList.size() - 1; j > -1; j --) {
679 | Polygon pol = maskList.get(j).getContour();
680 | for (int k = n; k > -1; k --) {
681 | Roi roi = this.roiManager.getRoi(k);
682 | Polygon roiPol = roi.getPolygon();
683 | if (pol.npoints != roiPol.npoints) continue;
684 | boolean equal = IntStream.range(0, pol.npoints)
685 | .allMatch(ii -> pol.xpoints[ii] == roiPol.xpoints[ii] &&
686 | pol.ypoints[ii] == roiPol.ypoints[ii]);
687 | if (equal) {
688 | RoiManagerPrivateViolator.deleteRoiAtPosition(this.roiManager, k);
689 | n --;
690 | break;
691 | }
692 |
693 | }
694 | }
695 | }
696 | this.redoAnnotatedMask.clear();
697 | } catch (Exception ex) {
698 | }
699 | }
700 |
701 | private void addTemporalRois() {
702 | //Overlay overlay = activeCanvas.getOverlay();
703 | Overlay overlay = OverlayLabels.createOverlay();
704 | for (Roi rr : this.roiManager.getRoisAsArray())
705 | overlay.add(rr);
706 | this.temporalROIs.stream().forEach(r -> overlay.add(r));
707 | this.temporalNegROIs.stream().forEach(r -> overlay.add(r));
708 | activeCanvas.setShowAllList(overlay);
709 | this.activeImage.draw();
710 | }
711 |
712 | /**
713 | * Add a single polygon to the ROI manager
714 | * @param pRoi
715 | */
716 | public void addToRoiManager(final PolygonRoi pRoi ) {
717 | if (isAddingToRoiManager) roiManager.addRoi(pRoi);
718 | }
719 |
720 | /**
721 | * Add the new roi to the ROI manager
722 | * @param polys
723 | * list of polygons that will be converted into polygon ROIs and sent to the ROI manager
724 | * @param promptShape
725 | * String giving information about which prompt was used to generate the ROI
726 | */
727 | void addToRoiManager(final List polys, final String promptShape) {
728 | if (this.roiManager.getCount() == 0 && annotatedMask.size() != 0)
729 | annotatedMask.clear();
730 | this.redoAnnotatedMask.clear();
731 | AddRoiCommand command = new AddRoiCommand(this.roiManager, polys);
732 | command.setModelName(this.selectedModel.getName());
733 | command.setPromptShape(promptShape);
734 | command.setPromptCount(++ promptsCreatedCnt);
735 | command.setAddingToRoiManager(this.isAddingToRoiManager);
736 | command.execute();
737 | this.annotatedMask.push(command);
738 | }
739 |
740 | @Override
741 | public void eventOccurred(int eventID) {
742 | if (eventID != IJEventListener.TOOL_CHANGED || callback == null)
743 | return;
744 | boolean isvalid = IJ.getToolName().equals("rectangle")
745 | || IJ.getToolName().equals("point")
746 | || IJ.getToolName().equals("multipoint");
747 | this.callback.validPromptChosen(isvalid);
748 | }
749 |
750 | @Override
751 | /**
752 | * {@inheritDoc}
753 | *
754 | * For more info about how the macros work, please go to
755 | * https://github.com/segment-anything-models-java/SAMJ-IJ/blob/main/README.md#macros
756 | */
757 | public void notifyBatchSamize(String modelName, String maskPrompt) {
758 | if (!Recorder.record)
759 | return;
760 |
761 | String formatedMacro = "run(\"SAMJ Annotator\", \"model=[%s]%s export=false\");" + System.lineSeparator();
762 | String formatedMaskPrompt = " maskPrompt=[%s]";
763 | String promptArg = maskPrompt == null ? "" : String.format(formatedMaskPrompt, maskPrompt);
764 | Recorder.recordString(String.format(formatedMacro, modelName, promptArg));
765 | }
766 |
767 | // ===== unused events =====
768 | @Override
769 | public void mouseEntered(MouseEvent e) {}
770 | @Override
771 | public void mouseClicked(MouseEvent e) {}
772 | @Override
773 | public void mousePressed(MouseEvent e) {}
774 | @Override
775 | public void mouseExited(MouseEvent e) {}
776 | @Override
777 | public void windowOpened(WindowEvent e) {}
778 | @Override
779 | public void windowClosing(WindowEvent e) {}
780 | @Override
781 | public void windowIconified(WindowEvent e) {}
782 | @Override
783 | public void windowDeiconified(WindowEvent e) {}
784 | @Override
785 | public void windowActivated(WindowEvent e) {}
786 | @Override
787 | public void windowDeactivated(WindowEvent e) {}
788 | @Override
789 | public void keyTyped(KeyEvent e) {}
790 | @Override
791 | public void contentsChanged(ListDataEvent e) {}
792 | @Override
793 | public void intervalAdded(ListDataEvent e) {}
794 |
795 | @Override
796 | public void intervalRemoved(ListDataEvent e) {
797 | if (isCommand) {
798 | isCommand = false;
799 | return;
800 | }
801 | List roiManagerNames = new ArrayList();
802 | List deleteList = new ArrayList();
803 | Enumeration elems = listModel.elements();
804 | while (elems.hasMoreElements())
805 | roiManagerNames.add(elems.nextElement());
806 | List deletedNames = new ArrayList();
807 | for (int i = annotatedMask.size() - 1; i >= 0; i --) {
808 | if (annotatedMask.get(i) instanceof DeleteRoiCommand) {
809 | deletedNames.addAll(
810 | annotatedMask.get(i).getMasks().stream()
811 | .map(mm -> mm.getName()).collect(Collectors.toList())
812 | );
813 | continue;
814 | }
815 | for (int j = annotatedMask.get(i).getMasks().size() - 1; j >= 0; j --) {
816 | if (roiManagerNames.contains(annotatedMask.get(i).getMasks().get(j).getName())
817 | || deletedNames.contains(annotatedMask.get(i).getMasks().get(j).getName()))
818 | continue;
819 | deleteList.add(annotatedMask.get(i).getMasks().get(j));
820 |
821 | }
822 | }
823 | Command command = new DeleteRoiCommand(this.roiManager, deleteList);
824 | command.setAddingToRoiManager(this.isAddingToRoiManager);
825 | //command.execute();
826 | this.annotatedMask.push(command);
827 | this.redoAnnotatedMask.clear();
828 | }
829 |
830 |
831 | }
832 |
--------------------------------------------------------------------------------
/src/main/java/ai/nets/samj/ij/ui/IJComboBoxItem.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * #%L
3 | * Plugin to help image annotation with SAM-based Deep Learning models
4 | * %%
5 | * Copyright (C) 2024 SAMJ developers.
6 | * %%
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * #L%
19 | */
20 | package ai.nets.samj.ij.ui;
21 |
22 |
23 | import ai.nets.samj.gui.components.ComboBoxItem;
24 | import ij.ImagePlus;
25 | import ij.plugin.CompositeConverter;
26 | import net.imglib2.RandomAccessibleInterval;
27 | import net.imglib2.img.Img;
28 | import net.imglib2.img.display.imagej.ImageJFunctions;
29 | import net.imglib2.type.NativeType;
30 | import net.imglib2.type.numeric.RealType;
31 |
32 | /**
33 | * Implementation of the SAMJ interface {@link ComboBoxItem} that provides the SAMJ GUI
34 | * an item for the combobox that references ImageJ {@link ImagePlus}
35 | *
36 | * @author Carlos Garcia
37 | */
38 | public class IJComboBoxItem extends ComboBoxItem {
39 |
40 | /**
41 | *
42 | * Combobox item that contains an Object associated to a unique identifier.
43 | * For ImageJ the object is an ImageJ {@link ImagePlus}
44 | * @param seq
45 | * the object of interest, whihc in the case of ImageJ is and {@link ImagePlus}
46 | */
47 | public IJComboBoxItem(Object seq) {
48 | super(seq);
49 | }
50 |
51 | /**
52 | * Create an empty {@link ComboBoxItem}. Its id is -1
53 | */
54 | public IJComboBoxItem() {
55 | super();
56 | }
57 |
58 | @Override
59 | /**
60 | * {@inheritDoc}
61 | *
62 | * For ImageJ is the name of the ImageJ {@link ImagePlus}
63 | */
64 | public String getImageName() {
65 | return ((ImagePlus) this.getValue()).getTitle();
66 | }
67 |
68 | @Override
69 | /**
70 | * {@inheritDoc}
71 | *
72 | * Convert the {@link ImagePlus} into a {@link RandomAccessibleInterval}
73 | */
74 | public & NativeType> RandomAccessibleInterval getImageAsImgLib2() {
75 | ImagePlus imp = (ImagePlus) this.getValue();
76 | boolean isColorRGB = imp.getType() == ImagePlus.COLOR_RGB;
77 | Img image = ImageJFunctions.wrap(isColorRGB ? CompositeConverter.makeComposite(imp) : imp);
78 | return image;
79 | }
80 |
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/ai/nets/samj/ij/ui/commands/AddRoiCommand.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * #%L
3 | * Plugin to help image annotation with SAM-based Deep Learning models
4 | * %%
5 | * Copyright (C) 2024 SAMJ developers.
6 | * %%
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * #L%
19 | */
20 | package ai.nets.samj.ij.ui.commands;
21 |
22 | import java.util.ArrayList;
23 | import java.util.Arrays;
24 | import java.util.List;
25 | import java.util.concurrent.ThreadLocalRandom;
26 |
27 | import ai.nets.samj.annotation.Mask;
28 | import ai.nets.samj.ij.utils.RoiManagerPrivateViolator;
29 | import ij.gui.PolygonRoi;
30 | import ij.plugin.frame.RoiManager;
31 |
32 | public class AddRoiCommand implements Command {
33 | private RoiManager roiManager;
34 | private final List polys;
35 | private List rois;
36 | private boolean isAddingToRoiManager = true;
37 | private String shape = "";
38 | private int promptCount = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE);
39 | private String modelName = "";
40 |
41 | public AddRoiCommand(RoiManager roiManager, List polys) {
42 | this.roiManager = roiManager;
43 | this.polys = polys;
44 | }
45 |
46 | public void setPromptShape(String shape) {
47 | this.shape = shape;
48 | }
49 |
50 | public void setPromptCount(int promptCount) {
51 | this.promptCount = promptCount;
52 | }
53 |
54 | public void setModelName(String modelName) {
55 | this.modelName = modelName;
56 | }
57 |
58 | public void setAddingToRoiManager(boolean addToRoiManager) {
59 | this.isAddingToRoiManager = addToRoiManager;
60 | }
61 |
62 | public List getImageJRois(){
63 | return rois;
64 | }
65 |
66 | public List getMasks(){
67 | return polys;
68 | }
69 |
70 | @Override
71 | public void execute() {
72 | rois = new ArrayList();
73 | int resNo = 1;
74 | for (Mask m : polys) {
75 | final PolygonRoi pRoi = new PolygonRoi(m.getContour(), PolygonRoi.POLYGON);
76 | String name = promptCount + "." + (resNo ++) + "_"+shape + "_" + modelName;
77 | if (shape.equals("") && modelName.equals(""))
78 | name = "" + promptCount;
79 | else if (modelName.equals(""))
80 | name = promptCount + "." + (resNo) + "_"+shape;
81 | else if (shape.equals(""))
82 | name = promptCount + "." + (resNo) + "_"+modelName;
83 |
84 | pRoi.setName(name);
85 | m.setName(name);
86 | rois.add(pRoi);
87 | if (isAddingToRoiManager) roiManager.addRoi(pRoi);;
88 | }
89 | }
90 |
91 | @Override
92 | public void undo() {
93 | try {
94 | for (PolygonRoi rr2 : rois) {
95 | for (int n = this.roiManager.getCount() - 1; n >= 0; n --) {
96 | PolygonRoi rr = (PolygonRoi) roiManager.getRoi(n);
97 | if (!Arrays.equals(rr.getXCoordinates(), rr2.getXCoordinates()))
98 | continue;
99 | if (!Arrays.equals(rr.getYCoordinates(), rr2.getYCoordinates()))
100 | continue;
101 | RoiManagerPrivateViolator.deleteRoiAtPosition(this.roiManager, n);
102 | break;
103 | }
104 |
105 | }
106 | } catch (Exception ex) {
107 | ex.printStackTrace();
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/src/main/java/ai/nets/samj/ij/ui/commands/Command.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * #%L
3 | * Plugin to help image annotation with SAM-based Deep Learning models
4 | * %%
5 | * Copyright (C) 2024 SAMJ developers.
6 | * %%
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * #L%
19 | */
20 | package ai.nets.samj.ij.ui.commands;
21 |
22 | import java.util.List;
23 |
24 | import ai.nets.samj.annotation.Mask;
25 | import ij.gui.PolygonRoi;
26 |
27 |
28 | public interface Command {
29 | public void execute();
30 |
31 | public void undo();
32 |
33 | public void setAddingToRoiManager(boolean addToRoiManager);
34 |
35 | public List getImageJRois();
36 |
37 | public List getMasks();
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/ai/nets/samj/ij/ui/commands/DeleteRoiCommand.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * #%L
3 | * Plugin to help image annotation with SAM-based Deep Learning models
4 | * %%
5 | * Copyright (C) 2024 SAMJ developers.
6 | * %%
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * #L%
19 | */
20 | package ai.nets.samj.ij.ui.commands;
21 |
22 | import java.util.ArrayList;
23 | import java.util.Arrays;
24 | import java.util.List;
25 |
26 | import ai.nets.samj.annotation.Mask;
27 | import ai.nets.samj.ij.utils.RoiManagerPrivateViolator;
28 | import ij.gui.PolygonRoi;
29 | import ij.gui.Roi;
30 | import ij.plugin.frame.RoiManager;
31 |
32 | public class DeleteRoiCommand implements Command {
33 | private RoiManager roiManager;
34 | private final List polys;
35 | private final List rois;
36 | private boolean isAddingToRoiManager = true;
37 |
38 | public DeleteRoiCommand(RoiManager roiManager, List polys) {
39 | this.roiManager = roiManager;
40 | this.polys = polys;
41 | rois = new ArrayList();
42 | for (Mask m : polys) {
43 | PolygonRoi roi = new PolygonRoi(m.getContour(), PolygonRoi.POLYGON);
44 | roi.setName(m.getName());
45 | rois.add(roi);
46 | }
47 | }
48 |
49 | public void execute() {
50 | if (!isAddingToRoiManager)
51 | return;
52 | try {
53 | for (PolygonRoi rr2 : rois) {
54 | for (int n = this.roiManager.getCount() - 1; n >= 0; n --) {
55 | PolygonRoi rr = (PolygonRoi) roiManager.getRoi(n);
56 | if (!Arrays.equals(rr.getXCoordinates(), rr2.getXCoordinates()))
57 | continue;
58 | if (!Arrays.equals(rr.getYCoordinates(), rr2.getYCoordinates()))
59 | continue;
60 | RoiManagerPrivateViolator.deleteRoiAtPosition(this.roiManager, n);
61 | break;
62 | }
63 |
64 | }
65 | } catch (Exception ex) {
66 | ex.printStackTrace();
67 | }
68 | }
69 |
70 | public void undo() {
71 | for (Roi m : rois) {
72 | if (isAddingToRoiManager) roiManager.addRoi(m);;
73 | }
74 | }
75 |
76 | @Override
77 | public void setAddingToRoiManager(boolean addToRoiManager) {
78 | this.isAddingToRoiManager = addToRoiManager;
79 | }
80 |
81 | @Override
82 | public List getImageJRois() {
83 | return rois;
84 | }
85 |
86 | @Override
87 | public List getMasks(){
88 | return polys;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/ai/nets/samj/ij/utils/Constants.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * #%L
3 | * Plugin to help image annotation with SAM-based Deep Learning models
4 | * %%
5 | * Copyright (C) 2024 SAMJ developers.
6 | * %%
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * #L%
19 | */
20 | package ai.nets.samj.ij.utils;
21 |
22 | import java.io.File;
23 |
24 | import io.bioimage.modelrunner.system.PlatformDetection;
25 |
26 | public class Constants {
27 |
28 | /**
29 | * The folder of Fiji
30 | */
31 | public static final String FIJI_FOLDER = getFijiFolder();
32 |
33 | private static String getFijiFolder() {
34 | File jvmFolder = new File(System.getProperty("java.home"));
35 | String imageJExecutable;
36 | if (PlatformDetection.isWindows())
37 | imageJExecutable = "fiji-windows-x64.exe";
38 | else if (PlatformDetection.isLinux())
39 | imageJExecutable = "fiji-linux-x64";
40 | else if (PlatformDetection.isMacOS() && PlatformDetection.getArch().equals(PlatformDetection.ARCH_ARM64))
41 | imageJExecutable = "Fiji.App/Contents/MacOS/fiji-macos-arm64";
42 | else if (PlatformDetection.isMacOS())
43 | imageJExecutable = "Fiji.App/Contents/MacOS/fiji-macos-x64";
44 | else
45 | throw new IllegalArgumentException("Unsupported Operating System");
46 | while (true && jvmFolder != null) {
47 | jvmFolder = jvmFolder.getParentFile();
48 | if (new File(jvmFolder + File.separator + imageJExecutable).isFile())
49 | return jvmFolder.getAbsolutePath();
50 | }
51 | return getImageJFolder();
52 | }
53 |
54 | private static String getImageJFolder() {
55 | File jvmFolder = new File(System.getProperty("java.home"));
56 | String imageJExecutable;
57 | if (PlatformDetection.isWindows())
58 | imageJExecutable = "ImageJ-win64.exe";
59 | else if (PlatformDetection.isLinux())
60 | imageJExecutable = "ImageJ-linux64";
61 | else if (PlatformDetection.isMacOS())
62 | imageJExecutable = "Contents/MacOS/ImageJ-macosx";
63 | else
64 | throw new IllegalArgumentException("Unsupported Operating System");
65 | while (true && jvmFolder != null) {
66 | jvmFolder = jvmFolder.getParentFile();
67 | if (new File(jvmFolder + File.separator + imageJExecutable).isFile())
68 | return jvmFolder.getAbsolutePath();
69 | }
70 | return new File("").getAbsolutePath();
71 | // TODO remove throw new RuntimeException("Unable to find the path to the ImageJ/Fiji being used.");
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/ai/nets/samj/ij/utils/RoiManagerPrivateViolator.java:
--------------------------------------------------------------------------------
1 | /*-
2 | * #%L
3 | * Plugin to help image annotation with SAM-based Deep Learning models
4 | * %%
5 | * Copyright (C) 2024 SAMJ developers.
6 | * %%
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * #L%
19 | */
20 | package ai.nets.samj.ij.utils;
21 |
22 | import java.awt.EventQueue;
23 | import java.lang.reflect.Field;
24 | import java.lang.reflect.InvocationTargetException;
25 | import java.lang.reflect.Method;
26 | import java.util.ArrayList;
27 | import java.util.Objects;
28 |
29 | import javax.swing.DefaultListModel;
30 |
31 | import ij.gui.Roi;
32 | import ij.plugin.frame.RoiManager;
33 |
34 | public class RoiManagerPrivateViolator {
35 |
36 | @SuppressWarnings("unchecked")
37 | public static void deleteRoiAtPosition(RoiManager roiM, int position) throws NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
38 | Objects.requireNonNull(roiM, "Please provide an initialized RoiManager.");
39 | int nRois = roiM.getCount();
40 | if (position >= nRois) return;
41 |
42 | Field roisPrivate = RoiManager.class.getDeclaredField("rois");
43 | roisPrivate.setAccessible(true);
44 | Field listModelPrivate = RoiManager.class.getDeclaredField("listModel");
45 | listModelPrivate.setAccessible(true);
46 |
47 | // Access private method
48 | Method deleteOnEDTPrivate = RoiManager.class.getDeclaredMethod("deleteOnEDT", int.class);
49 | deleteOnEDTPrivate.setAccessible(true);
50 | Method updateShowAllPrivate = RoiManager.class.getDeclaredMethod("updateShowAll");
51 | updateShowAllPrivate.setAccessible(true);
52 |
53 | ArrayList rois = ((ArrayList) roisPrivate.get(roiM));
54 | DefaultListModel listModel = ((DefaultListModel) listModelPrivate.get(roiM));
55 |
56 |
57 | if (EventQueue.isDispatchThread()) {
58 | rois.remove(position);
59 | listModel.remove(position);
60 | } else {
61 | deleteOnEDTPrivate.invoke(roiM, position);
62 | }
63 | updateShowAllPrivate.invoke(roiM);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/resources/.samj_ij_properties:
--------------------------------------------------------------------------------
1 | version=${project.version}
2 | name=${project.artifactId}
3 |
--------------------------------------------------------------------------------
/src/main/resources/plugins.config:
--------------------------------------------------------------------------------
1 | ###
2 | # #%L
3 | # Plugin to help image annotation with SAM-based Deep Learning models
4 | # %%
5 | # Copyright (C) 2024 SAMJ developers.
6 | # %%
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | # #L%
19 | ###
20 | Plugins>SAMJ, "SAMJ Annotator", ai.nets.samj.ij.SAMJ_Annotator
21 |
--------------------------------------------------------------------------------