在 Java 中实现分水岭分割

我正在尝试为一个项目编写自己的分水岭分割实现。我有一个版本,它返回类似于给出非常琐碎的图片的正确分割的东西。不幸的是,它非常慢/效率低下,在所有情况下都可能终止,也可能不会终止。

我一直在从伍兹和冈萨雷斯的“数字图像处理”中的描述以及分水岭维基百科页面进行工作。通用算法是编码并包含在下面的,但我有一种感觉,我正在循环很多我不需要的东西。我感谢这里的任何和所有帮助,提前感谢。

    public static Set<Point> watershed(double[][] im) {

    //Get the Gradient Magnitude to use as our "topographic map."

    double t1 = System.currentTimeMillis();
    double[][] edges = EdgeDetection.sobelMagnitude(im);


    //Only iterate over values in the gradient magnitude to speed up.

    double[] histogram = Image.getHistogram(edges);
    Image.drawHistogram(histogram, Color.red);
    int minPixelValue = 0;
    for (int i = 0; i < histogram.length; i++) {
        if (histogram[i] > 0) {
            minPixelValue = i;
            break;
        }
    }
    int h = im.length;
    int w = im[0].length;

    //SE is a 3x3 structuring element for morphological dilation.
    boolean[][] SE = {{true, true, true}, {true, true, true}, {true, true, true}};

    //Keeping track of last iteration's components to see if two flooded together.
    ArrayList<Set<Point>> lastComponents = connectedComponents(getSet(EdgeDetection.underThreshold(edges, minPixelValue + 1)));
    ArrayList<Set<Point>> components;
    Set<Point> boundary = new HashSet<Point>();

    for (int i = minPixelValue + 1; i < 256; i++) {
        if (histogram[i] != 0) {
            System.out.println("BEHHH " + i);
            t1 = System.currentTimeMillis();
            ArrayList<Integer> damLocations = new ArrayList<Integer>();
            HashMap<Integer, ArrayList<Integer>> correspondingSets = new HashMap<Integer, ArrayList<Integer>>();
            //Figure out which of the old sets the new sets incorporated.
            //Here is where we check if sets flooded into eachother.
            //System.out.println("Checking for flooding");
            components = connectedComponents(getSet(EdgeDetection.underThreshold(edges, i)));
            for (int nc = 0; nc < components.size(); nc++) {
                //System.out.println("Checking component " + nc);
                Set<Point> newComponent = components.get(nc);
                for (int oc = 0; oc < lastComponents.size(); oc++) {
                    //System.out.println("    Against component " + oc);
                    Set<Point> oldComponent = lastComponents.get(oc);
                    if (numberInCommon(newComponent, oldComponent) > 0) {
                        //System.out.println("     In there.");
                        ArrayList<Integer> oldSetsContained;
                        if (correspondingSets.containsKey(nc)) {
                            oldSetsContained = correspondingSets.get(nc);
                            damLocations.add(nc);
                        } else {
                            //System.out.println("     Nope.");
                            oldSetsContained = new ArrayList<Integer>();

                        }
                        oldSetsContained.add(oc);
                        correspondingSets.put(nc, oldSetsContained);
                    }
                }
            }
            System.out.println("Calculating overlapping sets: " + (System.currentTimeMillis() - t1));

            //System.out.println("Check done.");
            for (int key : correspondingSets.keySet()) {
                Integer[] cs = new Integer[correspondingSets.get(key).size()];
                correspondingSets.get(key).toArray(cs);
                if (cs.length == 1) {
                    //System.out.println("Set " + cs[0] + " has grown without flooding.");
                } else {
                    //System.out.println("The following sets have flooded together: " + Arrays.toString(cs));
                }
            }

            //Build Damns to prevent flooding

            for (int c : damLocations) {

                System.out.println("Building dam for component " + c);
                Set<Point> bigComponent = components.get(c);
                System.out.println("Total size: " + bigComponent.size());
                ArrayList<Set<Point>> littleComponent = new ArrayList<Set<Point>>();
                for (int lcindex : correspondingSets.get(c)) {
                    littleComponent.add(lastComponents.get(lcindex));
                }

                Set<Point> unionSet = new HashSet<Point>(boundary);
                for (Set<Point> lc : littleComponent) {
                    unionSet = union(unionSet, lc);

                }
                System.out.println("Building union sets: " + (System.currentTimeMillis() - t1));
                while (intersection(unionSet, bigComponent).size() < bigComponent.size()) {

                    for (int lIndex = 0; lIndex < littleComponent.size(); lIndex++) {
                        Set<Point> lc = littleComponent.get(lIndex);
                        Set<Point> lcBoundary = extractBoundaries(lc, SE, h, w);
                        Set<Point> toAdd = new HashSet<Point>();
                        Set<Point> otherComponents = new HashSet<Point>(unionSet);
                        otherComponents.removeAll(lc);
                        otherComponents.removeAll(boundary);
                        otherComponents = extractBoundaries(otherComponents, SE, h, w);
                        for (Point pt : lcBoundary) {
                            Set<Point> eightNbrs = get8Neighborhood(pt);
                            for (Point nbr : eightNbrs) {
                                if (bigComponent.contains(nbr) & !boundary.contains(nbr)) {
                                    Set<Point> neighborNbr = get8Neighborhood(nbr);
                                    if (intersection(neighborNbr, otherComponents).size() > 0) {
                                        boundary.add(nbr);
                                        edges[nbr.y][nbr.x] = 256;
                                        break;
                                    } else if (!lc.contains(nbr)) {
                                        toAdd.add(nbr);
                                        //if(i==65)System.out.println("Adding point " + nbr.y + " " + nbr.x);
                                    } else {
                                        //if(i==65)System.out.println("Already in here " + nbr.y + " " + nbr.x);
                                    }
                                }
                            }
                        }
                        t1 = System.currentTimeMillis();
                        lc.addAll(toAdd);
                        toAdd.removeAll(toAdd);

                        littleComponent.set(lIndex, lc);
                        unionSet = new HashSet<Point>(boundary);
                        for (Set<Point> ltc : littleComponent) {
                            unionSet = union(unionSet, ltc);
                        }
                        System.out.println("This is a donk " + intersection(unionSet, bigComponent).size());
                        otherComponents = new HashSet<Point>(unionSet);
                        otherComponents.removeAll(lc);
                        otherComponents.removeAll(boundary);
                    }
                }
            }
        }
    }
    boundary = close(boundary,h,w);
    Image.drawSet(boundary, h, w);
    return boundary;
}

