Files
2026-07-13 12:06:04 +08:00

255 lines
8.8 KiB
C++

// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html
#include "test_precomp.hpp"
namespace opencv_test { namespace {
//helps to temporarily change the number of threads and restore it back after the scope
struct CvNThreadScope{
int nprev;
CvNThreadScope(int n){
nprev=cv::getNumThreads();
cv::setNumThreads(n);
}
~CvNThreadScope(){
cv::setNumThreads(nprev);
}
};
// Order-independent contour-set comparison
static bool trucoContoursMatch(const vector<vector<Point>>& cont1, const vector<vector<Point>>& cont2)
{
//order senstive hash
auto Hash=[](const std::vector<cv::Point>& contour) {
// FNV-1a 64-bit hash constants
constexpr uint64_t FNV_OFFSET = 1469598103934665603ULL;
constexpr uint64_t FNV_PRIME = 1099511628211ULL;
uint64_t hash = FNV_OFFSET;
// Mix in the size so that contours with different lengths
// but same prefix produce different hashes
uint64_t size = static_cast<uint64_t>(contour.size());
for (int i = 0; i < 8; ++i) {
hash ^= (size >> (i * 8)) & 0xFF;
hash *= FNV_PRIME;
}
// Mix in each point's x and y coordinates byte by byte
for (const cv::Point& p : contour) {
uint32_t x = static_cast<uint32_t>(p.x);
uint32_t y = static_cast<uint32_t>(p.y);
for (int i = 0; i < 4; ++i) {
hash ^= (x >> (i * 8)) & 0xFF;
hash *= FNV_PRIME;
}
for (int i = 0; i < 4; ++i) {
hash ^= (y >> (i * 8)) & 0xFF;
hash *= FNV_PRIME;
}
}
return hash;
};
std::set<uint64> hashes1,hashes2;
for(auto &contour:cont1){
hashes1.insert( Hash(contour));
}
for(auto &contour:cont2){
hashes2.insert( Hash(contour));
}
for(auto &h1:hashes1){//element in cont and not in cont2
if( hashes2.find(h1) ==hashes2.end()) return false;
}
return true;
}
typedef testing::TestWithParam<ContourApproximationModes> Imgproc_FindTRUContours;
TEST_P(Imgproc_FindTRUContours, nthreads_consistency)
{
ContourApproximationModes method = GetParam();
const Size sz(1000, 1000);
RNG& rng = TS::ptr()->get_rng();
Mat noise(sz, CV_8UC1);
cvtest::randUni(rng, noise, 0, 255);
Mat blurred;
boxFilter(noise, blurred, CV_8U, Size(5, 5));
Mat img;
cv::threshold(blurred, img, 128, 255, THRESH_BINARY);
vector<vector<Point>> ref_contours;
vector<vector<Point>> ref_contours_m0;
{
CvNThreadScope nt(1);
findContours(img, ref_contours, RETR_LIST, method);
}
std::vector<int> thread_counts;
for(int i=2;i<40;i++) thread_counts.push_back(i);
for (int t : thread_counts)
{
SCOPED_TRACE(cv::format("nthreads=%d method=%d", t, (int)method));
CvNThreadScope nt(t);
vector<vector<Point>> contours;
findContours(img, contours, RETR_LIST, method); //will use TRUCO because NOT using hierarchy AND RETR_LIST
auto match=trucoContoursMatch(ref_contours, contours);
EXPECT_TRUE(match);
}
}
TEST_P(Imgproc_FindTRUContours, circles_vs_standard)
{
ContourApproximationModes method = GetParam();
const Size sz(4000, 4000);
const int ITER = cvtest::debugLevel >= 10?100:10;
const int NUM_CIRCLES = 250;
RNG& rng = TS::ptr()->get_rng();
for (int iter = 0; iter < ITER; ++iter)
{
SCOPED_TRACE(cv::format("iter=%d method=%d", iter, (int)method));
Mat img(sz, CV_8UC1, Scalar::all(0));
for (int i = 0; i < NUM_CIRCLES; ++i)
{
Point center(rng.uniform(50, sz.width - 50),
rng.uniform(50, sz.height - 50));
int radius = rng.uniform(10, 150);
circle(img, center, radius, Scalar::all(255), FILLED);
}
Mat binary;
adaptiveThreshold(img, binary, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 11, 0);
vector<vector<Point>> ref_contours;
vector<Vec4i> hierarchy;
findContours(binary, ref_contours, hierarchy, RETR_LIST, method); //will call suzuki abe because using hierarchy
EXPECT_TRUE(!hierarchy.empty());
vector<vector<Point>> truco_contours;
findContours(binary, truco_contours, RETR_LIST, method);
EXPECT_TRUE(trucoContoursMatch(ref_contours, truco_contours)); //will use TRUCO because NOT using hierarchy AND RETR_LIST
}
}
TEST_P(Imgproc_FindTRUContours, noise_threshold)
{
ContourApproximationModes method = GetParam();
const Size sz(1500, 1500);
RNG& rng = TS::ptr()->get_rng();
const int levels[] = {86, 128, 170};
const int ITER = 2;
std::vector<int> thread_counts;
for(int i=2; i<40; i+=3) thread_counts.push_back(i);
for(int i=0; i<ITER; i++)
{
for (int level : levels)
{
SCOPED_TRACE(cv::format("level=%d method=%d", level, (int)method));
Mat noise(sz, CV_8UC1);
cvtest::randUni(rng, noise, 0, 255);
Mat blurred;
boxFilter(noise, blurred, CV_8U, Size(5, 5));
Mat binary;
cv::threshold(blurred, binary, level, 255, THRESH_BINARY);
vector<vector<Point>> ref_contours;
vector<Vec4i> hierarchy;
findContours(binary, ref_contours, hierarchy, RETR_LIST, method);//will call suzuki&abe because using hierarchy
EXPECT_TRUE(!hierarchy.empty());
for(auto nt: thread_counts){
CvNThreadScope ts(nt);
vector<vector<Point>> truco_contours;
findContours(binary, truco_contours, RETR_LIST, method);//will call TRUCO abe because NOT using hierarchy
EXPECT_TRUE(trucoContoursMatch(ref_contours, truco_contours));
}
}
}
}
TEST_P(Imgproc_FindTRUContours, nested_rectangles)
{
ContourApproximationModes method = GetParam();
const int DIM = 1500;
const Size sz(DIM, DIM);
const int NUM = 25;
Mat img(sz, CV_8UC1, Scalar::all(0));
Rect rect(1, 1, DIM - 2, DIM - 2);
for (int i = 0; i < NUM; ++i)
{
rectangle(img, rect, Scalar::all(255));
rect.x += 10;
rect.y += 10;
rect.width -= 20;
rect.height -= 20;
if (rect.width <= 0 || rect.height <= 0)
break;
}
vector<vector<Point>> ref_contours;
vector<Vec4i> hierarchy;
findContours(img, ref_contours, hierarchy, RETR_LIST, method);//will call suzuki abe because using hierarchy
EXPECT_TRUE(!hierarchy.empty());
vector<vector<Point>> truco_contours;
findContours(img, truco_contours, RETR_LIST, method);//will use TRUCO because NOT using hierarchy AND RETR_LIST
EXPECT_TRUE(trucoContoursMatch(ref_contours, truco_contours));
}
TEST_P(Imgproc_FindTRUContours, mixed_figures)
{
ContourApproximationModes method = GetParam();
const Size sz(1800, 1600);
RNG& rng = TS::ptr()->get_rng();
const int ITER = cvtest::debugLevel >= 10?100:10;
for (int iter = 0; iter < ITER; ++iter)
{
SCOPED_TRACE(cv::format("iter=%d method=%d", iter, (int)method));
Mat img(sz, CV_8UC1, Scalar::all(0));
for (int i = 0; i < 5; ++i)
{
Rect r(rng.uniform(10, sz.width / 2),
rng.uniform(10, sz.height / 2),
rng.uniform(20, 100),
rng.uniform(20, 100));
r &= Rect(0, 0, sz.width - 1, sz.height - 1);
rectangle(img, r, Scalar::all(255), FILLED);
}
for (int i = 0; i < 5; ++i)
{
Point center(rng.uniform(50, sz.width - 50),
rng.uniform(50, sz.height - 50));
int radius = rng.uniform(10, 50);
circle(img, center, radius, Scalar::all(255), FILLED);
}
for (int i = 0; i < 3; ++i)
{
Point pts[3];
for (auto& p : pts)
p = Point(rng.uniform(10, sz.width - 10),
rng.uniform(10, sz.height - 10));
const Point* ppts = pts;
int npts = 3;
fillPoly(img, &ppts, &npts, 1, Scalar::all(255));
}
vector<vector<Point>> ref_contours;
vector<Vec4i> hierarchy;
findContours(img, ref_contours, hierarchy, RETR_LIST, method);//will call suzuki abe because using hierarchy
EXPECT_TRUE(!hierarchy.empty());
vector<vector<Point>> truco_contours;
findContours(img, truco_contours, RETR_LIST, method);//will use TRUCO because NOT using hierarchy AND RETR_LIST
EXPECT_TRUE(trucoContoursMatch(ref_contours, truco_contours));
}
}
INSTANTIATE_TEST_CASE_P(Imgproc, Imgproc_FindTRUContours,
testing::Values(CHAIN_APPROX_NONE,CHAIN_APPROX_SIMPLE, CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOS));
}} // namespace