package ini.trakem2.imaging; import ij.IJ; import ij.ImagePlus; import ij.gui.GenericDialog; import ij.gui.PointRoi; import ij.gui.PolygonRoi; import ij.gui.Roi; import ij.gui.ShapeRoi; import ij.gui.Wand; import ij.plugin.WandToolOptions; import ij.process.ImageProcessor; import ini.trakem2.display.AreaContainer; import ini.trakem2.display.AreaWrapper; import ini.trakem2.display.Display; import ini.trakem2.display.Layer; import ini.trakem2.display.Patch; import ini.trakem2.utils.Bureaucrat; import ini.trakem2.utils.IJError; import ini.trakem2.utils.M; import ini.trakem2.utils.OptionPanel; import ini.trakem2.utils.Utils; import ini.trakem2.utils.Worker; import java.awt.Checkbox; import java.awt.Color; import java.awt.Component; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.NoninvertibleTransformException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import levelsets.algorithm.Coordinate; import levelsets.algorithm.FastMarching; import levelsets.ij.ImageContainer; import levelsets.ij.StateContainer; import plugin.Lasso; public class Segmentation { static public class FastMarchingParam { // Fast-marching: public int fm_grey = 50; public double fm_dist = 0.5; public int max_iterations = 1000; public int iter_inc = 100; // Preprocess fast-marching with grey value erosion: public boolean apply_grey_value_erosion = true; // Lasso: public double ratio_space_color = 1.0; // Preprocess all with bandpass filter: public boolean apply_bandpass_filter = true; public int low_frequency_threshold = 1000; public int high_frequency_threshold = 5; public boolean autoscale_after_filtering = true; public boolean saturate_when_autoscaling = true; /** In pixels, of the the underlying image to copy around the mouse. May be enlarged by shift+scrollwheel with PENCIL tool on a selected Displayable. */ public int width = 100, height = 100; // Hack: options for semiautomatic neurite tracer public boolean SNT_invert_image = false; public OptionPanel asOptionPanel() { OptionPanel op = new OptionPanel(); op.addMessage("Fast Marching:"); op.addNumericField("Grey value threshold:", fm_grey, new OptionPanel.IntSetter(this, "fm_grey")); op.addNumericField("Distance threshold:", fm_dist, 2, new OptionPanel.DoubleSetter(this, "fm_dist")); op.addNumericField("Max iterations:", max_iterations, new OptionPanel.IntSetter(this, "max_iterations")); op.addNumericField("Iterations inc:", iter_inc, new OptionPanel.IntSetter(this, "iter_inc")); op.addCheckbox("Grey value erosion filter:", apply_grey_value_erosion, new OptionPanel.BooleanSetter(this, "apply_grey_value_erosion")); op.addMessage("Lasso:"); op.addNumericField("Ratio space/color:", ratio_space_color, 2, new OptionPanel.DoubleSetter(this, "ratio_space_color")); op.addMessage("Preprocessing by bandpass filter:"); op.addCheckbox("Bandpass filter:", apply_bandpass_filter, new OptionPanel.BooleanSetter(this, "apply_bandpass_filter")); op.addNumericField("Filter down to:", low_frequency_threshold, new OptionPanel.IntSetter(this, "low_frequency_threshold")); op.addNumericField("Filter up to:", high_frequency_threshold, new OptionPanel.IntSetter(this, "high_frequency_threshold")); op.addCheckbox("Saturate when autoscaling:", saturate_when_autoscaling, new OptionPanel.BooleanSetter(this, "saturate_when_autoscaling")); op.addMessage("Semiautomatic neurite tracer:"); op.addCheckbox("Invert image", SNT_invert_image, new OptionPanel.BooleanSetter(this, "SNT_invert_image")); return op; } public boolean setup() { GenericDialog gd = new GenericDialog("Fast Marching Options"); gd.addMessage("Fast Marching:"); gd.addNumericField("Grey value threshold", fm_grey, 0); gd.addNumericField("Distance threshold", fm_dist, 2); gd.addNumericField("Max iterations", max_iterations, 0); gd.addNumericField("Iterations inc", iter_inc, 0); gd.addCheckbox("Enable grey value erosion filter", apply_grey_value_erosion); gd.addMessage("Lasso:"); gd.addNumericField("ratio space/color:", ratio_space_color, 2); gd.addMessage("Preprocessing by bandpass filter:"); gd.addCheckbox("Enable bandpass filter", apply_bandpass_filter); gd.addNumericField("Filter_Large Structures Down to", low_frequency_threshold, 0, 4, "pixels"); gd.addNumericField("Filter_Small Structures Up to", high_frequency_threshold, 0, 4, "pixels"); gd.addCheckbox("Autoscale After Filtering", autoscale_after_filtering); gd.addCheckbox("Saturate Image when Autoscaling", saturate_when_autoscaling); final Component[] c = { (Component)gd.getNumericFields().get(gd.getNumericFields().size()-2), (Component)gd.getNumericFields().get(gd.getNumericFields().size()-1), (Component)gd.getCheckboxes().get(gd.getCheckboxes().size()-2), (Component)gd.getCheckboxes().get(gd.getCheckboxes().size()-1) }; Utils.addEnablerListener((Checkbox)gd.getCheckboxes().get(gd.getCheckboxes().size()-3), c, null); if (!apply_bandpass_filter) { for (Component comp : c) comp.setEnabled(false); } gd.addMessage("Semiautomatic neurite tracer:"); gd.addCheckbox("Invert image", SNT_invert_image); gd.showDialog(); if (gd.wasCanceled()) return false; // FastMarching: fm_grey = (int) gd.getNextNumber(); fm_dist = gd.getNextNumber(); max_iterations = (int)gd.getNextNumber(); iter_inc = (int)gd.getNextNumber(); // Grey value erosion: apply_grey_value_erosion = gd.getNextBoolean(); // Ratio space/color for lasso: ratio_space_color = gd.getNextNumber(); // Bandpass filter: apply_bandpass_filter = gd.getNextBoolean(); low_frequency_threshold = (int) gd.getNextNumber(); high_frequency_threshold = (int) gd.getNextNumber(); autoscale_after_filtering = gd.getNextBoolean(); saturate_when_autoscaling = gd.getNextBoolean(); // Semiautomatic neurite tracer: SNT_invert_image = gd.getNextBoolean(); return true; } public void resizeArea(final int sign, final double magnification) { double inc = (int)( (10 * sign) / magnification); this.width += inc; this.height += inc; Utils.log2("fmp w,h: " + width + ", " + height); } /** Return bounds relative to the given mouse position. */ public Rectangle getBounds(final int x_p, final int y_p) { return new Rectangle(x_p - width/2, y_p - height/2, width, height); } } static public final FastMarchingParam fmp = new FastMarchingParam(); static class EndTask<T,D> { T target; Runnable after; EndTask(T target, Runnable after) { this.target = target; this.after = after; } void run() { } } static public Bureaucrat fastMarching(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w, final Runnable post_task) { return fastMarching(aw, layer, srcRect, x_p_w, y_p_w, Arrays.asList(new Runnable[]{post_task})); } static public Bureaucrat fastMarching(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w, final List<Runnable> post_tasks) { // Capture pointers before they are set to null final AreaContainer ac = (AreaContainer)aw.getSource(); final AffineTransform source_aff = aw.getSource().getAffineTransform(); final Rectangle box = new Rectangle(x_p_w - Segmentation.fmp.width/2, y_p_w - Segmentation.fmp.height/2, Segmentation.fmp.width, Segmentation.fmp.height); Bureaucrat burro = Bureaucrat.create(new Worker.Task("Fast marching") { public void exec() { // Capture image as large as the fmp width,height centered on x_p_w,y_p_w Utils.log2("fmp box is " + box); ImagePlus imp = new ImagePlus("", Patch.makeFlatImage(ImagePlus.GRAY8, layer, box, 1.0, (Collection)layer.getDisplayables(Patch.class, new Area(box), true), Color.black)); // Bandpass filter if (fmp.apply_bandpass_filter) { IJ.run(imp, "Bandpass Filter...", "filter_large=" + fmp.low_frequency_threshold + " filter_small=" + fmp.high_frequency_threshold + " suppress=None tolerance=5" + (fmp.autoscale_after_filtering ? " autoscale" : "") + (fmp.saturate_when_autoscaling ? " saturate" : "")); } // Setup seed point PointRoi roi = new PointRoi(box.width/2, box.height/2); imp.setRoi(roi); Utils.log2("imp: " + imp); Utils.log2("proi: " + imp.getRoi() + " " + Utils.toString(new int[]{x_p_w - srcRect.x, y_p_w - srcRect.y})); // Setup state ImageContainer ic = new ImageContainer(imp); StateContainer state = new StateContainer(); state.setROI(roi, ic.getWidth(), ic.getHeight(), ic.getImageCount(), imp.getCurrentSlice()); state.setExpansionToInside(false); // Run FastMarching final FastMarching fm = new FastMarching(ic, null, state, true, fmp.fm_grey, fmp.fm_dist, fmp.apply_grey_value_erosion); final int max_iterations = fmp.max_iterations; final int iter_inc = fmp.iter_inc; for (int i=0; i<max_iterations; i++) { if (Thread.currentThread().isInterrupted()) { return; } if (!fm.step(iter_inc)) break; } // Extract ROI setTaskName("Adding area"); final ArrayList<Coordinate> vc = fm.getStateContainer().getXYZ(false); if (0 == vc.size()) { Utils.log("No area growth."); return; } final Area area = new Area(); Coordinate first = vc.remove(0); final Rectangle r = new Rectangle(first.x, first.y, 1, 1); int count = 0; // Scan and add line-wise for (final Coordinate c : vc) { count++; if (c.y == r.y && c.x == r.x + 1) { // same line: r.width += 1; continue; } else { // add previous one area.add(new Area(r)); } // start new line: r.x = c.x; r.y = c.y; r.width = 1; if (0 == count % 1024 && Thread.currentThread().isInterrupted()) { return; } } // add last: area.add(new Area(r)); /* // Trying from the image mask: JUST AS SLOW final byte[] b = (byte[]) fm.getStateContainer().getIPMask()[0].getPixels(); final int w = imp.getWidth(); for (int i=0; i<b.length; i++) { if (0 == b[i]) { r.x = i%w; r.y = i/w; area.add(new Area(r)); } } */ /* // DOESN'T FILL? // Trying to just get the contour, and then filling holes for (final Coordinate c : fm.getStateContainer().getXYZ(true)) { r.x = c.x; r.y = c.y; area.add(new Area(r)); } Polygon pol = new Polygon(); final float[] coords = new float[6]; for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) { switch (pit.currentSegment(coords)) { case PathIterator.SEG_MOVETO: case PathIterator.SEG_LINETO: pol.addPoint((int)coords[0], (int)coords[1]); break; case PathIterator.SEG_CLOSE: area.add(new Area(pol)); // prepare next: pol = new Polygon(); break; default: Utils.log2("WARNING: unhandled seg type."); break; } pit.next(); if (pit.isDone()) { break; } } */ /// FAILS because by now AreaWrapper's source is null //aw.add(area, new AffineTransform(1, 0, 0, 1, box.x, box.y)); // Instead, compose an Area that is local to the AreaWrapper's area final AffineTransform aff = new AffineTransform(1, 0, 0, 1, box.x, box.y); try { aff.preConcatenate(source_aff.createInverse()); } catch (NoninvertibleTransformException nite) { IJError.print(nite); return; } aw.getArea().add(area.createTransformedArea(aff)); ac.calculateBoundingBox(layer); Display.repaint(layer); }}, layer.getProject()); if (null != post_tasks) for (Runnable task : post_tasks) burro.addPostTask(task); burro.goHaveBreakfast(); return burro; } static public class BlowCommander { BlowRunner br = null; final ExecutorService dispatcher = Executors.newFixedThreadPool(1); final List<Runnable> post_tasks; final AffineTransform source_aff; final AreaContainer ac; public BlowCommander(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w, final List<Runnable> post_tasks) throws Exception { this.post_tasks = post_tasks; this.ac = (AreaContainer)aw.getSource(); this.source_aff = aw.getSource().getAffineTransform(); dispatcher.submit(new Runnable() { public void run() { // Creation in the context of the ExecutorService thread, so 'imp' will be local to it try { br = new BlowRunner(aw, layer, srcRect, x_p_w, y_p_w); } catch (Throwable t) { IJError.print(t); dispatcher.shutdownNow(); } } }); } public void mouseDragged(final MouseEvent me, final Layer la, final int x_p, final int y_p, final int x_d, final int y_d, final int x_d_old, final int y_d_old) { try { dispatcher.submit(new Runnable() { public void run() { try { // Move relative to starting point br.moveBlow(x_p - x_d, y_p - y_d); } catch (Throwable t) { IJError.print(t); mouseReleased(me, la, x_p, y_p, x_d_old, y_d_old, x_d, y_d); } } }); } catch (RejectedExecutionException ree) {} // Ignore: operations have been canceled } public void mouseReleased(final MouseEvent me, final Layer la, final int x_p, final int y_p, final int x_d, final int y_d, final int x_r, final int y_r) { dispatcher.submit(new Runnable() { public void run() { // Add the roi to the Area try { br.finish(ac, source_aff); } catch (Throwable t) { IJError.print(t); } // Execute post task if any if (null != post_tasks) { try { for (Runnable task : post_tasks) task.run(); } catch (Throwable t) { IJError.print(t); } } // Stop accepting tasks dispatcher.shutdownNow(); } }); } } static public class BlowRunner { final Rectangle box; final ImagePlus imp; final Lasso lasso; final Layer layer; final AreaWrapper aw; public BlowRunner(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w) throws Exception { this.aw = aw; this.layer = layer; // Capture image as large as the fmp width,height centered on x_p_w,y_p_w this.box = new Rectangle(x_p_w - Segmentation.fmp.width/2, y_p_w - Segmentation.fmp.height/2, Segmentation.fmp.width, Segmentation.fmp.height); Utils.log2("fmp box is " + box); this.imp = new ImagePlus("", Patch.makeFlatImage(ImagePlus.GRAY8, layer, box, 1.0, (Collection)layer.getDisplayables(Patch.class, new Area(box), true), Color.black)); // Bandpass filter if (fmp.apply_bandpass_filter) { IJ.run(imp, "Bandpass Filter...", "filter_large=" + fmp.low_frequency_threshold + " filter_small=" + fmp.high_frequency_threshold + " suppress=None tolerance=5" + (fmp.autoscale_after_filtering ? " autoscale" : "") + (fmp.saturate_when_autoscaling ? " saturate" : "")); } lasso = new Lasso(imp, Lasso.BLOW, box.width/2, box.height/2, false); lasso.setRatioSpaceColor(fmp.ratio_space_color); } public void moveBlow(int dx, int dy) throws Exception { int x = box.width/2 + dx; int y = box.height/2 + dy; // Keep within bounds if (x < 0) x = 0; if (y < 0) y = 0; if (x > box.width -1) x = box.width -1; if (y > box.height -1) y = box.height -1; lasso.moveBlow(x, y); // extract ROI Roi roi = imp.getRoi(); if (null == roi) Display.getFront().getCanvas().getFakeImagePlus().setRoi(roi); // can't set to null? Java, gimme a break else { Roi sroi = new ShapeRoi(roi); Rectangle b = sroi.getBounds(); sroi.setLocation(box.x + b.x, box.y + b.y); Display.getFront().getCanvas().getFakeImagePlus().setRoi(sroi); } } public void finish(final AreaContainer ac, final AffineTransform source_aff) throws Exception { Roi roi = imp.getRoi(); Utils.log2("roi is " + roi); if (null == roi) return; ShapeRoi sroi = new ShapeRoi(roi); Rectangle b = sroi.getBounds(); sroi.setLocation(box.x + b.x, box.y + b.y); try { aw.getArea().add(M.getArea(sroi).createTransformedArea(source_aff.createInverse())); ac.calculateBoundingBox(layer); Display.getFront().getCanvas().getFakeImagePlus().killRoi(); } catch (NoninvertibleTransformException nite) { IJError.print(nite); } } } static public BlowCommander blowRoi(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w, final Runnable post_task) throws Exception { return new BlowCommander(aw, layer, srcRect, x_p_w, y_p_w, Arrays.asList(new Runnable[]{post_task})); } static public BlowCommander blowRoi(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w, final List<Runnable> post_tasks) throws Exception { return new BlowCommander(aw, layer, srcRect, x_p_w, y_p_w, post_tasks); } static public Bureaucrat magicWand(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w, final Runnable post_task, final boolean inverse, final boolean subtract) { return magicWand(aw, layer, srcRect, x_p_w, y_p_w, Arrays.asList(new Runnable[]{post_task}), inverse, subtract); } static public Bureaucrat magicWand(final AreaWrapper aw, final Layer layer, final Rectangle srcRect, final int x_p_w, final int y_p_w, final List<Runnable> post_tasks, final boolean inverse, final boolean subtract) { // Capture pointers before they are set to null final AreaContainer ac = (AreaContainer) aw.getSource(); final AffineTransform source_aff = aw.getSource().getAffineTransform(); final Rectangle box = new Rectangle(x_p_w - Segmentation.fmp.width/2, y_p_w - Segmentation.fmp.height/2, Segmentation.fmp.width, Segmentation.fmp.height); Bureaucrat burro = Bureaucrat.create(new Worker.Task("Magic Wand") { public void exec() { // Capture image as large as the fmp width,height centered on x_p_w,y_p_w Utils.log2("fmp box is " + box); ImageProcessor ip = Patch.makeFlatImage(ImagePlus.GRAY8, layer, box, 1.0, (Collection)layer.getDisplayables(Patch.class, new Area(box), true), Color.black); // Apply wand Wand wand = new Wand(ip); String smode = WandToolOptions.getMode(); int mode = Wand.LEGACY_MODE; if (null == smode) {} else if (smode.startsWith("4")) mode = Wand.FOUR_CONNECTED; else if (smode.startsWith("8")) mode = Wand.EIGHT_CONNECTED; wand.autoOutline(ip.getWidth()/2, ip.getHeight()/2, WandToolOptions.getTolerance(), mode); // 8-bit image if (wand.npoints > 0) { Area area = M.getArea(new PolygonRoi(wand.xpoints, wand.ypoints, wand.npoints, PolygonRoi.FREEROI)); if (inverse) { Area b = new Area(new Rectangle(0, 0, box.width, box.height)); b.subtract(area); area = b; } // Compose an Area that is local to the AreaWrapper's area final AffineTransform aff = new AffineTransform(1, 0, 0, 1, box.x, box.y); try { aff.preConcatenate(source_aff.createInverse()); } catch (NoninvertibleTransformException nite) { IJError.print(nite); return; } area.transform(aff); if (subtract) aw.getArea().subtract(area); else aw.getArea().add(area); ac.calculateBoundingBox(layer); Display.repaint(layer); } }}, layer.getProject()); if (null != post_tasks) for (Runnable task : post_tasks) burro.addPostTask(task); burro.goHaveBreakfast(); return burro; } }