All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.alg.fiducial.calib.circle.EllipseClustersIntoHexagonalGrid Maven / Gradle / Ivy

Go to download

BoofCV is an open source Java library for real-time computer vision and robotics applications.

There is a newer version: 1.1.6
Show newest version
/*
 * Copyright (c) 2021, 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 boofcv.alg.fiducial.calib.circle.EllipsesIntoClusters.Node;
import georegression.metric.UtilAngle;
import georegression.struct.curve.EllipseRotated_F64;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 

Given a cluster of ellipses (created with {@link EllipsesIntoClusters}) order the ellipses into * a hexagonal grid pattern. In a hexagonal grid the center of each circle is the same distance from * its neighbors. Smallest grid size it will detect is 3 x 3.

* * *

Note that the returned grid is 'sparse'. every other node is skipped implicitly. * This is caused by the asymmetry. Each row is offset by one circle/grid element.

* *
Examples:
 * 3x6 grid will have 9 elements total.
 * grid(0,0) = [0]
 * grid(0,2) = [1]
 * grid(0,4) = [2]
 * grid(1,1) = [3]
 * grid(1,3) = [4]
 * grid(1,5) = [5]
 * 
* *

IMPORTANT: To properly construct the contour the clusters need to connect and jump over the "zig-zags". Thus * at a minimum search radius should be distance between centers*2 if not more. *

* *

See {@link Grid} for a description of how the output grids are described. It uses a sparse format.

*

See {@link DetectCircleHexagonalGrid} for an example of an hexagonal grid

* * @author Peter Abeles */ public class EllipseClustersIntoHexagonalGrid extends EllipseClustersIntoGrid { /** * {@inheritDoc} */ @Override public void process( List ellipses, List> clusters ) { foundGrids.reset(); if (clusters.size() == 0) return; for (int i = 0; i < clusters.size(); i++) { List cluster = clusters.get(i); int clusterSize = cluster.size(); if (clusterSize < 6) // 3 x 4 grid has 6 elements continue; computeNodeInfo(ellipses, cluster); // finds all the nodes in the outside of the cluster if (!findContour(true)) { if (verbose) System.out.println("Contour find failed"); continue; } // Find corner to start alignment NodeInfo corner = selectSeedCorner(); if (corner == null) { if (verbose) System.out.println("No corner found!"); continue; } List> grid = new ArrayList<>(); // traverse along the axis with closely spaced circles // double distLeft = corner.distance(corner.left); // double distRight = corner.distance(corner.right); // NodeInfo next = distLeft < distRight ? corner.left : corner.right; NodeInfo next = corner.left; next.marked = true; // System.out.println("corner "+corner.ellipse.center); // System.out.println("next "+next.ellipse.center); List column0 = new ArrayList<>(); List column1 = new ArrayList<>(); bottomTwoColumns(corner, next, column0, column1); if (column0.size() < 2 || column1.size() < 2) { if (verbose) System.out.println("First two columns to small! " + column0.size() + " " + column1.size()); continue; } grid.add(column0); grid.add(column1); boolean error = false; boolean even = true; while (true) { int expected = column0.size(); column0 = column1; column1 = new ArrayList<>(); if (!addRemainingColumns(column1, column0, even)) break; even = !even; grid.add(column1); if (expected != column1.size()) { error = true; if (verbose) System.out.println("Unexpected column length! " + expected + " " + column1.size()); break; } } if (!error) { if (grid.size() < 2) continue; if (checkDuplicates(grid)) { if (verbose) System.out.println("contains duplicates"); continue; } saveResults(grid); } } } /** * Pick a corner but avoid the pointy edges at the other end */ @Override NodeInfo selectSeedCorner() { NodeInfo best = null; double bestScore = 0; double minAngle = Math.PI + 0.1; for (int i = 0; i < contour.size; i++) { NodeInfo info = contour.get(i); if (info.angleBetween < minAngle) continue; Edge middleR = selectClosest(info.right, info, true); if (middleR == null) continue; Edge middleL = selectClosest(info, info.left, true); if (middleL == null) continue; if (middleL.target != middleR.target) continue; // With no perspective distortion, at the correct corners difference should be zero // while the bad ones will be around 60 degrees double r = UtilAngle.bound(middleR.angle + Math.PI); double difference = UtilAngle.dist(r, middleL.angle); double score = info.angleBetween - difference; if (score > bestScore) { best = info; bestScore = score; } } Objects.requireNonNull(best).marked = true; return best; } private boolean addRemainingColumns( List column1, List column0, boolean even ) { int start = 0; if (even) { NodeInfo second = selectClosestN(column0.get(0), column0.get(1)); if (second == null) return false; second.marked = true; NodeInfo first = selectClosestN(column0.get(0), second); if (first == null) return false; first.marked = true; column1.add(first); column1.add(second); start = 1; } for (int i = start; i < column0.size() - 1; i++) { NodeInfo n = selectClosestN(column0.get(i), column0.get(i + 1)); if (n != null) { n.marked = true; column1.add(n); } else { return false; } } NodeInfo n = selectClosestN(column1.get(column1.size() - 1), column0.get(column0.size() - 1)); if (n == null) return true; n.marked = true; column1.add(n); return true; } /** * Traverses along the first two columns and sets them up */ static void bottomTwoColumns( NodeInfo first, NodeInfo second, List column0, List column1 ) { column0.add(first); column0.add(second); NodeInfo a = selectClosestN(first, second); if (a == null) { return; } a.marked = true; column1.add(a); NodeInfo b = second; while (true) { NodeInfo t = selectClosestN(a, b); if (t == null) break; t.marked = true; column1.add(t); a = t; t = selectClosestN(a, b); if (t == null) break; t.marked = true; column0.add(t); b = t; } } /** * Finds the closest that is the same distance from the two nodes and part of an approximate equilateral triangle */ static @Nullable Edge selectClosest( NodeInfo a, NodeInfo b, boolean checkSide ) { double bestScore = Double.MAX_VALUE; Edge bestEdgeA = null; Edge edgeAB = a.findEdge(b); double distAB = a.distance(b); if (edgeAB == null) { return null;// TODO BUG! FIX! } for (int i = 0; i < a.edges.size; i++) { Edge edgeA = a.edges.get(i); NodeInfo aa = a.edges.get(i).target; if (aa.marked) continue; for (int j = 0; j < b.edges.size; j++) { Edge edgeB = b.edges.get(j); NodeInfo bb = b.edges.get(j).target; if (bb.marked) continue; if (aa == bb) { // System.out.println("center "+aa.ellipse.center); if (checkSide && UtilAngle.distanceCW(edgeAB.angle, edgeA.angle) > Math.PI*0.75) continue; double angle = UtilAngle.dist(edgeA.angle, edgeB.angle); if (angle < 0.3) continue; double da = EllipsesIntoClusters.axisAdjustedDistanceSq(a.ellipse, aa.ellipse); double db = EllipsesIntoClusters.axisAdjustedDistanceSq(b.ellipse, aa.ellipse); da = Math.sqrt(da); db = Math.sqrt(db); // see if they are approximately the same distance double diffRatio = Math.abs(da - db)/Math.max(da, db); if (diffRatio > 0.3) continue; // TODO reject if too far double d = (da + db)/distAB + 0.1*angle; if (d < bestScore) { bestScore = d; bestEdgeA = a.edges.get(i); } break; } } } return bestEdgeA; } static @Nullable NodeInfo selectClosestN( NodeInfo a, NodeInfo b ) { Edge e = selectClosest(a, b, true); if (e == null) return null; else return e.target; } /** * Selects the closest node with the assumption that it's along the side of the grid. */ static @Nullable NodeInfo selectClosestSide( NodeInfo a, NodeInfo b ) { double ratio = 1.7321; NodeInfo best = null; double bestDistance = Double.MAX_VALUE; Edge bestEdgeA = null; Edge bestEdgeB = null; for (int i = 0; i < a.edges.size; i++) { NodeInfo aa = a.edges.get(i).target; if (aa.marked) continue; for (int j = 0; j < b.edges.size; j++) { NodeInfo bb = b.edges.get(j).target; if (bb.marked) continue; if (aa == bb) { double da = EllipsesIntoClusters.axisAdjustedDistanceSq(a.ellipse, aa.ellipse); double db = EllipsesIntoClusters.axisAdjustedDistanceSq(b.ellipse, aa.ellipse); da = Math.sqrt(da); db = Math.sqrt(db); double max, min; if (da > db) { max = da; min = db; } else { max = db; min = da; } // see how much it deviates from the ideal length with no distortion double diffRatio = Math.abs(max - min*ratio)/max; if (diffRatio > 0.25) continue; // TODO reject if too far double d = da + db; if (d < bestDistance) { bestDistance = d; best = aa; bestEdgeA = a.edges.get(i); bestEdgeB = b.edges.get(j); } break; } } } // check the angles if (best != null) { Objects.requireNonNull(bestEdgeA); // sanity checks Objects.requireNonNull(bestEdgeB); double angleA = UtilAngle.distanceCW(bestEdgeA.angle, bestEdgeB.angle); if (angleA < Math.PI*0.25) // expected with zero distortion is 30 degrees return best; else return null; } return null; } /** * Combines the inner and outer grid into one grid for output. See {@link Grid} for a discussion * on how elements are ordered internally. */ void saveResults( List> graph ) { Grid g = foundGrids.grow(); g.reset(); g.columns = graph.get(0).size() + graph.get(1).size(); g.rows = graph.size(); for (int row = 0; row < g.rows; row++) { List list = graph.get(row); for (int i = 0; i < g.columns; i++) { if ((i%2) == (row%2)) g.ellipses.add(list.get(i/2).ellipse); else g.ellipses.add(null); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy