├── .gitignore ├── .travis.yml ├── README.md ├── pom.xml └── src ├── main ├── java │ └── ij │ │ └── blob │ │ ├── Blob.java │ │ ├── ConnectedComponentLabeler.java │ │ ├── CustomBlobFeature.java │ │ ├── FractalBoxCounterBlob.java │ │ ├── ManyBlobs.java │ │ ├── ManyBlobs.java~ │ │ └── RotatingCalipers.java └── resources │ ├── 3blobs.tif │ ├── 3blobsInv.tif │ ├── FiveBlobsOnEdge.tif │ ├── circle_r30.tif │ ├── complexImage.tif │ ├── correctcontour.png │ ├── nestedObjects.tif │ ├── rotatedsquare.tif │ ├── rotatedsquare2.tif │ ├── square100x100_minus30x30.png │ ├── squareOnBoarder_right.tif │ ├── squaresOnBoarder.tif │ ├── squaresOnBoarderInv.tif │ └── squares_20x20_30x30.tif └── test └── java └── ij └── blob └── tests ├── ExampleBlobFeature.java ├── FeatureTest.java ├── ManyBlobsTest.java └── MyBlobFeature.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.settings/ 3 | /.checkstyle 4 | /.classpath 5 | /.project 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![DOI](https://zenodo.org/badge/18649/thorstenwagner/ij-blob.svg)](https://zenodo.org/badge/latestdoi/18649/thorstenwagner/ij-blob) 2 | [![Build Status](https://travis-ci.org/thorstenwagner/ij-blob.svg?branch=master)](https://travis-ci.org/thorstenwagner/ij-blob) 3 | 4 | #IJBlob 5 | Please visit http://fiji.sc/IJ_Blob for more information 6 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | de.biomedical-imaging.ij 5 | ij_blob 6 | 1.4.10 7 | jar 8 | 9 | IJBlob 10 | Connected Components Library for ImageJ. 11 | https://github.com/jumpfunky/ij-blob 12 | 13 | 14 | 15 | GNU v3 License 16 | http://opensource.org/licenses/GPL-3.0 17 | 18 | 19 | 20 | 21 | 22 | https://github.com/thorstenwagner/ij-blob 23 | scm:git:git://github.com/thorstenwagner/ij-blob.git 24 | scm:git:git@github.com:thorstenwagner/ij-blob.git 25 | ij_blob-1.4.10 26 | 27 | 28 | 29 | 30 | net.imagej 31 | ij 32 | 1.49r 33 | 34 | 35 | junit 36 | junit 37 | 4.13.1 38 | 39 | 40 | 41 | 42 | 43 | twagner 44 | Thorsten Wagner 45 | wagner@biomedical-imaging.de 46 | 47 | true 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-release-plugin 58 | 2.5 59 | 60 | false 61 | release 62 | deploy 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.sonatype.plugins 70 | nexus-staging-maven-plugin 71 | 1.6.13 72 | true 73 | 74 | ossrh 75 | https://oss.sonatype.org/ 76 | true 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ossrh 85 | https://oss.sonatype.org/content/repositories/snapshots 86 | 87 | 88 | ossrh 89 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 90 | 91 | 92 | 93 | 94 | doclint-java8-disable 95 | 96 | [1.8,) 97 | 98 | 99 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-javadoc-plugin 104 | 2.10 105 | 106 | -Xdoclint:none 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | release 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-source-plugin 120 | 2.2.1 121 | 122 | 123 | attach-sources 124 | 125 | jar-no-fork 126 | 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-javadoc-plugin 133 | 2.9.1 134 | 135 | 136 | attach-javadocs 137 | 138 | jar 139 | 140 | 141 | 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-gpg-plugin 146 | 1.5 147 | 148 | 149 | sign-artifacts 150 | verify 151 | 152 | sign 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/main/java/ij/blob/Blob.java: -------------------------------------------------------------------------------- 1 | /* 2 | IJBlob is a ImageJ library for extracting connected components in binary Images 3 | Copyright (C) 2012 Thorsten Wagner wagner@biomedical-imaging.de 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package ij.blob; 20 | import ij.IJ; 21 | import ij.ImagePlus; 22 | import ij.gui.NewImage; 23 | import ij.gui.PolygonRoi; 24 | import ij.gui.Roi; 25 | import ij.measure.Calibration; 26 | import ij.plugin.filter.EDM; 27 | import ij.plugin.filter.MaximumFinder; 28 | import ij.process.ByteProcessor; 29 | import ij.process.EllipseFitter; 30 | import ij.process.FloatProcessor; 31 | import ij.process.ImageProcessor; 32 | import ij.process.PolygonFiller; 33 | 34 | import java.awt.Color; 35 | import java.awt.Point; 36 | import java.awt.Polygon; 37 | import java.awt.Rectangle; 38 | import java.awt.geom.Point2D; 39 | import java.lang.reflect.InvocationTargetException; 40 | import java.lang.reflect.Method; 41 | import java.util.ArrayList; 42 | 43 | //import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D; 44 | 45 | /** 46 | * Represents a connected component - a so called "blob". 47 | * @author Thorsten Wagner 48 | */ 49 | public class Blob { 50 | 51 | public final static int DRAW_HOLES = 1; 52 | public final static int DRAW_CONVEX_HULL = 2; 53 | public final static int DRAW_LABEL = 4; 54 | 55 | private static Color defaultColor = Color.black; 56 | 57 | 58 | private Polygon outerContour; 59 | private ArrayList innerContours; //Holes 60 | private int label; 61 | 62 | //Features 63 | private Point2D centerOfGrafity = null; 64 | private double perimeter = -1; 65 | private double perimeterConvexHull = -1; 66 | private double enclosedArea = -1; 67 | 68 | private double circularity = -1; 69 | private double thinnesRatio = -1; 70 | private double areaToPerimeterRatio = -1; 71 | private double temperature = -1; 72 | private double fractalBoxDimension = -1; 73 | private double fractalDimensionGoodness = -1; 74 | private double elongation = -1; 75 | private double eigenMajor = -1; 76 | private double eigenMinor = -1; 77 | private double orientation = -1; 78 | private double convexity = -1; 79 | private double solidity = -1; 80 | private double areaConvexHull = -1; 81 | private Calibration cal = new Calibration(); 82 | private double[][] centralMomentsLUT = {{-1,-1,-1},{-1,-1,-1},{-1,-1,-1}}; 83 | private double[][] momentsLUT = {{-1,-1,-1},{-1,-1,-1},{-1,-1,-1}}; 84 | EllipseFitter fittedEllipse = null; 85 | static ArrayList customFeatures = new ArrayList(); 86 | 87 | public Blob(Polygon outerContour, int label) { 88 | this.outerContour = outerContour; 89 | this.label = label; 90 | innerContours = new ArrayList(); 91 | } 92 | 93 | /** 94 | * @param outerContour Contur of the blob 95 | * @param label Its unique label 96 | * @param cal The blob will use the image calibration 97 | */ 98 | public Blob(Polygon outerContour, int label, Calibration cal) { 99 | this.outerContour = outerContour; 100 | this.label = label; 101 | innerContours = new ArrayList(); 102 | this.cal = cal; 103 | } 104 | 105 | public void setCalibration(Calibration cal){ 106 | this.cal = cal; 107 | } 108 | 109 | public static void addCustomFeature(CustomBlobFeature feature) { 110 | customFeatures.add(feature); 111 | } 112 | /** 113 | * Changes the default blob color. 114 | * @param defaultColor The default color. 115 | */ 116 | public static void setDefaultColor(Color defaultColor) { 117 | Blob.defaultColor = defaultColor; 118 | } 119 | 120 | /** 121 | * Evaluates the Custom Feature and return its value 122 | * @param Method name The method name of the method in the feature class 123 | * @param params the parameters of the method specified by the method name 124 | * @throws NoSuchMethodException 125 | */ 126 | public Object evaluateCustomFeature(String methodName, Object... params) throws NoSuchMethodException { 127 | Boolean methodfound = false; 128 | int featureIndex = -1; 129 | for(int i = 0; i < customFeatures.size(); i++){ 130 | Method customMethods[] = customFeatures.get(i).getClass().getDeclaredMethods(); 131 | for(int j = 0; j < customMethods.length; j++){ 132 | if(customMethods[j].getName() == methodName){ 133 | 134 | methodfound = true; 135 | featureIndex = i; 136 | break; 137 | } 138 | } 139 | if(methodfound){break;} 140 | } 141 | @SuppressWarnings("rawtypes") 142 | Class classparams[] = {}; 143 | if(params.length >0){ 144 | classparams = new Class[params.length]; 145 | for(int i = 0; i< params.length; i++){ 146 | classparams[i] = params[i].getClass(); 147 | } 148 | } 149 | Object value=0; 150 | try { 151 | customFeatures.get(featureIndex).setup(this); 152 | Method m = customFeatures.get(featureIndex).getClass().getMethod(methodName, classparams); 153 | 154 | value = m.invoke((customFeatures.get(featureIndex)), params); 155 | 156 | } catch (NoSuchMethodException e) { 157 | throw new NoSuchMethodException("The method " + methodName + " was not found"); 158 | } catch (SecurityException e) { 159 | // TODO Auto-generated catch block 160 | e.printStackTrace(); 161 | } catch (IllegalAccessException e) { 162 | // TODO Auto-generated catch block 163 | e.printStackTrace(); 164 | } catch (IllegalArgumentException e) { 165 | // TODO Auto-generated catch block 166 | e.printStackTrace(); 167 | } catch (InvocationTargetException e) { 168 | // TODO Auto-generated catch block 169 | e.printStackTrace(); 170 | } 171 | 172 | return value; 173 | } 174 | 175 | void draw(ImageProcessor ip, int options, Color col){ 176 | ip.setColor(col); 177 | fillPolygon(ip, outerContour, false); 178 | 179 | 180 | if((options&DRAW_HOLES)>0){ 181 | for(int i = 0; i < innerContours.size(); i++) { 182 | if(defaultColor==Color.white){ 183 | ip.setColor(Color.BLACK); 184 | } 185 | else 186 | { 187 | ip.setColor(Color.white); 188 | } 189 | fillPolygon(ip, innerContours.get(i), true); 190 | if(defaultColor==Color.white){ 191 | ip.setColor(Color.white); 192 | } 193 | else 194 | { 195 | ip.setColor(Color.black); 196 | } 197 | ip.drawPolygon(innerContours.get(i)); 198 | 199 | } 200 | } 201 | 202 | if((options&DRAW_CONVEX_HULL)>0){ 203 | ip.setColor(Color.RED); 204 | ip.drawPolygon(getConvexHull()); 205 | } 206 | 207 | if((options&DRAW_LABEL)>0){ 208 | Point2D cog = getCenterOfGravity(); 209 | ip.setColor(Color.MAGENTA); 210 | ip.drawString(""+getLabel(), (int)cog.getX(), (int)cog.getX()); 211 | } 212 | } 213 | 214 | /** 215 | * @return Outer contour as traced roi 216 | */ 217 | public Roi getOuterContourAsROI(){ 218 | Polygon p = getOuterContour(); 219 | int n = p.npoints; 220 | float[] x = new float[p.npoints]; 221 | float[] y = new float[p.npoints]; 222 | 223 | for (int j=0; j0){ 252 | for(int i = 0; i < innerContours.size(); i++) { 253 | ip.setColor(Color.WHITE); 254 | p = new Polygon(innerContours.get(i).xpoints,innerContours.get(i).ypoints,innerContours.get(i).npoints); 255 | p.translate(deltax, deltay); 256 | fillPolygon(ip, p, true); 257 | } 258 | } 259 | if((options&DRAW_CONVEX_HULL)>0){ 260 | ip.setColor(Color.RED); 261 | ip.drawPolygon(getConvexHull()); 262 | } 263 | 264 | if((options&DRAW_LABEL)>0){ 265 | Point2D cog = getCenterOfGravity(); 266 | ip.setColor(Color.MAGENTA); 267 | ip.drawString(""+getLabel(), (int)cog.getX(), (int)cog.getY()); 268 | } 269 | } 270 | 271 | 272 | 273 | /** 274 | * Draws the Blob with its holes. 275 | * @param ip The ImageProcesser in which the blob has to be drawn. 276 | */ 277 | public void draw(ImageProcessor ip){ 278 | draw(ip,DRAW_HOLES); 279 | } 280 | 281 | void drawLabels(ImageProcessor ip, Color col) { 282 | draw(ip,DRAW_HOLES,col); 283 | } 284 | 285 | @SuppressWarnings("unused") 286 | private final double getArea(Polygon p) { 287 | if (p==null) return Double.NaN; 288 | 289 | int carea = 0; 290 | int iminus1; 291 | for (int i=0; ivalueb){ 510 | value = valueb; 511 | } 512 | } 513 | return value; 514 | } 515 | 516 | /** 517 | * Calculates Eigenvalue from the major axis using the moments of the boundary 518 | * @return Return the Eigenvalue from the major axis (computational expensive!) 519 | */ 520 | public double getEigenvalueMajorAxis() { 521 | if(eigenMajor!=-1){ 522 | return eigenMajor; 523 | } 524 | eigenMajor = getEigenvalue(true); 525 | return eigenMajor; 526 | } 527 | 528 | /** 529 | * Calculates Eigenvalue from the minor axis using the moments of the boundary 530 | * @return Return the Eigenvalue from the minor axis (computational expensive!) 531 | */ 532 | public double getEigenvalueMinorAxis() { 533 | if(eigenMinor!=-1){ 534 | return eigenMinor; 535 | } 536 | eigenMinor = getEigenvalue(false); 537 | return eigenMinor; 538 | } 539 | 540 | /** 541 | * Method name of getElongation (for filtering). 542 | */ 543 | public final static String GETELONGATION = "getElongation"; 544 | 545 | /** 546 | * The Elongation of the Blob based on a fitted ellipse (1 - minor axis / major axis) 547 | * @return The Elongation (normed between 0 and 1) 548 | */ 549 | public double getElongation() { 550 | if(elongation!= -1){ 551 | return elongation; 552 | } 553 | fitEllipse(); 554 | elongation = 1- fittedEllipse.minor/fittedEllipse.major; 555 | elongation = Math.sqrt(elongation); 556 | 557 | return elongation; 558 | } 559 | 560 | public Point[] getMinimumBoundingRectangle(){ 561 | int[] xp = new int[getOuterContour().npoints]; 562 | int[] yp = new int[getOuterContour().npoints]; 563 | for(int i = 0; i < getOuterContour().npoints; i++){ 564 | xp[i] = getOuterContour().xpoints[i]; 565 | yp[i] = getOuterContour().ypoints[i]; 566 | } 567 | Point2D.Double[] mbr; 568 | try{ 569 | mbr = RotatingCalipers.getMinimumBoundingRectangle(xp, yp); 570 | } 571 | catch(IllegalArgumentException e){ 572 | return null; 573 | } 574 | Point[] p = new Point[4]; 575 | for(int i = 0; i < mbr.length; i++){ 576 | //IJ.log("i " + i); 577 | p[i] = new Point(); 578 | p[i].x = (int)mbr[i].x; 579 | p[i].y = (int)mbr[i].y; 580 | } 581 | return p; 582 | 583 | } 584 | 585 | /** 586 | * Method name of getLongSideMBR (for filtering). 587 | */ 588 | public final static String GETLONGSIDEMBR = "getLongSideMBR"; 589 | 590 | /** 591 | * @return The long side length of the minimum enclosing rectangle 592 | */ 593 | public double getLongSideMBR(){ 594 | Point[] mbr = getMinimumBoundingRectangle(); 595 | 596 | if(mbr == null){ 597 | return Double.NaN; 598 | } 599 | 600 | double firstSide = Math.sqrt(Math.pow(cal.getX(mbr[1].x) -cal.getX(mbr[0].x),2)+Math.pow(cal.getY(mbr[1].y) - cal.getY(mbr[0].y),2)); 601 | double secondSide = Math.sqrt(Math.pow(cal.getX(mbr[1].x) -cal.getX(mbr[2].x),2)+Math.pow(cal.getY(mbr[1].y) -cal.getY(mbr[2].y),2)); 602 | 603 | return firstSide>secondSide?firstSide:secondSide; 604 | } 605 | 606 | /** 607 | * Method name of getLongSideMBR (for filtering). 608 | */ 609 | public final static String GETSHORTSIDEMBR = "getShortSideMBR"; 610 | 611 | /** 612 | * @return The short side length of the minimum enclosing rectangle 613 | */ 614 | public double getShortSideMBR(){ 615 | Point[] mbr = getMinimumBoundingRectangle(); 616 | if(mbr == null){ 617 | return Double.NaN; 618 | } 619 | double firstSide = Math.sqrt(Math.pow(cal.getX(mbr[1].x) -cal.getX(mbr[0].x),2)+Math.pow(cal.getY(mbr[1].y) - cal.getY(mbr[0].y),2)); 620 | double secondSide = Math.sqrt(Math.pow(cal.getX(mbr[1].x) -cal.getX(mbr[2].x),2)+Math.pow(cal.getY(mbr[1].y) -cal.getY(mbr[2].y),2)); 621 | 622 | return firstSide getInnerContours() { 692 | return innerContours; 693 | } 694 | 695 | /** 696 | * Adds an inner contour (hole) to blob. 697 | * @param contour Contour of the hole. 698 | */ 699 | void addInnerContour(Polygon contour) { 700 | innerContours.add(contour); 701 | } 702 | 703 | /** 704 | * Return the label of the blob in the labeled image 705 | * @return Return blob's label in the labeled image 706 | */ 707 | public int getLabel() { 708 | return label; 709 | } 710 | 711 | public void setLabel(int newlabel) { 712 | label = newlabel; 713 | } 714 | 715 | 716 | /** 717 | * Method name of getPerimeter (for filtering). 718 | */ 719 | public final static String GETPERIMETER = "getPerimeter"; 720 | /** 721 | * Calculates the perimeter of the outer contour using its chain code 722 | * @return The perimeter of the outer contour. 723 | */ 724 | public double getPerimeter() { 725 | 726 | if(perimeter!=-1){ 727 | return perimeter; 728 | } 729 | 730 | return getPerimeterOfContour(getOuterContour()); 731 | } 732 | 733 | private double getPerimeterOfContour(Polygon contour){ 734 | double peri = 0; 735 | 736 | if(contour.npoints == 1) 737 | { 738 | peri=1; 739 | return peri; 740 | } 741 | int[] cc = contourToChainCode(contour); 742 | int sum_gerade= 0; 743 | for(int i = 0; i < cc.length;i++){ 744 | if(cc[i]%2 == 0){ 745 | sum_gerade++; 746 | } 747 | } 748 | 749 | peri = sum_gerade*0.948 + (cc.length-sum_gerade)*1.340; 750 | 751 | 752 | 753 | PolygonRoi roi = new PolygonRoi(outerContour, Roi.POLYLINE); 754 | ImagePlus dummy = new ImagePlus(); 755 | dummy.setCalibration(cal); 756 | roi.setImage(dummy); 757 | 758 | return peri*cal.pixelHeight; 759 | } 760 | 761 | private int[] contourToChainCode(Polygon contour) { 762 | int[] chaincode = new int[contour.npoints-1]; 763 | for(int i = 1; i 1){ 841 | convexity=1; 842 | } 843 | return convexity; 844 | } 845 | 846 | /** 847 | * Checks if the blob is on the edge of the image. 848 | * @param ip The imageprocesser which contains the blob 849 | * @return true if the blob is on a edge. 850 | */ 851 | public boolean isOnEdge(ImageProcessor ip){ 852 | 853 | Polygon p = getOuterContour(); 854 | for(int i = 0; i < p.npoints; i++){ 855 | int x = p.xpoints[i]; 856 | int y = p.ypoints[i]; 857 | if(x == 0 || y == 0 || x == (ip.getWidth()-1) || y == (ip.getHeight()-1)){ 858 | return true; 859 | } 860 | } 861 | 862 | return false; 863 | } 864 | 865 | /** 866 | * Method name of getSolidity (for filtering). 867 | */ 868 | public final static String GETSOLIDITY = "getSolidity"; 869 | /** 870 | * @return enclosed area / enclosed of the convex hull 871 | */ 872 | public double getSolidity() { 873 | if(solidity!=-1){ 874 | return solidity; 875 | } 876 | solidity = getEnclosedArea()/getAreaConvexHull(); 877 | if(solidity>1){ 878 | solidity=1; 879 | } 880 | return solidity; 881 | } 882 | 883 | /** 884 | * Returns the convex hull of the blob. 885 | * @return The convex hull as polygon 886 | */ 887 | 888 | public Polygon getConvexHull() { 889 | PolygonRoi roi = new PolygonRoi(outerContour, Roi.POLYGON); 890 | Polygon hull = roi.getConvexHull(); 891 | if(hull==null){ 892 | return getOuterContour(); 893 | } 894 | return hull; 895 | } 896 | 897 | @SuppressWarnings("unused") 898 | private double getAreaOfChainCode(int[] cc){ 899 | int B = 1; 900 | double A = 0; 901 | for(int i = 0; i < cc.length; i++){ 902 | switch(cc[i]){ 903 | 904 | case 0: 905 | A -= B; 906 | break; 907 | case 1: 908 | B += 1; 909 | A += -(B + 0.5); 910 | break; 911 | case 2: 912 | B += 1; 913 | break; 914 | case 3: 915 | B += 1; 916 | A += B+0.5; 917 | break; 918 | case 4: 919 | A += B; 920 | break; 921 | case 5: 922 | B += -1; 923 | A += B - 0.5; 924 | break; 925 | case 6: 926 | B += -1; 927 | break; 928 | case 7: 929 | B += -1; 930 | A += -(B-0.5); 931 | break; 932 | } 933 | } 934 | 935 | double area = Math.abs(A); 936 | 937 | if(area==0){ 938 | area=1; 939 | } 940 | return area; 941 | } 942 | 943 | /** 944 | * Method name of getEnclosedArea (for filtering). 945 | */ 946 | public final static String GETENCLOSEDAREA = "getEnclosedArea"; 947 | /** 948 | * Calculates the enclosed are of the outer contour without subsctracting possible holes. 949 | * @return The enclosed area of the outer contour (without substracting the holes). 950 | */ 951 | public double getEnclosedArea() { 952 | if(enclosedArea!=-1){ 953 | return enclosedArea; 954 | } 955 | /* 956 | int[] cc = contourToChainCode(getOuterContour()); 957 | enclosedArea = getAreaOfChainCode(cc)*cal.pixelHeight*cal.pixelWidth; 958 | */ 959 | 960 | //enclosedArea = getArea(getOuterContour())*cal.pixelHeight*cal.pixelWidth; 961 | 962 | ImagePlus imp = generateBlobImage(this); 963 | enclosedArea = imp.getStatistics().histogram[0]*cal.pixelHeight*cal.pixelWidth; 964 | 965 | return enclosedArea; 966 | } 967 | 968 | 969 | /** 970 | * Method name of getAreaConvexHull (for filtering). 971 | */ 972 | public final static String GETAREACONVEXHULL = "getAreaConvexHull"; 973 | /** 974 | * @return Area of the convex hull 975 | */ 976 | public double getAreaConvexHull(){ 977 | if(areaConvexHull!=-1){ 978 | return areaConvexHull; 979 | } 980 | Polygon polyPoints = getConvexHull(); 981 | /* 982 | int i, j, n = polyPoints.npoints; 983 | areaConvexHull = 0; 984 | 985 | for (i = 0; i < n; i++) { 986 | j = (i + 1) % n; 987 | areaConvexHull += polyPoints.xpoints[i] * polyPoints.ypoints[j]; 988 | areaConvexHull -= polyPoints.xpoints[j] * polyPoints.ypoints[i]; 989 | } 990 | areaConvexHull /= 2.0; 991 | areaConvexHull = Math.abs(areaConvexHull)*cal.pixelHeight*cal.pixelWidth;; 992 | */ 993 | 994 | Blob helpblob = new Blob(polyPoints, -1); 995 | ImagePlus imp = generateBlobImage(helpblob); 996 | areaConvexHull = imp.getStatistics().getHistogram()[0]*cal.pixelHeight*cal.pixelWidth; 997 | return areaConvexHull; 998 | 999 | } 1000 | 1001 | /** 1002 | * Method name of getCircularity (for filtering). 1003 | */ 1004 | public final static String GETCIRCULARITY = "getCircularity"; 1005 | /** 1006 | * Calculates the circularity of the outer contour: (perimeter*perimeter) / (enclosed area). If the value approaches 0.0, it indicates that the polygon is increasingly elongated. 1007 | * @return Circularity (perimeter*perimeter) / (enclosed area) 1008 | */ 1009 | public double getCircularity() { 1010 | if(circularity!=-1){ 1011 | return circularity; 1012 | } 1013 | double perimeter = getPerimeter(); 1014 | double size = getEnclosedArea(); 1015 | circularity = (perimeter*perimeter) / size; 1016 | return circularity; 1017 | } 1018 | /** 1019 | * Method name of getThinnesRatio (for filtering). 1020 | */ 1021 | public final static String GETTHINNESRATIO = "getThinnesRatio"; 1022 | /** 1023 | * The Thinnes Ratio of the blob (normed). A circle has a thinnes ratio of 1. 1024 | * @return Thinnes Ratio defined as: (4*PI)/Circularity 1025 | */ 1026 | public double getThinnesRatio() { 1027 | if(thinnesRatio!=-1){ 1028 | return thinnesRatio; 1029 | } 1030 | thinnesRatio = (4*Math.PI)/getCircularity(); 1031 | thinnesRatio = (thinnesRatio>1)?1:thinnesRatio; 1032 | return thinnesRatio; 1033 | } 1034 | 1035 | /** 1036 | * Method name of getAreaToPerimeterRatio (for filtering). 1037 | */ 1038 | public final static String GETAREATOPERIMETERRATIO = "getAreaToPerimeterRatio"; 1039 | /** 1040 | * Area/Perimeter 1041 | * @return Area to perimeter ratio 1042 | */ 1043 | public double getAreaToPerimeterRatio() { 1044 | if(areaToPerimeterRatio != -1){ 1045 | return areaToPerimeterRatio; 1046 | } 1047 | areaToPerimeterRatio = getEnclosedArea()/getPerimeter(); 1048 | return areaToPerimeterRatio; 1049 | } 1050 | 1051 | /** 1052 | * Method name of getContourTemperature (for filtering). 1053 | */ 1054 | public final static String GETCONTOURTEMPERATURE = "getContourTemperature"; 1055 | /** 1056 | * Calculates the Contour Temperatur. It has a strong relationship to the fractal dimension. 1057 | * @return Contour Temperatur 1058 | * @see Datails in Luciano da Fontoura Costa, Roberto Marcondes Cesar, 1059 | * Jr.Shape Classification and Analysis: Theory and Practice, Second Edition, 2009, CRC Press 1060 | */ 1061 | public double getContourTemperature() { 1062 | if(temperature!=-1){ 1063 | return temperature; 1064 | } 1065 | double chp = getPerimeterConvexHull(); 1066 | double peri = getPerimeter(); 1067 | temperature = 1/(Math.log((2*peri)/(Math.abs(peri-chp)))/Math.log(2)); 1068 | return temperature; 1069 | } 1070 | 1071 | /** 1072 | * Box Dimension of the blob boundary. 1073 | * @return Calculates the fractal box dimension of the blob. 1074 | * @param boxSizes ordered array of Box-Sizes 1075 | */ 1076 | public double getFractalBoxDimension(int[] boxSizes) { 1077 | if(fractalBoxDimension !=-1){ 1078 | return fractalBoxDimension; 1079 | } 1080 | FractalBoxCounterBlob boxcounter = new FractalBoxCounterBlob(); 1081 | boxcounter.setBoxSizes(boxSizes); 1082 | double[] FDandGOF = boxcounter.getFractcalDimension(this); 1083 | fractalBoxDimension = FDandGOF[0]; 1084 | fractalDimensionGoodness = FDandGOF[1]; 1085 | return fractalBoxDimension; 1086 | } 1087 | 1088 | /** 1089 | * Method name of getMaximumInscribedCircle (for filtering). 1090 | */ 1091 | public final static String GETDIAMETERMAXIMUMINSCRIBEDCIRCLE = "getDiamaterMaximumInscribedCircle"; 1092 | public double getDiamaterMaximumInscribedCircle() { 1093 | ImagePlus help = generateBlobImage(this); 1094 | ImageProcessor ipHelp = help.getProcessor(); 1095 | ipHelp.invert(); 1096 | EDM dm = new EDM(); 1097 | FloatProcessor fp = dm.makeFloatEDM (ipHelp, 0, false); 1098 | 1099 | MaximumFinder mf = new MaximumFinder(); 1100 | ByteProcessor bp = mf.findMaxima(fp, 0.5, ImageProcessor.NO_THRESHOLD, MaximumFinder.SINGLE_POINTS, false, true); 1101 | Polygon pl = mf.getMaxima(bp, 0, true); 1102 | return fp.getf(pl.xpoints[0], pl.ypoints[0])*2*cal.getX(1); 1103 | 1104 | } 1105 | 1106 | public static ImagePlus generateBlobImage(Blob b){ 1107 | Rectangle r = b.getOuterContour().getBounds(); 1108 | r.setBounds(r.x, r.y, (int)r.getWidth()+1, (int)r.getHeight()+1); 1109 | ImagePlus help = NewImage.createByteImage("", r.width+2, r.height+2, 1, NewImage.FILL_WHITE); 1110 | ImageProcessor ip = help.getProcessor(); 1111 | b.draw(ip, Blob.DRAW_HOLES, -(r.x-1), -(r.y-1)); 1112 | help.setProcessor(ip); 1113 | return help; 1114 | } 1115 | 1116 | /** 1117 | * Method name of getContourTemperature (for filtering). 1118 | */ 1119 | public final static String GETFRACTALBOXDIMENSION = "getFractalBoxDimension"; 1120 | /** 1121 | * @return The fractal box dimension of the blob. 1122 | */ 1123 | public double getFractalBoxDimension() { 1124 | if(fractalBoxDimension !=-1){ 1125 | return fractalBoxDimension; 1126 | } 1127 | FractalBoxCounterBlob boxcounter = new FractalBoxCounterBlob(); 1128 | double[] FDandGOF = boxcounter.getFractcalDimension(this); 1129 | fractalBoxDimension = FDandGOF[0]; 1130 | fractalDimensionGoodness = FDandGOF[1]; 1131 | return fractalBoxDimension; 1132 | } 1133 | 1134 | /** 1135 | * The goodness of the "best fit" line of the fractal box dimension estimation. 1136 | * @return The goodness of the "best fit" line of the fractal box dimension estimation. 1137 | */ 1138 | public double getFractalDimensionGoodness(){ 1139 | return fractalDimensionGoodness; 1140 | } 1141 | 1142 | /** 1143 | * Method name of getNumberofHoles (for filtering). 1144 | */ 1145 | public final static String GETNUMBEROFHOLES = "getNumberofHoles"; 1146 | /** 1147 | * The number of inner contours (Holes) of a blob. 1148 | * @return The number of inner contours (Holes) of a blob. 1149 | */ 1150 | public int getNumberofHoles() { 1151 | return innerContours.size(); 1152 | } 1153 | } 1154 | -------------------------------------------------------------------------------- /src/main/java/ij/blob/ConnectedComponentLabeler.java: -------------------------------------------------------------------------------- 1 | /* 2 | IJBlob is a ImageJ library for extracting connected components in binary Images 3 | Copyright (C) 2012 Thorsten Wagner wagner@biomedical-imaging.de 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package ij.blob; 19 | 20 | import ij.IJ; 21 | import ij.ImagePlus; 22 | import ij.Prefs; 23 | import ij.gui.Toolbar; 24 | import ij.measure.Calibration; 25 | import ij.plugin.CanvasResizer; 26 | import ij.process.ByteProcessor; 27 | import ij.process.ColorProcessor; 28 | import ij.process.ImageProcessor; 29 | 30 | import java.awt.Color; 31 | import java.awt.Point; 32 | import java.awt.Polygon; 33 | import java.awt.Rectangle; 34 | import java.util.ArrayList; 35 | import java.util.Arrays; 36 | 37 | /** 38 | * Does Connected Component Labeling 39 | * @author Thorsten Wagner 40 | * 41 | */ 42 | class ConnectedComponentLabeler { 43 | 44 | private ImagePlus imp; 45 | private ImageProcessor labledImage; 46 | private int NOLABEL = 0; 47 | private int labelCount = 1; 48 | private int BACKGROUND = 255; 49 | private int OBJECT = 0; 50 | private ManyBlobs allBlobs; 51 | private boolean removeBorder = false; 52 | private int offSetX = 0; 53 | private int offsetY = 0; 54 | /* 55 | * 56 | * The read-order of the neighberhood of p. 57 | * 58 | * 5 * 6 * 7 59 | * 4 * p * 0 60 | * 3 * 2 * 1 61 | */ 62 | int iterationorder[] = { 5, 4, 3, 6, 2, 7, 0, 1 }; 63 | 64 | /** 65 | * @param allBlobs A ManyBlobs Object where the Blobs has to be stored 66 | * @param imp The image 67 | */ 68 | public ConnectedComponentLabeler(ManyBlobs allBlobs, ImagePlus imp, int BACKGROUND, int OBJECT) { 69 | this.allBlobs = allBlobs; 70 | this.imp = imp; 71 | this.BACKGROUND = BACKGROUND; 72 | this.OBJECT = OBJECT; 73 | 74 | 75 | addWhiteBorder(imp); 76 | 77 | labledImage = new ColorProcessor(this.imp.getWidth(), this.imp.getHeight()); 78 | 79 | } 80 | 81 | /** 82 | * Start the Connected Component Algorithm 83 | * @see F. Chang, A linear-time component-labeling algorithm using contour tracing technique, Computer Vision and Image Understanding, vol. 93, no. 2, pp. 206-220, 2004. 84 | */ 85 | public void doConnectedComponents() { 86 | 87 | ImageProcessor ip = imp.getProcessor(); 88 | Calibration c = imp.getCalibration(); 89 | 90 | ByteProcessor proc = (ByteProcessor) ip; 91 | byte[] pixels = (byte[]) proc.getPixels(); 92 | int w = proc.getWidth(); 93 | 94 | Rectangle roi = ip.getRoi(); 95 | int value; 96 | for (int i = roi.y; i < roi.y + roi.height; ++i) { 97 | int offset = i * w; 98 | for (int j = roi.x; j < roi.x + roi.width; ++j) { 99 | value = pixels[offset + j] & 255; 100 | 101 | if (value == OBJECT) { 102 | 103 | if (isNewExternalContour(j, i, proc) && hasNoLabel(j, i)) { 104 | 105 | labledImage.set(j, i, labelCount); 106 | Polygon outerContour = traceContour(j, i, proc, 107 | labelCount, 1); 108 | outerContour.translate(offSetX, offsetY); 109 | 110 | allBlobs.add(new Blob(outerContour, labelCount,c)); 111 | ++labelCount; 112 | 113 | } 114 | if (isNewInternalContour(j, i, proc)) { 115 | int label = labledImage.get(j, i); 116 | if (hasNoLabel(j, i)) { 117 | //printImage(labledImage); 118 | label = labledImage.get(j-1, i); 119 | labledImage.set(j, i, label); 120 | 121 | } 122 | try{ 123 | Polygon innerContour = traceContour(j, i, proc, label, 124 | 2); 125 | innerContour.translate(offSetX, offsetY); 126 | getBlobByLabel(label).addInnerContour(innerContour); 127 | }catch(Exception e){ 128 | 129 | IJ.log("x " + j + " y " +i + " label " + label); 130 | } 131 | 132 | } else if (hasNoLabel(j, i)) { 133 | 134 | int precedinglabel = labledImage.get(j - 1, i); 135 | labledImage.set(j, i, precedinglabel); 136 | } 137 | 138 | } 139 | } 140 | } 141 | if(removeBorder){ 142 | removeBorder(imp); 143 | } 144 | //printImage(labledImage); 145 | } 146 | 147 | @SuppressWarnings("unused") 148 | private void printImage(ImageProcessor img){ 149 | System.out.println("================="); 150 | ImageProcessor proc = img; 151 | 152 | for(int y = 0; y < proc.getHeight(); y++){ 153 | String oneline=""; 154 | int numberSpaces = 0; 155 | for(int x = 0; x < proc.getWidth(); x++){ 156 | String pixel = "" + proc.getPixel(x, y); 157 | 158 | for(int i = 0; i < numberSpaces; i++){ 159 | oneline += " "; 160 | } 161 | oneline += "" + pixel; 162 | numberSpaces = 8-pixel.length()%8; 163 | } 164 | System.out.println(oneline); 165 | 166 | } 167 | } 168 | 169 | public ImagePlus getLabledImage() { 170 | ImagePlus img = new ImagePlus("Labeled", labledImage); 171 | ColorProcessor proc = (ColorProcessor) img.getProcessor(); 172 | int[] pixels = (int[]) proc.getPixels(); 173 | int w = proc.getWidth(); 174 | int h = proc.getHeight(); 175 | int value; 176 | for (int i = 0; i < h; ++i) { 177 | int offset = i * w; 178 | for (int j = 0; j < w; ++j) { 179 | value = pixels[offset + j]; 180 | if(value==-1){ 181 | pixels[offset + j] = BACKGROUND; 182 | } 183 | } 184 | } 185 | if(removeBorder){ 186 | removeBorder(img); 187 | } 188 | return img; 189 | } 190 | 191 | 192 | 193 | private Polygon traceContour(int x, int y, ByteProcessor proc, int label, 194 | int start) { 195 | 196 | Polygon contour = new Polygon(); 197 | Point startPoint = new Point(x, y); 198 | contour.addPoint(x, y); 199 | 200 | Point nextPoint = nextPointOnContour(startPoint, proc, start); 201 | 202 | if (nextPoint.x == -1) { 203 | // Point is isolated; 204 | return contour; 205 | } 206 | Point T = new Point(nextPoint.x,nextPoint.y); 207 | boolean equalsStartpoint = false; 208 | do { 209 | contour.addPoint(nextPoint.x, nextPoint.y); 210 | labledImage.set(nextPoint.x, nextPoint.y, label); 211 | equalsStartpoint = nextPoint.equals(startPoint); 212 | nextPoint = nextPointOnContour(nextPoint, proc, -1); 213 | } while (!equalsStartpoint || !nextPoint.equals(T)); 214 | 215 | return contour; 216 | } 217 | 218 | Point prevContourPoint; 219 | 220 | // start = 1 -> External Contour 221 | // start = 2 -> Internal Contour 222 | private final Point nextPointOnContour(Point startPoint, ByteProcessor proc, 223 | int start) { 224 | 225 | /* 226 | ************ 227 | *5 * 6 * 7 * 228 | *4 * p * 0 * 229 | *3 * 2 * 1 * 230 | ************ 231 | */ 232 | Point[] helpindexToPoint = new Point[8]; 233 | 234 | int[] neighbors = new int[8]; // neighbors of p 235 | int x = startPoint.x; 236 | int y = startPoint.y; 237 | 238 | int I = 2; 239 | int k = I - 1; 240 | 241 | int u = 0; 242 | for (int i = 0; i < 3; i++) { 243 | for (int j = 0; j < 3; j++) { 244 | int window_x = (x - k + i); 245 | int window_y = (y - k + j); 246 | if (window_x != x || window_y != y) { 247 | neighbors[iterationorder[u]] = proc.get(window_x, window_y); 248 | helpindexToPoint[iterationorder[u]] = new Point(window_x, 249 | window_y); 250 | u++; 251 | } 252 | } 253 | } 254 | ArrayList indexToPoint = new ArrayList( 255 | Arrays.asList(helpindexToPoint)); 256 | 257 | final int NOSTARTPOINT = -1; 258 | final int STARTEXTERNALCONTOUR = 1; 259 | final int STARTINTERNALCONTOUR = 2; 260 | 261 | switch (start) { 262 | case NOSTARTPOINT: 263 | int prevContourPointIndex = indexToPoint.indexOf(prevContourPoint); 264 | start = (prevContourPointIndex + 2) % 8; 265 | break; 266 | case STARTEXTERNALCONTOUR: 267 | start = 7; 268 | break; 269 | case STARTINTERNALCONTOUR: 270 | start = 3; 271 | 272 | break; 273 | } 274 | 275 | int counter = start; 276 | int pos = -2; 277 | 278 | Point returnPoint = null; 279 | while (pos != start) { 280 | pos = counter % 8; 281 | if (neighbors[pos] == OBJECT) { 282 | prevContourPoint = startPoint; 283 | returnPoint = indexToPoint.get(pos); 284 | return returnPoint; 285 | } 286 | Point p = indexToPoint.get(pos); 287 | if (neighbors[pos] == BACKGROUND) { 288 | try { 289 | labledImage.set(p.x, p.y, -1); 290 | } catch (Exception e) { 291 | IJ.log("x " + p.x + " y " + p.y); 292 | } 293 | } 294 | 295 | counter++; 296 | pos = counter % 8; 297 | } 298 | 299 | Point isIsolated = new Point(-1, -1); 300 | return isIsolated; 301 | } 302 | 303 | private boolean isNewExternalContour(int x, int y, ByteProcessor proc) { 304 | return isBackground(x, y - 1, proc); 305 | } 306 | 307 | private boolean hasNoLabel(int x, int y) { 308 | int label = labledImage.get(x, y); 309 | return label == NOLABEL; 310 | } 311 | 312 | private boolean isMarked(int x, int y) { 313 | return labledImage.get(x, y) == -1; 314 | } 315 | 316 | private boolean isBackground(int x, int y, ByteProcessor proc) { 317 | return (proc.get(x, y) == BACKGROUND); 318 | } 319 | 320 | private boolean isNewInternalContour(int x, int y, ByteProcessor proc) { 321 | return isBackground(x, y + 1, proc) && !isMarked(x, y + 1); 322 | } 323 | 324 | private Blob getBlobByLabel(int label) { 325 | for (int i = 0; i < allBlobs.size(); i++) { 326 | if (allBlobs.get(i).getLabel() == label) { 327 | return allBlobs.get(i); 328 | } 329 | } 330 | return null; 331 | } 332 | 333 | private void addWhiteBorder(ImagePlus img) { 334 | offSetX=0; 335 | offsetY=0; 336 | boolean hasWhiteBorder = true; 337 | ImageProcessor oldip = img.getProcessor(); 338 | ByteProcessor oldproc = (ByteProcessor) oldip; 339 | byte[] pixels = (byte[]) oldproc.getPixels(); 340 | int w = oldproc.getWidth(); 341 | for (int i = 0; i < oldproc.getHeight(); i++) { 342 | 343 | int offset = i * w; 344 | //First and last Scanrow 345 | if (i == 0 || i == oldproc.getHeight()-1) { 346 | 347 | for (int j = 0; j < oldproc.getWidth(); j++) { 348 | int value = pixels[offset + j] & 255; 349 | if (value == OBJECT) { 350 | hasWhiteBorder = false; 351 | } 352 | } 353 | } 354 | // First and last Pixel per scan row 355 | int firstvalue = pixels[offset + 0] & 255; 356 | int lastvalue = pixels[offset + oldproc.getWidth() - 1] & 255; 357 | if (firstvalue == OBJECT || lastvalue == OBJECT) { 358 | hasWhiteBorder = false; 359 | } 360 | 361 | if (!hasWhiteBorder) { 362 | i = oldproc.getHeight(); // Stop searching 363 | } 364 | } 365 | //hasWhiteBorder=false; 366 | if (!hasWhiteBorder) 367 | { 368 | offSetX=-1; 369 | offsetY=-1; 370 | removeBorder=true; 371 | CanvasResizer resizer = new CanvasResizer(); 372 | Color oldbg = Toolbar.getBackgroundColor(); 373 | Prefs.set("resizer.zero", false); 374 | 375 | if(BACKGROUND==255){ 376 | Color bgcolor = (img.isInvertedLut()) ? Color.BLACK : Color.WHITE; 377 | Toolbar.setBackgroundColor(bgcolor); 378 | 379 | 380 | }else{ 381 | Color bgcolor = (img.isInvertedLut()) ? Color.WHITE : Color.BLACK; 382 | Toolbar.setBackgroundColor(bgcolor); 383 | } 384 | 385 | img.setProcessor(resizer.expandImage(img.getProcessor(), img.getWidth()+2, img.getHeight()+2, 1, 1)); 386 | Toolbar.setBackgroundColor(oldbg); 387 | } else 388 | { 389 | imp = img; 390 | } 391 | } 392 | 393 | public void removeBorder(ImagePlus img) { 394 | CanvasResizer resizer = new CanvasResizer(); 395 | /* 396 | if(BACKGROUND==255){ 397 | Color bgcolor = (img.isInvertedLut()) ? Color.BLACK : Color.WHITE; 398 | Toolbar.setBackgroundColor(bgcolor); 399 | }else{ 400 | Color bgcolor = (img.isInvertedLut()) ? Color.WHITE : Color.BLACK; 401 | Toolbar.setBackgroundColor(bgcolor); 402 | } 403 | */ 404 | img.setProcessor(resizer.expandImage(img.getProcessor(), img.getWidth()-2, img.getHeight()-2, -1, -1)); 405 | } 406 | 407 | 408 | } 409 | -------------------------------------------------------------------------------- /src/main/java/ij/blob/CustomBlobFeature.java: -------------------------------------------------------------------------------- 1 | package ij.blob; 2 | 3 | /** 4 | * Abstract class for define own features. 5 | * @author Thorsten Wagner 6 | */ 7 | public abstract class CustomBlobFeature { 8 | 9 | private Blob blob; 10 | 11 | void setup(Blob blob){ 12 | this.blob = blob; 13 | } 14 | 15 | /** 16 | * Getter method for the blob. 17 | * @return The reference to the blob 18 | */ 19 | public Blob getBlob(){ 20 | return blob; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/ij/blob/FractalBoxCounterBlob.java: -------------------------------------------------------------------------------- 1 | package ij.blob; 2 | import java.awt.*; 3 | import java.util.*; 4 | import ij.*; 5 | import ij.process.*; 6 | import ij.measure.*; 7 | import ij.util.*; 8 | /** 9 | * This ImageJ Class was adapted by Thorsten Wagner for IJBlob Project 10 | */ 11 | 12 | /** 13 | Calculate the so-called "capacity" fractal dimension. The algorithm 14 | is called, in fractal parlance, the "box counting" method. In the 15 | simplest terms, the routine counts the number of boxes of a given size 16 | needed to cover a one pixel wide, binary (black on white) border. 17 | The procedure is repeated for boxes that are 2 to 64 pixels wide. 18 | The output consists of two columns labeled "size" and "count". A plot 19 | is generated with the log of size on the x-axis and the log of count on 20 | the y-axis and the data is fitted with a straight line. The slope (S) 21 | of the line is the negative of the fractal dimension, i.e. D=-S. 22 | 23 | A full description of the technique can be found in T. G. Smith, 24 | Jr., G. D. Lange and W. B. Marks, Fractal Methods and Results in Cellular Morphology, 25 | which appeared in J. Neurosci. Methods, 69:1123-126, 1996. 26 | 27 | --- 28 | 12/Jun/2006 G. Landini added "set is white" option, otherwise the plugin 29 | assumes that the object is always low-dimensional (i.e. the phase with 30 | the smallest number of pixels). Now it works fine for sets with D near to 2.0 31 | 32 | */ 33 | class FractalBoxCounterBlob { 34 | static String sizes = "2,3,4,6,8,12,16,32,64"; 35 | static boolean blackBackground; 36 | int[] boxSizes; 37 | float[] boxCountSums; 38 | int maxBoxSize; 39 | int[] counts; 40 | Rectangle roi; 41 | int foreground; 42 | ImagePlus imp; 43 | 44 | public FractalBoxCounterBlob() { 45 | // TODO Auto-generated constructor stub 46 | boxSizes = s2ints(sizes); 47 | } 48 | /** 49 | * 50 | * @param blob The for which the fractal dimension have to determined 51 | * @return An 2 element array. [0] = Fractal Dimension, [1] = Goodness of Fit 52 | */ 53 | public double[] getFractcalDimension(Blob blob) { 54 | /* 55 | Rectangle r = blob.getOuterContour().getBounds(); 56 | r.setBounds(r.x, r.y, (int)r.getWidth()+1, (int)r.getHeight()+1); 57 | ImagePlus help = NewImage.createByteImage("", r.width, r.height, 1, NewImage.FILL_WHITE); 58 | ImageProcessor ip = help.getProcessor(); 59 | blob.draw(ip, Blob.DRAW_HOLES, -r.x, -r.y); 60 | */ 61 | ImagePlus blobImage = Blob.generateBlobImage(blob); 62 | ImageProcessor ip = blobImage.getProcessor(); 63 | imp = new ImagePlus("abc",ip); 64 | boxCountSums = new float[boxSizes.length]; 65 | for (int i=0; i=width) { 106 | IJ.error("No non-backround pixels found."); 107 | return false; 108 | } 109 | ip.setRoi(left, 0, 1, height); 110 | histogram = ip.getHistogram(); 111 | } while (histogram[foreground]==0); 112 | //Find top edge 113 | top = -1; 114 | do { 115 | top++; 116 | ip.setRoi(left, top, width-left, 1); 117 | histogram = ip.getHistogram(); 118 | } while (histogram[foreground]==0); 119 | 120 | //Find right edge 121 | right =width+1; 122 | do { 123 | right--; 124 | ip.setRoi(right-1, top, 1, height-top); 125 | histogram = ip.getHistogram(); 126 | } while (histogram[foreground]==0); 127 | 128 | //Find bottom edge 129 | bottom =height+1; 130 | do { 131 | bottom--; 132 | ip.setRoi(left, bottom-1, right-left, 1); 133 | histogram = ip.getHistogram(); 134 | } while (histogram[foreground]==0); 135 | 136 | roi = new Rectangle(left, top, right-left, bottom-top); 137 | return true; 138 | } 139 | 140 | int count(int size, ImageProcessor ip) { 141 | int[] histogram = new int[256]; 142 | int x = roi.x; 143 | int y = roi.y; 144 | int w = (size<=roi.width)?size:roi.width; 145 | int h = (size<=roi.height)?size:roi.height; 146 | int right = roi.x+roi.width; 147 | int bottom = roi.y+roi.height; 148 | int maxCount = size*size; 149 | 150 | for (int i=1; i<=maxCount; i++) 151 | counts[i] = 0; 152 | boolean done = false; 153 | do { 154 | ip.setRoi(x, y, w, h); 155 | histogram = ip.getHistogram(); 156 | counts[histogram[foreground]]++; 157 | x+=size; 158 | if (x+size>=right) { 159 | w = right-x; 160 | if (x>=right) { 161 | w = size; 162 | x = roi.x; 163 | y += size; 164 | if (y+size>=bottom) 165 | h = bottom-y; 166 | done = y>=bottom; 167 | } 168 | } 169 | } while (!done); 170 | int boxSum = 0; 171 | int nBoxes; 172 | for (int i=1; i<=maxCount; i++) { 173 | nBoxes = counts[i]; 174 | if (nBoxes!=0) 175 | boxSum += nBoxes; 176 | } 177 | return boxSum; 178 | } 179 | 180 | double[] getSlopeAndGoodnessOfFit() { 181 | int n = boxSizes.length; 182 | float[] sizes = new float[boxSizes.length]; 183 | for (int i=0; i. 18 | */ 19 | 20 | package ij.blob; 21 | import ij.IJ; 22 | import ij.ImagePlus; 23 | import ij.gui.NewImage; 24 | import ij.process.ColorProcessor; 25 | import ij.process.ImageStatistics; 26 | 27 | import java.awt.Color; 28 | import java.awt.Point; 29 | import java.lang.reflect.InvocationTargetException; 30 | import java.lang.reflect.Method; 31 | import java.util.ArrayList; 32 | 33 | /* 34 | * This library extracts connected components . For this purpose it uses the 35 | * following algorithm : F. Chang, A linear-time 36 | * component-labeling algorithm using contour tracing technique, Computer 37 | * Vision and Image Understanding, vol. 93, no. 2, pp. 206-220, 2004. 38 | */ 39 | 40 | /** 41 | * Represents the result-set of all detected blobs as ArrayList 42 | * @author Thorsten Wagner 43 | */ 44 | 45 | public class ManyBlobs extends ArrayList { 46 | 47 | /** 48 | * 49 | */ 50 | private static final long serialVersionUID = 1L; 51 | private ImagePlus binaryImage = null; 52 | private ImagePlus labeledImage = null; 53 | private int BACKGROUND = 255; 54 | private int OBJECT = 0; 55 | 56 | public ManyBlobs() { 57 | 58 | } 59 | 60 | /** 61 | * @param imp Binary Image 62 | */ 63 | public ManyBlobs(ImagePlus binaryImage) { 64 | setImage(binaryImage); 65 | } 66 | 67 | 68 | 69 | 70 | /** 71 | * Mutator to modify the background target. This method will switch 72 | * the background to the user's specification and also swap the OBJECT 73 | * value to be the opposite. e.g. If the users specifies the background 74 | * to be black, the objects (blobs) looked for will be white. 75 | 76 | * @param backgroundVal : 0 or 1 (black/white respectively) 77 | */ 78 | public void setBackground(int val){ 79 | if(val > 1) 80 | throw new IllegalArgumentException("Value must be 0 or 1 (black/white respectively)"); 81 | 82 | if(val == 0){ 83 | BACKGROUND = val; 84 | OBJECT = 255; 85 | } 86 | else { 87 | BACKGROUND = 255; 88 | OBJECT = 0; 89 | } 90 | } 91 | 92 | private void setImage(ImagePlus imp) { 93 | this.binaryImage = imp; 94 | ImageStatistics stats = imp.getStatistics(); 95 | 96 | boolean notBinary = (stats.histogram[0] + stats.histogram[255]) != stats.pixelCount; 97 | boolean toManyChannels = (imp.getNChannels()>1); 98 | boolean wrongBitDepth = (imp.getBitDepth()!=8); 99 | if (notBinary | toManyChannels | wrongBitDepth) { 100 | throw new java.lang.IllegalArgumentException("Wrong Image Format. IJ Blob only supports 8-bit, single-channel binary images"); 101 | } 102 | } 103 | 104 | /** 105 | * Start the Connected Component Algorithm 106 | * @see F. Chang, A linear-time component-labeling algorithm using contour tracing technique, Computer Vision and Image Understanding, vol. 93, no. 2, pp. 206-220, 2004. 107 | */ 108 | public void findConnectedComponents() { 109 | if(binaryImage==null){ 110 | throw new RuntimeException("Cannot run findConnectedComponents: No input image specified"); 111 | } 112 | ConnectedComponentLabeler labeler = new ConnectedComponentLabeler(this,binaryImage,BACKGROUND,OBJECT); 113 | labeler.doConnectedComponents(); 114 | labeledImage = labeler.getLabledImage(); 115 | } 116 | /** 117 | * 118 | * @return Return the labeled Image. 119 | */ 120 | public ImagePlus getLabeledImage() { 121 | if(labeledImage == null){ 122 | throw new RuntimeException("No input image was analysed for connected components"); 123 | } 124 | return labeledImage; 125 | } 126 | 127 | 128 | public void setLabeledImage(ImagePlus p) { 129 | labeledImage = p; 130 | } 131 | 132 | /** 133 | * Returns a specific {@link Blob} which encompasses a point 134 | * @param x x coordinate of the point 135 | * @param y y coordinate of the point 136 | * @return The blob which contains the point, otherwise null 137 | */ 138 | public Blob getSpecificBlob(int x, int y){ 139 | 140 | for(int i = 0; i < this.size(); i++){ 141 | if(this.get(i).getOuterContour().contains(x, y)){ 142 | return this.get(i); 143 | } 144 | } 145 | return null; 146 | } 147 | 148 | /** 149 | * Returns a specific blob which encompasses a point 150 | * @return The blob which contains the point, otherwise null 151 | */ 152 | public Blob getSpecificBlob(Point p){ 153 | return getSpecificBlob(p.x,p.y); 154 | } 155 | 156 | public Blob getBlobByLabel(int id){ 157 | for (Blob b : this) { 158 | if(b.getLabel()==id){ 159 | return b; 160 | } 161 | } 162 | return null; 163 | } 164 | 165 | /** 166 | * Filter all blobs which feature (specified by the methodName) is higher than 167 | * the lowerLimit or lower than the upper limit. 168 | * For instance: filterBlobs(Blob.GETENCLOSEDAREA,40,100) will filter all blobs between 40 and 100 pixel². 169 | * @param methodName Getter method of the blob feature (double as return value). 170 | * @param lowerLimit Lower limit for the feature to filter blobs. 171 | * @param upperLimit Upper limit for the feature to filter blobs. 172 | * @return The filtered blobs. 173 | */ 174 | public ManyBlobs filterBlobs(double lowerLimit, double upperLimit, String methodName, Object... methodparams){ 175 | ManyBlobs result = null; 176 | try { 177 | result = filterBlobs2(lowerLimit,upperLimit,methodName,methodparams); 178 | } catch (NoSuchMethodException e) { 179 | // TODO Auto-generated catch block 180 | IJ.error("The method " + methodName + "does not exist"); 181 | e.printStackTrace(); 182 | return null; 183 | } 184 | return result; 185 | } 186 | /** 187 | * Filter all blobs which feature (specified by the methodName) is higher than 188 | * the lowerLimit or lower than the upper limit. 189 | * For instance: filterBlobs(Blob.GETENCLOSEDAREA,40,100) will filter all blobs between 40 and 100 pixel². 190 | * @param methodName Getter method of the blob feature (double as return value). 191 | * @param lowerLimit Lower limit for the feature to filter blobs. 192 | * @param upperLimit Upper limit for the feature to filter blobs. 193 | * @return The filtered blobs. 194 | * @throws NoSuchMethodException 195 | */ 196 | private ManyBlobs filterBlobs2(double lowerLimit, double upperLimit, String methodName, Object... methodparams) throws NoSuchMethodException{ 197 | ManyBlobs blobs = new ManyBlobs(); 198 | blobs.setImage(binaryImage); 199 | @SuppressWarnings("rawtypes") 200 | Class classparams[] = {}; 201 | if(methodparams.length >0){ 202 | classparams = new Class[methodparams.length]; 203 | for(int i = 0; i< methodparams.length; i++){ 204 | classparams[i] = methodparams[i].getClass(); 205 | } 206 | } 207 | 208 | try { 209 | boolean methodInBuild = true; 210 | boolean methodIsCustom = false; 211 | Method m = null; 212 | try { 213 | m = Blob.class.getMethod(methodName, classparams); 214 | } 215 | catch (NoSuchMethodException e) { 216 | methodInBuild = false; 217 | 218 | } 219 | 220 | if(!methodInBuild){ 221 | for(int i = 0; i < Blob.customFeatures.size(); i++){ 222 | Method customMethods[] = Blob.customFeatures.get(i).getClass().getDeclaredMethods(); 223 | for(int j = 0; j < customMethods.length; j++){ 224 | if(customMethods[j].getName() == methodName){ 225 | 226 | methodIsCustom = true; 227 | m = customMethods[j]; 228 | break; 229 | } 230 | } 231 | if(methodIsCustom){break;} 232 | } 233 | } 234 | 235 | for(int i = 0; i < this.size(); i++) { 236 | if(this.get(i).getOuterContour().npoints< 4){ 237 | continue; 238 | } 239 | double value = 0; 240 | Object methodvalue = null; 241 | if(methodInBuild){ 242 | methodvalue = m.invoke(this.get(i), methodparams); 243 | } 244 | else if(methodIsCustom){ 245 | try{ 246 | methodvalue = this.get(i).evaluateCustomFeature(methodName, methodparams); 247 | } 248 | catch(NoSuchMethodException e){ 249 | throw new NoSuchMethodException("The method " + methodName + " was not found"); 250 | } 251 | 252 | } 253 | else{ 254 | 255 | throw new NoSuchMethodException("The method " + methodName + " was not found"); 256 | } 257 | 258 | if (methodvalue instanceof Integer){ 259 | int help = (Integer) methodvalue; 260 | value = (double)help; 261 | } 262 | else if (methodvalue instanceof Double){ 263 | value= (Double) methodvalue; 264 | } 265 | else { 266 | IJ.log("Return type not supported"); 267 | } 268 | 269 | boolean included= false; 270 | 271 | if (Double.isNaN(value)){ 272 | included = true; 273 | } 274 | else if (Double.isInfinite(upperLimit)) { 275 | included = (value >= lowerLimit) ? true : false; 276 | if(!included){ 277 | included = (Math.abs(lowerLimit-value)<0.0001) ? true:false; 278 | } 279 | } 280 | else 281 | { 282 | included = (value >= lowerLimit && value <= upperLimit) ? true : false; 283 | if(!included){ 284 | included = (!included && Math.abs(lowerLimit-value)<0.0001) ? true:false; 285 | } 286 | if(!included){ 287 | included = (!included && Math.abs(upperLimit-value)<0.0001) ? true:false; 288 | } 289 | } 290 | if(included){ 291 | blobs.add(this.get(i)); 292 | // IJ.log("ADD"); 293 | 294 | }else{ 295 | //IJ.log("NOT INC " + methodName + " v " + value); 296 | } 297 | } 298 | } catch (NoSuchMethodException e) { 299 | throw new NoSuchMethodException("The method " + methodName + " was not found"); 300 | 301 | } catch (SecurityException e) { 302 | IJ.log(e.getMessage()); 303 | 304 | e.printStackTrace(); 305 | } catch (IllegalAccessException e) { 306 | IJ.log(e.getMessage()); 307 | e.printStackTrace(); 308 | } catch (IllegalArgumentException e) { 309 | IJ.log(e.getMessage()); 310 | throw new IllegalArgumentException("Method " + methodName + " was called with wrong types of parameters"); 311 | } catch (InvocationTargetException e) { 312 | IJ.log(e.getMessage()); 313 | e.printStackTrace(); 314 | } 315 | blobs.setLabeledImage(generateLabeledImageFromBlobs(blobs)); 316 | // IJ.log("Blobs Nachher " + blobs.size()); 317 | return blobs; 318 | 319 | } 320 | 321 | 322 | 323 | private ImagePlus generateLabeledImageFromBlobs(ManyBlobs blobs){ 324 | 325 | ImagePlus labImg = NewImage.createRGBImage("Labeled Image", labeledImage.getWidth() , labeledImage.getHeight(), 1, NewImage.FILL_WHITE); 326 | ColorProcessor labledImageProc = (ColorProcessor)labImg.getProcessor(); 327 | for(int i = 0; i < blobs.size(); i++){ 328 | int helpcol = (int)(((double)i)/blobs.size() * (255*255*255)); 329 | blobs.get(i).drawLabels(labledImageProc,new Color(helpcol)); 330 | } 331 | 332 | return labImg; 333 | } 334 | 335 | /** 336 | * Filter all blobs which feature (specified by the methodName) is higher than 337 | * the lowerLimit and lower than the upper limit. 338 | * For instance: filterBlobs(Blob.GETENCLOSEDAREA,40,100) will filter all blobs between 40 and 100 pixel². 339 | * @param methodName Getter method of the blob feature (double as return value). 340 | * @param limits First Element is the lower limit, second element is the upper limit 341 | * @return The filtered blobs. 342 | * @throws NoSuchMethodException 343 | */ 344 | public ManyBlobs filterBlobs(double[] limits, String methodName, Object... methodparams){ 345 | ManyBlobs result = null; 346 | try { 347 | result = filterBlobs2(limits[0], limits[1], methodName,methodparams); 348 | } catch (NoSuchMethodException e) { 349 | IJ.error("The method " + methodName + "does not exist"); 350 | e.printStackTrace(); 351 | return null; 352 | } 353 | return result; 354 | } 355 | /** 356 | * Filter all blobs which feature (specified by the methodName) is higher than 357 | * the lower limit. 358 | * For instance: filterBlobs(Blob.GETENCLOSEDAREA,40) will filter all blobs with an area higher than 40 pixel² 359 | * @param methodName Getter method of the blob feature (double as return value). 360 | * @param lowerlimit Lower limit for the feature to filter blobs. 361 | * @return The filtered blobs. 362 | * @throws NoSuchMethodException 363 | * */ 364 | public ManyBlobs filterBlobs(double lowerlimit, String methodName, Object... methodparams) { 365 | ManyBlobs result = null; 366 | try { 367 | result = filterBlobs2(lowerlimit, Double.POSITIVE_INFINITY, methodName, methodparams); 368 | } catch (NoSuchMethodException e) { 369 | IJ.error("The method " + methodName + "does not exist"); 370 | e.printStackTrace(); 371 | return null; 372 | } 373 | return result; 374 | } 375 | 376 | 377 | } 378 | -------------------------------------------------------------------------------- /src/main/java/ij/blob/ManyBlobs.java~: -------------------------------------------------------------------------------- 1 | /* 2 | IJBlob is a ImageJ library for extracting connected components in binary Images 3 | Copyright (C) 2012 Thorsten Wagner wagner@biomedical-imaging.de 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | package ij.blob; 20 | import ij.ImagePlus; 21 | import ij.process.ImageStatistics; 22 | import java.util.ArrayList; 23 | 24 | /* 25 | * This library extracts connected components . For this purpose it uses the 26 | * following algorithm : F. Chang, A linear-time 27 | * component-labeling algorithm using contour tracing technique, Computer 28 | * Vision and Image Understanding, vol. 93, no. 2, pp. 206-220, 2004. 29 | */ 30 | 31 | public class ManyBlobs extends ArrayList { 32 | 33 | /** 34 | * 35 | */ 36 | private static final long serialVersionUID = 1L; 37 | private ImagePlus imp; 38 | private int BACKGROUND = 255; 39 | private int OBJECT = 0; 40 | 41 | 42 | /** 43 | * @param imp Binary Image 44 | */ 45 | public ManyBlobs(ImagePlus imp) { 46 | setImage(imp); 47 | } 48 | 49 | private void setImage(ImagePlus imp) { 50 | this.imp = imp; 51 | ImageStatistics stats = imp.getStatistics(); 52 | 53 | if ((stats.histogram[0] + stats.histogram[255]) != stats.pixelCount) { 54 | throw new java.lang.IllegalArgumentException("Not a binary image"); 55 | } 56 | 57 | if(imp.isInvertedLut()){ 58 | BACKGROUND = 0; 59 | OBJECT = 255; 60 | } 61 | } 62 | 63 | /** 64 | * Start the Connected Component Algorithm 65 | * @see ���F. Chang, ���A linear-time component-labeling algorithm using contour tracing technique,��� Computer Vision and Image Understanding, vol. 93, no. 2, pp. 206-220, 2004. 66 | */ 67 | public void findConnectedComponents() { 68 | ConnectedComponentLabeler labeler = new ConnectedComponentLabeler(this,imp,BACKGROUND,OBJECT); 69 | labeler.doConnectedComponents(); 70 | 71 | } 72 | 73 | /** 74 | * 75 | * @param filter 76 | */ 77 | public void removeBlobs(BlobFilter filter){ 78 | for(int i = 0; i < this.size(); i++) { 79 | if(filter.isIncluded(this.get(i))==false){ 80 | this.remove(i); 81 | } 82 | } 83 | } 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/ij/blob/RotatingCalipers.java: -------------------------------------------------------------------------------- 1 | package ij.blob; 2 | 3 | /* 4 | * Copyright (c) 2010, Bart Kiers 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, 10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the 12 | * Software is furnished to do so, subject to the following 13 | * conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | * OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | 28 | import java.awt.Point; 29 | import java.awt.geom.Point2D; 30 | import java.util.*; 31 | 32 | public final class RotatingCalipers { 33 | 34 | protected enum Corner { UPPER_RIGHT, UPPER_LEFT, LOWER_LEFT, LOWER_RIGHT } 35 | 36 | public static double getArea(Point2D.Double[] rectangle) { 37 | 38 | double deltaXAB = rectangle[0].x - rectangle[1].x; 39 | double deltaYAB = rectangle[0].y - rectangle[1].y; 40 | 41 | double deltaXBC = rectangle[1].x - rectangle[2].x; 42 | double deltaYBC = rectangle[1].y - rectangle[2].y; 43 | 44 | double lengthAB = Math.sqrt((deltaXAB * deltaXAB) + (deltaYAB * deltaYAB)); 45 | double lengthBC = Math.sqrt((deltaXBC * deltaXBC) + (deltaYBC * deltaYBC)); 46 | 47 | return lengthAB * lengthBC; 48 | } 49 | 50 | public static List getAllBoundingRectangles(int[] xs, int[] ys) throws IllegalArgumentException { 51 | 52 | if(xs.length != ys.length) { 53 | throw new IllegalArgumentException("xs and ys don't have the same size"); 54 | } 55 | 56 | List points = new ArrayList(); 57 | 58 | for(int i = 0; i < xs.length; i++) { 59 | points.add(new Point(xs[i], ys[i])); 60 | } 61 | 62 | return getAllBoundingRectangles(points); 63 | } 64 | 65 | public static List getAllBoundingRectangles(List points) throws IllegalArgumentException { 66 | 67 | List rectangles = new ArrayList(); 68 | 69 | List convexHull = GrahamScan.getConvexHull(points); 70 | 71 | Caliper I = new Caliper(convexHull, getIndex(convexHull, Corner.UPPER_RIGHT), 90); 72 | Caliper J = new Caliper(convexHull, getIndex(convexHull, Corner.UPPER_LEFT), 180); 73 | Caliper K = new Caliper(convexHull, getIndex(convexHull, Corner.LOWER_LEFT), 270); 74 | Caliper L = new Caliper(convexHull, getIndex(convexHull, Corner.LOWER_RIGHT), 0); 75 | 76 | while(L.currentAngle < 90.0) { 77 | 78 | rectangles.add(new Point2D.Double[]{ 79 | L.getIntersection(I), 80 | I.getIntersection(J), 81 | J.getIntersection(K), 82 | K.getIntersection(L) 83 | }); 84 | 85 | double smallestTheta = getSmallestTheta(I, J, K, L); 86 | 87 | I.rotateBy(smallestTheta); 88 | J.rotateBy(smallestTheta); 89 | K.rotateBy(smallestTheta); 90 | L.rotateBy(smallestTheta); 91 | } 92 | 93 | return rectangles; 94 | } 95 | 96 | public static Point2D.Double[] getMinimumBoundingRectangle(int[] xs, int[] ys) throws IllegalArgumentException { 97 | 98 | if(xs.length != ys.length) { 99 | throw new IllegalArgumentException("xs and ys don't have the same size"); 100 | } 101 | 102 | List points = new ArrayList(); 103 | 104 | for(int i = 0; i < xs.length; i++) { 105 | points.add(new Point(xs[i], ys[i])); 106 | } 107 | 108 | return getMinimumBoundingRectangle(points); 109 | } 110 | 111 | public static Point2D.Double[] getMinimumBoundingRectangle(List points) throws IllegalArgumentException { 112 | 113 | List rectangles = getAllBoundingRectangles(points); 114 | 115 | Point2D.Double[] minimum = null; 116 | double area = Long.MAX_VALUE; 117 | 118 | for (Point2D.Double[] rectangle : rectangles) { 119 | 120 | double tempArea = getArea(rectangle); 121 | 122 | if (minimum == null || tempArea < area) { 123 | minimum = rectangle; 124 | area = tempArea; 125 | } 126 | } 127 | 128 | return minimum; 129 | } 130 | 131 | private static double getSmallestTheta(Caliper I, Caliper J, Caliper K, Caliper L) { 132 | 133 | double thetaI = I.getDeltaAngleNextPoint(); 134 | double thetaJ = J.getDeltaAngleNextPoint(); 135 | double thetaK = K.getDeltaAngleNextPoint(); 136 | double thetaL = L.getDeltaAngleNextPoint(); 137 | 138 | if(thetaI <= thetaJ && thetaI <= thetaK && thetaI <= thetaL) { 139 | return thetaI; 140 | } 141 | else if(thetaJ <= thetaK && thetaJ <= thetaL) { 142 | return thetaJ; 143 | } 144 | else if(thetaK <= thetaL) { 145 | return thetaK; 146 | } 147 | else { 148 | return thetaL; 149 | } 150 | } 151 | 152 | protected static int getIndex(List convexHull, Corner corner) { 153 | 154 | int index = 0; 155 | Point point = convexHull.get(index); 156 | 157 | for(int i = 1; i < convexHull.size() - 1; i++) { 158 | 159 | Point temp = convexHull.get(i); 160 | boolean change = false; 161 | 162 | switch(corner) { 163 | case UPPER_RIGHT: 164 | change = (temp.x > point.x || (temp.x == point.x && temp.y > point.y)); 165 | break; 166 | case UPPER_LEFT: 167 | change = (temp.y > point.y || (temp.y == point.y && temp.x < point.x)); 168 | break; 169 | case LOWER_LEFT: 170 | change = (temp.x < point.x || (temp.x == point.x && temp.y < point.y)); 171 | break; 172 | case LOWER_RIGHT: 173 | change = (temp.y < point.y || (temp.y == point.y && temp.x > point.x)); 174 | break; 175 | } 176 | 177 | if(change) { 178 | index = i; 179 | point = temp; 180 | } 181 | } 182 | 183 | return index; 184 | } 185 | 186 | protected static class Caliper { 187 | 188 | final static double SIGMA = 0.00000000001; 189 | 190 | final List convexHull; 191 | int pointIndex; 192 | double currentAngle; 193 | 194 | Caliper(List convexHull, int pointIndex, double currentAngle) { 195 | this.convexHull = convexHull; 196 | this.pointIndex = pointIndex; 197 | this.currentAngle = currentAngle; 198 | } 199 | 200 | double getAngleNextPoint() { 201 | 202 | Point p1 = convexHull.get(pointIndex); 203 | Point p2 = convexHull.get((pointIndex + 1) % convexHull.size()); 204 | 205 | double deltaX = p2.x - p1.x; 206 | double deltaY = p2.y - p1.y; 207 | 208 | double angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI; 209 | 210 | return angle < 0 ? 360 + angle : angle; 211 | } 212 | 213 | double getConstant() { 214 | 215 | Point p = convexHull.get(pointIndex); 216 | 217 | return p.y - (getSlope() * p.x); 218 | } 219 | 220 | double getDeltaAngleNextPoint() { 221 | 222 | double angle = getAngleNextPoint(); 223 | 224 | angle = angle < 0 ? 360 + angle - currentAngle : angle - currentAngle; 225 | 226 | return angle < 0 ? 360 : angle; 227 | } 228 | 229 | Point2D.Double getIntersection(Caliper that) { 230 | 231 | // the x-intercept of 'this' and 'that': x = ((c2 - c1) / (m1 - m2)) 232 | double x; 233 | // the y-intercept of 'this' and 'that', given 'x': (m*x) + c 234 | double y; 235 | 236 | if(this.isVertical()) { 237 | x = convexHull.get(pointIndex).x; 238 | } 239 | else if(this.isHorizontal()) { 240 | x = that.convexHull.get(that.pointIndex).x; 241 | } 242 | else { 243 | x = (that.getConstant() - this.getConstant()) / (this.getSlope() - that.getSlope()); 244 | } 245 | 246 | if(this.isVertical()) { 247 | y = that.getConstant(); 248 | } 249 | else if(this.isHorizontal()) { 250 | y = this.getConstant(); 251 | } 252 | else { 253 | y = (this.getSlope() * x) + this.getConstant(); 254 | } 255 | 256 | return new Point2D.Double(x, y); 257 | } 258 | 259 | double getSlope() { 260 | return Math.tan(Math.toRadians(currentAngle)); 261 | } 262 | 263 | boolean isHorizontal() { 264 | return (Math.abs(currentAngle) < SIGMA) || (Math.abs(currentAngle - 180.0) < SIGMA); 265 | } 266 | 267 | boolean isVertical() { 268 | return (Math.abs(currentAngle - 90.0) < SIGMA) || (Math.abs(currentAngle - 270.0) < SIGMA); 269 | } 270 | 271 | void rotateBy(double angle) { 272 | 273 | if(this.getDeltaAngleNextPoint() == angle) { 274 | pointIndex++; 275 | } 276 | 277 | this.currentAngle += angle; 278 | } 279 | } 280 | 281 | /** 282 | * For a documented (and unit tested version) of the class below, see: 283 | * github.com/bkiers/GrahamScan 284 | */ 285 | private static class GrahamScan { 286 | 287 | protected static enum Turn { CLOCKWISE, COUNTER_CLOCKWISE, COLLINEAR } 288 | 289 | protected static boolean areAllCollinear(List points) { 290 | 291 | if(points.size() < 2) { 292 | return true; 293 | } 294 | 295 | final Point a = points.get(0); 296 | final Point b = points.get(1); 297 | 298 | for(int i = 2; i < points.size(); i++) { 299 | 300 | Point c = points.get(i); 301 | 302 | if(getTurn(a, b, c) != Turn.COLLINEAR) { 303 | return false; 304 | } 305 | } 306 | 307 | return true; 308 | } 309 | 310 | public static List getConvexHull(List points) throws IllegalArgumentException { 311 | 312 | List sorted = new ArrayList(getSortedPointSet(points)); 313 | 314 | if(sorted.size() < 3) { 315 | throw new IllegalArgumentException("can only create a convex hull of 3 or more unique points"); 316 | } 317 | 318 | if(areAllCollinear(sorted)) { 319 | throw new IllegalArgumentException("cannot create a convex hull from collinear points"); 320 | } 321 | 322 | Stack stack = new Stack(); 323 | stack.push(sorted.get(0)); 324 | stack.push(sorted.get(1)); 325 | 326 | for (int i = 2; i < sorted.size(); i++) { 327 | 328 | Point head = sorted.get(i); 329 | Point middle = stack.pop(); 330 | Point tail = stack.peek(); 331 | 332 | Turn turn = getTurn(tail, middle, head); 333 | 334 | switch(turn) { 335 | case COUNTER_CLOCKWISE: 336 | stack.push(middle); 337 | stack.push(head); 338 | break; 339 | case CLOCKWISE: 340 | i--; 341 | break; 342 | case COLLINEAR: 343 | stack.push(head); 344 | break; 345 | } 346 | } 347 | 348 | stack.push(sorted.get(0)); 349 | 350 | return new ArrayList(stack); 351 | } 352 | 353 | protected static Point getLowestPoint(List points) { 354 | 355 | Point lowest = points.get(0); 356 | 357 | for(int i = 1; i < points.size(); i++) { 358 | 359 | Point temp = points.get(i); 360 | 361 | if(temp.y < lowest.y || (temp.y == lowest.y && temp.x < lowest.x)) { 362 | lowest = temp; 363 | } 364 | } 365 | 366 | return lowest; 367 | } 368 | 369 | protected static Set getSortedPointSet(List points) { 370 | 371 | final Point lowest = getLowestPoint(points); 372 | 373 | TreeSet set = new TreeSet(new Comparator() { 374 | @Override 375 | public int compare(Point a, Point b) { 376 | 377 | if(a == b || a.equals(b)) { 378 | return 0; 379 | } 380 | 381 | double thetaA = Math.atan2((long)a.y - lowest.y, (long)a.x - lowest.x); 382 | double thetaB = Math.atan2((long)b.y - lowest.y, (long)b.x - lowest.x); 383 | 384 | if(thetaA < thetaB) { 385 | return -1; 386 | } 387 | else if(thetaA > thetaB) { 388 | return 1; 389 | } 390 | else { 391 | double distanceA = Math.sqrt((((long)lowest.x - a.x) * ((long)lowest.x - a.x)) + 392 | (((long)lowest.y - a.y) * ((long)lowest.y - a.y))); 393 | double distanceB = Math.sqrt((((long)lowest.x - b.x) * ((long)lowest.x - b.x)) + 394 | (((long)lowest.y - b.y) * ((long)lowest.y - b.y))); 395 | 396 | if(distanceA < distanceB) { 397 | return -1; 398 | } 399 | else { 400 | return 1; 401 | } 402 | } 403 | } 404 | }); 405 | 406 | set.addAll(points); 407 | 408 | return set; 409 | } 410 | 411 | protected static Turn getTurn(Point a, Point b, Point c) { 412 | 413 | double crossProduct = (((long)b.x - a.x) * ((long)c.y - a.y)) - 414 | (((long)b.y - a.y) * ((long)c.x - a.x)); 415 | 416 | if(crossProduct > 0) { 417 | return Turn.COUNTER_CLOCKWISE; 418 | } 419 | else if(crossProduct < 0) { 420 | return Turn.CLOCKWISE; 421 | } 422 | else { 423 | return Turn.COLLINEAR; 424 | } 425 | } 426 | } 427 | } -------------------------------------------------------------------------------- /src/main/resources/3blobs.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/3blobs.tif -------------------------------------------------------------------------------- /src/main/resources/3blobsInv.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/3blobsInv.tif -------------------------------------------------------------------------------- /src/main/resources/FiveBlobsOnEdge.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/FiveBlobsOnEdge.tif -------------------------------------------------------------------------------- /src/main/resources/circle_r30.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/circle_r30.tif -------------------------------------------------------------------------------- /src/main/resources/complexImage.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/complexImage.tif -------------------------------------------------------------------------------- /src/main/resources/correctcontour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/correctcontour.png -------------------------------------------------------------------------------- /src/main/resources/nestedObjects.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/nestedObjects.tif -------------------------------------------------------------------------------- /src/main/resources/rotatedsquare.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/rotatedsquare.tif -------------------------------------------------------------------------------- /src/main/resources/rotatedsquare2.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/rotatedsquare2.tif -------------------------------------------------------------------------------- /src/main/resources/square100x100_minus30x30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/square100x100_minus30x30.png -------------------------------------------------------------------------------- /src/main/resources/squareOnBoarder_right.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/squareOnBoarder_right.tif -------------------------------------------------------------------------------- /src/main/resources/squaresOnBoarder.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/squaresOnBoarder.tif -------------------------------------------------------------------------------- /src/main/resources/squaresOnBoarderInv.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/squaresOnBoarderInv.tif -------------------------------------------------------------------------------- /src/main/resources/squares_20x20_30x30.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorstenwagner/ij-blob/d3e2af4ac64147e4e2b43e60e14f6c7200ad8296/src/main/resources/squares_20x20_30x30.tif -------------------------------------------------------------------------------- /src/test/java/ij/blob/tests/ExampleBlobFeature.java: -------------------------------------------------------------------------------- 1 | package ij.blob.tests; 2 | 3 | import ij.blob.*; 4 | 5 | public class ExampleBlobFeature extends CustomBlobFeature { 6 | 7 | public double myFancyFeature(Integer a, Float b){ 8 | double feature = b*getBlob().getEnclosedArea()*a; 9 | return feature; 10 | } 11 | 12 | public int mySecondFancyFeature(Integer a, Double b){ 13 | int feature = (int)(b*getBlob().getAreaToPerimeterRatio() *a); 14 | return feature; 15 | } 16 | 17 | public int myThirdFancyFeature(){ 18 | return 5; 19 | } 20 | 21 | public int myFourthFancyFeature(Integer a, Double b){ 22 | return 5; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/ij/blob/tests/FeatureTest.java: -------------------------------------------------------------------------------- 1 | package ij.blob.tests; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.awt.Polygon; 6 | import java.net.URL; 7 | 8 | import ij.IJ; 9 | import ij.ImagePlus; 10 | import ij.blob.Blob; 11 | import ij.blob.ManyBlobs; 12 | 13 | import org.junit.Test; 14 | 15 | public class FeatureTest { 16 | 17 | @Test 18 | public void testConvexHullFeature(){ 19 | URL url = this.getClass().getClassLoader().getResource("square100x100_minus30x30.png"); 20 | ImagePlus ip = new ImagePlus(url.getPath()); 21 | ManyBlobs mb = new ManyBlobs(ip); 22 | mb.findConnectedComponents(); 23 | assertEquals(4, mb.get(0).getConvexHull().npoints-1); 24 | } 25 | 26 | @Test 27 | public void testSpecialBlobFeature() throws NoSuchMethodException { 28 | URL url = this.getClass().getClassLoader().getResource("3blobs.tif"); 29 | ImagePlus ip = new ImagePlus(url.getPath()); 30 | ManyBlobs mb = new ManyBlobs(ip); 31 | mb.findConnectedComponents(); 32 | MyBlobFeature test = new MyBlobFeature(); 33 | Blob.addCustomFeature(test); 34 | ManyBlobs filtered = mb.filterBlobs(0,10, "LocationFeature",ip.getWidth(),ip.getHeight()); 35 | assertEquals(0, filtered.size()); //All blobs have a greater distance. So it should be 0 36 | 37 | filtered = mb.filterBlobs(0,600, "LocationFeature",ip.getWidth(),ip.getHeight()); 38 | assertEquals(3, filtered.size()); //All blobs have a distance inside the threshold. No blob should be filtered. 39 | } 40 | 41 | @Test 42 | public void testCustomBlobFeature() throws NoSuchMethodException { 43 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 44 | ImagePlus ip = new ImagePlus(url.getPath()); 45 | ManyBlobs mb = new ManyBlobs(ip); 46 | mb.findConnectedComponents(); 47 | ExampleBlobFeature test = new ExampleBlobFeature(); 48 | Blob.addCustomFeature(test); 49 | int a = 10; 50 | float c = 1.5f; 51 | double featurevalue = (Double)mb.get(0).evaluateCustomFeature("myFancyFeature",a,c); 52 | double diff = mb.get(0).getEnclosedArea()-featurevalue; 53 | assertEquals(-(c*a-1)*mb.get(0).getEnclosedArea(), diff,0); 54 | 55 | } 56 | 57 | @Test 58 | public void testCustomBlobFeature2() throws NoSuchMethodException { 59 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 60 | ImagePlus ip = new ImagePlus(url.getPath()); 61 | ManyBlobs mb = new ManyBlobs(ip); 62 | mb.findConnectedComponents(); 63 | ExampleBlobFeature test = new ExampleBlobFeature(); 64 | Blob.addCustomFeature(test); 65 | int a = 10; 66 | double c = 1.5; 67 | int featurevalue = (Integer)mb.get(0).evaluateCustomFeature("mySecondFancyFeature",a,c); 68 | double diff = mb.get(0).getAreaToPerimeterRatio()-featurevalue; 69 | assertEquals(-(c*a-1)*mb.get(0).getAreaToPerimeterRatio(), diff,0.5); 70 | } 71 | 72 | @Test 73 | public void testGetAreaEquivalentSphericalDiameter() { 74 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 75 | ImagePlus ip = new ImagePlus(url.getPath()); 76 | ManyBlobs mb = new ManyBlobs(ip); 77 | mb.findConnectedComponents(); 78 | double diameter = mb.get(0).getAreaEquivalentSphericalDiameter(); 79 | assertEquals(2*30, diameter,0.5); 80 | } 81 | 82 | @Test 83 | public void testFilterCustomBlobFeature() throws NoSuchMethodException { 84 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 85 | ImagePlus ip = new ImagePlus(url.getPath()); 86 | ManyBlobs mb = new ManyBlobs(ip); 87 | mb.findConnectedComponents(); 88 | ExampleBlobFeature test = new ExampleBlobFeature(); 89 | Blob.addCustomFeature(test); 90 | int a = 10; 91 | double b = 20; 92 | ManyBlobs filtered = mb.filterBlobs(6, "myFourthFancyFeature",a,b); 93 | assertEquals(filtered.size(), 0,0); 94 | } 95 | 96 | @Test 97 | public void testFilterCustomBlobFeatureWrongArgument() throws NoSuchMethodException { 98 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 99 | ImagePlus ip = new ImagePlus(url.getPath()); 100 | ManyBlobs mb = new ManyBlobs(ip); 101 | mb.findConnectedComponents(); 102 | ExampleBlobFeature test = new ExampleBlobFeature(); 103 | Blob.addCustomFeature(test); 104 | int a = 10; 105 | int b = 20; 106 | ManyBlobs result = mb.filterBlobs(6, "myFourthFancyFeature",a,b); 107 | assertEquals(null, result); 108 | } 109 | 110 | 111 | @Test 112 | public void testFilterCustomBlobFeature2() throws NoSuchMethodException { 113 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 114 | ImagePlus ip = new ImagePlus(url.getPath()); 115 | ManyBlobs mb = new ManyBlobs(ip); 116 | mb.findConnectedComponents(); 117 | mb.setBackground(1); 118 | ExampleBlobFeature test = new ExampleBlobFeature(); 119 | Blob.addCustomFeature(test); 120 | 121 | ManyBlobs filtered = mb.filterBlobs(4, "myThirdFancyFeature"); 122 | assertEquals(filtered.size(), 1,0); 123 | } 124 | 125 | 126 | @Test 127 | public void testGetCenterOfGravity() { 128 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 129 | ImagePlus ip = new ImagePlus(url.getPath()); 130 | ManyBlobs mb = new ManyBlobs(ip); 131 | mb.findConnectedComponents(); 132 | int centerx = (int)mb.get(0).getCenterOfGravity().getX(); 133 | int centery = (int)mb.get(0).getCenterOfGravity().getY(); 134 | double diff = Math.abs(48-centerx)+Math.abs(48-centery); 135 | assertEquals(0, diff,0); 136 | } 137 | 138 | @Test 139 | public void testGetDiameterMaximumInscribedCircle() { 140 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 141 | ImagePlus ip = new ImagePlus(url.getPath()); 142 | ManyBlobs mb = new ManyBlobs(ip); 143 | mb.findConnectedComponents(); 144 | double max = mb.get(0).getDiamaterMaximumInscribedCircle(); 145 | assertEquals(60, max,2); 146 | } 147 | 148 | /* 149 | @Test 150 | public void testGetElongation() { 151 | 152 | fail("Not yet implemented"); 153 | } 154 | */ 155 | @Test 156 | public void testGetMinimumBoundingRectangle() { 157 | URL url = this.getClass().getClassLoader().getResource("rotatedsquare2.tif"); 158 | ImagePlus ip = new ImagePlus(url.getPath()); 159 | ManyBlobs mb = new ManyBlobs(ip); 160 | mb.findConnectedComponents(); 161 | mb.get(0).getMinimumBoundingRectangle(); 162 | IJ.log("LS " + mb.get(0).getLongSideMBR()); 163 | IJ.log("SS " + mb.get(0).getShortSideMBR()); 164 | } 165 | 166 | 167 | @Test 168 | public void testGetPerimeterCircleRad30() { 169 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 170 | ImagePlus ip = new ImagePlus(url.getPath()); 171 | int peri = (int)(2*Math.PI*30); 172 | ManyBlobs mb = new ManyBlobs(ip); 173 | mb.findConnectedComponents(); 174 | assertEquals(peri, mb.get(0).getPerimeter(),peri*0.02); 175 | } 176 | 177 | @Test 178 | public void testGetPerimeterConvexHull2() { 179 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 180 | ImagePlus ip = new ImagePlus(url.getPath()); 181 | int periConv = (int)(2*Math.PI*30); 182 | ManyBlobs mb = new ManyBlobs(ip); 183 | mb.findConnectedComponents(); 184 | assertEquals(periConv, mb.get(0).getPerimeterConvexHull(),periConv*0.02); 185 | } 186 | 187 | 188 | @Test 189 | public void testGetPerimeterConvexHull() { 190 | URL url = this.getClass().getClassLoader().getResource("square100x100_minus30x30.png"); 191 | ImagePlus ip = new ImagePlus(url.getPath()); 192 | int periConv = 4*100-4; //400-4(-4 Because the Edges doesnt mutiple counted 193 | ManyBlobs mb = new ManyBlobs(ip); 194 | mb.findConnectedComponents(); 195 | assertEquals(periConv, mb.get(0).getPerimeterConvexHull(),2); 196 | } 197 | 198 | @Test 199 | public void testEnclosedAreaCircleRad30() { 200 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 201 | ImagePlus ip = new ImagePlus(url.getPath()); 202 | int area = (int)(Math.PI*30*30); 203 | ManyBlobs mb = new ManyBlobs(ip); 204 | mb.findConnectedComponents(); 205 | assertEquals(area, mb.get(0).getEnclosedArea(),3); 206 | 207 | } 208 | 209 | @Test 210 | public void testFilterEnclosedAreaSqaures() throws NoSuchMethodException { 211 | URL url = this.getClass().getClassLoader().getResource("squares_20x20_30x30.tif"); 212 | ImagePlus ip = new ImagePlus(url.getPath()); 213 | int areaSmallSquare = (20*20); 214 | int areaBigSquare = (30*30); 215 | ManyBlobs mb = new ManyBlobs(ip); 216 | mb.findConnectedComponents(); 217 | ManyBlobs filter = mb.filterBlobs(areaSmallSquare+1, Blob.GETENCLOSEDAREA); 218 | assertEquals(4, filter.size(),0); 219 | filter = mb.filterBlobs(areaBigSquare+1, Blob.GETENCLOSEDAREA); 220 | assertEquals(0, filter.size(),0); 221 | } 222 | 223 | @Test 224 | public void testGetCircularity() { 225 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 226 | ImagePlus ip = new ImagePlus(url.getPath()); 227 | ManyBlobs mb = new ManyBlobs(ip); 228 | mb.findConnectedComponents(); 229 | 230 | double expectedCircularity = 4*Math.PI; 231 | assertEquals(expectedCircularity, mb.get(0).getCircularity(),0.5); 232 | 233 | } 234 | 235 | @Test 236 | public void testGetThinnesRatio() { 237 | URL url = this.getClass().getClassLoader().getResource("circle_r30.tif"); 238 | ImagePlus ip = new ImagePlus(url.getPath()); 239 | int circ = 1; 240 | ManyBlobs mb = new ManyBlobs(ip); 241 | mb.findConnectedComponents(); 242 | assertEquals(circ, mb.get(0).getThinnesRatio(),0.01); 243 | } 244 | 245 | @Test 246 | public void testFind4holes() { 247 | URL url = this.getClass().getClassLoader().getResource("nestedObjects.tif"); 248 | ImagePlus ip = new ImagePlus(url.getPath()); 249 | int holes = 4; 250 | ManyBlobs mb = new ManyBlobs(ip); 251 | mb.findConnectedComponents(); 252 | assertEquals(holes, mb.get(0).getInnerContours().size(),0); 253 | } 254 | 255 | @Test 256 | public void testFindThreeBlobs() { 257 | URL url = this.getClass().getClassLoader().getResource("3blobs.tif"); 258 | ImagePlus ip = new ImagePlus(url.getPath()); 259 | int count = 3; 260 | ManyBlobs mb = new ManyBlobs(ip); 261 | mb.findConnectedComponents(); 262 | assertEquals(count, mb.size(),0); 263 | } 264 | @Test 265 | public void testNestedFindFiveBlobs() { 266 | URL url = this.getClass().getClassLoader().getResource("nestedObjects.tif"); 267 | ImagePlus ip = new ImagePlus(url.getPath()); 268 | int blobs = 5; 269 | ManyBlobs mb = new ManyBlobs(ip); 270 | mb.findConnectedComponents(); 271 | assertEquals(blobs, mb.size(),0); 272 | } 273 | 274 | @Test 275 | public void testGetOuterContourIsCorrect() { 276 | //Pfad des Beispielbildes 277 | URL url = this.getClass().getClassLoader().getResource("correctcontour.png"); 278 | //Lade das Beispielbild 279 | ImagePlus ip = new ImagePlus(url.getPath()); 280 | 281 | //Analysiere die Blobs 282 | ManyBlobs mb = new ManyBlobs(ip); 283 | // mb.setBackground(0); 284 | mb.findConnectedComponents(); 285 | 286 | //Die Kontur die ermittelt werden sollte 287 | int[] xp = {3,4,5,6,7,8,9,10,11,11,11,10,9,8,7,6,5,4,3,2,2,2,3}; 288 | int[] yp = {1,1,2,2,2,1,1,2,2,3,4,4,5,5,4,4,4,5,5,4,3,2,1}; 289 | 290 | //Die Kontur die ermittelt wurde 291 | Polygon contour = mb.get(0).getOuterContour(); 292 | 293 | //Differenz der beiden Konturen 294 | int diff=0; 295 | for(int i = 0; i < contour.npoints; i++){ 296 | diff += Math.abs(contour.xpoints[i] - xp[i]) + Math.abs(contour.ypoints[i] - yp[i]); 297 | } 298 | 299 | //Überprüfe ob die Differenz 0 ergibt. Dann sind beide Konturen gleich. 300 | assertEquals(0,diff,0); 301 | } 302 | @Test 303 | public void testFind5ObjectsOnEdge(){ 304 | 305 | URL url = this.getClass().getClassLoader().getResource("FiveBlobsOnEdge.tif"); 306 | ImagePlus ip = new ImagePlus(url.getPath()); 307 | //Analysiere die Blobs 308 | ManyBlobs mb = new ManyBlobs(ip); 309 | // mb.setBackground(0); 310 | mb.findConnectedComponents(); 311 | 312 | int objectOnEdgeCounter=0; 313 | for (Blob blob : mb) { 314 | if(blob.isOnEdge(ip.getProcessor())){ 315 | objectOnEdgeCounter++; 316 | } 317 | } 318 | assertEquals(5,objectOnEdgeCounter,0); 319 | } 320 | 321 | } 322 | -------------------------------------------------------------------------------- /src/test/java/ij/blob/tests/ManyBlobsTest.java: -------------------------------------------------------------------------------- 1 | package ij.blob.tests; 2 | import static org.junit.Assert.assertEquals; 3 | 4 | import java.net.URL; 5 | 6 | import ij.ImagePlus; 7 | import ij.blob.Blob; 8 | import ij.blob.ManyBlobs; 9 | 10 | import org.junit.Test; 11 | public class ManyBlobsTest { 12 | @Test 13 | public void testFilterBlobs () throws NoSuchMethodException { 14 | URL url = this.getClass().getClassLoader().getResource("3blobs.tif"); 15 | ImagePlus ip = new ImagePlus(url.getPath()); 16 | ManyBlobs mb = new ManyBlobs(ip); 17 | mb.findConnectedComponents(); 18 | ManyBlobs t = mb.filterBlobs(0.9, 1, Blob.GETTHINNESRATIO); 19 | assertEquals(1, t.size(),0); 20 | } 21 | @Test 22 | public void testFilterBlobs_NoUpperLimit () throws NoSuchMethodException { 23 | URL url = this.getClass().getClassLoader().getResource("3blobs.tif"); 24 | ImagePlus ip = new ImagePlus(url.getPath()); 25 | ManyBlobs mb = new ManyBlobs(ip); 26 | mb.findConnectedComponents(); 27 | ManyBlobs t = mb.filterBlobs(0.9, Blob.GETTHINNESRATIO); 28 | assertEquals(1, t.size(),0); 29 | } 30 | 31 | @Test 32 | public void testBlobsOnBorder() { 33 | URL url = this.getClass().getClassLoader().getResource("squaresOnBoarder.tif"); 34 | ImagePlus ip = new ImagePlus(url.getPath()); 35 | ManyBlobs mb = new ManyBlobs(ip); 36 | 37 | mb.findConnectedComponents(); 38 | assertEquals(4, mb.size(),0); 39 | } 40 | 41 | @Test 42 | public void testBlobsOnBorderInvertedLUT() { 43 | URL url = this.getClass().getClassLoader().getResource("squaresOnBoarder.tif"); 44 | ImagePlus ip = new ImagePlus(url.getPath()); 45 | ip.getProcessor().invertLut(); 46 | ManyBlobs mb = new ManyBlobs(ip); 47 | mb.findConnectedComponents(); 48 | assertEquals(4, mb.size(),0); 49 | } 50 | 51 | @Test 52 | public void testBlobsOnBorderInverted() { 53 | URL url = this.getClass().getClassLoader().getResource("squaresOnBoarderInv.tif"); 54 | ImagePlus ip = new ImagePlus(url.getPath()); 55 | ManyBlobs mb = new ManyBlobs(ip); 56 | mb.setBackground(0); 57 | mb.findConnectedComponents(); 58 | assertEquals(4, mb.size(),0); 59 | } 60 | 61 | @Test 62 | public void testBlackBackground() { 63 | URL url = this.getClass().getClassLoader().getResource("3blobs.tif"); 64 | ImagePlus ip = new ImagePlus(url.getPath()); 65 | ip.getProcessor().invert(); 66 | ManyBlobs mb = new ManyBlobs(ip); 67 | mb.setBackground(0); 68 | mb.findConnectedComponents(); 69 | assertEquals(3, mb.size(),0); 70 | } 71 | 72 | @Test 73 | public void testGetSpecificBlobNotFound() { 74 | URL url = this.getClass().getClassLoader().getResource("squares_20x20_30x30.tif"); 75 | ImagePlus ip = new ImagePlus(url.getPath()); 76 | ManyBlobs mb = new ManyBlobs(ip); 77 | mb.findConnectedComponents(); 78 | Blob resultBlob = mb.getSpecificBlob(125, 84); 79 | assertEquals(null, resultBlob); 80 | } 81 | 82 | @Test 83 | public void testGetSpecificBlob() { 84 | URL url = this.getClass().getClassLoader().getResource("squares_20x20_30x30.tif"); 85 | ImagePlus ip = new ImagePlus(url.getPath()); 86 | ManyBlobs mb = new ManyBlobs(ip); 87 | mb.findConnectedComponents(); 88 | Blob resultBlob = mb.getSpecificBlob(21, 41); 89 | assertEquals(mb.get(0), resultBlob); 90 | } 91 | 92 | @Test 93 | public void testBlobOnBorder_right() { 94 | 95 | URL url = this.getClass().getClassLoader().getResource("squareOnBoarder_right.tif"); 96 | ImagePlus ip = new ImagePlus(url.getPath()); 97 | ManyBlobs mb = new ManyBlobs(ip); 98 | mb.findConnectedComponents(); 99 | assertEquals(1, mb.size(),0); 100 | } 101 | 102 | @Test (expected=RuntimeException.class) 103 | public void testNewObject_findConnectedComponents() { 104 | ManyBlobs t = new ManyBlobs(); 105 | t.findConnectedComponents(); 106 | } 107 | 108 | @Test (expected=RuntimeException.class) 109 | public void testNewObject_getLabeledImage() { 110 | ManyBlobs t = new ManyBlobs(); 111 | t.findConnectedComponents(); 112 | } 113 | 114 | @Test 115 | public void testComplexImageNoException() { 116 | URL url = this.getClass().getClassLoader().getResource("complexImage.tif"); 117 | ImagePlus ip = new ImagePlus(url.getPath()); 118 | ManyBlobs mb = new ManyBlobs(ip); 119 | try{ 120 | mb.findConnectedComponents(); 121 | } 122 | catch(Exception ex){ 123 | assertEquals("Error on complex image: " + ex.getMessage(),false,true); 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/test/java/ij/blob/tests/MyBlobFeature.java: -------------------------------------------------------------------------------- 1 | package ij.blob.tests; 2 | 3 | import ij.IJ; 4 | import ij.blob.CustomBlobFeature; 5 | 6 | public class MyBlobFeature extends CustomBlobFeature { 7 | 8 | public double LocationFeature(Integer width, Integer height) { 9 | double feature = getBlob().getCenterOfGravity().distance((double)width,(double)height); 10 | IJ.log("F " + feature); 11 | return feature; 12 | } 13 | 14 | } 15 | --------------------------------------------------------------------------------