答案 1

该算法似乎最多是O(n^2)。你有很多很多嵌套的循环。我无法找到伍兹的描述。Christopher Mei的这段代码实现了算法:这是一个非常简单的实现。

分水岭像素.java

/*
 * Watershed algorithm
 *
 * Copyright (c) 2003 by Christopher Mei (christopher.mei@sophia.inria.fr)
 *
 * This plugin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 
 * as published by the Free Software Foundation.
 *
 * 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 plugin; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.lang.*;
import java.util.*;
import ij.*;

/**
 *  The aim of WatershedPixel is to enable
 *  sorting the pixels of an Image according
 *  to their grayscale value.
 *
 *  This is the first step of the Vincent
 *  and Soille Watershed algorithm (1991)
 *  
 **/

public class WatershedPixel implements Comparable {
    /** Value used to initialise the image */
    final static int INIT = -1;
    /** Value used to indicate the new pixels that
     *  are going to be processed (intial value 
     *  at each level)
     **/
    final static int MASK = -2;
    /** Value indicating that the pixel belongs 
     *  to a watershed.
     **/
    final static int WSHED = 0;
    /** Fictitious pixel **/
    final static int FICTITIOUS = -3;

    /** x coordinate of the pixel **/
    private int x;
    /** y coordinate of the pixel **/
    private int y;
    /** grayscale value of the pixel **/
    private byte height; 
    /** Label used in the Watershed immersion algorithm **/
    private int label;
    /** Distance used for working on pixels */
    private int dist;

