/* * #%L * AnalyzeSkeleton_ plugin for ImageJ. * %% * Copyright (C) 2008 - 2017 Ignacio Arganda-Carreras. * %% * 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 sc.fiji.analyzeSkeleton; import ij.*; import ij.gui.DialogListener; import ij.gui.GenericDialog; import ij.gui.Roi; import ij.measure.Calibration; import ij.measure.ResultsTable; import ij.plugin.filter.PlugInFilter; import ij.plugin.frame.Recorder; import ij.process.ByteProcessor; import ij.process.FloatProcessor; import ij.process.ImageProcessor; import java.awt.AWTEvent; import java.awt.Checkbox; import java.awt.Font; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.ListIterator; /** * AnalyzeSkeleton_ plugin for ImageJ and Fiji. * * 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 (http://www.gnu.org/licenses/gpl.txt ) * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ /** * Main class of the ImageJ/Fiji plugin for skeleton analysis. * This class is a plugin for the ImageJ and Fiji interfaces for analyzing * 2D/3D skeleton images. * <p> * For more detailed information, visit the AnalyzeSkeleton home page: * <A target="_blank" href="http://fiji.sc/AnalyzeSkeleton">http://fiji.sc/AnalyzeSkeleton</A> * * * @author Ignacio Arganda-Carreras * */ public class AnalyzeSkeleton_ implements PlugInFilter, DialogListener { private static final String PRUNE_MODE_INDEX_KEY = "sc.fiji.analyzeSkeleton.pruneModeIndex"; private static final String PRUNE_ENDS_KEY = "sc.fiji.analyzeSkeleton.pruneEnds"; private static final String CALCULATE_PATH_KEY = "sc.fiji.analyzeSkeleton.shortestPath"; private static final String VERBOSE_KEY = "sc.fiji.analyzeSkeleton.showDetailedInfo"; private static final String DISPLAY_SKELETONS_KEY = "sc.fiji.analyzeSkeleton.displayLabeledSkeletons"; private static final int DEFAULT_PRUNE_MODE_INDEX = AnalyzeSkeleton_.NONE; private static final boolean DEFAULT_PRUNE_ENDS = false; private static final boolean DEFAULT_CALCULATE_SHORTEST_PATH = false; private static final boolean DEFAULT_VERBOSE = false; private static final boolean DEFAULT_PROTECT_ROI = false; private static final boolean DEFAULT_DISPLAY_SKELETONS = false; private static final String HELP_URL = "http://fiji.sc/wiki/index.php/AnalyzeSkeleton"; /** end point flag */ public static byte END_POINT = 30; /** junction flag */ public static byte JUNCTION = 70; /** slab flag */ public static byte SLAB = 127; /** shortest path flag */ public static byte SHORTEST_PATH = 96; private GenericDialog settingsDialog; /** working image plus */ private ImagePlus imRef = null; /** working image width */ private int width = 0; /** working image height */ private int height = 0; /** working image depth */ private int depth = 0; /** working image stack */ private ImageStack inputImage = null; /** visit flags */ private boolean [][][] visited = null; // Measures /** total number of end points voxels */ private int totalNumberOfEndPoints = 0; /** total number of junctions voxels */ private int totalNumberOfJunctionVoxels = 0; /** total number of slab voxels */ private int totalNumberOfSlabs = 0; /** auxiliary variable to store the length of longest shortest path in a graph */ private double shortestPath = 0; // Shortest path variables /** list of longest shortest paths from the skeletons in the image */ private ArrayList< Double > shortestPathList; /** list containing longest shortest path points (one per tree) */ private ArrayList< Point >[] shortestPathPoints = null; /** shortest path x start position */ private int spx = 0; /** shortest path y start position */ private int spy = 0; /** shortest path z start position */ private int spz = 0; /** shortest path start position array */ private double[][] spStartPosition; /** shortest path output stack */ private ImageStack shortPathImage = null; // Tree fields /** image stack containing all skeletons marked with their corresponding id */ ImageStack labeledSkeletons = null; /** number of branches for every specific tree */ private int[] numberOfBranches = null; /** number of end points voxels of every tree */ private int[] numberOfEndPoints = null; /** number of junctions voxels of every tree*/ private int[] numberOfJunctionVoxels = null; /** number of slab voxels of every specific tree */ private int[] numberOfSlabs = null; /** number of junctions of every specific tree*/ private int[] numberOfJunctions = null; /** number of triple points in every tree */ private int[] numberOfTriplePoints = null; /** number of quadruple points in every tree */ private int[] numberOfQuadruplePoints = null; /** list of end points in every tree */ private ArrayList <Point> endPointsTree [] = null; /** list of junction voxels in every tree */ private ArrayList <Point> junctionVoxelTree [] = null; /** list of special slab coordinates where circular tree starts */ private ArrayList <Point> startingSlabTree [] = null; /** average branch length */ private double[] averageBranchLength = null; /** maximum branch length */ private double[] maximumBranchLength = null; /** list of end point coordinates in the entire image */ private ArrayList <Point> listOfEndPoints = null; /** list of junction coordinates in the entire image */ private ArrayList <Point> listOfJunctionVoxels = null; /** list of slab coordinates in the entire image */ private ArrayList <Point> listOfSlabVoxels = null; /** list of slab coordinates in the entire image */ private ArrayList <Point> listOfStartingSlabVoxels = null; /** list of groups of junction voxels that belong to the same tree junction (in every tree) */ private ArrayList < ArrayList <Point> > listOfSingleJunctions[] = null; /** array of junction vertex per tree */ private Vertex[][] junctionVertex = null; /** stack image containing the corresponding skeleton tags (end point, junction or slab) */ private ImageStack taggedImage = null; /** auxiliary temporary point */ private Point auxPoint = null; /** number of trees (skeletons) in the image */ private int numOfTrees = 0; /** pruning option */ private boolean bPruneCycles = true; /** dead-end pruning option */ public static boolean pruneEnds = DEFAULT_PRUNE_ENDS; /** protective-ROI option (branches inside ROI are spared from pruning) */ public static boolean protectRoi = DEFAULT_PROTECT_ROI; /** calculate largest shortest path option */ public static boolean calculateShortestPath = DEFAULT_CALCULATE_SHORTEST_PATH; /** array of graphs (one per tree) */ private Graph[] graph = null; /** auxiliary list of slabs */ private ArrayList<Point> slabList = null; /** auxiliary final vertex */ private Vertex auxFinalVertex = null; /** prune cycle options */ public static final String[] pruneCyclesModes = {"none", "shortest branch", "lowest intensity voxel", "lowest intensity branch"}; /** no pruning mode index */ public static final int NONE = 0; /** shortest branch pruning mode index */ public static final int SHORTEST_BRANCH = 1; /** lowest pixel intensity pruning mode index */ public static final int LOWEST_INTENSITY_VOXEL = 2; /** lowest intensity branch pruning mode index */ public static final int LOWEST_INTENSITY_BRANCH = 3; /** original grayscale image (for lowest pixel intensity pruning mode) */ private ImageStack originalImage = null; /** prune cycle options index */ public static int pruneIndex = DEFAULT_PRUNE_MODE_INDEX; /** x- neighborhood offset */ private int x_offset = 1; /** y- neighborhood offset */ private int y_offset = 1; /** z- neighborhood offset */ private int z_offset = 1; /** boolean flag to display extra information in result tables */ public static boolean verbose = DEFAULT_VERBOSE; /** silent run flag, to distinguish between GUI and plugin calls */ protected boolean silent = false; /** debugging flag */ private static final boolean debug = false; /** flag to output the labeled skeletons in a new image */ public static boolean displaySkeletons = DEFAULT_DISPLAY_SKELETONS; /* -----------------------------------------------------------------------*/ /** * This method is called once when the filter is loaded. * * @param arg argument specified for this plugin * @param imp currently active image * @return flag word that specifies the filters capabilities */ public int setup(String arg, ImagePlus imp) { this.imRef = imp; if (arg.equals("about")) { showAbout(); return DONE; } return DOES_8G; } // end method setup /* -----------------------------------------------------------------------*/ /** * Process the image: tag skeleton and show results. * * @see ij.plugin.filter.PlugInFilter#run(ij.process.ImageProcessor) */ public void run(ImageProcessor ip) { loadDialogSettings(); createSettingsDialog(); settingsDialog.showDialog(); if (settingsDialog.wasCanceled()) { return; } setSettingsFromDialog(); saveDialogSettings(); // pre-checking if another image is needed and also setting bPruneCycles ImagePlus origIP = null; switch(pruneIndex) { // No pruning case AnalyzeSkeleton_.NONE: this.bPruneCycles = false; break; // Pruning cycles by shortest branch case AnalyzeSkeleton_.SHORTEST_BRANCH: this.bPruneCycles = true; break; // Pruning cycles by lowest pixel intensity case AnalyzeSkeleton_.LOWEST_INTENSITY_VOXEL: case AnalyzeSkeleton_.LOWEST_INTENSITY_BRANCH: // Select original image between the open images int[] ids = WindowManager.getIDList(); if ( ids == null || ids.length < 1 ) { IJ.showMessage( "You should have at least one image open." ); return; } String[] titles = new String[ ids.length ]; for ( int i = 0; i < ids.length; ++i ) { titles[ i ] = ( WindowManager.getImage( ids[ i ] ) ).getTitle(); } final GenericDialog gd2 = new GenericDialog( "Image selection" ); gd2.addMessage( "Select original grayscale image:" ); final String current = WindowManager.getCurrentImage().getTitle(); gd2.addChoice( "original_image", titles, current ); gd2.showDialog(); if (gd2.wasCanceled()) return; // Get original stack origIP = WindowManager.getImage( ids[ gd2.getNextChoiceIndex() ] ); this.bPruneCycles = true; break; default: } // now we have all the information that's needed for running the plugin // as if it was called from somewhere else run(pruneIndex, pruneEnds, calculateShortestPath, origIP, false, verbose, (protectRoi) ? this.imRef.getRoi() : null); if(debug) IJ.log("num of skeletons = " + this.numOfTrees); // Show labeled skeletons if( AnalyzeSkeleton_.displaySkeletons ) { ImagePlus labeledSkeletons = new ImagePlus( this.imRef.getShortTitle() + "-labeled-skeletons", this.labeledSkeletons.duplicate() ); IJ.run( labeledSkeletons, "Fire", null ); labeledSkeletons.show(); } // Show results table showResults(); } // end run method /** Disables dialog components that are irrelevant to GUI-based analysis. */ public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { if ( this.imRef.getRoi() == null && null != gd && null != gd.getCheckboxes() ) { Checkbox roiOption = (Checkbox)gd.getCheckboxes().elementAt(1); roiOption.setEnabled(false); if (Recorder.record) roiOption.setState(false); } return true; } /** * This method is intended for non-interactively using this plugin. * <p> * @param pruneIndex The pruneIndex, as asked by the initial gui dialog. * @param pruneEnds flag to prune end-point-ending branches * @param shortPath flag to calculate the longest shortest path * @param origIP original grayscale input image (for lowest pixel intensity pruning mode) * @param silent * @param verbose flag to display running information */ public SkeletonResult run( int pruneIndex, boolean pruneEnds, boolean shortPath, ImagePlus origIP, boolean silent, boolean verbose) { return run(pruneIndex, pruneEnds, shortPath, origIP, silent, verbose, null); } /** * This method is intended for non-interactively using this plugin. * <p> * @param pruneIndex The pruneIndex, as asked by the initial gui dialog. * @param pruneEnds flag to prune end-point-ending branches * @param shortPath flag to calculate the longest shortest path * @param origIP original grayscale input image (for lowest pixel intensity pruning mode) * @param silent * @param verbose flag to display running information * @param roi points inside this region are spared from elimination when * pruning end branches. ROI can be associated to a single image * in the stack or all images as per {@link ij.gui.Roi#getPosition * ij.gui.Roi.getPosition()} */ public SkeletonResult run( int pruneIndex, boolean pruneEnds, boolean shortPath, ImagePlus origIP, boolean silent, boolean verbose, Roi roi) { AnalyzeSkeleton_.pruneIndex = pruneIndex; this.silent = silent; AnalyzeSkeleton_.pruneEnds = pruneEnds; AnalyzeSkeleton_.calculateShortestPath = shortPath; AnalyzeSkeleton_.verbose = verbose; switch(pruneIndex) { // No pruning case AnalyzeSkeleton_.NONE: this.bPruneCycles = false; break; // Pruning cycles by shortest branch case AnalyzeSkeleton_.SHORTEST_BRANCH: this.bPruneCycles = true; break; // Pruning cycles by lowest pixel intensity case AnalyzeSkeleton_.LOWEST_INTENSITY_VOXEL: case AnalyzeSkeleton_.LOWEST_INTENSITY_BRANCH: // calculate neighborhood size given the calibration calculateNeighborhoodOffsets(origIP.getCalibration()); this.originalImage = origIP.getStack(); this.bPruneCycles = true; break; default: } this.width = this.imRef.getWidth(); this.height = this.imRef.getHeight(); this.depth = this.imRef.getStackSize(); this.inputImage = this.imRef.getStack(); // initialize visit flags resetVisited(); // Tag skeleton, differentiate trees and visit them processSkeleton(this.inputImage); // prune ends if (pruneEnds) { pruneEndBranches(this.inputImage, this.taggedImage, roi); } // Prune cycles if necessary if(bPruneCycles) { if(pruneCycles(this.inputImage, this.originalImage, AnalyzeSkeleton_.pruneIndex)) { // initialize visit flags resetVisited(); // Recalculate analysis over the new image bPruneCycles = false; processSkeleton(this.inputImage); } } // Calculate triple points (junctions with exactly 3 branches) calculateTripleAndQuadruplePoints(); if(shortPath) { if(debug) IJ.log("Calculating longest shortest paths..."); // Copy input image this.shortPathImage = new ImageStack(this.width, this.height, this.inputImage.getColorModel()); for(int i=1; i<=this.inputImage.getSize(); i++) shortPathImage.addSlice(this.inputImage.getSliceLabel(i), this.inputImage.getProcessor(i).duplicate()); shortestPathList = new ArrayList < Double >(); this.shortestPathPoints = new ArrayList [ this.numOfTrees ]; // Visit skeleton and measure distances. // and apply Warshall algorithm spStartPosition = new double[this.numOfTrees][3]; for(int i = 0; i < this.numOfTrees; i++) { shortestPathPoints[ i ] = new ArrayList<Point>(); // Warshall algorithm including tag positions this.shortestPath = warshallAlgorithm(this.graph[i], shortestPathPoints[ i ]); shortestPathList.add(this.shortestPath); spStartPosition[i][0] = spx * this.imRef.getCalibration().pixelWidth; spStartPosition[i][1] = spy * this.imRef.getCalibration().pixelHeight; spStartPosition[i][2] = spz * this.imRef.getCalibration().pixelDepth; } if (!silent) { // Display short paths in a new stack ImagePlus shortIP = new ImagePlus("Longest shortest paths", shortPathImage); shortIP.show(); // Set same calibration as the input image shortIP.setCalibration(this.imRef.getCalibration()); // We apply the Fire LUT and reset the min and max to be between 0-255. IJ.run(shortIP, "Fire", null); //IJ.resetMinAndMax(); shortIP.resetDisplayRange(); shortIP.updateAndDraw(); } } // Return the analysis results return assembleResults(); } /** * This method is intended for non-interactively using this plugin. * <p> * @param pruneIndex The pruneIndex, as asked by the initial gui dialog. * @param thresholdLength maximum length of the branches to prune (all branches below that value are removed) * @param shortPath flag to calculate the longest shortest path * @param origIP original input image * @param silent * @param verbose flag to display running information */ public SkeletonResult run( int pruneIndex, double thresholdLength, boolean shortPath, ImagePlus origIP, boolean silent, boolean verbose) { AnalyzeSkeleton_.pruneIndex = pruneIndex; this.silent = silent; AnalyzeSkeleton_.pruneEnds = true; AnalyzeSkeleton_.calculateShortestPath = shortPath; AnalyzeSkeleton_.verbose = verbose; switch(pruneIndex) { // No pruning case AnalyzeSkeleton_.NONE: this.bPruneCycles = false; break; // Pruning cycles by shortest branch case AnalyzeSkeleton_.SHORTEST_BRANCH: this.bPruneCycles = true; break; // Pruning cycles by lowest pixel intensity case AnalyzeSkeleton_.LOWEST_INTENSITY_VOXEL: case AnalyzeSkeleton_.LOWEST_INTENSITY_BRANCH: // calculate neighborhood size given the calibration calculateNeighborhoodOffsets(origIP.getCalibration()); this.originalImage = origIP.getStack(); this.bPruneCycles = true; break; default: } this.width = this.imRef.getWidth(); this.height = this.imRef.getHeight(); this.depth = this.imRef.getStackSize(); this.inputImage = this.imRef.getStack(); // initialize visit flags resetVisited(); // Tag skeleton, differentiate trees and visit them processSkeleton(this.inputImage); // prune ends pruneEndBranches(this.inputImage, this.taggedImage, thresholdLength); // Prune cycles if necessary if(bPruneCycles) { if(pruneCycles(this.inputImage, this.originalImage, AnalyzeSkeleton_.pruneIndex)) { // initialize visit flags resetVisited(); // Recalculate analysis over the new image bPruneCycles = false; processSkeleton(this.inputImage); } } // Calculate triple points (junctions with exactly 3 branches) calculateTripleAndQuadruplePoints(); if(shortPath) { if(debug) IJ.log("Calculating longest shortest paths..."); // Copy input image this.shortPathImage = new ImageStack(this.width, this.height, this.inputImage.getColorModel()); for(int i=1; i<=this.inputImage.getSize(); i++) shortPathImage.addSlice(this.inputImage.getSliceLabel(i), this.inputImage.getProcessor(i).duplicate()); shortestPathList = new ArrayList < Double >(); this.shortestPathPoints = new ArrayList [ this.numOfTrees ]; // Visit skeleton and measure distances. // and apply Warshall algorithm spStartPosition = new double[this.numOfTrees][3]; for(int i = 0; i < this.numOfTrees; i++) { shortestPathPoints[ i ] = new ArrayList<Point>(); // Warshall algorithm including tag positions this.shortestPath = warshallAlgorithm(this.graph[i], shortestPathPoints[ i ]); shortestPathList.add(this.shortestPath); spStartPosition[i][0] = spx * this.imRef.getCalibration().pixelWidth; spStartPosition[i][1] = spy * this.imRef.getCalibration().pixelHeight; spStartPosition[i][2] = spz * this.imRef.getCalibration().pixelDepth; } if (!silent) { // Display short paths in a new stack ImagePlus shortIP = new ImagePlus("Longest shortest paths", shortPathImage.duplicate()); shortIP.show(); // Set same calibration as the input image shortIP.setCalibration(this.imRef.getCalibration()); // We apply the Fire LUT and reset the min and max to be between 0-255. IJ.run(shortIP, "Fire", null); //IJ.resetMinAndMax(); shortIP.resetDisplayRange(); shortIP.updateAndDraw(); } } // Return the analysis results return assembleResults(); } /** * Get the graphs of the current skeletons * @return array of graphs (one per tree/skeleton) */ public Graph[] getGraphs() { return graph; } /** * Get the list of points (including junctions and end points) of the * largest shortest paths in the skeleton image (one per tree). * @return array with the lists of points of the shortest paths */ public ArrayList<Point>[] getShortestPathPoints() { return this.shortestPathPoints; } /** * Get the list of endpoints in the skeleton image per tree. * @return array with the lists of endpoints */ public ArrayList<Point>[] getEndPointsTree() { return this.endPointsTree; } /** * Get the list of endpoints in the skeleton image. * @return array with the endpoints */ public ArrayList<Point> getEndPoints() { return this.listOfEndPoints; } /** * A simpler standalone running method, for analysis without pruning * or showing images. * <p> * This one just calls run(AnalyzeSkeleton_.NONE, false, null, true, false) */ public SkeletonResult run() { return run(NONE, false, false, null, true, false); } /** * Prune end branches outside the specified ROI * * @param stack input skeleton image * @param taggedImage tagged skeleton image * @param roi 'protective' ROI: points inside this region are spared from pruning * */ private void pruneEndBranches(ImageStack stack, ImageStack taggedImage, Roi roi) { if(debug) IJ.log("Pruning end-point branches..."); for (int t = 0; t < this.numOfTrees; t++) { if(debug) IJ.log("Pruning tree #" + t); Graph g = graph[t]; ArrayList<Vertex> vertices = g.getVertices(); ListIterator<Vertex> vit = vertices.listIterator(); if(debug) IJ.log("Initial number of vertices: " + graph[t].getVertices().size()); while (vit.hasNext()) { Vertex v = vit.next(); // Check if the vertex is an end point if (v.getBranches().size() == 1 && isEndPoint( v.getPoints().get( 0 ), roi) ) { if(debug) IJ.log("Pruning branch starting at " + v.getPoints().get(0)); // Remove end point voxels ArrayList<Point> points = v.getPoints(); final int nPoints = points.size(); for (int i = 0; i < nPoints; i++) { Point p = points.get(i); setPixel(stack, p.x, p.y, p.z, (byte) 0); setPixel(taggedImage, p.x, p.y, p.z, (byte) 0); this.numberOfEndPoints[t]--; this.totalNumberOfEndPoints--; Iterator<Point> pit = this.listOfEndPoints.listIterator(); while (pit.hasNext()){ Point ep = pit.next(); if (ep.equals(p)){ pit.remove(); break; } } } // Remove branch voxels Edge branch = v.getBranches().get(0); points = branch.getSlabs(); final int nSlabs = points.size(); for (int i = 0; i < nSlabs; i++) { Point p = points.get(i); setPixel(stack, p.x, p.y, p.z, (byte) 0); setPixel(taggedImage, p.x, p.y, p.z, (byte) 0); this.numberOfSlabs[t]--; this.totalNumberOfSlabs--; Iterator<Point> pit = this.listOfSlabVoxels.listIterator(); while (pit.hasNext()) { Point ep = pit.next(); if (ep.equals(p)){ pit.remove(); break; } } } // remove the Edge from the Graph ArrayList<Edge> gEdges = graph[t].getEdges(); Iterator<Edge> git = gEdges.listIterator(); while (git.hasNext()) { Edge e = git.next(); if (e.equals(branch)) { git.remove(); break; } } // remove the Edge from the opposite Vertex Vertex opp = branch.getOppositeVertex(v); ArrayList<Edge> oppBranches = opp.getBranches(); Iterator<Edge> oppIt = oppBranches.listIterator(); while (oppIt.hasNext()) { Edge oppBranch = oppIt.next(); if (oppBranch.equals(branch)) { oppIt.remove(); break; } } // remove the Edge from the Vertex v.getBranches().remove(0); // remove the Vertex from the Graph vit.remove(); } } if(debug) IJ.log("Final number of vertices: " + graph[t].getVertices().size()); } return; } /** * Prune end branches of a specific length * * @param stack input skeleton image * @param taggedImage tagged skeleton image * @param length limit length to prune the branches (in calibrated units) * */ private void pruneEndBranches( ImageStack stack, ImageStack taggedImage, double length) { if(debug) IJ.log("Pruning end-point branches..."); for (int t = 0; t < this.numOfTrees; t++) { if(debug) IJ.log("Pruning tree #" + t); Graph g = graph[t]; ArrayList<Vertex> vertices = g.getVertices(); ListIterator<Vertex> vit = vertices.listIterator(); if(debug) IJ.log("Initial number of vertices: " + graph[t].getVertices().size()); while (vit.hasNext()) { Vertex v = vit.next(); if (v.getBranches().size() == 1 && v.getBranches().get(0).getLength() <= length) { if(debug) IJ.log("Pruning branch starting at " + v.getPoints().get(0)); // Remove end point voxels ArrayList<Point> points = v.getPoints(); final int nPoints = points.size(); for (int i = 0; i < nPoints; i++) { Point p = points.get(i); setPixel(stack, p.x, p.y, p.z, (byte) 0); setPixel(taggedImage, p.x, p.y, p.z, (byte) 0); this.numberOfEndPoints[t]--; this.totalNumberOfEndPoints--; Iterator<Point> pit = this.listOfEndPoints.listIterator(); while (pit.hasNext()){ Point ep = pit.next(); if (ep.equals(p)){ pit.remove(); break; } } } // Remove branch voxels Edge branch = v.getBranches().get(0); points = branch.getSlabs(); final int nSlabs = points.size(); for (int i = 0; i < nSlabs; i++) { Point p = points.get(i); setPixel(stack, p.x, p.y, p.z, (byte) 0); setPixel(taggedImage, p.x, p.y, p.z, (byte) 0); this.numberOfSlabs[t]--; this.totalNumberOfSlabs--; Iterator<Point> pit = this.listOfSlabVoxels.listIterator(); while (pit.hasNext()) { Point ep = pit.next(); if (ep.equals(p)){ pit.remove(); break; } } } // remove the Edge from the Graph ArrayList<Edge> gEdges = graph[t].getEdges(); Iterator<Edge> git = gEdges.listIterator(); while (git.hasNext()) { Edge e = git.next(); if (e.equals(branch)) { git.remove(); break; } } // remove the Edge from the opposite Vertex Vertex opp = branch.getOppositeVertex(v); ArrayList<Edge> oppBranches = opp.getBranches(); Iterator<Edge> oppIt = oppBranches.listIterator(); while (oppIt.hasNext()) { Edge oppBranch = oppIt.next(); if (oppBranch.equals(branch)) { oppIt.remove(); break; } } // remove the Edge from the Vertex v.getBranches().remove(0); // remove the Vertex from the Graph vit.remove(); } } if(debug) IJ.log("Final number of vertices: " + graph[t].getVertices().size()); } return; } // --------------------------------------------------------------------------- /** * Calculate the neighborhood size based on the calibration of the image. * @param calibration image calibration */ private void calculateNeighborhoodOffsets(Calibration calibration) { double max = calibration.pixelDepth; if(calibration.pixelHeight > max) max = calibration.pixelHeight; if(calibration.pixelWidth > max) max = calibration.pixelWidth; this.x_offset = ((int) Math.round(max / calibration.pixelWidth) > 1) ? (int) Math.round(max / calibration.pixelWidth) : 1; this.y_offset = ((int) Math.round(max / calibration.pixelHeight) > 1) ? (int) Math.round(max / calibration.pixelHeight) : 1; this.z_offset = ((int) Math.round(max / calibration.pixelDepth) > 1) ? (int) Math.round(max / calibration.pixelDepth) : 1; if(debug) { IJ.log("x_offset = " + this.x_offset); IJ.log("y_offset = " + this.y_offset); IJ.log("z_offset = " + this.z_offset); } }// end method calculateNeighborhoodOffsets // --------------------------------------------------------------------------- /** * Process skeleton: tag image, mark trees and visit. * * @param inputImage2 input skeleton image to process */ public void processSkeleton(ImageStack inputImage2) { // Initialize global lists of points this.listOfEndPoints = new ArrayList<Point>(); this.listOfJunctionVoxels = new ArrayList<Point>(); this.listOfSlabVoxels = new ArrayList<Point>(); this.listOfStartingSlabVoxels = new ArrayList<Point>(); this.totalNumberOfEndPoints = 0; this.totalNumberOfJunctionVoxels = 0; this.totalNumberOfSlabs = 0; // Prepare data: classify voxels and tag them. this.taggedImage = tagImage(inputImage2); // Show tags image. if(!bPruneCycles && !silent) { displayTagImage(taggedImage); } // Mark trees labeledSkeletons = markTrees(taggedImage); if(this.numOfTrees == 0) return; // Ask memory for every tree initializeTrees(); // Divide groups of end-points and junction voxels if(this.numOfTrees > 1) divideVoxelsByTrees( labeledSkeletons ); if(this.numOfTrees == 1) { if(debug) IJ.log("list of end points size = " + this.listOfEndPoints.size()); this.endPointsTree[0] = this.listOfEndPoints; this.numberOfEndPoints[0] = this.listOfEndPoints.size(); this.junctionVoxelTree[0] = this.listOfJunctionVoxels; this.numberOfJunctionVoxels[0] = this.listOfJunctionVoxels.size(); this.startingSlabTree[0] = this.listOfStartingSlabVoxels; } // Calculate number of junctions (skipping neighbor junction voxels) groupJunctions( labeledSkeletons ); // Mark all unvisited resetVisited(); // Visit skeleton and measure distances. for(int i = 0; i < this.numOfTrees; i++) visitSkeleton(taggedImage, labeledSkeletons, i+1); } // end method processSkeleton // ----------------------------------------------------------------------- /** * Prune cycles from tagged image and update it. * * @param inputImage input skeleton image * @param originalImage original gray-scale image * @param pruningMode (SHORTEST_BRANCH, LOWEST_INTENSITY_VOXEL, LOWEST_INTENSITY_BRANCH) * @return true if the input image was pruned or false if there were no cycles */ private boolean pruneCycles( ImageStack inputImage, final ImageStack originalImage, final int pruningMode) { boolean pruned = false; for(int iTree = 0 ; iTree < this.numOfTrees; iTree ++) { // For circular trees we just remove one slab if(this.startingSlabTree[iTree].size() == 1) { setPixel(inputImage, this.startingSlabTree[iTree].get(0),(byte) 0); pruned = true; } else // For the rest, we do depth-first search to detect the cycles { // DFS ArrayList <Edge> backEdges = this.graph[iTree].depthFirstSearch(); if(debug) { IJ.log( " --------------------------- "); final String[] s = new String[]{"UNDEFINED", "TREE" , "BACK"}; for(final Edge e : this.graph[iTree].getEdges()) { IJ.log(" edge " + e.getV1().getPoints().get(0) + " - " + e.getV2().getPoints().get(0) + " : " + s[e.getType()+1]); } } // If DFS returned backEdges, we need to delete the loops if(backEdges.size() > 0) { // Find all edges of each loop (backtracking the predecessors) for(final Edge e : backEdges) { ArrayList<Edge> loopEdges = new ArrayList<Edge>(); loopEdges.add(e); Edge minEdge = e; // backtracking (starting at the vertex with higher order index final Vertex finalLoopVertex = e.getV1().getVisitOrder() < e.getV2().getVisitOrder() ? e.getV1() : e.getV2(); Vertex backtrackVertex = e.getV1().getVisitOrder() < e.getV2().getVisitOrder() ? e.getV2() : e.getV1(); // backtrack until reaching final loop vertex while(!finalLoopVertex.equals(backtrackVertex)) { // Extract predecessor final Edge pre = backtrackVertex.getPredecessor(); // Update shortest loop edge if necessary if(pruningMode == AnalyzeSkeleton_.SHORTEST_BRANCH && pre.getSlabs().size() < minEdge.getSlabs().size()) minEdge = pre; // Add to loop edge list loopEdges.add(pre); // Extract predecessor backtrackVertex = pre.getV1().equals(backtrackVertex) ? pre.getV2() : pre.getV1(); } // Prune cycle if(pruningMode == AnalyzeSkeleton_.SHORTEST_BRANCH) { // Remove middle slab from the shortest loop edge Point removeCoords = null; if(minEdge.getSlabs().size() > 0) removeCoords = minEdge.getSlabs().get(minEdge.getSlabs().size()/2); else removeCoords = minEdge.getV1().getPoints().get(0); setPixel(inputImage, removeCoords,(byte) 0); } else if (pruningMode == AnalyzeSkeleton_.LOWEST_INTENSITY_VOXEL) { removeLowestIntensityVoxel(loopEdges, inputImage, originalImage); } else if(pruningMode == AnalyzeSkeleton_.LOWEST_INTENSITY_BRANCH) { cutLowestIntensityBranch(loopEdges, inputImage, originalImage); } }// endfor backEdges pruned = true; } } } return pruned; }// end method pruneCycles // ----------------------------------------------------------------------- /** * Cut the a list of edges in the lowest pixel intensity voxel (calculated * from the original -grayscale- image). * * @param loopEdges list of edges to be analyzed * @param inputImage2 input skeleton image * @param originalGrayImage original gray image */ private void removeLowestIntensityVoxel( final ArrayList<Edge> loopEdges, ImageStack inputImage2, ImageStack originalGrayImage) { Point lowestIntensityVoxel = null; double lowestIntensityValue = Double.MAX_VALUE; for(final Edge e : loopEdges) { for(final Point p : e.getSlabs()) { final double avg = getAverageNeighborhoodValue(originalGrayImage, p, this.x_offset, this.y_offset, this.z_offset); if(avg < lowestIntensityValue) { lowestIntensityValue = avg; lowestIntensityVoxel = p; } } // Check vertices /* for(final Point p : e.getV1().getPoints()) { final double avg = getAverageNeighborhoodValue(originalGrayImage, p, this.x_offset, this.y_offset, this.z_offset); if(avg < lowestIntensityValue) { lowestIntensityValue = avg; lowestIntensityVoxel = p; } } for(final Point p : e.getV2().getPoints()) { final double avg = getAverageNeighborhoodValue(originalGrayImage, p, this.x_offset, this.y_offset, this.z_offset); if(avg < lowestIntensityValue) { lowestIntensityValue = avg; lowestIntensityVoxel = p; } } */ } // Cut loop in the lowest intensity pixel value position if(debug) IJ.log("Cut loop at coordinates: " + lowestIntensityVoxel); setPixel(inputImage2, lowestIntensityVoxel,(byte) 0); }//end method removeLowestIntensityVoxel // ----------------------------------------------------------------------- /** * Cut the a list of edges in the lowest pixel intensity branch. * * @param loopEdges list of edges to be analyzed * @param inputImage2 input skeleton image */ private void cutLowestIntensityBranch( final ArrayList<Edge> loopEdges, ImageStack inputImage2, ImageStack originalGrayImage) { Edge lowestIntensityEdge = null; double lowestIntensityValue = Double.MAX_VALUE; Point cutPoint = null; for(final Edge e : loopEdges) { // Calculate average intensity of the edge neighborhood double min_val = Double.MAX_VALUE; Point darkestPoint = null; double edgeIntensity = 0; double n_vox = 0; // Check slab points for(final Point p : e.getSlabs()) { final double avg = getAverageNeighborhoodValue(originalGrayImage, p, this.x_offset, this.y_offset, this.z_offset); // Keep track of the darkest slab point of the edge if(avg < min_val) { min_val = avg; darkestPoint = p; } edgeIntensity += avg; n_vox++; } // Check vertices for(final Point p : e.getV1().getPoints()) { edgeIntensity += getAverageNeighborhoodValue(originalGrayImage, p, this.x_offset, this.y_offset, this.z_offset); n_vox++; } for(final Point p : e.getV2().getPoints()) { edgeIntensity += getAverageNeighborhoodValue(originalGrayImage, p, this.x_offset, this.y_offset, this.z_offset); n_vox++; } if(n_vox != 0) edgeIntensity /= n_vox; if(debug) { IJ.log("Loop edge between " + e.getV1().getPoints().get(0) + " and " + e.getV2().getPoints().get(0) + ":"); IJ.log("avg edge intensity = " + edgeIntensity + " darkest slab point = " + darkestPoint.toString()); } // Keep track of the lowest intensity edge if(edgeIntensity < lowestIntensityValue) { lowestIntensityEdge = e; lowestIntensityValue = edgeIntensity; cutPoint = darkestPoint; } } // Cut loop in the lowest intensity branch medium position Point removeCoords = null; if (lowestIntensityEdge.getSlabs().size() > 0) removeCoords = cutPoint; else { IJ.error("Lowest intensity branch without slabs?!: vertex " + lowestIntensityEdge.getV1().getPoints().get(0)); removeCoords = lowestIntensityEdge.getV1().getPoints().get(0); } if(debug) IJ.log("Cut loop at coordinates: " + removeCoords); setPixel(inputImage2, removeCoords,(byte) 0); }// end method cutLowestIntensityBranch // ----------------------------------------------------------------------- /** * Display tag image on a new window. * * @param taggedImage tag image to be displayed */ void displayTagImage(ImageStack taggedImage) { final int slices = imRef.getNSlices(); final int frames = imRef.getNFrames(); final int channels = imRef.getNChannels(); ImagePlus tagIP = IJ.createHyperStack("Tagged skeleton", width, height, channels, slices, frames, imRef.getBitDepth()); tagIP.setStack(taggedImage.duplicate(), channels, slices, frames); tagIP.show(); // Set same calibration as the input image tagIP.setCalibration(this.imRef.getCalibration()); // We apply the Fire LUT and reset the min and max to be between 0-255. IJ.run(tagIP, "Fire", null); //IJ.resetMinAndMax(); tagIP.resetDisplayRange(); tagIP.updateAndDraw(); } // end method displayTagImage // ----------------------------------------------------------------------- /** * Divide the end point, junction and special (starting) slab voxels in the * corresponding tree lists. * * @param treeIS tree image */ private void divideVoxelsByTrees(ImageStack treeIS) { // Add end points to the corresponding tree for(int i = 0; i < this.totalNumberOfEndPoints; i++) { final Point p = this.listOfEndPoints.get(i); this.endPointsTree[ (int)getFloatPixel( treeIS, p ) - 1 ].add(p); } // Add junction voxels to the corresponding tree for(int i = 0; i < this.totalNumberOfJunctionVoxels; i++) { final Point p = this.listOfJunctionVoxels.get(i); this.junctionVoxelTree[ (int)getFloatPixel( treeIS, p ) - 1 ].add(p);; } // Add special slab voxels to the corresponding tree for(int i = 0; i < this.listOfStartingSlabVoxels.size(); i++) { final Point p = this.listOfStartingSlabVoxels.get(i); this.startingSlabTree[ (int)getFloatPixel( treeIS, p ) - 1 ].add(p);; } // Assign number of end points and junction voxels per tree for(int iTree = 0; iTree < this.numOfTrees; iTree++) { this.numberOfEndPoints[iTree] = this.endPointsTree[iTree].size(); this.numberOfJunctionVoxels[iTree] = this.junctionVoxelTree[iTree].size(); } } // end divideVoxelsByTrees // ----------------------------------------------------------------------- /** * Ask memory for trees. */ private void initializeTrees() { this.numberOfBranches = new int[this.numOfTrees]; this.numberOfEndPoints = new int[this.numOfTrees]; this.numberOfJunctionVoxels = new int[this.numOfTrees]; this.numberOfJunctions = new int[this.numOfTrees]; this.numberOfSlabs = new int[this.numOfTrees]; this.numberOfTriplePoints = new int[this.numOfTrees]; this.numberOfQuadruplePoints = new int[this.numOfTrees]; this.averageBranchLength = new double[this.numOfTrees]; this.maximumBranchLength = new double[this.numOfTrees]; this.endPointsTree = new ArrayList[this.numOfTrees]; this.junctionVoxelTree = new ArrayList[this.numOfTrees]; this.startingSlabTree = new ArrayList[this.numOfTrees]; this.listOfSingleJunctions = new ArrayList[this.numOfTrees]; this.graph = new Graph[this.numOfTrees]; for(int i = 0; i < this.numOfTrees; i++) { this.endPointsTree[i] = new ArrayList <Point>(); this.junctionVoxelTree[i] = new ArrayList <Point>(); this.startingSlabTree[i] = new ArrayList <Point>(); this.listOfSingleJunctions[i] = new ArrayList < ArrayList <Point> > (); } this.junctionVertex = new Vertex[this.numOfTrees][]; }// end method initializeTrees // ----------------------------------------------------------------------- /** * Show results table. */ private void showResults() { final ResultsTable rt = new ResultsTable(); final String[] head = {"Skeleton", "# Branches","# Junctions", "# End-point voxels", "# Junction voxels","# Slab voxels","Average Branch Length", "# Triple points", "# Quadruple points", "Maximum Branch Length", "Longest Shortest Path", "spx", "spy", "spz"}; for(int i = 0 ; i < this.numOfTrees; i++) { rt.incrementCounter(); rt.addValue(head[ 1], this.numberOfBranches[i]); rt.addValue(head[ 2], this.numberOfJunctions[i]); rt.addValue(head[ 3], this.numberOfEndPoints[i]); rt.addValue(head[ 4], this.numberOfJunctionVoxels[i]); rt.addValue(head[ 5], this.numberOfSlabs[i]); rt.addValue(head[ 6], this.averageBranchLength[i]); rt.addValue(head[ 7], this.numberOfTriplePoints[i]); rt.addValue(head[ 8], this.numberOfQuadruplePoints[i]); rt.addValue(head[ 9], this.maximumBranchLength[i]); if(null != this.shortestPathList) { rt.addValue(head[10],this.shortestPathList.get(i)); rt.addValue(head[11],this.spStartPosition[i][0]); rt.addValue(head[12],this.spStartPosition[i][1]); rt.addValue(head[13],this.spStartPosition[i][2]); } if (0 == i % 100) rt.show("Results"); } rt.show("Results"); // Extra information if(AnalyzeSkeleton_.verbose) { // New results table final ResultsTable extra_rt = new ResultsTable(); final String[] extra_head = {"Branch", "Skeleton ID", "Branch length","V1 x", "V1 y", "V1 z","V2 x","V2 y", "V2 z", "Euclidean distance","running average length", "average intensity (inner 3rd)", "average intensity"}; // Edge comparator (by branch length) Comparator<Edge> comp = new Comparator<Edge>(){ public int compare(Edge o1, Edge o2) { final double diff = o1.getLength() - o2.getLength(); if(diff < 0) return 1; else if(diff == 0) return 0; else return -1; } public boolean equals(Object o) { return false; } }; // Display branch information for each tree for(int i = 0 ; i < this.numOfTrees; i++) { final ArrayList<Edge> listEdges = this.graph[i].getEdges(); // Sort branches by length Collections.sort(listEdges, comp); for(final Edge e : listEdges) { extra_rt.incrementCounter(); extra_rt.addValue(extra_head[1], i+1); extra_rt.addValue(extra_head[2], e.getLength()); extra_rt.addValue(extra_head[3], e.getV1().getPoints().get(0).x * this.imRef.getCalibration().pixelWidth); extra_rt.addValue(extra_head[4], e.getV1().getPoints().get(0).y * this.imRef.getCalibration().pixelHeight); extra_rt.addValue(extra_head[5], e.getV1().getPoints().get(0).z * this.imRef.getCalibration().pixelDepth); extra_rt.addValue(extra_head[6], e.getV2().getPoints().get(0).x * this.imRef.getCalibration().pixelWidth); extra_rt.addValue(extra_head[7], e.getV2().getPoints().get(0).y * this.imRef.getCalibration().pixelHeight); extra_rt.addValue(extra_head[8], e.getV2().getPoints().get(0).z * this.imRef.getCalibration().pixelDepth); extra_rt.addValue(extra_head[9], this.calculateDistance(e.getV1().getPoints().get(0), e.getV2().getPoints().get(0))); extra_rt.addValue(extra_head[10], e.getLength_ra()); extra_rt.addValue(extra_head[11], e.getColor3rd()); extra_rt.addValue(extra_head[12], e.getColor()); } } extra_rt.show("Branch information"); } }// end method showResults /** * Returns one of the two result images in an ImageStack object. * * @param longestShortestPath Get the tagged longest shortest paths instead of the standard tagged image * * @return The results image with a tagged skeleton */ public ImageStack getResultImage(boolean longestShortestPath) { if (longestShortestPath) { return this.shortPathImage; } return this.taggedImage; } /** * Returns the analysis results in a SkeletonResult object. * <p> * * @return The results of the skeleton analysis. */ protected SkeletonResult assembleResults() { SkeletonResult result = new SkeletonResult(numOfTrees); result.setBranches(numberOfBranches); result.setJunctions(numberOfJunctions); result.setEndPoints(numberOfEndPoints); result.setJunctionVoxels(numberOfJunctionVoxels); result.setSlabs(numberOfSlabs); result.setAverageBranchLength(averageBranchLength); result.setTriples(numberOfTriplePoints); result.setQuadruples(numberOfQuadruplePoints); result.setMaximumBranchLength(maximumBranchLength); result.setListOfEndPoints(listOfEndPoints); result.setListOfJunctionVoxels(listOfJunctionVoxels); result.setListOfSlabVoxels(listOfSlabVoxels); result.setListOfStartingSlabVoxels(listOfStartingSlabVoxels); result.setShortestPathList(shortestPathList); result.setSpStartPosition(spStartPosition); result.setGraph(graph); result.calculateNumberOfVoxels(); return result; } // ----------------------------------------------------------------------- /** * Visit skeleton starting at end-points, junctions and slab of circular * skeletons, and record measurements. * * @param taggedImage tag skeleton image * @param treeImage skeleton image with tree classification * @param currentTree number of the tree to be visited */ private void visitSkeleton(ImageStack taggedImage, ImageStack treeImage, int currentTree) { // tree index final int iTree = currentTree - 1; if(debug) { // Junction vertices in the tree IJ.log("this.junctionVertex["+(iTree)+"].length = " + this.junctionVertex[iTree].length); for(int i = 0; i < this.junctionVertex[iTree].length; i++) { IJ.log(" vertices points: " + this.junctionVertex[iTree][i]); } } // Create new graph this.graph[iTree] = new Graph(); // Add all junction vertices for(int i = 0; i < this.junctionVertex[iTree].length; i++) this.graph[iTree].addVertex(this.junctionVertex[iTree][i]); if(debug) IJ.log(" Analyzing tree number " + currentTree); // length of branches double branchLength = 0; this.maximumBranchLength[iTree] = 0; this.numberOfSlabs[iTree] = 0; // Visit branches starting at end points for(int i = 0; i < this.numberOfEndPoints[iTree]; i++) { final Point endPointCoord = this.endPointsTree[iTree].get(i); if(debug) IJ.log("\n*** visit from end point: " + endPointCoord + " *** "); // Skip when visited if(isVisited(endPointCoord)) { //if(this.initialPoint[iTree] == null) // IJ.error("WEIRD:" + " (" + endPointCoord.x + ", " + endPointCoord.y + ", " + endPointCoord.z + ")"); if(debug) IJ.log("visited = (" + endPointCoord.x + ", " + endPointCoord.y + ", " + endPointCoord.z + ")"); continue; } // Initial vertex Vertex v1 = new Vertex(); v1.addPoint(endPointCoord); this.graph[iTree].addVertex(v1); if(i == 0) this.graph[iTree].setRoot(v1); // slab list for the edge this.slabList = new ArrayList<Point>(); // Otherwise, visit branch until next junction or end point. double[] properties = visitBranch(endPointCoord, iTree); double length = properties[0]; double color3rd = properties[1]; double color = properties[2]; double length_ra = properties[3]; // If length is 0, it means the tree is formed by only one voxel. if(length == 0) { // If there is an adjacent visited junction, count it // as a single voxel branch final Point aux = getVisitedJunctionNeighbor(endPointCoord, v1); if(null != aux) { this.auxFinalVertex = findPointVertex(this.junctionVertex[iTree], aux); length += calculateDistance(endPointCoord, aux); // Add the length to the first point of the vertex (to prevent later from having // euclidean distances larger than the actual distance) length += calculateDistance(auxFinalVertex.getPoints().get(0), endPointCoord); // Add branch to graph if(debug) IJ.log( "adding branch from " + v1.getPoints().get(0) + " to " + this.auxFinalVertex.getPoints().get(0) ); this.graph[iTree].addVertex(this.auxFinalVertex); this.graph[iTree].addEdge(new Edge(v1, this.auxFinalVertex, this.slabList, length, color3rd, color, length_ra)); // increase number of branches this.numberOfBranches[iTree]++; if(debug) IJ.log("increased number of branches, length = " + length); branchLength += length; } else if(debug) IJ.log("set initial point to final point"); continue; } // If the final point is a slab, then we add the path to the // neighbor junction voxel not belonging to the initial vertex // (unless it is a self loop) if(isSlab(this.auxPoint)) { final Point aux = this.auxPoint; //IJ.log("Looking for " + this.auxPoint + " in the list of vertices..."); this.auxPoint = getVisitedJunctionNeighbor(this.auxPoint, v1); this.auxFinalVertex = findPointVertex(this.junctionVertex[iTree], this.auxPoint); if(this.auxPoint == null) { //IJ.log("Point "+ aux + " has not neighbor end junction! (inner loop)"); // Inner loop this.auxFinalVertex = v1; this.auxPoint = aux; } length += calculateDistance(this.auxPoint, aux); // Add the length to the first point of the vertex (to prevent later from having // euclidean distances larger than the actual distance) length += calculateDistance(auxFinalVertex.getPoints().get(0), auxPoint); } // Add branch to graph if(debug) IJ.log("adding branch from " + v1.getPoints().get(0) + " to " + this.auxFinalVertex.getPoints().get(0) + ", aux point = " + this.auxPoint); this.graph[iTree].addVertex(this.auxFinalVertex); this.graph[iTree].addEdge(new Edge(v1, this.auxFinalVertex, this.slabList, length, color3rd, color, length_ra)); // increase number of branches this.numberOfBranches[iTree]++; if(debug) IJ.log("increased number of branches, length = " + length); branchLength += length; // update maximum branch length if(length > this.maximumBranchLength[iTree]) { this.maximumBranchLength[iTree] = length; } } // If there is no end points, set the first junction as root. if(this.numberOfEndPoints[iTree] == 0 && this.junctionVoxelTree[iTree].size() > 0) this.graph[iTree].setRoot(this.junctionVertex[iTree][0]); if(debug) IJ.log( " --------------------------- "); // Now visit branches starting at junctions // 08/26/2009 Changed the loop to visit first the junction voxels that are // forming a single junction. for(int i = 0; i < this.junctionVertex[iTree].length; i++) { for(int j = 0; j < this.junctionVertex[iTree][i].getPoints().size(); j++) { final Point junctionCoord = this.junctionVertex[iTree][i].getPoints().get(j); if(debug) IJ.log("\n*** visit from junction " + junctionCoord + " *** "); // Mark junction as visited setVisited(junctionCoord, true); Point nextPoint = getNextUnvisitedVoxel(junctionCoord); while(nextPoint != null) { // Do not count adjacent junctions if( !isJunction(nextPoint)) { if (debug) IJ.log("visiting " + nextPoint); // Create graph edge this.slabList = new ArrayList<Point>(); this.slabList.add(nextPoint); this.numberOfSlabs[iTree]++; // Calculate distance from junction to that point double length = calculateDistance(junctionCoord, nextPoint); // Visit branch this.auxPoint = null; double[] properties = visitBranch(nextPoint, iTree); length += properties[0]; double color3rd = properties[1]; double color = properties[2]; double length_ra = properties[3]; // Increase number of branches if(length != 0) { if(this.auxPoint == null) this.auxPoint = nextPoint; this.numberOfBranches[iTree]++; // Initial vertex Vertex initialVertex = null; for(int k = 0; k < this.junctionVertex[iTree].length; k++) if(this.junctionVertex[iTree][k].isVertexPoint(junctionCoord)) { initialVertex = this.junctionVertex[iTree][k]; break; } // If the final point is a slab, then we add the path to the // neighbor junction voxel not belonging to the initial vertex // (unless it is a self loop) if(isSlab(this.auxPoint)) { final Point aux = this.auxPoint; //IJ.log("Looking for " + this.auxPoint + " in the list of vertices..."); this.auxPoint = getVisitedJunctionNeighbor(this.auxPoint, initialVertex); this.auxFinalVertex = findPointVertex(this.junctionVertex[iTree], this.auxPoint); if(this.auxPoint == null) { //IJ.log("Point "+ aux + " has not neighbor end junction! (inner loop)"); // Inner loop this.auxFinalVertex = initialVertex; this.auxPoint = aux; } length += calculateDistance(this.auxPoint, aux); } if(debug) IJ.log("increased number of branches, length = " + length + " (last point = " + this.auxPoint + ")"); // update maximum branch length if(length > this.maximumBranchLength[iTree]) { this.maximumBranchLength[iTree] = length; } // Add the distance between the main vertex of the junction // and the initial junction vertex of the branch (this prevents from // having branches in the graph larger than the calculated branch length) length += calculateDistance(initialVertex.getPoints().get(0), junctionCoord); // Create graph branch // Add branch to graph if(debug) IJ.log("adding branch from " + initialVertex.getPoints().get(0) + " to " + this.auxFinalVertex.getPoints().get(0)); this.graph[iTree].addEdge(new Edge(initialVertex, this.auxFinalVertex, this.slabList, length, color3rd, color, length_ra)); // Increase total length of branches branchLength += length; // update maximum branch length if(length > this.maximumBranchLength[iTree]) { this.maximumBranchLength[iTree] = length; } } } else setVisited(nextPoint, true); nextPoint = getNextUnvisitedVoxel(junctionCoord); } } } if(debug) IJ.log( " --------------------------- "); // Finally visit branches starting at slabs (special case for circular trees) if(this.startingSlabTree[iTree].size() == 1) { if(debug) IJ.log("visit from slabs"); final Point startCoord = this.startingSlabTree[iTree].get(0); // Create circular graph (only one vertex) final Vertex v1 = new Vertex(); v1.addPoint(startCoord); this.graph[iTree].addVertex(v1); this.slabList = new ArrayList<Point>(); this.slabList.add(startCoord); this.numberOfSlabs[iTree]++; // visit branch until finding visited voxel. double[] properties = visitBranch(startCoord, iTree); final double length = properties[0]; double color3rd = properties[1]; double color = properties[2]; double length_ra = properties[3]; if(length != 0) { // increase number of branches this.numberOfBranches[iTree]++; branchLength += length; // update maximum branch length if(length > this.maximumBranchLength[iTree]) { this.maximumBranchLength[iTree] = length; } } // Create circular edge this.graph[iTree].addEdge(new Edge(v1, v1, this.slabList, length, color3rd, color, length_ra)); } if(debug) IJ.log( " --------------------------- "); if(this.numberOfBranches[iTree] == 0) return; // Average length this.averageBranchLength[iTree] = branchLength / this.numberOfBranches[iTree]; if(debug) { IJ.log("Num of vertices = " + this.graph[iTree].getVertices().size() + " num of edges = " + this.graph[iTree].getEdges().size()); for(int i = 0; i < this.graph[iTree].getVertices().size(); i++) { Vertex v = this.graph[iTree].getVertices().get(i); IJ.log(" vertex " + v.getPoints().get(0) + " has neighbors: "); for(int j = 0; j < v.getBranches().size(); j++) { final Vertex v1 = v.getBranches().get(j).getV1(); final Vertex oppositeVertex = v1.equals(v) ? v.getBranches().get(j).getV2() : v1 ; IJ.log(j + ": " + oppositeVertex.getPoints().get(0)); } } IJ.log( " --------------------------- "); for(int i = 0; i < this.junctionVertex[iTree].length; i ++) { IJ.log("Junction #" + i + " is formed by: "); for(int j = 0; j < this.junctionVertex[iTree][i].getPoints().size(); j++) IJ.log(j + ": " + this.junctionVertex[iTree][i].getPoints().get(j)); } } } // end visitSkeleton /* -----------------------------------------------------------------------*/ /** * Color the different trees in the skeleton. * * @param taggedImage * * @return image with every tree tagged with a different number */ private ImageStack markTrees(ImageStack taggedImage) { if(debug) IJ.log("=== Mark Trees ==="); // Create output image ImageStack outputImage = new ImageStack( this.width, this.height ); for (int z = 0; z < depth; z++) { outputImage.addSlice(taggedImage.getSliceLabel(z+1), new FloatProcessor(this.width, this.height)); } this.numOfTrees = 0; int color = 0; // Visit trees starting at end points for(int i = 0; i < this.totalNumberOfEndPoints; i++) { Point endPointCoord = this.listOfEndPoints.get(i); if(isVisited(endPointCoord)) continue; color++; if(color == Integer.MAX_VALUE) { IJ.error("More than " + (Integer.MAX_VALUE-1) + " skeletons in the image. AnalyzeSkeleton can only process up to "+ (Integer.MAX_VALUE-1)); return null; } if(debug) IJ.log("-- Visit tree from end-point:"); // Visit the entire tree. int numOfVoxelsInTree = visitTree(endPointCoord, outputImage, color); // increase number of trees this.numOfTrees++; } // Visit trees starting at junction points // (some circular trees do not have end points) // Visit trees starting at end points for(int i = 0; i < this.totalNumberOfJunctionVoxels; i++) { Point junctionCoord = this.listOfJunctionVoxels.get(i); if(isVisited(junctionCoord)) continue; color++; if(color == Short.MAX_VALUE) { IJ.error("More than " + (Short.MAX_VALUE-1) + " skeletons in the image. AnalyzeSkeleton can only process up to 255"); return null; } if(debug) IJ.log("-- Visit tree from junction:"); // else, visit branch until next junction or end point. int length = visitTree(junctionCoord, outputImage, color); if(length == 0) { color--; // the color was not used continue; } // increase number of trees this.numOfTrees++; } // Check for unvisited slab voxels // (just in case there are circular trees without junctions) for(int i = 0; i < this.listOfSlabVoxels.size(); i++) { Point p = (Point) this.listOfSlabVoxels.get(i); if(isVisited(p) == false) { // Mark that voxel as the start point of the circular skeleton this.listOfStartingSlabVoxels.add(p); color++; if(color == Short.MAX_VALUE) { IJ.error("More than " + (Short.MAX_VALUE-1) + " skeletons in the image. AnalyzeSkeleton can only process up to 255"); return null; } if(debug) IJ.log("-- Visit tree from slab:"); // else, visit branch until next junction or end point. int length = visitTree(p, outputImage, color); if(length == 0) { color--; // the color was not used continue; } // increase number of trees this.numOfTrees++; } } //System.out.println("Number of trees = " + this.numOfTrees); // Show tree image. if(debug) { ImagePlus treesIP = new ImagePlus("Trees skeleton", outputImage); treesIP.show(); // Set same calibration as the input image treesIP.setCalibration(this.imRef.getCalibration()); // We apply the Fire LUT and reset the min and max to be between 0-255. IJ.run("Fire"); //IJ.resetMinAndMax(); treesIP.resetDisplayRange(); treesIP.updateAndDraw(); } // Reset visited variable resetVisited(); //IJ.log("Number of trees: " + this.numOfTrees + ", # colors = " + color); return outputImage; } /* end markTrees */ // -------------------------------------------------------------- /** * Visit tree marking the voxels with a reference tree color. * * @param startingPoint starting tree point * @param outputImage 3D image to visit * @param color reference tree color * @return number of voxels in the tree */ private int visitTree(Point startingPoint, ImageStack outputImage, int color) { int numOfVoxels = 0; if(debug) IJ.log("visiting " + startingPoint + " color = " + color); if(isVisited(startingPoint)) return 0; // Set pixel color this.setPixel(outputImage, startingPoint.x, startingPoint.y, startingPoint.z, color); setVisited(startingPoint, true); ArrayList <Point> toRevisit = new ArrayList <Point>(); // Add starting point to revisit list if it is a junction if(isJunction(startingPoint)) toRevisit.add(startingPoint); Point nextPoint = getNextUnvisitedVoxel(startingPoint); while(nextPoint != null || toRevisit.size() != 0) { if(nextPoint != null) { if(!isVisited(nextPoint)) { numOfVoxels++; if(debug) IJ.log("visiting " + nextPoint+ " color = " + color); // Set color and visit flat this.setPixel(outputImage, nextPoint.x, nextPoint.y, nextPoint.z, color); setVisited(nextPoint, true); // If it is a junction, add it to the revisit list if(isJunction(nextPoint)) toRevisit.add(nextPoint); // Calculate next point to visit nextPoint = getNextUnvisitedVoxel(nextPoint); } } else // revisit list { nextPoint = toRevisit.get(0); if(debug) IJ.log("visiting " + nextPoint+ " color = " + color); // Calculate next point to visit nextPoint = getNextUnvisitedVoxel(nextPoint); // Maintain junction in the list until there is no more branches if (nextPoint == null) toRevisit.remove(0); } } return numOfVoxels; } // end method visitTree // ----------------------------------------------------------------------- /** * Visit a branch and calculate length. * * @param startingPoint starting coordinates * @return branch length * * @deprecated */ private double visitBranch(Point startingPoint) { double length = 0; // mark starting point as visited setVisited(startingPoint, true); // Get next unvisited voxel Point nextPoint = getNextUnvisitedVoxel(startingPoint); if (nextPoint == null) return 0; Point previousPoint = startingPoint; // We visit the branch until we find an end point or a junction while(nextPoint != null && isSlab(nextPoint)) { // Add length length += calculateDistance(previousPoint, nextPoint); // Mark as visited setVisited(nextPoint, true); // Move in the graph previousPoint = nextPoint; nextPoint = getNextUnvisitedVoxel(previousPoint); } if(nextPoint != null) { // Add distance to last point length += calculateDistance(previousPoint, nextPoint); // Mark last point as visited setVisited(nextPoint, true); } this.auxPoint = previousPoint; return length; }// end visitBranch // ----------------------------------------------------------------------- /** * Visit a branch and calculate length in a specific tree * * @param startingPoint starting coordinates * @param iTree tree index * @return branch length and color */ private double[] visitBranch(Point startingPoint, int iTree) { //IJ.log("startingPoint = (" + startingPoint.x + ", " + startingPoint.y + ", " + startingPoint.z + ")"); double length = 0; double intensity = 0.0; double intensity3rd = 0.0; double length_ra = 0.0; double[] ret = new double[4]; List<Point> pointHistory= new ArrayList<Point>(0); pointHistory.add(0,startingPoint); // mark starting point as visited setVisited(startingPoint, true); // Get next unvisited voxel Point nextPoint = getNextUnvisitedVoxel(startingPoint); if (nextPoint == null) return ret; Point previousPoint = startingPoint; // We visit the branch until we find an end point or a junction while(nextPoint != null && isSlab(nextPoint)) { this.numberOfSlabs[iTree]++; // Add slab voxel to the edge this.slabList.add(nextPoint); // Add length length += calculateDistance(previousPoint, nextPoint); pointHistory.add(0,nextPoint); length_ra += calculateDistance(pointHistory); // Mark as visited setVisited(nextPoint, true); // Move in the graph previousPoint = nextPoint; if (debug) IJ.log("visiting " + previousPoint); nextPoint = getNextUnvisitedVoxel(previousPoint); } // If we find an unvisited end-point or junction, we set it // as final vertex of the branch if(nextPoint != null) { // Add distance to last point length += calculateDistance(previousPoint, nextPoint); pointHistory.add(0,nextPoint); length_ra += calculateDistance(pointHistory); // Mark last point as visited setVisited(nextPoint, true); // Mark final vertex if(isEndPoint(nextPoint)) { if(debug) IJ.log("found unvisited end point: " + nextPoint); this.auxFinalVertex = new Vertex(); this.auxFinalVertex.addPoint(nextPoint); } else if(isJunction(nextPoint)) { if(debug) IJ.log("found unvisited junction point: " + nextPoint); this.auxFinalVertex = findPointVertex(this.junctionVertex[iTree], nextPoint); // Add the length to the first point of the vertex (to prevent later from having // euclidean distances larger than the actual distance) length += calculateDistance(auxFinalVertex.getPoints().get(0), nextPoint); length_ra += calculateDistance(auxFinalVertex.getPoints().get(0), nextPoint); /* int j = 0; for(j = 0; j < this.junctionVertex[iTree].length; j++) if(this.junctionVertex[iTree][j].isVertexPoint(nextPoint)) { this.auxFinalVertex = this.junctionVertex[iTree][j]; IJ.log(" " + nextPoint + " belongs to junction " + this.auxFinalVertex.getPoints().get(0)); break; } if(j == this.junctionVertex[iTree].length) IJ.log("point " + nextPoint + " was not found in vertex list!"); */ } this.auxPoint = nextPoint; } else this.auxPoint = previousPoint; //IJ.log("finalPoint = (" + nextPoint.x + ", " + nextPoint.y + ", " + nextPoint.z + ")"); // calculate average intensity (thickness) value, but only take the inner third of a branch. // at both ends the intensity (thickness) is most likely affected by junctions. int size = pointHistory.size(); int start = (int)(size/3.0); int end = (int)(2*size/3.0); for (int i = 0; i < size ;i++){ int value = getPixel(this.inputImage, pointHistory.get(i)); if (value < 0){ value += 256; } intensity += value; if (i>=start && i<end){ intensity3rd += value; } } intensity /= size; intensity3rd /= (end-start); ret[0] = length; ret[1] = intensity3rd; ret[2] = intensity; ret[3] = length_ra; return ret; } // end visitBranch // ----------------------------------------------------------------------- /** * Find vertex in an array given a specific vertex point. * * @param vertex array of search * @param p vertex point * @return vertex containing that point */ public Vertex findPointVertex(Vertex[] vertex, Point p) { int j = 0; for(j = 0; j < vertex.length; j++) if(vertex[j].isVertexPoint(p)) { if(debug) IJ.log(" " + p + " belongs to junction " + vertex[j].getPoints().get(0)); return vertex[j]; } if(debug) IJ.log("point " + p + " was not found in vertex list! (vertex.length= " + vertex.length +")"); return null; } // ----------------------------------------------------------------------- /** * Calculate Euclidean distance between two points in 3D. * * @param point1 first point coordinates * @param point2 second point coordinates * @return distance (in the corresponding units) */ public double calculateDistance(Point point1, Point point2) { final double dx = (point1.x - point2.x) * this.imRef.getCalibration().pixelWidth; final double dy = (point1.y - point2.y) * this.imRef.getCalibration().pixelHeight; final double dz = (point1.z - point2.z) * this.imRef.getCalibration().pixelDepth; return Math.sqrt(dx * dx + dy * dy + dz * dz); } // ----------------------------------------------------------------------- /** * Calculate linear corrected distance between two points in 3D. * Uses the PointHistory of a branch and its 5 last points. * * @param Points - the last visited Points (most recent has Index 0) * @return linear corrected distance between the last two Points (in the corresponding units) */ private double calculateDistance(List<Point> Points) { int indexOfLast = Points.size()-1; //no Distance to be calculated here... if (indexOfLast < 1) return 0; // Point Of Interest int poi = 5; //poi is indexOflast if List is shorter than 5 if (indexOfLast < 5){ poi = indexOfLast; } return calculateDistance(Points.get(poi), Points.get(0))/poi; } // ----------------------------------------------------------------------- /** * Calculate number of junction skipping neighbor junction voxels * * @param treeIS tree stack */ private void groupJunctions(ImageStack treeIS) { // Mark all unvisited resetVisited(); for (int iTree = 0; iTree < this.numOfTrees; iTree++) { // Visit list of junction voxels for(int i = 0; i < this.numberOfJunctionVoxels[iTree]; i ++) { Point pi = this.junctionVoxelTree[iTree].get(i); if(! isVisited(pi)) fusionNeighborJunction(pi, this.listOfSingleJunctions[iTree]); } } // Count number of single junctions for every tree in the image for (int iTree = 0; iTree < this.numOfTrees; iTree++) { if(debug) IJ.log("this.listOfSingleJunctions["+iTree+"].size() = " + this.listOfSingleJunctions[iTree].size()); this.numberOfJunctions[iTree] = this.listOfSingleJunctions[iTree].size(); // Create array of junction vertices for the graph this.junctionVertex[iTree] = new Vertex[this.listOfSingleJunctions[iTree].size()]; for(int j = 0 ; j < this.listOfSingleJunctions[iTree].size(); j++) { final ArrayList<Point> list = this.listOfSingleJunctions[iTree].get(j); this.junctionVertex[iTree][j] = new Vertex(); for(final Point p : list) this.junctionVertex[iTree][j].addPoint(p); } } // Mark all unvisited resetVisited(); } // ----------------------------------------------------------------------- /** * Reset visit variable and set it to false. */ private void resetVisited() { // Reset visited variable this.visited = null; this.visited = new boolean[this.width][this.height][this.depth]; /* for(int i = 0; i < this.width; i ++) for(int j = 0; j < this.height; j++) for(int k = 0; k < this.depth; k++) this.visited[i][j][k] = false; */ } // ----------------------------------------------------------------------- /** * Fusion neighbor junctions voxels into the same list. * * @param startingPoint starting junction voxel * @param singleJunctionsList list of single junctions */ private void fusionNeighborJunction(Point startingPoint, ArrayList<ArrayList<Point>> singleJunctionsList) { // Create new group of junctions ArrayList <Point> newGroup = new ArrayList<Point>(); newGroup.add(startingPoint); // Mark the starting junction as visited setVisited(startingPoint, true); // Look for neighbor junctions and add them to the new group ArrayList <Point> toRevisit = new ArrayList <Point>(); toRevisit.add(startingPoint); Point nextPoint = getNextUnvisitedJunctionVoxel(startingPoint); while(nextPoint != null || toRevisit.size() != 0) { if(nextPoint != null && !isVisited(nextPoint)) { // Add to the group newGroup.add(nextPoint); // Mark as visited setVisited(nextPoint, true); // add it to the revisit list toRevisit.add(nextPoint); // Calculate next junction point to visit nextPoint = getNextUnvisitedJunctionVoxel(nextPoint); } else // revisit list { nextPoint = toRevisit.get(0); //IJ.log("visiting " + nextPoint + " color = " + color); // Calculate next point to visit nextPoint = getNextUnvisitedJunctionVoxel(nextPoint); // Maintain junction in the list until there is no more branches if (nextPoint == null) toRevisit.remove(0); } } // Add group to the single junction list singleJunctionsList.add(newGroup); }// end method fusionNeighborJunction // ----------------------------------------------------------------------- /** * Check if two groups of voxels are neighbors. * * @param g1 first group * @param g2 second group * * @return true if the groups have any neighbor voxel */ boolean checkNeighborGroups(ArrayList <Point> g1, ArrayList <Point> g2) { for(int i =0 ; i < g1.size(); i++) { Point pi = g1.get(i); for(int j =0 ; j < g2.size(); j++) { Point pj = g2.get(j); if(isNeighbor(pi, pj)) return true; } } return false; } // ----------------------------------------------------------------------- /** * Calculate number of triple and quadruple points in the skeleton. Triple and * quadruple points are junctions with exactly 3 and 4 branches respectively. */ private void calculateTripleAndQuadruplePoints() { for (int iTree = 0; iTree < this.numOfTrees; iTree++) { // Visit the groups of junction voxels for(int i = 0; i < this.numberOfJunctions[iTree]; i ++) { ArrayList <Point> groupOfJunctions = this.listOfSingleJunctions[iTree].get(i); // Count the number of slab and end-points neighbors of every voxel in the group int nBranch = 0; for(int j = 0; j < groupOfJunctions.size(); j++) { Point pj = groupOfJunctions.get(j); // Get neighbors and check the slabs or end-points byte[] neighborhood = this.getNeighborhood(this.taggedImage, pj.x, pj.y, pj.z); for(int k = 0; k < 27; k++) if (neighborhood[k] == AnalyzeSkeleton_.SLAB || neighborhood[k] == AnalyzeSkeleton_.END_POINT) nBranch++; } // If the junction has only 3 slab/end-point neighbors, then it is a triple point if (nBranch == 3) this.numberOfTriplePoints[iTree] ++; else if(nBranch == 4) // quadruple point if 4 this.numberOfQuadruplePoints[iTree] ++; } } }// end calculateTripleAndQuadruplePoints /* -----------------------------------------------------------------------*/ /** * Calculate if two points are neighbors. * * @param point1 first point * @param point2 second point * @return true if the points are neighbors (26-pixel neighborhood) */ private boolean isNeighbor(Point point1, Point point2) { return Math.sqrt( Math.pow( (point1.x - point2.x), 2) + Math.pow( (point1.y - point2.y), 2) + Math.pow( (point1.z - point2.z), 2)) <= Math.sqrt(3); } /* -----------------------------------------------------------------------*/ /** * Check if the point is slab. * * @param point actual point * @return true if the point has slab status */ private boolean isSlab(Point point) { return getPixel(this.taggedImage, point.x, point.y, point.z) == AnalyzeSkeleton_.SLAB; } /* -----------------------------------------------------------------------*/ /** * Check if the point is a junction. * * @param point actual point * @return true if the point has slab status */ private boolean isJunction(Point point) { return getPixel(this.taggedImage, point.x, point.y, point.z) == AnalyzeSkeleton_.JUNCTION; } /* -----------------------------------------------------------------------*/ /** * Check if the point is an end point. * * @param point actual point * @return true if the point has slab status */ private boolean isEndPoint(Point point) { return getPixel(this.taggedImage, point.x, point.y, point.z) == AnalyzeSkeleton_.END_POINT; } /* -----------------------------------------------------------------------*/ /** * Check if the point is an 'unprotected' end point. * * @param point actual point * @param roi Only points outside this ROI will have end-point status. ROI * can be associated to a single image in the stack (or all) as * per {@link ij.gui.Roi#getPosition ij.gui.Roi.getPosition()} * @return true if the point has end-point status */ private boolean isEndPoint(Point point, Roi roi) { boolean endPoint = isEndPoint(point); if (endPoint && roi != null) { // Is roi associated with all the images in the ImageStack? boolean roiContainsZ = (roi.getPosition()==0) ? true : roi.getPosition()==point.z; // Maintain end-point status only if point lies outside roi boundaries endPoint = !(roi.contains(point.x, point.y) && roiContainsZ); } return endPoint; } /* -----------------------------------------------------------------------*/ /** * Check if the point is a junction. * * @param x x- voxel coordinate * @param y y- voxel coordinate * @param z z- voxel coordinate * @return true if the point has slab status */ private boolean isJunction(int x, int y, int z) { return getPixel(this.taggedImage, x, y, z) == AnalyzeSkeleton_.JUNCTION; } /* -----------------------------------------------------------------------*/ /** * Get next unvisited neighbor voxel. * * @param point starting point * @return unvisited neighbor or null if all neighbors are visited */ private Point getNextUnvisitedVoxel(Point point) { Point unvisitedNeighbor = null; // Check neighbors status for(int x = -1; x < 2; x++) for(int y = -1; y < 2; y++) for(int z = -1; z < 2; z++) { if(x == 0 && y == 0 && z == 0) continue; if(getPixel(this.inputImage, point.x + x, point.y + y, point.z + z) != 0 && isVisited(point.x + x, point.y + y, point.z + z) == false) { unvisitedNeighbor = new Point(point.x + x, point.y + y, point.z + z); break; } } return unvisitedNeighbor; }// end getNextUnvisitedVoxel /* -----------------------------------------------------------------------*/ /** * Get next unvisited junction neighbor voxel. * * @param point starting point * @return unvisited neighbor or null if all neighbors are visited */ private Point getNextUnvisitedJunctionVoxel(Point point) { Point unvisitedNeighbor = null; // Check neighbors status for(int x = -1; x < 2; x++) for(int y = -1; y < 2; y++) for(int z = -1; z < 2; z++) { if(x == 0 && y == 0 && z == 0) continue; if(getPixel(this.inputImage, point.x + x, point.y + y, point.z + z) != 0 && isVisited(point.x + x, point.y + y, point.z + z) == false && isJunction(point.x + x, point.y + y, point.z + z)) { unvisitedNeighbor = new Point(point.x + x, point.y + y, point.z + z); break; } } return unvisitedNeighbor; }// end getNextUnvisitedJunctionVoxel // ----------------------------------------------------------------------- /** * Get next visited junction neighbor voxel excluding the ones belonging * to a give vertex * * @param point starting point * @param exclude exclusion vertex * @return unvisited neighbor or null if all neighbors are visited */ private Point getVisitedJunctionNeighbor(Point point, Vertex exclude) { Point finalNeighbor = null; // Check neighbors status for(int x = -1; x < 2; x++) for(int y = -1; y < 2; y++) for(int z = -1; z < 2; z++) { if(x == 0 && y == 0 && z == 0) continue; final Point neighbor = new Point( point.x + x, point.y + y, point.z + z); if(getPixel(this.inputImage, neighbor) != 0 && isVisited(neighbor) && isJunction(neighbor) && ! exclude.getPoints().contains(neighbor)) { finalNeighbor = neighbor; break; } } return finalNeighbor; }// end getNextUnvisitedJunctionVoxel // ----------------------------------------------------------------------- /** * Check if a voxel is visited taking into account the borders. * Out of range voxels are considered as visited. * * @param point * @return true if the voxel is visited */ private boolean isVisited(Point point) { return isVisited(point.x, point.y, point.z); } /* -----------------------------------------------------------------------*/ /** * Check if a voxel is visited taking into account the borders. * Out of range voxels are considered as visited. * * @param x x- voxel coordinate * @param y y- voxel coordinate * @param z z- voxel coordinate * @return true if the voxel is visited */ private boolean isVisited(int x, int y, int z) { if(x >= 0 && x < this.width && y >= 0 && y < this.height && z >= 0 && z < this.depth) return this.visited[x][y][z]; return true; } /* -----------------------------------------------------------------------*/ /** * Set value in the visited flags matrix. * * @param x x- voxel coordinate * @param y y- voxel coordinate * @param z z- voxel coordinate * @param b */ private void setVisited(int x, int y, int z, boolean b) { if(x >= 0 && x < this.width && y >= 0 && y < this.height && z >= 0 && z < this.depth) this.visited[x][y][z] = b; } /* -----------------------------------------------------------------------*/ /** * Set value in the visited flags matrix. * * @param point voxel coordinates * @param b visited flag value */ private void setVisited(Point point, boolean b) { int x = point.x; int y = point.y; int z = point.z; setVisited(x, y, z, b); } /* -----------------------------------------------------------------------*/ /** * Tag skeleton dividing the voxels between end points, junctions and slabs. * * @param inputImage2 skeleton image to be tagged * @return tagged skeleton image */ private ImageStack tagImage(ImageStack inputImage2) { // Create output image ImageStack outputImage = new ImageStack(this.width, this.height, inputImage2.getColorModel()); // Tag voxels for (int z = 0; z < depth; z++) { outputImage.addSlice(inputImage2.getSliceLabel(z+1), new ByteProcessor(this.width, this.height)); for (int x = 0; x < width; x++) for (int y = 0; y < height; y++) { if(getPixel(inputImage2, x, y, z) != 0) { int numOfNeighbors = getNumberOfNeighbors(inputImage2, x, y, z); if(numOfNeighbors < 2) { setPixel(outputImage, x, y, z, AnalyzeSkeleton_.END_POINT); this.totalNumberOfEndPoints++; Point endPoint = new Point(x, y, z); this.listOfEndPoints.add(endPoint); } else if(numOfNeighbors > 2) { setPixel(outputImage, x, y, z, AnalyzeSkeleton_.JUNCTION); Point junction = new Point(x, y, z); this.listOfJunctionVoxels.add(junction); this.totalNumberOfJunctionVoxels++; } else { setPixel(outputImage, x, y, z, AnalyzeSkeleton_.SLAB); Point slab = new Point(x, y, z); this.listOfSlabVoxels.add(slab); this.totalNumberOfSlabs++; } } } } return outputImage; }// end method tagImage /* -----------------------------------------------------------------------*/ /** * Get number of neighbors of a voxel in a 3D image (0 border conditions). * * @param image 3D image (ImageStack) * @param x x- coordinate * @param y y- coordinate * @param z z- coordinate (in image stacks the indexes start at 1) * @return corresponding 27-pixels neighborhood (0 if out of image) */ private int getNumberOfNeighbors(ImageStack image, int x, int y, int z) { int n = 0; byte[] neighborhood = getNeighborhood(image, x, y, z); for(int i = 0; i < 27; i ++) if(neighborhood[i] != 0) n++; // We return n-1 because neighborhood includes the actual voxel. return (n-1); }// end method getNumberOfNeighbors // ----------------------------------------------------------------------- /** * Get average 3x3x3 neighborhood pixel value of a given point * * @param image input image * @param p image coordinates */ private double getAverageNeighborhoodValue(ImageStack image, Point p) { byte[] neighborhood = getNeighborhood(image, p); double avg = 0; for(int i = 0; i < neighborhood.length; i++) avg += (double) (neighborhood[i] & 0xFF); if(neighborhood.length > 0) return avg / (double) neighborhood.length; else return 0; }// end method getAverageNeighborhoodValue // ----------------------------------------------------------------------- /** * Get average neighborhood pixel value of a given point. * * @param image input image * @param p image coordinates * @param x_offset x- neighborhood offset * @param y_offset y- neighborhood offset * @param z_offset z- neighborhood offset * @return average neighborhood pixel value */ public static double getAverageNeighborhoodValue( final ImageStack image, final Point p, final int x_offset, final int y_offset, final int z_offset) { byte[] neighborhood = getNeighborhood(image, p, x_offset, y_offset, z_offset); double avg = 0; for(int i = 0; i < neighborhood.length; i++) avg += (double) (neighborhood[i] & 0xFF); if(neighborhood.length > 0) return avg / (double) neighborhood.length; else return 0; }// end method getAverageNeighborhoodValue // ----------------------------------------------------------------------- /** * Get neighborhood of a pixel in a 3D image (0 border conditions). * * @param image 3D image (ImageStack) * @param p point coordinates * @param x_offset x- neighborhood offset * @param y_offset y- neighborhood offset * @param z_offset z- neighborhood offset * @return corresponding neighborhood (0 if out of image) */ public static byte[] getNeighborhood( final ImageStack image, final Point p, final int x_offset, final int y_offset, final int z_offset) { final byte[] neighborhood = new byte[(2*x_offset+1) * (2*y_offset+1) * (2*z_offset+1)]; for(int l= 0, k = p.z - z_offset; k <= p.z + z_offset; k++) for(int j = p.y - y_offset; j <= p.y + y_offset; j++) for(int i = p.x - x_offset; i <= p.x + x_offset; i++, l++) neighborhood[l] = getPixel(image, i, j, k); return neighborhood; } // end getNeighborhood // ----------------------------------------------------------------------- /** * Get neighborhood of a pixel in a 3D image (0 border conditions). * * @param image 3D image (ImageStack) * @param p 3D point coordinates * @return corresponding 27-pixels neighborhood (0 if out of image) */ private byte[] getNeighborhood(ImageStack image, Point p) { return getNeighborhood(image, p.x, p.y, p.z); } // ----------------------------------------------------------------------- /** * Get neighborhood of a pixel in a 3D image (0 border conditions). * * @param image 3D image (ImageStack) * @param x x- coordinate * @param y y- coordinate * @param z z- coordinate (in image stacks the indexes start at 1) * @return corresponding 27-pixels neighborhood (0 if out of image) */ private byte[] getNeighborhood(ImageStack image, int x, int y, int z) { byte[] neighborhood = new byte[27]; neighborhood[ 0] = getPixel(image, x-1, y-1, z-1); neighborhood[ 1] = getPixel(image, x , y-1, z-1); neighborhood[ 2] = getPixel(image, x+1, y-1, z-1); neighborhood[ 3] = getPixel(image, x-1, y, z-1); neighborhood[ 4] = getPixel(image, x, y, z-1); neighborhood[ 5] = getPixel(image, x+1, y, z-1); neighborhood[ 6] = getPixel(image, x-1, y+1, z-1); neighborhood[ 7] = getPixel(image, x, y+1, z-1); neighborhood[ 8] = getPixel(image, x+1, y+1, z-1); neighborhood[ 9] = getPixel(image, x-1, y-1, z ); neighborhood[10] = getPixel(image, x, y-1, z ); neighborhood[11] = getPixel(image, x+1, y-1, z ); neighborhood[12] = getPixel(image, x-1, y, z ); neighborhood[13] = getPixel(image, x, y, z ); neighborhood[14] = getPixel(image, x+1, y, z ); neighborhood[15] = getPixel(image, x-1, y+1, z ); neighborhood[16] = getPixel(image, x, y+1, z ); neighborhood[17] = getPixel(image, x+1, y+1, z ); neighborhood[18] = getPixel(image, x-1, y-1, z+1); neighborhood[19] = getPixel(image, x, y-1, z+1); neighborhood[20] = getPixel(image, x+1, y-1, z+1); neighborhood[21] = getPixel(image, x-1, y, z+1); neighborhood[22] = getPixel(image, x, y, z+1); neighborhood[23] = getPixel(image, x+1, y, z+1); neighborhood[24] = getPixel(image, x-1, y+1, z+1); neighborhood[25] = getPixel(image, x, y+1, z+1); neighborhood[26] = getPixel(image, x+1, y+1, z+1); return neighborhood; } // end getNeighborhood // ----------------------------------------------------------------------- /** * Get pixel in 3D image (0 border conditions) * * @param image 3D image * @param x x- coordinate * @param y y- coordinate * @param z z- coordinate (in image stacks the indexes start at 1) * @return corresponding pixel (0 if out of image) */ public static byte getPixel(final ImageStack image, final int x, final int y, final int z) { final int width = image.getWidth(); final int height = image.getHeight(); final int depth = image.getSize(); if(x >= 0 && x < width && y >= 0 && y < height && z >= 0 && z < depth) return ((byte[]) image.getPixels(z + 1))[x + y * width]; else return 0; } // end getPixel /* -----------------------------------------------------------------------*/ /** * Get pixel in 3D image (0 border conditions). * * @param image 3D image * @param x x- coordinate * @param y y- coordinate * @param z z- coordinate (in image stacks the indexes start at 1) * @return corresponding pixel (0 if out of image) */ private float getFloatPixel( ImageStack image, int x, int y, int z ) { if(x >= 0 && x < this.width && y >= 0 && y < this.height && z >= 0 && z < this.depth) return ( (float[]) image.getPixels(z + 1) )[x + y * this.width]; else return 0; } // end getFloatPixel /* -----------------------------------------------------------------------*/ /** * Get pixel in 3D image (0 border conditions). * * @param image 3D image * @param point point to be evaluated * @return corresponding pixel (0 if out of image) */ private float getFloatPixel( ImageStack image, Point point ) { return getFloatPixel( image, point.x, point.y, point.z ); } // end getFloatPixel /* -----------------------------------------------------------------------*/ /** * Get pixel in 3D image (0 border conditions). * * @param image 3D image * @param point point to be evaluated * @return corresponding pixel (0 if out of image) */ private byte getPixel(ImageStack image, Point point) { return getPixel(image, point.x, point.y, point.z); } // end getPixel /* -----------------------------------------------------------------------*/ /** * Set pixel in 3D image. * * @param image 3D image * @param p point coordinates * @param value pixel value */ private void setPixel(ImageStack image, Point p, byte value) { if(p.x >= 0 && p.x < this.width && p.y >= 0 && p.y < this.height && p.z >= 0 && p.z < this.depth) ((byte[]) image.getPixels(p.z + 1))[p.x + p.y * this.width] = value; } // end setPixel /* -----------------------------------------------------------------------*/ /** * Set pixel in 3D image. * * @param image 3D image * @param x x- coordinate * @param y y- coordinate * @param z z- coordinate (in image stacks the indexes start at 1) * @param value pixel value */ private void setPixel(ImageStack image, int x, int y, int z, byte value) { if(x >= 0 && x < this.width && y >= 0 && y < this.height && z >= 0 && z < this.depth) ((byte[]) image.getPixels(z + 1))[x + y * this.width] = value; } // end setPixel /* -----------------------------------------------------------------------*/ /** * Set pixel in 3D (short) image. * * @param image 3D image * @param x x- coordinate * @param y y- coordinate * @param z z- coordinate (in image stacks the indexes start at 1) * @param value pixel value */ private void setPixel(ImageStack image, int x, int y, int z, float value) { if(x >= 0 && x < this.width && y >= 0 && y < this.height && z >= 0 && z < this.depth) ((float[]) image.getPixels(z + 1))[x + y * this.width] = value; } // end setPixel /* -----------------------------------------------------------------------*/ /** * Show plug-in information. * */ void showAbout() { IJ.showMessage( "About AnalyzeSkeleton...", "This plug-in filter analyzes a 2D/3D image skeleton.\n"); } // end showAbout /** * Determine the longest shortest path using the APSP (all pairs shortest path) * warshall algorithm * * @param graph the graph of a tree * @param shortestPathPoints list to store the longest shortest path points * @return longest shortest path length * @author Huub Hovens */ private double warshallAlgorithm(Graph graph, ArrayList <Point> shortestPathPoints) { // local fields /** vertex 1 of an edge */ Vertex v1 = null; /** vertex 2 of an edge */ Vertex v2 = null; /** the equivalent of row in a matrix */ int row = 0; /** the equivalent of column in a matrix */ int column = 0; /** the value of the longest shortest path */ double maxPath = 0; /** row that contains the longest shortest path value */ int a = 0; /** column that contains the longest shortest path value */ int b = 0; ArrayList< Edge > edgeList = graph.getEdges(); ArrayList< Vertex > vertexList = graph.getVertices(); // check for paths of only one vertex if( vertexList.size() == 1 ) { shortestPathPoints.add( vertexList.get( 0 ).getPoints().get( 0 ) ); spx = vertexList.get( 0 ).getPoints().get( 0 ).x; spy = vertexList.get( 0 ).getPoints().get( 0 ).y; spz = vertexList.get( 0 ).getPoints().get( 0 ).z; return 0; } //create empty adjacency and predecessor matrix /** the matrix that contains the length of the shortest path from vertex a to vertex b */ double[][] adjacencyMatrix = new double[vertexList.size()][vertexList.size()]; /** the matrix that contains the predecessor vertex of vertex b in the shortest path from vertex a to b */ int[][] predecessorMatrix = new int[vertexList.size()][vertexList.size()]; // initial conditions for both matrices /* * create 2D-adjacency array with the distance between the nodes. * distance i --> i = 0 * distance i -/> j = infinite (edge does not exist) * distance i --> j = length of branch */ /* * create 2D-predecessor array using interconnected vertices. * the predecessor matrix contains the predecessor of j in a path from node i to j. * distance i --> i = NIL (there is no edge between a single vertex) * distance i -/> j = NIL (there is no edge between the vertices) * distance i --> j = i (the initial matrix only contains paths with a single edge) * * I'm using -1 as NIL since it cannot refer to an index (and thus a vertex) */ // applying initial conditions for(int i = 0 ; i < vertexList.size(); i++) { for(int j = 0 ; j < vertexList.size(); j++) { adjacencyMatrix[i][j]= Double.POSITIVE_INFINITY; predecessorMatrix[i][j]= -1; } } for (Edge edge : edgeList) { v1 = edge.getV1(); v2 = edge.getV2(); // use the index of the vertices as the index in the matrix row = vertexList.indexOf(v1); if(row == -1) { IJ.log("Vertex " + v1.getPoints().get(0) + " not found in the list of vertices!"); continue; } column = vertexList.indexOf(v2); if(column == -1) { IJ.log("Vertex " + v2.getPoints().get(0) + " not found in the list of vertices!"); continue; } /* * the diagonal is 0. * * Because not every vertex is a 'v1 vertex' * the [column][column] statement is needed as well. * * in an undirected graph the adjacencyMatrix is symmetric * thus A = Transpose(A) */ adjacencyMatrix[row][row] = 0; adjacencyMatrix[column][column] = 0; adjacencyMatrix[row][column] = edge.getLength(); adjacencyMatrix[column][row] = edge.getLength(); /* * the diagonal remains -1. * for the rest I use the index of the vertex so I can later refer to the vertexList * for the correct information * * Determining what belongs where requires careful consideration of the definition * * the array contains the predecessor of "column" in a path from "row" to "column" * therefore in the other statement it is the other way around. */ predecessorMatrix[row][row] = -1; predecessorMatrix[column][column] = -1; predecessorMatrix[row][column] = row; predecessorMatrix[column][row] = column; } // matrices now have their initial conditions // the warshall algorithm with k as candidate vertex and i and j walk through the adjacencyMatrix // the predecessor matrix is updated at the same time. for(int k = 0 ; k < vertexList.size(); k++) { for(int i = 0 ; i < vertexList.size(); i++) { for(int j = 0 ; j < vertexList.size(); j++) { if(adjacencyMatrix[i][k] + adjacencyMatrix[k][j] < adjacencyMatrix[i][j]) { adjacencyMatrix[i][j] = adjacencyMatrix[i][k] + adjacencyMatrix[k][j]; predecessorMatrix[i][j] = predecessorMatrix[k][j]; } } } } // find the maximum of all shortest paths for(int i = 0; i < vertexList.size(); i++) { for(int j = 0; j < vertexList.size(); j++) { // sometimes infinities still remain if (adjacencyMatrix[i][j] > maxPath && adjacencyMatrix[i][j] != Double.POSITIVE_INFINITY) { maxPath = adjacencyMatrix[i][j]; a = i; b = j; } } } // trace back the longest shortest path reconstructPath(predecessorMatrix, a, b, edgeList, vertexList, shortestPathPoints); // !important return maxPath; return maxPath; } // end method warshallAlgorithm /** * Reconstruction and visualisation of the longest shortest path found by the APSP warshall algorithm * * @param predecessorMatrix the Matrix which contains the predecessor of vertex b in the shortest path from a to b * @param startIndex the index of the row which contains the longest shortest path * @param endIndex the index of the column which contains the longest shortest path * @param edgeList the list of edges * @param vertexList the list of vertices * @param shortestPathPoints contains points of the longest shortest path for each graph * @author Huub Hovens */ private void reconstructPath( int[][] predecessorMatrix, int startIndex, int endIndex, ArrayList<Edge> edgeList, ArrayList<Vertex> vertexList, ArrayList<Point> shortestPathPoints) { // We know the first and last vertex of the longest shortest path, namely a and b // using the predecessor matrix we can now determine the path that is taken from a to b // remember a and b are indices and not the actual vertices. int b = endIndex; final int a = startIndex; while (b != a) { Vertex predecessor = vertexList.get(predecessorMatrix[a][b]); Vertex endvertex = vertexList.get(b); ArrayList< Edge > sp_edgeslist = new ArrayList< Edge >(); Double lengthtest = Double.POSITIVE_INFINITY; Edge shortestedge = null; // search all edges for a combination of the two vertices for (Edge edge : edgeList) { if ((edge.getV1()==predecessor && edge.getV2()==endvertex) || (edge.getV1()==endvertex && edge.getV2()==predecessor)) { // sometimes there are multiple edges between two vertices so add them to a list // for a second test sp_edgeslist.add(edge); } } // the second test // this test looks which edge has the shortest length in sp_edgeslist for (Edge edge : sp_edgeslist) { if (edge.getLength() < lengthtest) { shortestedge = edge; lengthtest = edge.getLength(); } } // add vertex 1 points Vertex v1 = shortestedge.getV2() != predecessor ? shortestedge.getV2() : shortestedge.getV1(); for (Point p : v1.getPoints()) { if( ! shortestPathPoints.contains( p )) { shortestPathPoints.add(p); //setPixel(this.shortPathImage, p.x, p.y, p.z, SHORTEST_PATH); } } // add slab points of the shortest edge to the list of points ArrayList<Point> slabs = shortestedge.getSlabs(); // reverse order if needed if( shortestedge.getV2() != predecessor ) Collections.reverse( slabs ); for (Point p : slabs) { shortestPathPoints.add(p); setPixel(this.shortPathImage, p.x, p.y, p.z, SHORTEST_PATH); } // add vertex 2 points too Vertex v2 = shortestedge.getV2() != predecessor ? shortestedge.getV1() : shortestedge.getV2(); for (Point p : v2.getPoints()) { if( ! shortestPathPoints.contains( p )) { shortestPathPoints.add(p); //setPixel(this.shortPathImage, p.x, p.y, p.z, SHORTEST_PATH); } } // now make the index of the endvertex the index of the predecessor so that the path now goes from // a to predecessor and repeat cycle b = predecessorMatrix[a][b]; } if (shortestPathPoints.size() != 0) { this.spx = shortestPathPoints.get(0).x; this.spy = shortestPathPoints.get(0).y; this.spz = shortestPathPoints.get(0).z; } } // end method reconstructPath /** * Get the stack containing all the trees labeld with their corresponding * skeleton id. * * @return labeled-skeleton image stack */ public ImageStack getLabeledSkeletons() { return labeledSkeletons; } private void createSettingsDialog() { settingsDialog = new GenericDialog("Analyze Skeleton"); Font headerFont = new Font("SansSerif", Font.BOLD, 12); settingsDialog.setInsets(0, 0, 0); settingsDialog.addMessage("Elimination of Loops:", headerFont); settingsDialog.addChoice("Prune cycle method: ", AnalyzeSkeleton_.pruneCyclesModes, AnalyzeSkeleton_.pruneCyclesModes[pruneIndex]); settingsDialog.setInsets(20, 0, -15); //default top inset for 1st checkbox is 15 settingsDialog.addMessage("Elimination of End-points:", headerFont); settingsDialog.addCheckbox("Prune ends", pruneEnds); settingsDialog.addCheckbox("Exclude ROI from pruning", protectRoi); settingsDialog.setInsets(20, 0, 0); //default top inset for subsequent checkboxes is 0 settingsDialog.addMessage("Results and Output:", headerFont); settingsDialog.addCheckbox("Calculate largest shortest path", calculateShortestPath); settingsDialog.addCheckbox("Show detailed info", verbose); settingsDialog.addCheckbox("Display labeled skeletons", displaySkeletons); settingsDialog.addHelp(HELP_URL); dialogItemChanged(settingsDialog, null); } private void loadDialogSettings() { String index = Prefs.get(PRUNE_MODE_INDEX_KEY, String.valueOf(DEFAULT_PRUNE_MODE_INDEX)); pruneIndex = Integer.parseInt(index); // fails to find the key pruneEnds = Prefs.get(PRUNE_ENDS_KEY, DEFAULT_PRUNE_ENDS); calculateShortestPath = Prefs.get(CALCULATE_PATH_KEY, DEFAULT_CALCULATE_SHORTEST_PATH); verbose = Prefs.get(VERBOSE_KEY, DEFAULT_VERBOSE); displaySkeletons = Prefs.get(DISPLAY_SKELETONS_KEY, DEFAULT_DISPLAY_SKELETONS); } private void saveDialogSettings() { Prefs.set(PRUNE_MODE_INDEX_KEY, String.valueOf(pruneIndex)); Prefs.set(PRUNE_ENDS_KEY, pruneEnds); Prefs.set(CALCULATE_PATH_KEY, calculateShortestPath); Prefs.set(VERBOSE_KEY, verbose); Prefs.set(DISPLAY_SKELETONS_KEY, displaySkeletons); } private void setSettingsFromDialog() { pruneIndex = settingsDialog.getNextChoiceIndex(); pruneEnds = settingsDialog.getNextBoolean(); protectRoi = settingsDialog.getNextBoolean(); calculateShortestPath = settingsDialog.getNextBoolean(); verbose = settingsDialog.getNextBoolean(); displaySkeletons = settingsDialog.getNextBoolean(); } }// end class AnalyzeSkeleton_