ipnat.skel.Strahler.java Source code

Java tutorial

Introduction

Here is the source code for ipnat.skel.Strahler.java

Source

/*
 * #%L
 * hIPNAT plugins for Fiji distribution of ImageJ
 * %%
 * Copyright (C) 2016 Tiago Ferreira
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */
package ipnat.skel;

import java.awt.Checkbox;
import java.awt.Choice;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Vector;

import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;

import fiji.Debug;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.WindowManager;
import ij.gui.DialogListener;
import ij.gui.GenericDialog;
import ij.gui.ImageCanvas;
import ij.gui.Roi;
import ij.measure.Calibration;
import ij.measure.ResultsTable;
import ij.plugin.PlugIn;
import ij.plugin.ZProjector;
import ij.process.ImageProcessor;
import ipnat.ColorMaps;
import ipnat.IPNAT;
import ipnat.Utils;
import ipnat.processing.Binary;
import sc.fiji.analyzeSkeleton.AnalyzeSkeleton_;
import sc.fiji.analyzeSkeleton.Point;
import sc.fiji.analyzeSkeleton.SkeletonResult;
import sc.fiji.skeletonize3D.Skeletonize3D_;
import sholl.gui.EnhancedGenericDialog;

/**
 * This class implements the ImageJ {@code Strahler Analysis} plugin. For more
 * information, visit the hIPNAT repository
 * {@literal https://github.com/tferr/hIPNAT} and the the plugin's documentation
 * page: {@literal http://imagej.net/Strahler_Analysis}
 *
 *
 * @author Tiago Ferreira
 */
public class Strahler implements PlugIn, DialogListener {

    protected static final String URL = "http://imagej.net/Strahler_Analysis";

    /* Default value for max. number of pruning cycles */
    int maxOrder = 30;

    /* Default option for loop detection */
    int pruneChoice = AnalyzeSkeleton_.SHORTEST_BRANCH;

    /* Default option for 'root-protection' ROI */
    boolean protectRoot = true;

    /* Default option for 'iteration-stack' output */
    boolean outIS = false;

    /* Default option for verbose mode */
    boolean verbose = true;

    /* Default option for tabular option */
    boolean tabular = false;

    /* Remove isolated pixels from thinned images? */
    boolean erodeIsolatedPixels = true;

    /* Title of main results window */
    String STRAHLER_TABLE = "Strahler_Table";

    /* Title of detailed results window */
    String VERBOSE_TABLE = "Strahler_Iteration_Log";

    /*
     * Grayscale image for intensity-based pruning of skel. loops. While it is
     * unlikely that the iterative pruning of terminal branches will cause new
     * loops on pre-existing skeletons, offering the option to resolve loops
     * with intensity based methods remains useful specially when analyzing
     * non-thinned grayscale images.
     */
    ImagePlus grayscaleImp = null;
    int grayscaleImpChoice;

    ImagePlus srcImp; // Image to be analyzed (we'll be working on a copy)
    boolean validRootRoi; // Flag assessing validity of 'root-protective' ROI
    String title; // Title of active image
    Roi rootRoi; // Reference to the "root-protecting" ROI
    ImageProcessor ip;

    /**
     * Calls {@link fiji.Debug#run(String, String) fiji.Debug.run()} so that the
     * plugin can be debugged from an IDE
     *
     * @param args
     *            the arguments as specified in {@code plugins.config}
     */
    public static void main(final String[] args) {
        Debug.run("Strahler Analysis...", null);
    }