    /** Neighbours **/
    private Vector neighbours;

    public WatershedPixel(int x, int y, byte height) {
    this.x = x;
    this.y = y;
    this.height = height;
    label = INIT;
    dist = 0;
    neighbours = new Vector(8);
    }

    public WatershedPixel() {
    label = FICTITIOUS;
    }

    public void addNeighbour(WatershedPixel neighbour) {
    /*IJ.write("In Pixel, adding :");
      IJ.write(""+neighbour);
      IJ.write("Add done");
    */
    neighbours.add(neighbour);
    }

    public Vector getNeighbours() {
    return neighbours;
    }



    public String toString() {
    return new String("("+x+","+y+"), height : "+getIntHeight()+", label : "+label+", distance : "+dist); 
    }



    public final byte getHeight() {
    return height;
    } 

    public final int getIntHeight() {
    return (int) height&0xff;
    } 

    public final int getX() {
    return x;
    } 

    public final int getY() {
    return y;
    }


    /** Method to be able to use the Collections.sort static method. **/
    public int compareTo(Object o) {
    if(!(o instanceof WatershedPixel))
        throw new ClassCastException();

    WatershedPixel obj =  (WatershedPixel) o;

    if( obj.getIntHeight() < getIntHeight() ) 
        return 1;

    if( obj.getIntHeight() > getIntHeight() )
        return -1;

    return 0;
    }

    public void setLabel(int label) {
    this.label = label;
    }

    public void setLabelToINIT() {
    label = INIT;
    }

    public void setLabelToMASK() {
    label = MASK;
    }

    public void setLabelToWSHED() {
    label = WSHED;
    }


    public boolean isLabelINIT() {
    return label == INIT;
    }
    public boolean isLabelMASK() {
    return label == MASK;
    }    
    public boolean isLabelWSHED() {
    return label == WSHED;
    }

    public int getLabel() {
    return label;
    }

    public void setDistance(int distance) {
    dist = distance;
    }

    public int getDistance() {
    return dist;
    }

    public boolean isFICTITIOUS() {
    return label == FICTITIOUS;
    }

    public boolean allNeighboursAreWSHED() {
    for(int i=0 ; i<neighbours.size() ; i++) {
        WatershedPixel r = (WatershedPixel) neighbours.get(i);

        if( !r.isLabelWSHED() ) 
        return false;
    }
    return true;
    }
}

分水岭FIFO.java

/*
 * Watershed plugin
 *
 * Copyright (c) 2003 by Christopher Mei (christopher.mei@sophia.inria.fr)
 *
 * This plugin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 
 * as published by the Free Software Foundation.
 *
 * 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 plugin; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.util.*;
import ij.*;

/** This class implements a FIFO queue that
 *  uses the same formalism as the Vincent
 *  and Soille algorithm (1991)
 **/

public class WatershedFIFO {
    private LinkedList watershedFIFO;

    public WatershedFIFO() {
    watershedFIFO = new LinkedList();
    }

    public void fifo_add(WatershedPixel p) {
    watershedFIFO.addFirst(p);
    }

    public WatershedPixel fifo_remove() {
    return (WatershedPixel) watershedFIFO.removeLast();
    }

    public boolean fifo_empty() {
    return watershedFIFO.isEmpty();
    }

    public void fifo_add_FICTITIOUS() {
    watershedFIFO.addFirst(new WatershedPixel());
    }

    public String toString() {
    StringBuffer ret = new StringBuffer();
    for(int i=0; i<watershedFIFO.size(); i++) {
        ret.append( ((WatershedPixel)watershedFIFO.get(i)).toString() );
        ret.append( "\n" );
    }

    return ret.toString();
    }
}

