1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-21 19:33:03 +04:00

calib3d: use Delaunay triangulation in computeRNG for findCirclesGrid

The relative neighborhood graph (RNG) is a subgraph of the Delaunay
triangulation, so only Delaunay edges are candidates for RNG membership.
The previous implementation tested all N^2 pairs against all N points,
giving O(N^3). The new implementation builds the Delaunay triangulation
with Subdiv2D (O(N log N)), then checks only those ~3N edges for the RNG
lune-emptiness condition, reducing computeRNG to O(N^2) in the worst case
and much better in practice for regular grids.

Added synthetic-grid accuracy tests for both symmetric and asymmetric
patterns across several sizes, and a perf test parameterized by grid size.
This commit is contained in:
Sofian Elmotiem
2026-05-24 00:10:02 +02:00
parent 656f3395a7
commit 3d02d863f9
3 changed files with 274 additions and 94 deletions
+31
View File
@@ -39,4 +39,35 @@ PERF_TEST_P(String_Size, asymm_circles_grid, testing::Values(
SANITY_CHECK(ptvec, 2);
}
// Perf test using synthetic keypoints (no image I/O). Exercises the RNG and
// findLongestPath code paths directly with a pre-detected point set.
typedef perf::TestBaseWithParam<cv::Size> CirclesGrid_RNG_Size;
PERF_TEST_P(CirclesGrid_RNG_Size, detect_keypoints_symmetric,
testing::Values(cv::Size(6, 5), cv::Size(8, 6), cv::Size(10, 8), cv::Size(15, 12), cv::Size(20, 15)))
{
const cv::Size patternSize = GetParam();
const float spacing = 30.f;
std::vector<cv::Point2f> pts;
pts.reserve(patternSize.area());
for (int r = 0; r < patternSize.height; r++)
for (int c = 0; c < patternSize.width; c++)
pts.push_back(cv::Point2f(c * spacing, r * spacing));
// Shuffle so the detector works from an unordered set, same as real use.
cv::RNG& rng = cv::theRNG();
for (int k = (int)pts.size() - 1; k > 0; k--)
std::swap(pts[k], pts[rng.uniform(0, k + 1)]);
std::vector<cv::Point2f> centers;
centers.resize(patternSize.area());
declare.in(cv::Mat(pts)).out(centers);
TEST_CYCLE() ASSERT_TRUE(findCirclesGrid(cv::Mat(pts), patternSize, centers,
CALIB_CB_SYMMETRIC_GRID, cv::Ptr<cv::FeatureDetector>()));
SANITY_CHECK_NOTHING();
}
} // namespace
+152 -94
View File
@@ -43,6 +43,7 @@
#include "precomp.hpp"
#include "circlesgrid.hpp"
#include <limits>
#include <queue>
// Requires CMake flag: DEBUG_opencv_calib3d=ON
//#define DEBUG_CIRCLES
@@ -569,8 +570,6 @@ CirclesGridFinder::Segment::Segment(cv::Point2f _s, cv::Point2f _e) :
{
}
void computeShortestPath(Mat &predecessorMatrix, int v1, int v2, std::vector<int> &path);
void computePredecessorMatrix(const Mat &dm, int verticesCount, Mat &predecessorMatrix);
CirclesGridFinderParameters::CirclesGridFinderParameters()
{
@@ -1204,79 +1203,91 @@ void CirclesGridFinder::computeRNG(Graph &rng, std::vector<cv::Point2f> &vectors
rng = Graph(keypoints.size());
vectors.clear();
//TODO: use more fast algorithm instead of naive N^3
for (size_t i = 0; i < keypoints.size(); i++)
{
for (size_t j = 0; j < keypoints.size(); j++)
{
if (i == j)
continue;
Point2f vec = keypoints[i] - keypoints[j];
double dist = norm(vec);
bool isNeighbors = true;
for (size_t k = 0; k < keypoints.size(); k++)
{
if (k == i || k == j)
continue;
double dist1 = norm(keypoints[i] - keypoints[k]);
double dist2 = norm(keypoints[j] - keypoints[k]);
if (dist1 < dist && dist2 < dist)
{
isNeighbors = false;
break;
}
}
if (isNeighbors)
{
rng.addEdge(i, j);
vectors.push_back(keypoints[i] - keypoints[j]);
if (drawImage != 0)
{
line(*drawImage, keypoints[i], keypoints[j], Scalar(255, 0, 0), 2);
circle(*drawImage, keypoints[i], 3, Scalar(0, 0, 255), -1);
circle(*drawImage, keypoints[j], 3, Scalar(0, 0, 255), -1);
}
}
}
}
}
void computePredecessorMatrix(const Mat &dm, int verticesCount, Mat &predecessorMatrix)
{
CV_Assert( dm.type() == CV_32SC1 );
predecessorMatrix.create(verticesCount, verticesCount, CV_32SC1);
predecessorMatrix = -1;
for (int i = 0; i < predecessorMatrix.rows; i++)
{
for (int j = 0; j < predecessorMatrix.cols; j++)
{
int dist = dm.at<int> (i, j);
for (int k = 0; k < verticesCount; k++)
{
if (dm.at<int> (i, k) == dist - 1 && dm.at<int> (k, j) == 1)
{
predecessorMatrix.at<int> (i, j) = k;
break;
}
}
}
}
}
static void computeShortestPath(Mat &predecessorMatrix, size_t v1, size_t v2, std::vector<size_t> &path)
{
if (predecessorMatrix.at<int> ((int)v1, (int)v2) < 0)
{
path.push_back(v1);
const size_t n = keypoints.size();
if (n < 2)
return;
// RNG is a subgraph of the Delaunay triangulation, so we only need to test
// Delaunay edges as candidates. This brings the complexity from O(N^3) down
// to O(N^2) in the worst case, and much better in practice for regular grids
// where Delaunay edges (~3N) are almost all RNG edges anyway.
float minX = keypoints[0].x, minY = keypoints[0].y;
float maxX = minX, maxY = minY;
for (size_t i = 1; i < n; i++)
{
minX = std::min(minX, keypoints[i].x);
minY = std::min(minY, keypoints[i].y);
maxX = std::max(maxX, keypoints[i].x);
maxY = std::max(maxY, keypoints[i].y);
}
computeShortestPath(predecessorMatrix, v1, predecessorMatrix.at<int> ((int)v1, (int)v2), path);
path.push_back(v2);
// Subdiv2D requires a rect that strictly contains all points.
const float margin = 1.f;
Rect2f rect(minX - margin, minY - margin,
(maxX - minX) + 2*margin,
(maxY - minY) + 2*margin);
Subdiv2D subdiv(rect);
subdiv.insert(std::vector<Point2f>(keypoints.begin(), keypoints.end()));
// Map coordinates back to keypoint indices. Subdiv2D stores and returns the
// exact float values we inserted, so direct comparison is safe here.
std::map<std::pair<float, float>, size_t> ptToIdx;
for (size_t i = 0; i < n; i++)
ptToIdx[{keypoints[i].x, keypoints[i].y}] = i;
std::vector<Vec4f> edgeList;
subdiv.getEdgeList(edgeList);
for (const Vec4f& e : edgeList)
{
auto it1 = ptToIdx.find({e[0], e[1]});
auto it2 = ptToIdx.find({e[2], e[3]});
// Edges involving the virtual bounding-rect vertices won't be in ptToIdx.
if (it1 == ptToIdx.end() || it2 == ptToIdx.end())
continue;
size_t i = it1->second;
size_t j = it2->second;
if (i == j)
continue;
if (i > j)
std::swap(i, j);
Point2f vec = keypoints[i] - keypoints[j];
double distSq = (double)vec.x*vec.x + (double)vec.y*vec.y;
bool isRNG = true;
for (size_t k = 0; k < n; k++)
{
if (k == i || k == j)
continue;
Point2f d1 = keypoints[i] - keypoints[k];
Point2f d2 = keypoints[j] - keypoints[k];
double d1Sq = (double)d1.x*d1.x + (double)d1.y*d1.y;
double d2Sq = (double)d2.x*d2.x + (double)d2.y*d2.y;
if (d1Sq < distSq && d2Sq < distSq)
{
isRNG = false;
break;
}
}
if (isRNG)
{
rng.addEdge(i, j);
// Push both directions; findBasis needs the full set to cluster into
// the 4 groups (two grid axes and their negatives) via k-means.
vectors.push_back(keypoints[i] - keypoints[j]);
vectors.push_back(keypoints[j] - keypoints[i]);
if (drawImage != 0)
{
line(*drawImage, keypoints[i], keypoints[j], Scalar(255, 0, 0), 2);
circle(*drawImage, keypoints[i], 3, Scalar(0, 0, 255), -1);
circle(*drawImage, keypoints[j], 3, Scalar(0, 0, 255), -1);
}
}
}
}
size_t CirclesGridFinder::findLongestPath(std::vector<Graph> &basisGraphs, Path &bestPath)
@@ -1285,45 +1296,93 @@ size_t CirclesGridFinder::findLongestPath(std::vector<Graph> &basisGraphs, Path
std::vector<int> confidences;
size_t bestGraphIdx = 0;
const int infinity = -1;
for (size_t graphIdx = 0; graphIdx < basisGraphs.size(); graphIdx++)
{
const Graph &g = basisGraphs[graphIdx];
Mat distanceMatrix;
g.floydWarshall(distanceMatrix, infinity);
Mat predecessorMatrix;
computePredecessorMatrix(distanceMatrix, (int)g.getVerticesCount(), predecessorMatrix);
const int n = (int)g.getVerticesCount();
double maxVal;
Point maxLoc;
minMaxLoc(distanceMatrix, 0, &maxVal, 0, &maxLoc);
// BFS from every vertex to find the diameter (longest shortest path).
// basisGraphs are sparse -- each vertex connects only to grid neighbors in
// one direction -- so this is O(N^2) vs Floyd-Warshall's O(N^3).
std::vector<int> dist(n);
std::queue<int> q;
if (maxVal > longestPaths[0].length)
int maxDist = 0;
int srcBest = 0, dstBest = 0;
for (int src = 0; src < n; src++)
{
std::fill(dist.begin(), dist.end(), -1);
dist[src] = 0;
q.push(src);
while (!q.empty())
{
int v = q.front(); q.pop();
for (size_t nb : g.getNeighbors((size_t)v))
{
int u = (int)nb;
if (dist[u] < 0)
{
dist[u] = dist[v] + 1;
q.push(u);
}
}
}
for (int dst = 0; dst < n; dst++)
{
if (dist[dst] > maxDist)
{
maxDist = dist[dst];
srcBest = src;
dstBest = dst;
}
}
}
if (maxDist > longestPaths[0].length)
{
longestPaths.clear();
confidences.clear();
bestGraphIdx = graphIdx;
}
if (longestPaths.empty() || (maxVal == longestPaths[0].length && graphIdx == bestGraphIdx))
if (longestPaths.empty() || (maxDist == longestPaths[0].length && graphIdx == bestGraphIdx))
{
Path path = Path(maxLoc.x, maxLoc.y, cvRound(maxVal));
CV_Assert(maxLoc.x >= 0 && maxLoc.y >= 0)
;
size_t id1 = static_cast<size_t> (maxLoc.x);
size_t id2 = static_cast<size_t> (maxLoc.y);
computeShortestPath(predecessorMatrix, id1, id2, path.vertices);
Path path = Path(srcBest, dstBest, maxDist);
// BFS again from srcBest to reconstruct the path to dstBest
std::vector<int> pred(n, -1);
std::fill(dist.begin(), dist.end(), -1);
dist[srcBest] = 0;
q.push(srcBest);
while (!q.empty())
{
int v = q.front(); q.pop();
for (size_t nb : g.getNeighbors((size_t)v))
{
int u = (int)nb;
if (dist[u] < 0)
{
dist[u] = dist[v] + 1;
pred[u] = v;
q.push(u);
}
}
}
std::vector<size_t> pathVertices;
for (int cur = dstBest; cur != srcBest; cur = pred[cur])
pathVertices.push_back((size_t)cur);
pathVertices.push_back((size_t)srcBest);
std::reverse(pathVertices.begin(), pathVertices.end());
path.vertices = pathVertices;
longestPaths.push_back(path);
int conf = 0;
for (int v2 = 0; v2 < (int)path.vertices.size(); v2++)
{
conf += (int)basisGraphs[1 - (int)graphIdx].getDegree(v2);
}
conf += (int)basisGraphs[1 - (int)graphIdx].getDegree(path.vertices[v2]);
confidences.push_back(conf);
}
}
//if( bestGraphIdx != 0 )
//CV_Error( 0, "" );
int maxConf = -1;
int bestPathIdx = -1;
@@ -1336,7 +1395,6 @@ size_t CirclesGridFinder::findLongestPath(std::vector<Graph> &basisGraphs, Path
}
}
//int bestPathIdx = rand() % longestPaths.size();
bestPath = longestPaths.at(bestPathIdx);
bool needReverse = (bestGraphIdx == 0 && keypoints[bestPath.lastVertex].x < keypoints[bestPath.firstVertex].x)
|| (bestGraphIdx == 1 && keypoints[bestPath.lastVertex].y < keypoints[bestPath.firstVertex].y);
@@ -849,6 +849,97 @@ TEST(Calib3d_RotatedCirclesPatternDetector, issue_24964)
EXPECT_LE(error, precise_success_error_level);
}
// Generate a perfect W x H symmetric circle grid at the given spacing.
// Points are returned in shuffled order so the detector can't rely on input ordering.
static std::vector<Point2f> makeSyntheticSymmetricGrid(int cols, int rows, float spacing)
{
std::vector<Point2f> pts;
pts.reserve(cols * rows);
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
pts.push_back(Point2f(c * spacing, r * spacing));
cv::RNG& rng = cv::theRNG();
for (int k = (int)pts.size() - 1; k > 0; k--)
std::swap(pts[k], pts[rng.uniform(0, k + 1)]);
return pts;
}
// Generate an asymmetric circle grid. Even rows start at x=0, odd rows are offset by spacing/2.
static std::vector<Point2f> makeSyntheticAsymmetricGrid(int cols, int rows, float spacing)
{
std::vector<Point2f> pts;
pts.reserve(cols * rows);
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
pts.push_back(Point2f(c * spacing + (r % 2) * spacing * 0.5f, r * spacing * 0.5f));
cv::RNG& rng = cv::theRNG();
for (int k = (int)pts.size() - 1; k > 0; k--)
std::swap(pts[k], pts[rng.uniform(0, k + 1)]);
return pts;
}
typedef testing::TestWithParam<Size> Calib3d_CirclesGrid_RNG_Symmetric;
TEST_P(Calib3d_CirclesGrid_RNG_Symmetric, synthetic)
{
// Verify that findCirclesGrid correctly detects synthetic perfect symmetric grids of
// various sizes. This exercises the computeRNG path (Delaunay-based) end-to-end.
const float spacing = 30.f;
const Size gridSize = GetParam();
std::vector<Point2f> pts = makeSyntheticSymmetricGrid(gridSize.width, gridSize.height, spacing);
std::vector<Point2f> centers;
bool found = findCirclesGrid(Mat(pts), gridSize, centers,
CALIB_CB_SYMMETRIC_GRID, Ptr<FeatureDetector>());
ASSERT_TRUE(found) << "Symmetric grid " << gridSize.width << "x" << gridSize.height << " not detected";
ASSERT_EQ((int)centers.size(), gridSize.area());
for (const Point2f& c : centers)
{
bool matched = false;
for (const Point2f& p : pts)
if (cv::norm(c - p) < 1.f) { matched = true; break; }
EXPECT_TRUE(matched) << "Detected center " << c << " does not match any input point "
<< "for grid " << gridSize.width << "x" << gridSize.height;
}
}
INSTANTIATE_TEST_CASE_P(/**/, Calib3d_CirclesGrid_RNG_Symmetric,
testing::Values(Size(4, 4), Size(6, 5), Size(8, 6), Size(10, 8)));
typedef testing::TestWithParam<Size> Calib3d_CirclesGrid_RNG_Asymmetric;
TEST_P(Calib3d_CirclesGrid_RNG_Asymmetric, synthetic)
{
const float spacing = 30.f;
const Size gridSize = GetParam();
std::vector<Point2f> pts = makeSyntheticAsymmetricGrid(gridSize.width, gridSize.height, spacing);
std::vector<Point2f> centers;
bool found = findCirclesGrid(Mat(pts), gridSize, centers,
CALIB_CB_ASYMMETRIC_GRID, Ptr<FeatureDetector>());
ASSERT_TRUE(found) << "Asymmetric grid " << gridSize.width << "x" << gridSize.height << " not detected";
ASSERT_EQ((int)centers.size(), gridSize.area());
for (const Point2f& c : centers)
{
bool matched = false;
for (const Point2f& p : pts)
if (cv::norm(c - p) < 1.f) { matched = true; break; }
EXPECT_TRUE(matched) << "Detected center " << c << " does not match any input point "
<< "for grid " << gridSize.width << "x" << gridSize.height;
}
}
INSTANTIATE_TEST_CASE_P(/**/, Calib3d_CirclesGrid_RNG_Asymmetric,
testing::Values(Size(4, 6), Size(5, 8)));
TEST(Calib3d_CornerOrdering, issue_26830) {
const cv::String dataDir = string(TS::ptr()->get_data_path()) + "cv/cameracalibration/";
const cv::Mat image = cv::imread(dataDir + "checkerboard_marker_white.png");