├── .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 | [](https://zenodo.org/badge/latestdoi/18649/thorstenwagner/ij-blob)
2 | [](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 |
--------------------------------------------------------------------------------