流域结构.java

/*
 * Watershed algorithm
 *
 * Copyright (c) 2003 by Christopher Mei (christopher.mei@sophia.inria.fr)
 *
 * This plugin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 
 * as published by the Free Software Foundation.
 *
 * 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 plugin; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.lang.*;
import java.util.*;
import ij.process.*;
import ij.*;
import java.awt.*;

/**
 *  WatershedStructure contains the pixels
 *  of the image ordered according to their
 *  grayscale value with a direct access to their
 *  neighbours.
 *  
 **/

public class WatershedStructure {
    private Vector watershedStructure;

    public WatershedStructure(ImageProcessor ip) {
    byte[] pixels = (byte[])ip.getPixels();
    Rectangle r = ip.getRoi();
    int width = ip.getWidth();
    int offset, topOffset, bottomOffset, i;

    watershedStructure = new Vector(r.width*r.height);

    /** The structure is filled with the pixels of the image. **/
    for (int y=r.y; y<(r.y+r.height); y++) {
        offset = y*width;

        IJ.showProgress(0.1+0.3*(y-r.y)/(r.height));

        for (int x=r.x; x<(r.x+r.width); x++) {
        i = offset + x;

        int indiceY = y-r.y;
        int indiceX = x-r.x;

        watershedStructure.add(new WatershedPixel(indiceX, indiceY, pixels[i]));
        }
    }

    /** The WatershedPixels are then filled with the reference to their neighbours. **/
    for (int y=0; y<r.height; y++) {

        offset = y*width;
        topOffset = offset+width;
        bottomOffset = offset-width;

        IJ.showProgress(0.4+0.3*(y-r.y)/(r.height));

        for (int x=0; x<r.width; x++) {     
        WatershedPixel currentPixel = (WatershedPixel)watershedStructure.get(x+offset);

        if(x+1<r.width) {
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x+1+offset));

            if(y-1>=0)
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x+1+bottomOffset));

            if(y+1<r.height)
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x+1+topOffset));
        }

        if(x-1>=0) {
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x-1+offset));

            if(y-1>=0)
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x-1+bottomOffset));

            if(y+1<r.height)
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x-1+topOffset));
        }

        if(y-1>=0)
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x+bottomOffset));

        if(y+1<r.height)
            currentPixel.addNeighbour((WatershedPixel)watershedStructure.get(x+topOffset));
        }
    } 

    Collections.sort(watershedStructure);
    //IJ.showProgress(0.8);
    }

    public String toString() {
    StringBuffer ret = new StringBuffer();

    for(int i=0; i<watershedStructure.size() ; i++) {
        ret.append( ((WatershedPixel) watershedStructure.get(i)).toString() );
        ret.append( "\n" );
        ret.append( "Neighbours :\n" );

        Vector neighbours = ((WatershedPixel) watershedStructure.get(i)).getNeighbours();

        for(int j=0 ; j<neighbours.size() ; j++) {
        ret.append( ((WatershedPixel) neighbours.get(j)).toString() );
        ret.append( "\n" );
        }
        ret.append( "\n" );
    }
    return ret.toString();
    }

    public int size() {
    return watershedStructure.size();
    }

    public WatershedPixel get(int i) {
    return (WatershedPixel) watershedStructure.get(i);
    }
}

Watershed_Algorithm.java

