/* * Copyright (c) 2011-2016, Peter Abeles. All Rights Reserved. * * This file is part of BoofCV (http://boofcv.org). * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package boofcv.alg.fiducial.calib.circle; import georegression.struct.shapes.EllipseRotated_F64; import org.ddogleg.nn.FactoryNearestNeighbor; import org.ddogleg.nn.NearestNeighbor; import org.ddogleg.nn.NnData; import org.ddogleg.struct.FastQueue; import org.ddogleg.struct.GrowQueue_I32; import java.util.ArrayList; import java.util.List; /** * Given an unordered list of ellipses found in the image connect them into clusters. A cluster of * ellipses will be composed of ellipses which are spatially close to each other and have major * axises which are of similar size. * * @author Peter Abeles */ public class EllipsesIntoClusters { // ratio between center distance and major axis private double maxDistanceToMajorAxisRatio; // minimum allowed ratio difference between major and minor axis private double sizeSimilarityTolerance = 0.5; // minimum number of elements in a cluster private int minimumClusterSize = 2; private NearestNeighbor<Node> search = FactoryNearestNeighbor.kdtree(); private FastQueue<double[]> searchPoints; private FastQueue<NnData<Node>> searchResults = new FastQueue(NnData.class,true); FastQueue<Node> nodes = new FastQueue<>(Node.class,true); FastQueue<List<Node>> clusters; /** * Configures clustering * * @param maxDistanceToMajorAxisRatio The maxium distance away the center of another ellipse can be relative * to the major axis of the ellipse being examined * @param sizeSimilarityTolerance How similar two ellipses must be to be connected. 0 to 1.0. 1.0 = perfect * match and 0.0 = infinite difference in size */ public EllipsesIntoClusters( double maxDistanceToMajorAxisRatio, double sizeSimilarityTolerance ) { this.maxDistanceToMajorAxisRatio = maxDistanceToMajorAxisRatio; this.sizeSimilarityTolerance = sizeSimilarityTolerance; search.init(2); searchPoints = new FastQueue<double[]>(double[].class,true) { @Override protected double[] createInstance() { return new double[2]; } }; clusters = new FastQueue(List.class,true) { @Override protected List<Node> createInstance() { return new ArrayList<>(); } }; } /** * Processes the ellipses and creates clusters. * * @param ellipses Set of unordered ellipses * @param output Resulting found clusters. Cleared automatically. Returned lists are recycled on next call. */ public void process(List<EllipseRotated_F64> ellipses , List<List<Node>> output ) { init(ellipses); connect(ellipses); output.clear(); for (int i = 0; i < clusters.size(); i++) { List<Node> c = clusters.get(i); if( c.size() >= minimumClusterSize) { output.add(c); } } } /** * Internal function which connects ellipses together */ void connect(List<EllipseRotated_F64> ellipses) { for (int i = 0; i < ellipses.size(); i++) { EllipseRotated_F64 e1 = ellipses.get(i); Node node1 = nodes.get(i); // Only search the maximum of the major axis times two // add a fudge factor. won't ever be perfect double maxDistance = e1.a * maxDistanceToMajorAxisRatio; maxDistance *= maxDistance; searchResults.reset(); search.findNearest( searchPoints.get(i), maxDistance, Integer.MAX_VALUE, searchResults ); // if this node already has a cluster look it up, otherwise create a new one List<Node> cluster1; if( node1.cluster == -1 ) { node1.cluster = clusters.size; cluster1 = clusters.grow(); cluster1.clear(); cluster1.add( node1 ); } else { cluster1 = clusters.get( node1.cluster ); } // only accept ellipses which have a similar size for (int j = 0; j < searchResults.size(); j++) { NnData<Node> d = searchResults.get(j); EllipseRotated_F64 e2 = ellipses.get(d.data.which); if( e2 == e1 ) continue; // the initial search was based on size of major axis. Now prune and take in account the distance // from the minor axis if( axisAdjustedDistance(e1,e2) > maxDistance ) continue; // smallest shape divided by largest shape double ratioA = e1.a > e2.a ? e2.a / e1.a : e1.a / e2.a; double ratioB = e1.b > e2.b ? e2.b / e1.b : e1.b / e2.b; int indexNode2 = d.data.which; Node node2 = nodes.get(indexNode2); // connect if they have a similar size to each other if( ratioA >= sizeSimilarityTolerance && ratioB >= sizeSimilarityTolerance ) { // node2 isn't in a cluster already. Add it to this one if( node2.cluster == -1 ) { node2.cluster = node1.cluster; cluster1.add( node2 ); node1.connections.add( indexNode2 ); node2.connections.add( i ); } else if( node2.cluster != node1.cluster ) { // Node2 is in a different cluster. Merge the clusters joinClusters( node1.cluster , node2.cluster ); node1.connections.add( indexNode2 ); node2.connections.add( i ); } else { // see if they are already connected, if not connect them if( node1.connections.indexOf(indexNode2) == -1 ) { node1.connections.add( indexNode2 ); node2.connections.add( i ); } } } } } } /** * Compute a new distance that two ellipses are apart using major/minor axis size. If the axises are the * same size then there is no change. If the minor axis is much smaller and ellipse b lies along that * axis then the returned distance will be greater. */ static double axisAdjustedDistance( EllipseRotated_F64 a , EllipseRotated_F64 b ) { double dx = b.center.x - a.center.x; double dy = b.center.y - a.center.y; double c = Math.cos(a.phi); double s = Math.sin(a.phi); // rotate into ellipse's coordinate frame // scale by ratio of major/minor axis double x = (dx*c + dy*s); double y = (-dx*s + dy*c)*a.a/a.b; return x*x + y*y; } /** * Recycles and initializes all internal data structures */ private void init(List<EllipseRotated_F64> ellipses) { searchPoints.resize(ellipses.size()); nodes.resize(ellipses.size()); clusters.reset(); for (int i = 0; i < ellipses.size(); i++) { EllipseRotated_F64 e = ellipses.get(i); double[] p = searchPoints.get(i); p[0] = e.center.x; p[1] = e.center.y; Node n = nodes.get(i); n.connections.reset(); n.which = i; n.cluster = -1; } search.setPoints(searchPoints.toList(),nodes.toList()); } /** * Moves all the members of 'food' into 'mouth' * @param mouth The group which will not be changed. * @param food All members of this group are put into mouth */ void joinClusters( int mouth , int food ) { List<Node> listMouth = clusters.get(mouth); List<Node> listFood = clusters.get(food); // put all members of food into mouth for (int i = 0; i < listFood.size(); i++) { listMouth.add( listFood.get(i) ); listFood.get(i).cluster = mouth; } // zero food members listFood.clear(); } public double getMaxDistanceToMajorAxisRatio() { return maxDistanceToMajorAxisRatio; } public void setMaxDistanceToMajorAxisRatio(double maxDistanceToMajorAxisRatio) { this.maxDistanceToMajorAxisRatio = maxDistanceToMajorAxisRatio; } public double getSizeSimilarityTolerance() { return sizeSimilarityTolerance; } public void setSizeSimilarityTolerance(double sizeSimilarityTolerance) { this.sizeSimilarityTolerance = sizeSimilarityTolerance; } public int getMinimumClusterSize() { return minimumClusterSize; } public void setMinimumClusterSize(int minimumClusterSize) { this.minimumClusterSize = minimumClusterSize; } public static class Node { /** * index of the ellipse in the input list */ public int which; /** * ID number of the cluster */ public int cluster; /** * Index of all the ellipses which it is connected to. Both node should be * connected to each other */ public GrowQueue_I32 connections = new GrowQueue_I32(); } }