/* * This file is part of the JFeatureLib project: https://github.com/locked-fg/JFeatureLib * JFeatureLib is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * JFeatureLib is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with JFeatureLib; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * You are kindly asked to refer to the papers of the according authors which * should be mentioned in the Javadocs of the respective classes as well as the * JFeatureLib project itself. * * Hints how to cite the projects can be found at * https://github.com/locked-fg/JFeatureLib/wiki/Citation */ package de.lmu.ifi.dbs.jfeaturelib.shapeFeatures; import de.lmu.ifi.dbs.jfeaturelib.Descriptor; import de.lmu.ifi.dbs.jfeaturelib.Progress; import de.lmu.ifi.dbs.jfeaturelib.edgeDetector.Susan; import de.lmu.ifi.dbs.jfeaturelib.features.AbstractFeatureDescriptor; import ij.process.ByteProcessor; import ij.process.ImageProcessor; import java.util.ArrayList; import java.util.EnumSet; /** * Computes a normalized distance shape feature as Array List containing the max * distances measured for each angle or null if no boundary was found at that * angle. * * This algorithm is described in "Yang Mingqiang, Kpalma Kidiyo and Ronsin * Joseph (2008). A Survey of Shape Feature Extraction Techniques, Pattern * Recognition Techniques, Technology and Applications, Peng-Yeng Yin (Ed.), * ISBN: 978-953-7619-24-4, InTech, Available from: * http://www.intechopen.com/articles/show/title/a_survey_of_shape_feature_extraction_techniques * " as "centroid-to-boundary distance approach". * * @author Johannes Stadler * @since 09/29/2012 */ public class CentroidBoundaryDistance extends AbstractFeatureDescriptor { private double angles; private ArrayList<Double> distances = new ArrayList(1); private int backgroundColor = 0; /** * Constructs a CentroidBoundaryDistance object with distances measured all * 22,5° */ public CentroidBoundaryDistance() { angles = 16; } /** * Constructs a CentroidBoundaryDistance object * * @param angles Number of angles, i.e. 36 for measurements all 10° */ public CentroidBoundaryDistance(int angles) { this.angles = angles; } public ArrayList getDistances() { return distances; } public double distance(int x1, int y1, int x2, int y2) { double dx = (double) x1 - (double) x2; double dy = (double) y1 - (double) y2; return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); } //Adds a neighbourpixel to each pixel to eliminate missed hits public ImageProcessor thicken(ImageProcessor ip) { for (int i = 0; i < ip.getWidth(); i++) { for (int j = 0; j < ip.getHeight(); j++) { if (ip.get(i, j) == backgroundColor) { if (i - 1 > 0) { ip.set(i - 1, j, backgroundColor); } } } } return ip; } @Override public void run(ImageProcessor ip) { pcs.firePropertyChange(Progress.getName(), null, Progress.START); ImagePCA pca = new ImagePCA(ip, backgroundColor); ip = pca.getResultImage(); double[] centroid; { CentroidFeature cf = new CentroidFeature(); cf.run(ip); centroid = cf.getFeatures().get(0); } //turns the image by 180° if the centroid is above the main axis for (int j = 0; j < ip.getHeight(); j++) { for (int i = 0; i < ip.getHeight(); i++) { if (ip.getPixel(j, i) != backgroundColor) { if (i < centroid[1]) { ip.rotate(180); break; } } } } distances = new ArrayList(); // xC and yC are the centroids coordinates int xC; int yC; { CentroidFeature cf = new CentroidFeature(); cf.run(ip); double[] xCyC = cf.getFeatures().get(0); xC = (int) xCyC[0]; yC = (int) xCyC[1]; } //ip = ip.convertToRGB(); new Susan().run(ip); if (!ByteProcessor.class.isAssignableFrom(ip.getClass())) { ip = ip.convertToByte(true); } ByteProcessor image = (ByteProcessor) ip; ip = thicken(ip); //Fix outer border error from SUSAN for (int i = 0; i < ip.getWidth(); i++) { for (int j = 0; j < ip.getHeight(); j++) { if (i <= 1 || i >= ip.getWidth() - 3) { ip.putPixel(i, j, 255); } else if (j <= 1 || j >= ip.getHeight() - 2) { ip.putPixel(i, j, 255); } } } //Angles from the centroid to the corners of the image for boundary checking double angleTopRight = Math.toDegrees(Math.atan((double) yC / (image.getWidth() - (double) xC))); double angleTopLeft = 90.0 + Math.toDegrees(Math.atan(((double) xC / (double) yC))); double angleBottomLeft = 180.0 + Math.toDegrees(Math.atan(((image.getHeight() - (double) yC) / (double) xC))); double angleBottomRight = 270.0 + Math.toDegrees(Math.atan((image.getWidth() - (double) xC) / (image.getHeight() - (double) yC))); //Distances for each angle for (int i = 0; i < angles; i++) { int border = 0; ArrayList<Double> angleDist = new ArrayList(); double angle = i * (360.0 / angles); if (angle == 0) { for (int j = 0; j < image.getWidth() - xC; j++) { if (image.getPixel(xC + j, yC) == backgroundColor) { angleDist.add((double) j); } } } else if (angle <= 45) { if (angle < angleTopRight) { border = image.getWidth() - xC; } else { border = (int) (yC / Math.tan(Math.toRadians(angle))); } for (int j = 0; j < border; j++) { int y = (int) (Math.tan(Math.toRadians(angle)) * j); if (image.getPixel(xC + j, yC - y) == backgroundColor) { angleDist.add(distance(xC, yC, xC + j, yC - y)); } } } else if (angle < 90) { if (angle > angleTopRight) { border = yC; } else { border = (int) (Math.tan(Math.toRadians(angle)) * (image.getWidth() - xC)); } for (int j = 0; j < border; j++) { int x = (int) (j / Math.tan(Math.toRadians(angle))); if (image.getPixel(xC + x, yC - j) == backgroundColor) { angleDist.add(distance(xC, yC, xC + x, yC - j)); } } } else if (angle == 90) { for (int j = 0; j < yC; j++) { if (image.getPixel(xC, yC - j) == backgroundColor) { angleDist.add((double) j); } } } else if (angle <= 135) { if (angle < angleTopLeft) { border = yC; } else { border = (int) (Math.tan(Math.toRadians(180.0 - angle)) * xC); } for (int j = 0; j < border; j++) { int x = (int) (j / Math.tan(Math.toRadians(angle))); if (image.getPixel(xC + x, yC - j) == backgroundColor) { angleDist.add(distance(xC, yC, xC + x, yC - j)); } } } else if (angle < 180) { if (angle > angleTopLeft) { border = xC; } else { border = (int) (yC / Math.tan(Math.toRadians(180.0 - angle))); } for (int j = 0; j < border; j++) { int y = (int) (Math.tan(Math.toRadians(angle)) * j); if (image.getPixel(xC - j, yC + y) == backgroundColor) { angleDist.add(distance(xC, yC, xC - j, yC + y)); } } } else if (angle == 180) { for (int j = 0; j < xC; j++) { if (image.getPixel(xC - j, yC) == backgroundColor) { angleDist.add((double) j); } } } else if (angle <= 225) { if (angle < angleBottomLeft) { border = xC; } else { border = (int) ((image.getHeight() - yC) / Math.tan(Math.toRadians(angle - 180.0))); } for (int j = 0; j < border; j++) { int y = (int) (Math.tan(Math.toRadians(angle)) * j); if (image.getPixel(xC - j, yC + y) == backgroundColor) { angleDist.add(distance(xC, yC, xC - j, yC + y)); } } } else if (angle < 270) { if (angle > angleBottomLeft) { border = image.getHeight() - yC; } else { border = (int) (Math.tan(Math.toRadians(angle - 180.0)) * xC); } for (int j = 0; j < border; j++) { int x = (int) (j / Math.tan(Math.toRadians(angle))); if (image.getPixel(xC - x, yC + j) == backgroundColor) { angleDist.add(distance(xC, yC, xC - x, yC + j)); } } } else if (angle == 270) { for (int j = 0; j < (image.getHeight() - yC); j++) { if (image.getPixel(xC, yC + j) == backgroundColor) { angleDist.add((double) j); } } } else if (angle <= 315) { if (angle < angleBottomRight) { border = image.getHeight() - yC; } else { border = (int) ((image.getWidth() - xC) / (Math.tan(Math.toRadians(angle - 270.0)))); } for (int j = 0; j < border; j++) { int x = (int) (j / Math.tan(Math.toRadians(angle))); if (image.getPixel(xC - x, yC + j) == backgroundColor) { angleDist.add(distance(xC, yC, xC - x, yC + j)); } } } else { if (angle > angleBottomRight) { border = image.getWidth() - xC; } else { border = (int) ((image.getHeight() - yC) * (Math.tan(Math.toRadians(angle - 270.0)))); } for (int j = 0; j < border; j++) { int y = (int) (Math.tan(Math.toRadians(angle)) * j); if (image.getPixel(xC + j, yC - y) == backgroundColor) { angleDist.add(distance(xC, yC, xC + j, yC - y)); } } } if (angleDist.isEmpty()) { distances.add(null); } else { distances.add((double) angleDist.get(angleDist.size() - 1)); } } //Normalisation to guarantee rotation and scaling invariance. //Transition invariance is ensured by the use of a centroid. if (!distances.isEmpty()) { for (int i = 0; i < distances.size(); i++) { if (distances.get(i) != null && (double) distances.get(i) == 0.0) { distances.set(i, null); } } double minEle = Double.MAX_VALUE; double maxEle = Double.MIN_VALUE; for (int i = 0; i < distances.size(); i++) { if (distances.get(i) != null && (double) distances.get(i) < minEle) { minEle = (double) distances.get(i); } if (distances.get(i) != null) { maxEle = Math.max(maxEle, distances.get(i)); } } maxEle /= minEle; //Scaling invariance for (int i = 0; i < distances.size(); i++) { if (distances.get(i) != null) { distances.set(i, ((double) distances.get(i) / minEle)); } else { distances.set(i, 0.0); } } //Rotation invariance /*for (int i = 0; i < distances.size(); i++) { if (distances.get(0) > 0.0) { if ((double) distances.get(0) > 1.0) { distances.add(distances.get(0)); distances.remove(distances.get(0)); } else { break; } } else { distances.add(distances.get(0)); distances.remove(distances.get(0)); } }*/ } // set data generateFeatures(); pcs.firePropertyChange(Progress.getName(), null, Progress.END); } public void generateFeatures() { double[] x = new double[distances.size()]; for (int i = 0; i < (distances.size()); i++) { x[i] = distances.get(i); } addData(x); } @Override public EnumSet<Descriptor.Supports> supports() { return EnumSet.of(Descriptor.Supports.NoChanges, Descriptor.Supports.DOES_16); } @Override public String getDescription() { return "Shape feature descriptor that returns the distances between the centroid and the boundary as feature"; } }