    /**
     * This method is called when the plugin is loaded.
     *
     * @param arg
     *            the arguments as specified in {@code plugins.config}
     *
     */
    @Override
    public void run(final String arg) {

        // Retrieve analysis image and its ROI
        srcImp = WindowManager.getCurrentImage();
        if (!validRequirements(srcImp))
            return;

        title = srcImp.getTitle();
        rootRoi = srcImp.getRoi();
        validRootRoi = (rootRoi != null && rootRoi.getType() == Roi.RECTANGLE);

        // TODO: 3D Roots are special. We need to:
        // 1) Check if ROI is associated with all slices or just one
        // 2) Ignore counts above/below the ROI, as needed
        // 3) Extend ip operations to stack
        if (srcImp.getNSlices() > 1) {
            final String warning = "3D images are currently supported with the following limitations:\n"
                    + "    - 'Root-protecting' ROIs are not yet supported\n"
                    + "    - Lengths are estimated from Z-projections\n \n"
                    + "These issues will be addressed in future releases.";
            if (IJ.macroRunning())
                IJ.log(warning);
            else
                IJ.showMessage("Warning", warning);
            validRootRoi = false;
        }

        // Retrieve grayscale image for intensity-based pruning of skel. loops
        if (!getSettings())
            return;

        // Work on a skeletonized copy since we'll be modifing the image
        if (rootRoi != null)
            srcImp.killRoi();
        final ImagePlus imp = srcImp.duplicate();
        if (rootRoi != null)
            srcImp.setRoi(rootRoi);
        ip = imp.getProcessor();
        skeletonizeWithoutHermits(imp);

        // Initialize ResultsTable: main and detailed info
        final ResultsTable rt = Utils.getTable(STRAHLER_TABLE);
        final ResultsTable logrt = Utils.getTable(VERBOSE_TABLE);

        // Analyze root
        ImagePlus rootImp;
        ImageProcessor rootIp = null;
        SkeletonResult rootResult = null;
        ArrayList<Point> rootEndpointsList = null;
        int nRootEndpoints = 0, nRootJunctions = 0;

        if (validRootRoi && verbose) {

            // Duplicate entire canvas. Ignore tree(s) outside ROI
            rootImp = imp.duplicate();
            rootIp = rootImp.getProcessor();
            rootIp.setValue(0.0);
            rootIp.fillOutside(rootRoi);

            // Get root properties
            final AnalyzeSkeleton_ root = new AnalyzeSkeleton_();
            root.setup("", rootImp);
            rootResult = root.run(pruneChoice, false, false, grayscaleImp, true, false);
            rootImp.flush();

            // We assume ROI contains only end-point branches, slab voxels and
            // no junction points. We'll thus remove end-points at ROI
            // boundaries
            nRootJunctions = sum(rootResult.getJunctions());
            rootEndpointsList = rootResult.getListOfEndPoints();
            final ListIterator<Point> it = rootEndpointsList.listIterator();
            final Rectangle r = rootRoi.getBounds();
            while (it.hasNext()) {
                final Point p = it.next();
                if (p.x == r.x || p.y == r.y || p.x == (int) (r.x + r.getWidth() - 1)
                        || p.y == (int) (r.y + r.getHeight() - 1))
                    it.remove();
            }
            rootResult.setListOfEndPoints(rootEndpointsList);
            nRootEndpoints = rootEndpointsList.size();

        }

        // Initialize display images. Use Z-projections to populate
        // iteration stack when dealing with 3D skeletons
        final int nSlices = imp.getNSlices();
        ZProjector zp = null;

        final ImageStack iterationStack = new ImageStack(imp.getWidth(), imp.getHeight());
        if (nSlices > 1) {
            zp = new ZProjector(imp);
            zp.setMethod(ZProjector.MAX_METHOD);
            zp.setStartSlice(1);
            zp.setStopSlice(nSlices);
        }

        // Initialize AnalyzeSkeleton_
        final AnalyzeSkeleton_ as = new AnalyzeSkeleton_();
        as.setup("", imp);

        // Perform the iterative pruning
        int order = 1, nEndpoints = 0, nJunctions = 0, nJunctions2 = 0;
        ArrayList<Point> endpointsList = null, junctionsList = null;
        String errorMsg = "";

        do {

            IJ.showStatus("Retrieving measurements for order " + order + "...");
            IJ.showProgress(order, getMaxOrder());

            // (Re)skeletonize image
            if (order > 1)
                skeletonizeWithoutHermits(imp);

            // Get properties of loop-resolved tree(s)
            final SkeletonResult sr = as.run(pruneChoice, false, false, grayscaleImp, true, false);
            nEndpoints = sum(sr.getEndPoints());
            nJunctions = sum(sr.getJunctions());

            if (order == 1) {
                // Remember initial properties
                endpointsList = sr.getListOfEndPoints();
                junctionsList = sr.getListOfJunctionVoxels();

                // Do not include root in 1st order calculations
                nEndpoints -= nRootEndpoints;
                nJunctions -= nRootJunctions;
            }

            // Is it worth proceeding?
            if (nEndpoints == 0 || nJunctions2 == nJunctions) {
                errorMsg = "Error! Iteration " + order + " aborted: ";
                errorMsg += (nEndpoints == 0) ? "No end-poins found" : "Unsolved loop(s) detected";
                break;
            }

            // Add current tree(s) to debug animation
            ImageProcessor ipd;
            if (nSlices > 1) {
                zp.doProjection();
                ipd = zp.getProjection().getProcessor();
            } else {
                ipd = ip.duplicate();
            }
            iterationStack.addSlice("Order " + IJ.pad(order, 2), ipd);

            // Report properties of pruned structures
            if (verbose) {
                logrt.incrementCounter();
                logrt.addValue("Image", title);
                logrt.addValue("Structure", "Skel. at iteration " + Integer.toString(order));
                logrt.addValue("Notes", errorMsg);
                logrt.addValue("# Trees", sr.getNumOfTrees());
                logrt.addValue("# Branches", sum(sr.getBranches()));
                logrt.addValue("# End-points", nEndpoints);
                logrt.addValue("# Junctions", nJunctions);
                logrt.addValue("# Triple points", sum(sr.getTriples()));
                logrt.addValue("# Quadruple points", sum(sr.getQuadruples()));
                logrt.addValue("Average branch length", average(sr.getAverageBranchLength()));
            }

            // Remember main results
            nJunctions2 = nJunctions;

            // Eliminate end-points
            as.run(pruneChoice, true, false, grayscaleImp, true, false, rootRoi);

        } while (order++ <= getMaxOrder() && nJunctions > 0);

        // Set counter to the de facto order
        order -= 1;

        // Append root properties to log table
        if (validRootRoi && verbose) {

            // Check if ROI contains unexpected structures
            final String msg = (nRootJunctions > 0) ? "Warning: ROI contains ramified root(s)"
                    : "Root-branches inferred from ROI";
            logrt.incrementCounter();
            logrt.addValue("Image", title);
            logrt.addValue("Structure", "Root");
            logrt.addValue("Notes", msg);
            logrt.addValue("# Trees", rootResult.getNumOfTrees());
            logrt.addValue("# Branches", sum(rootResult.getBranches()));
            logrt.addValue("# End-points", nRootEndpoints);
            logrt.addValue("# Junctions", nRootJunctions);
            logrt.addValue("# Triple points", sum(rootResult.getTriples()));
            logrt.addValue("# Quadruple points", sum(rootResult.getQuadruples()));
            logrt.addValue("Average branch length", average(rootResult.getAverageBranchLength()));

        }

        // Safety check
        if (iterationStack == null || iterationStack.getSize() < 1) {
            error("Enable \"detailed\" mode and check " + VERBOSE_TABLE + " for details.");
            return;
        }

        // Create iteration stack
        final Calibration cal = srcImp.getCalibration();
        final ImagePlus imp2 = new ImagePlus("StrahlerIteration_" + title, iterationStack);
        imp2.setCalibration(cal);
        if (outIS) {
            if (validRootRoi) {
                iterationStack.addSlice("Root", rootIp);
                paintPoints(iterationStack, rootEndpointsList, 255, "Root end-points");
                imp2.setRoi(rootRoi);
            }
            paintPoints(iterationStack, endpointsList, 255, "End-points");
            paintPoints(iterationStack, junctionsList, 255, "Junction-points");
        }

        // Generate Strahler mask
        zp = new ZProjector(imp2);
        zp.setMethod(ZProjector.SUM_METHOD);
        zp.setStartSlice(1);
        zp.setStopSlice(order);
        zp.doProjection();
        final ImageProcessor ip3 = zp.getProjection().getProcessor().convertToShortProcessor(false);
        clearPoints(ip3, junctionsList); // disconnect branches
        ip3.multiply(1 / 255.0); // map intensities to Strahler orders
        final ImagePlus imp3 = new ImagePlus("StrahlerMask_" + title, ip3);
        imp3.setCalibration(cal);

        // Measure segmented orders
        double prevNbranches = Double.NaN;
        for (int i = 1; i <= order; i++) {

            // Segment branches by order
            final ImagePlus maskImp = imp3.duplicate(); // Calibration is
            // retained
            IJ.setThreshold(maskImp, i, i);
            IJ.run(maskImp, "Convert to Mask", "");

            // Analyze segmented order
            final AnalyzeSkeleton_ maskAs = new AnalyzeSkeleton_();
            maskAs.setup("", maskImp);
            final SkeletonResult maskSr = maskAs.run(pruneChoice, false, false, grayscaleImp, true, false);
            maskImp.flush();

            // Since all branches are disconnected at this stage, the n. of
            // branches is
            // the same as the # the trees unless zero-branches trees exist,
            // i.e., trees
            // with no slab voxels (defined by just an end-point). We will
            // ignore those
            // trees if the user requested it
            final int nBranches = (erodeIsolatedPixels) ? sum(maskSr.getBranches()) : maskSr.getNumOfTrees();

            // Log measurements
            rt.incrementCounter();
            rt.addValue("Image", title);
            rt.addValue("Strahler Order", i);
            rt.addValue("# Branches", nBranches);
            rt.addValue("Ramification ratios", prevNbranches / nBranches);
            rt.addValue("Average branch length", average(maskSr.getAverageBranchLength()));
            rt.addValue("Unit", cal.getUnit());
            String noteMsg = "";
            if (i == 1) {
                noteMsg = (erodeIsolatedPixels) ? "Ignoring" : "Including";
                noteMsg += " single-point arbors...";
            }
            rt.addValue("Notes", noteMsg);

            // Remember results for previous order
            prevNbranches = nBranches;

        }

        // Append any errors to last row
        rt.addValue("Notes", errorMsg);

        // Display outputs
        if (!tabular) {
            if (outIS)
                imp2.show();
            ip3.setMinAndMax(0, order);
            ColorMaps.applyMagmaColorMap(imp3, 200, false);
            if (validRootRoi)
                imp3.setRoi(rootRoi);
            imp3.show();
            addCalibrationBar(imp3, Math.min(order, 5), "Black");
        }
        if (verbose)
            logrt.show(VERBOSE_TABLE);
        rt.show(STRAHLER_TABLE);

        IJ.showProgress(0, 0);
        IJ.showTime(imp, imp.getStartTime(), "Strahler Analysis concluded... ");
        imp.flush();

    }