/*
 * Watershed algorithm
 *
 * Copyright (c) 2003 by Christopher Mei (christopher.mei@sophia.inria.fr)
 *
 * This plugin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 
 * as published by the Free Software Foundation.
 *
 * 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 plugin; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import ij.*;
import ij.plugin.filter.PlugInFilter;
import ij.process.*;
import ij.gui.*;
import ij.plugin.frame.PlugInFrame;

import java.awt.*;
import java.util.*;

/**
 *  This algorithm is an implementation of the watershed immersion algorithm
 *  written by Vincent and Soille (1991).
 *
 *  @Article{Vincent/Soille:1991,
 *   author =       "Lee Vincent and Pierre Soille",
 *   year =         "1991",
 *   keywords =     "IMAGE-PROC SKELETON SEGMENTATION GIS",
 *   institution =  "Harvard/Paris+Louvain",
 *   title =        "Watersheds in digital spaces: An efficient algorithm
 *                   based on immersion simulations",
 *   journal =      "IEEE PAMI, 1991",
 *   volume =       "13",
 *   number =       "6",
 *   pages =        "583--598",
 *   annote =       "Watershed lines (e.g. the continental divide) mark the
 *                  boundaries of catchment regions in a topographical map.
 *                  The height of a point on this map can have a direct
 *                  correlation to its pixel intensity. WIth this analogy,
 *                  the morphological operations of closing (or opening)
 *                  can be understood as smoothing the ridges (or filling
 *                  in the valleys). Develops a new algorithm for obtaining
 *                  the watershed lines in a graph, and then uses this in
 *                  developing a new segmentation approach based on the
 *                  {"}depth of immersion{"}.",
 *  }
 *
 *  A review of Watershed algorithms can be found at :
 *  http://www.cs.rug.nl/~roe/publications/parwshed.pdf
 *
 *  @Article{RoeMei00,
 *   author =       "Roerdink and Meijster",
 *   title =        "The Watershed Transform: Definitions, Algorithms and
 *                   Parallelization Strategies",
 *   journal =      "FUNDINF: Fundamenta Informatica",
 *   volume =       "41",
 *   publisher =    "IOS Press",
 *   year =         "2000",
 *  }
 **/

public class Watershed_Algorithm implements PlugInFilter {
    private int threshold;
    final static int HMIN = 0;
    final static int HMAX = 256;

    public int setup(String arg, ImagePlus imp) {
    if (arg.equals("about"))
        {showAbout(); return DONE;}
    return DOES_8G+DOES_STACKS+SUPPORTS_MASKING+NO_CHANGES;
    }

