/*- * #%L * Mathematical morphology library and plugins for ImageJ/Fiji. * %% * Copyright (C) 2014 - 2017 INRA. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Lesser Public License for more details. * * You should have received a copy of the GNU General Lesser Public * License along with this program. If not, see * <http://www.gnu.org/licenses/lgpl-3.0.html>. * #L% */ package inra.ijpb.plugins; import ij.ImagePlus; import ij.gui.DialogListener; import ij.gui.GenericDialog; import ij.plugin.filter.ExtendedPlugInFilter; import ij.plugin.filter.PlugInFilterRunner; import ij.process.FloatProcessor; import ij.process.ImageProcessor; import inra.ijpb.algo.DefaultAlgoListener; import inra.ijpb.morphology.attrfilt.AreaOpeningQueue; import inra.ijpb.morphology.attrfilt.BoxDiagonalOpeningQueue; import java.awt.AWTEvent; /** * Select binary particles in a planar image based on number of pixels. * This version also provides preview of result. * * @see AreaOpeningPlugin * * @author David Legland */ public class GrayscaleAttributeFilteringPlugin implements ExtendedPlugInFilter, DialogListener { enum Operation { CLOSING("Closing"), OPENING("Opening"), TOP_HAT("Top Hat"), BOTTOM_HAT("Bottom Hat"); String label; Operation(String label) { this.label = label; } public static String[] getAllLabels() { int n = Operation.values().length; String[] result = new String[n]; int i = 0; for (Operation op : Operation.values()) result[i++] = op.label; return result; } /** * Determines the operation type from its label. * * @param opLabel * the label of the operation * @return the parsed Operation * @throws IllegalArgumentException * if label is not recognized. */ public static Operation fromLabel(String opLabel) { if (opLabel != null) opLabel = opLabel.toLowerCase(); for (Operation op : Operation.values()) { String cmp = op.label.toLowerCase(); if (cmp.equals(opLabel)) return op; } throw new IllegalArgumentException("Unable to parse Operation with label: " + opLabel); } }; enum Attribute { AREA("Area"), BOX_DIAGONAL("Box Diagonal"); String label; Attribute(String label) { this.label = label; } public static String[] getAllLabels() { int n = Attribute.values().length; String[] result = new String[n]; int i = 0; for (Attribute att : Attribute.values()) result[i++] = att.label; return result; } /** * Determines the Attribute type from its label. * * @param opLabel * the label of the Attribute * @return the parsed Attribute * @throws IllegalArgumentException * if label is not recognized. */ public static Attribute fromLabel(String attrLabel) { if (attrLabel != null) attrLabel = attrLabel.toLowerCase(); for (Attribute op : Attribute.values()) { String cmp = op.label.toLowerCase(); if (cmp.equals(attrLabel)) return op; } throw new IllegalArgumentException("Unable to parse Attribute with label: " + attrLabel); } }; private final static String[] connectivityLabels = {"4", "8"}; private final static int[] connectivityValues = {4, 8}; /** keep flags in plugin */ private int flags = DOES_ALL | KEEP_PREVIEW | FINAL_PROCESSING | NO_CHANGES; PlugInFilterRunner pfr; int nPasses; boolean previewing = false; /** keep the instance of ImagePlus */ private ImagePlus imagePlus; /** keep the original image, to restore it after the preview */ private ImageProcessor baseImage; /** Keep instance of result image */ private ImageProcessor result; Operation operation = Operation.OPENING; Attribute attribute = Attribute.AREA; int minimumValue = 100; int connectivity = 4; @Override public int setup(String arg, ImagePlus imp) { // Called at the end for cleaning up the results if (arg.equals("final")) { // replace the preview image by the original image resetPreview(); imagePlus.updateAndDraw(); // Create a new ImagePlus with the result String newName = imagePlus.getShortTitle() + "-attrFilt"; ImagePlus resPlus = new ImagePlus(newName, result); // copy spatial calibration and display settings resPlus.copyScale(imagePlus); result.setColorModel(baseImage.getColorModel()); resPlus.show(); return DONE; } // Normal setup this.imagePlus = imp; this.baseImage = imp.getProcessor().duplicate(); return flags; } @Override public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { // Create the configuration dialog GenericDialog gd = new GenericDialog("Gray Scale Attribute Filtering"); gd.addChoice("Operation", Operation.getAllLabels(), Operation.OPENING.label); gd.addChoice("Attribute", Attribute.getAllLabels(), Attribute.AREA.label); gd.addNumericField("Minimum Value", 100, 0, 10, "pixels"); gd.addChoice("Connectivity", connectivityLabels, connectivityLabels[0]); gd.addPreviewCheckbox(pfr); gd.addDialogListener(this); previewing = true; gd.showDialog(); previewing = false; if (gd.wasCanceled()) { resetPreview(); return DONE; } parseDialogParameters(gd); // clean up an return gd.dispose(); return flags; } @Override public void run(ImageProcessor image) { // Identify image to process (original, or inverted) ImageProcessor image2 = baseImage; if (this.operation == Operation.CLOSING || this.operation == Operation.BOTTOM_HAT) { image2 = image2.duplicate(); image2.invert(); } // switch depending on attribute to use if (attribute == Attribute.AREA) { AreaOpeningQueue algo = new AreaOpeningQueue(); algo.setConnectivity(this.connectivity); DefaultAlgoListener.monitor(algo); this.result = algo.process(image2, this.minimumValue); } else { BoxDiagonalOpeningQueue algo = new BoxDiagonalOpeningQueue(); algo.setConnectivity(this.connectivity); DefaultAlgoListener.monitor(algo); this.result = algo.process(image2, this.minimumValue); } // For top-hat and bottom-hat, we consider difference with original image if (this.operation == Operation.TOP_HAT || this.operation == Operation.BOTTOM_HAT) { double maxDiff = 0; for (int i = 0; i < image.getPixelCount(); i++) { float diff = Math.abs(this.result.getf(i) - image2.getf(i)); this.result.setf(i, diff); maxDiff = Math.max(diff, maxDiff); } this.result.setMinAndMax(0, maxDiff); } // For closing, invert back the result else if (this.operation == Operation.CLOSING) { this.result.invert(); } if (previewing) { // Iterate over pixels to change value of reference image for (int i = 0; i < image.getPixelCount(); i++) { image.setf(i, result.getf(i)); } image.setMinAndMax(result.getMin(), result.getMax()); } } @Override public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { boolean wasPreview = this.previewing; parseDialogParameters(gd); // if preview checkbox was unchecked, replace the preview image by the original image if (wasPreview && !this.previewing) { resetPreview(); } return true; } private void resetPreview() { ImageProcessor image = this.imagePlus.getProcessor(); if (image instanceof FloatProcessor) { for (int i = 0; i < image.getPixelCount(); i++) image.setf(i, this.baseImage.getf(i)); } else { for (int i = 0; i < image.getPixelCount(); i++) image.set(i, this.baseImage.get(i)); } image.resetMinAndMax(); imagePlus.updateAndDraw(); } /** * Extract chosen parameters * @param gd the instance of GenericDialog used to parse parameters */ private void parseDialogParameters(GenericDialog gd) { this.operation = Operation.fromLabel(gd.getNextChoice()); this.attribute = Attribute.fromLabel(gd.getNextChoice()); this.minimumValue = (int) gd.getNextNumber(); this.connectivity = connectivityValues[gd.getNextChoiceIndex()]; this.previewing = gd.getPreviewCheckbox().getState(); } @Override public void setNPasses(int nPasses) { this.nPasses = nPasses; } }