    /**
     * Checks if image to be analyzed fulfills analysis requirements and warns
     * the user if required dependencies are present (i.e,, if all the required
     * update sites have been subscribed).
     *
     * @param imp
     *            the image to be analyzed
     * @return {@code true}, if assessment was successful. If {@code false} a
     *         macro friendly {@link Utils#error(String) error} is displayed.
     */
    boolean validRequirements(final ImagePlus imp) {
        boolean validImp = imp != null && imp.getBitDepth() == 8;
        final boolean validSetup = Utils.validSkelDependencies();
        if (!validImp) {
            final String msg = (imp == null) ? "An 8-bit image is required but none was found."
                    : imp.getTitle() + " is not an 8-bit image.";
            if (IJ.macroRunning()) {
                Utils.error("Invalid image", msg, imp);
            } else {
                final GenericDialog gd = new GenericDialog("Invalid Image");
                gd.addMessage(msg);
                gd.enableYesNoCancel("OK", "Analyze Sample Image");
                gd.hideCancelButton();
                gd.showDialog();
                if (!gd.wasOKed() && !gd.wasCanceled()) {
                    final LSystemsTree lst = new LSystemsTree();
                    this.srcImp = lst.sampleTree();
                    this.srcImp.setRoi(58, 130, 25, 35);
                    srcImp.show();
                    new ij.plugin.Zoom().run("in");
                    validImp = true;
                }
            }
        }
        return validSetup && validImp;
    }