    public void run(ImageProcessor ip) {
    boolean debug = false;

    IJ.showStatus("Sorting pixels...");
    IJ.showProgress(0.1);

    /** First step : the pixels are sorted according to increasing grey values **/
    WatershedStructure watershedStructure = new WatershedStructure(ip);
    if(debug)
        IJ.write(""+watershedStructure.toString());

    IJ.showProgress(0.8);
    IJ.showStatus("Start flooding...");

    if(debug)
        IJ.write("Starting algorithm...\n");

    /** Start flooding **/
    WatershedFIFO queue = new WatershedFIFO();
    int curlab = 0;

    int heightIndex1 = 0;
    int heightIndex2 = 0;

    for(int h=HMIN; h<HMAX; h++) /*Geodesic SKIZ of level h-1 inside level h */ {

        for(int pixelIndex = heightIndex1 ; pixelIndex<watershedStructure.size() ; pixelIndex++) /*mask all pixels at level h*/ {
        WatershedPixel p = watershedStructure.get(pixelIndex);

        if(p.getIntHeight() != h) {
            /** This pixel is at level h+1 **/
            heightIndex1 = pixelIndex;
            break;
        }

        p.setLabelToMASK();

        Vector neighbours = p.getNeighbours();
        for(int i=0 ; i<neighbours.size() ; i++) {
            WatershedPixel q = (WatershedPixel) neighbours.get(i);

            if(q.getLabel()>=0) {/*Initialise queue with neighbours at level h of current basins or watersheds*/
            p.setDistance(1);
            queue.fifo_add(p);
            break;
            } // end if
        } // end for
        } // end for


        int curdist = 1;
        queue.fifo_add_FICTITIOUS();

        while(true) /** extend basins **/{
        WatershedPixel p = queue.fifo_remove();

        if(p.isFICTITIOUS())
            if(queue.fifo_empty())
            break;
            else {
            queue.fifo_add_FICTITIOUS();
            curdist++;
            p = queue.fifo_remove();
            }
        if(debug) {
            IJ.write("\nWorking on :");
            IJ.write(""+p);
        }

        Vector neighbours = p.getNeighbours();
        for(int i=0 ; i<neighbours.size() ; i++) /* Labelling p by inspecting neighbours */{
            WatershedPixel q = (WatershedPixel) neighbours.get(i);

            if(debug)
            IJ.write("Neighbour : "+q);

            /* Original algorithm : 
               if( (q.getDistance() < curdist) &&
               (q.getLabel()>0 || q.isLabelWSHED()) ) {*/
            if( (q.getDistance() <= curdist) && 
            (q.getLabel()>=0) ) {
            /* q belongs to an existing basin or to a watershed */

            if(q.getLabel() > 0) {
                if( p.isLabelMASK() )
                // Removed from original algorithm || p.isLabelWSHED() )
                p.setLabel(q.getLabel());
                else
                if(p.getLabel() != q.getLabel())
                    p.setLabelToWSHED();
            } // end if lab>0
            else
                if(p.isLabelMASK())
                p.setLabelToWSHED();
            }
            else
            if( q.isLabelMASK() &&
                (q.getDistance() == 0) ) {

                if(debug)
                IJ.write("Adding value");
                q.setDistance( curdist+1 );
                queue.fifo_add( q );
            }       
        } // end for, end processing neighbours

        if(debug) {
            IJ.write("End processing neighbours");
            IJ.write("New val :\n"+p);
            IJ.write("Queue :\n"+queue);
        }
        } // end while (loop)

        /* Detect and process new minima at level h */
        for(int pixelIndex = heightIndex2 ; pixelIndex<watershedStructure.size() ; pixelIndex++) {
        WatershedPixel p = watershedStructure.get(pixelIndex);

        if(p.getIntHeight() != h) {
            /** This pixel is at level h+1 **/
            heightIndex2 = pixelIndex;
            break;
        }

        p.setDistance(0); /* Reset distance to zero */

        if(p.isLabelMASK()) { /* the pixel is inside a new minimum */
            curlab++;
            p.setLabel(curlab);         
            queue.fifo_add(p);


            while(!queue.fifo_empty()) {
            WatershedPixel q = queue.fifo_remove();

            Vector neighbours = q.getNeighbours();

            for(int i=0 ; i<neighbours.size() ; i++) /* inspect neighbours of p2*/{
                WatershedPixel r = (WatershedPixel) neighbours.get(i);

                if( r.isLabelMASK() ) {
                r.setLabel(curlab);
                queue.fifo_add(r);
                }
            }
            } // end while
        } // end if
        } // end for
    } /** End of flooding **/

    IJ.showProgress(0.9);
    IJ.showStatus("Putting result in a new image...");

    /** Put the result in a new image **/
    int width = ip.getWidth();

    ImageProcessor outputImage = new ByteProcessor(width, ip.getHeight());
    byte[] newPixels = (byte[]) outputImage.getPixels();

    for(int pixelIndex = 0 ; pixelIndex<watershedStructure.size() ; pixelIndex++) {
        WatershedPixel p = watershedStructure.get(pixelIndex);

        if(p.isLabelWSHED() && !p.allNeighboursAreWSHED())
        newPixels[p.getX()+p.getY()*width] = (byte)255;
    }

    IJ.showProgress(1);
    IJ.showStatus("Displaying result...");

    new ImagePlus("Watershed", outputImage).show();
    }

    void showAbout() {
    IJ.showMessage("About Watershed_Algorithm...",
               "This plug-in filter calculates the watershed of a 8-bit images.\n" +
               "It uses the immersion algorithm written by Vincent and Soille (1991)\n"
               );
    }
}

答案 2

推荐