    /**
     * Displays an error message that will not disrupt macro calls. This is
     * useful for batch processing of images: Even if the analysis of a
     * particular image fails, remaining images can still be analyzed by the
     * same macro
     *
     * @param errorMsg
     *            the error message
     */
    void error(final String errorMsg) {
        Utils.error("Strahler Analysis", errorMsg, srcImp);
    }

    /**
     * Gets the analysis parameters from the user.
     *
     * @return {@code true} if the dialog input is valid and dialog was not
     *         dismissed.
     */
    boolean getSettings() {

        final EnhancedGenericDialog gd = new EnhancedGenericDialog("Strahler Analysis :: " + IPNAT.getVersion());
        final Font headerFont = new Font("SansSerif", Font.BOLD, 12);
        gd.setSmartRecording(true);

        // Part 1. Main Options
        gd.setInsets(0, 0, 0);
        gd.addMessage("Tree Classification:", headerFont);
        gd.addCheckbox("Infer root end-points from rectangular ROI", protectRoot);
        gd.addCheckbox("Ignore single-point arbors (Isolated pixels)", erodeIsolatedPixels);

        // Part 2: Loop elimination
        gd.setInsets(25, 0, 0);
        gd.addMessage("Elimination of Skeleton Loops:", headerFont);
        gd.addChoice("Method:", AnalyzeSkeleton_.pruneCyclesModes, AnalyzeSkeleton_.pruneCyclesModes[pruneChoice]);

        // 8-bit grayscale is the only image type recognized by
        // AnalyzeSkeleton_,
        // so we'll provide the user with a pre-filtered list of valid choices
        final ArrayList<Integer> validIds = new ArrayList<Integer>();
        final ArrayList<String> validTitles = new ArrayList<String>();
        final int[] ids = WindowManager.getIDList();
        for (int i = 0; i < ids.length; ++i) {
            final ImagePlus imp = WindowManager.getImage(ids[i]);
            if (imp.getBitDepth() == 8) { // TODO: ignore composites?
                validIds.add(ids[i]);
                validTitles.add(imp.getTitle());
            }
        }
        gd.addChoice("8-bit grayscale image:", validTitles.toArray(new String[validTitles.size()]), title);

        // Part 3: Output
        gd.setInsets(25, 0, 0);
        gd.addMessage("Output Options:", headerFont);
        gd.addCheckbox("Display_iteration stack", outIS);
        gd.addCheckbox("Show detailed information", verbose);
        gd.addCheckbox("Tabular data only (no image output)", tabular);

        gd.addDialogListener(this);
        dialogItemChanged(gd, null); // update prompt

        // Add More>> dropdown menu
        final JPopupMenu popup = new JPopupMenu();
        JMenuItem mi;
        mi = new JMenuItem("Online documentation");
        mi.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                IJ.runPlugIn("ij.plugin.BrowserLauncher", URL);
            }
        });
        popup.add(mi);
        popup.addSeparator();
        mi = new JMenuItem("List hIPNAT commands...");
        mi.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                IJ.runPlugIn("ij.plugin.BrowserLauncher", IPNAT.DOC_URL + "#List_of_commands");
            }
        });
        popup.add(mi);
        mi = new JMenuItem("About hIPNAT plugins...");
        mi.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                IJ.runPlugIn("ipnat.Help", "");
            }
        });
        popup.add(mi);
        gd.assignPopupToHelpButton(popup);

        gd.showDialog();

        // Set grayscale image for intensity-based pruning of skel. loops.
        if (pruneChoice == AnalyzeSkeleton_.LOWEST_INTENSITY_VOXEL
                || pruneChoice == AnalyzeSkeleton_.LOWEST_INTENSITY_BRANCH) {
            grayscaleImp = WindowManager.getImage(validIds.get(grayscaleImpChoice));
        } else {
            grayscaleImp = null;
        }

        return gd.wasOKed();

    }

    /*
     * Retrieve dialog options using the DialogListener interface
     *
     * @see ij.gui.DialogListener#dialogItemChanged(ij.gui.GenericDialog,
     * java.awt.AWTEvent)
     */
    @Override
    public boolean dialogItemChanged(final GenericDialog gd, final java.awt.AWTEvent e) {

        protectRoot = gd.getNextBoolean();
        erodeIsolatedPixels = gd.getNextBoolean();
        pruneChoice = gd.getNextChoiceIndex();
        grayscaleImpChoice = gd.getNextChoiceIndex();
        outIS = gd.getNextBoolean();
        verbose = gd.getNextBoolean();
        tabular = gd.getNextBoolean();

        // Enable/Disable key components of GenericDialog
        if (!IJ.macroRunning()) {
            final Choice cImgChoice = (Choice) gd.getChoices().elementAt(1);
            final Vector<?> checkboxes = gd.getCheckboxes();
            final Checkbox roiOption = (Checkbox) checkboxes.elementAt(0);
            final Checkbox stackOption = (Checkbox) checkboxes.elementAt(2);

            cImgChoice.setEnabled(pruneChoice == AnalyzeSkeleton_.LOWEST_INTENSITY_VOXEL
                    || pruneChoice == AnalyzeSkeleton_.LOWEST_INTENSITY_BRANCH);
            roiOption.setEnabled(validRootRoi);
            stackOption.setEnabled(!tabular);

        }

        return !gd.wasCanceled();

    }

    /**
     * Returns the sum of the values in the input array, or zero if the array is
     * empty or {@code null}.
     *
     * @param the
     *            array of values to be summed
     * @return the sum of elements in the array. Returns zero if array is
     *         {@code null} or empty.
     */
    int sum(final int[] array) {
        int sum = 0;
        if (array != null)
            for (final int i : array)
                sum += i;
        return sum;
    }

    /**
     * Returns the sum of the values in the input array, or zero if the array is
     * empty or {@code null}.
     *
     * @param the
     *            array of values to be summed
     * @return the sum of elements in the array. Returns zero if array is
     *         {@code null} or empty.
     */
    double sum(final double[] array) {
        double sum = 0; // TODO Use org.apache.commons.math3.stat.StatUtils?
        if (array != null && array.length > 0)
            for (final double i : array)
                sum += i;
        return sum;
    }

    /**
     * Returns the arithmetic mean of the values in the input array, or
     * {@code Double.NaN} if the array is empty or {@code null}.
     *
     * @param the
     *            array of values to be averaged
     * @return the arithmetic mean of the array. Returns {@code Double.NaN} if
     *         array is {@code null} or empty.
     */
    double average(final double[] array) {
        if (array != null && array.length > 0)
            return sum(array) / array.length; // TODO Use
        // org.apache.commons.math3.stat.StatUtils?
        else
            return Double.NaN;
    }

    /* Paints point positions. */
    void paintPoints(final ImageStack stack, final ArrayList<Point> points, final int value,
            final String sliceLabel) {
        if (points != null) {
            final ImageProcessor ipp = ip.createProcessor(stack.getWidth(), stack.getHeight());
            for (int j = 0; j < points.size(); j++) {
                final Point point = points.get(j);
                ipp.putPixel(point.x, point.y, value);
            }
            stack.addSlice(sliceLabel, ipp);
        }
    }

    /* Clears point positions */
    void clearPoints(final ImageProcessor ip, final ArrayList<Point> points) {
        if (points != null) {
            for (int j = 0; j < points.size(); j++) {
                final Point point = points.get(j);
                ip.putPixel(point.x, point.y, 0);
            }
        }
    }

    /*
     * Skeletonization method that erodes the thinned structure in order to
     * eliminate isolated pixels. Thinning and pruning may give rise to single
     * point arbors. These 'debris' trees have 1 end-point but no branches or
     * junctions. If present they overestimate the total number of end-points
     */
    void skeletonizeWithoutHermits(final ImagePlus imp) {
        final Skeletonize3D_ thin = new Skeletonize3D_();
        thin.setup("", imp);
        thin.run(null);
        if (erodeIsolatedPixels)
            Binary.removeIsolatedPixels(imp);
    }

    /**
     * Runs {@link ij.plugin.CalibrationBar} on the specified image using
     * sensible settings.
     *
     * @param imp
     *            the image to processed
     * @param nLabels
     *            the n. of labels in the calibration bar
     * @param color
     *            Labels' foreground color as per {@link ij.plugin.Colors}
     **/
    void addCalibrationBar(final ImagePlus imp, final int nLabels, final String color) {
        final ImageCanvas ic = imp.getCanvas();
        double zoom = (imp.getHeight() > 200) ? 1.0 : 0.8;
        final double mag = (ic != null) ? ic.getMagnification() : 1.0;
        if (zoom <= 1 && mag < 1)
            zoom = 1.0 / mag;
        IJ.run(imp, "Calibration Bar...",
                "fill=None label=" + color + " number=" + nLabels + " zoom=" + zoom + " overlay");
    }

    /**
     * Returns the maximum Strahler order being considered by the plugin.
     *
     * @return The maximum number of pruning cycles of end-point branches that
     *         the plugin should perform
     * @see #setMaxOrder(int)
     */
    public int getMaxOrder() {
        return maxOrder;
    }

    /**
     * Sets the maximum Strahler order to be considered by the plugin.
     *
     * @param maxOrder
     *            The maximum number of pruning cycles of end-point branches
     *            that the plugin should perform
     * @see #getMaxOrder()
     */
    public void setMaxOrder(final int maxOrder) {
        this.maxOrder = maxOrder;
    }

}