mirror of
https://github.com/opencv/opencv.git
synced 2026-07-21 19:33:03 +04:00
Merge pull request #28773 from rmsalinas:imgproc_find_truco_contours
imgproc: add findTRUContours - lock-free parallel contour extraction #28773 Integrates the TRUCO algorithm (Threaded Raster Unrestricted Contour Ownership, @cite TRUCO2026) as a transparent fast path inside `cv::findContours`. No API change is required: existing code automatically benefits when `mode=RETR_LIST` and no hierarchy output is requested. ### How it works When `mode=RETR_LIST` and the caller does not request hierarchy, `findContours` delegates to the TRUCO parallel engine instead of Suzuki-Abe. All ContourApproximationModes are supported; approximation is applied in parallel after extraction. In all other cases the original Suzuki-Abe path is used unchanged. ### Key design ideas - Row-strip domain decomposition parallelised via `cv::parallel_for_` - Start-point ownership rule + speculative downward tracing eliminate tile stitching and synchronisation primitives (lock-free) - 8-bit state space instead of the 32-bit integer labeling required by Suzuki-Abe, giving higher SIMD throughput and lower memory-bandwidth pressure - Paged contour buffer (`TRUCOPagedContour`) avoids heap reallocation on the hot tracing path - SIMD-accelerated row scanning via `cv::v_uint8` intrinsics - Thread count controlled globally via `cv::setNumThreads()`, consistent with OpenCV conventions ### Output correctness Significant effort has gone into ensuring the output is identical to the original `findContours` in every respect: same contour set, same ordering, and same approximation results for all methods. A dedicated test suite (`test_contours_truco.cpp`) verifies exact match against Suzuki-Abe across all four `ContourApproximationModes` and thread counts from 1 to 39, using noise images, circles, nested rectangles, and mixed scenes. ### Performance vs. Suzuki-Abe (from submitted paper) - single-thread: ~1.8–1.9× faster (8-bit SIMD advantage) - 20 threads: ~14–20× faster on i7-13700H (6P+8E, 20T) - 20 threads: ~10–12× faster on Xeon Silver 4510 (12C/24T) ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
committed by
GitHub
parent
7cc6d8576c
commit
cc1ab3e3ac
@@ -1347,6 +1347,13 @@
|
||||
year = {1991},
|
||||
url = {http://mesh.brown.edu/taubin/pdfs/taubin-pami91.pdf}
|
||||
}
|
||||
@article{TRUCO2026,
|
||||
title={TRUCO: A Scalable Lock-Free Algorithm for Parallel Contour Extraction},
|
||||
author={Mu\~noz-Salinas, Rafael and Romero-Ramírez, Francisco J. and Marín-Jiménez, Manuel J.},
|
||||
journal={Pattern Recognition under review},
|
||||
year={2026}
|
||||
}
|
||||
|
||||
@article{TehChin89,
|
||||
author = {Teh, C-H and Chin, Roland T.},
|
||||
title = {On the detection of dominant points on digital curves},
|
||||
|
||||
@@ -4084,9 +4084,14 @@ CV_EXPORTS_W int connectedComponentsWithStats(InputArray image, OutputArray labe
|
||||
|
||||
/** @brief Finds contours in a binary image.
|
||||
|
||||
The function retrieves contours from the binary image using the algorithm @cite Suzuki85 . The contours
|
||||
The function retrieves contours from the binary image. The contours
|
||||
are a useful tool for shape analysis and object detection and recognition. See squares.cpp in the
|
||||
OpenCV sample directory.
|
||||
|
||||
@note Since OpenCV 4.14, when mode is #RETR_LIST and no hierarchy is requested, this function
|
||||
automatically uses the TRUCO parallel algorithm @cite TRUCO2026, a scalable lock-free method for
|
||||
contour extraction. In all other cases, the sequential @cite Suzuki85 algorithm is used.
|
||||
|
||||
@note Since opencv 3.2 source image is not modified by this function.
|
||||
|
||||
@param image Source, an 8-bit single-channel image. Non-zero pixels are treated as 1's. Zero
|
||||
@@ -4116,6 +4121,7 @@ CV_EXPORTS_W void findContours( InputArray image, OutputArrayOfArrays contours,
|
||||
CV_EXPORTS void findContours( InputArray image, OutputArrayOfArrays contours,
|
||||
int mode, int method, Point offset = Point());
|
||||
|
||||
|
||||
//! @brief Find contours using link runs algorithm
|
||||
//!
|
||||
//! This function implements an algorithm different from cv::findContours:
|
||||
@@ -4129,6 +4135,7 @@ CV_EXPORTS_W void findContoursLinkRuns(InputArray image, OutputArrayOfArrays con
|
||||
//! @overload
|
||||
CV_EXPORTS_W void findContoursLinkRuns(InputArray image, OutputArrayOfArrays contours);
|
||||
|
||||
|
||||
/** @brief Approximates a polygonal curve(s) with the specified precision.
|
||||
|
||||
The function cv::approxPolyDP approximates a curve or a polygon with another curve/polygon with less
|
||||
|
||||
@@ -141,4 +141,78 @@ PERF_TEST_P(TestMinEnclosingCircleWorstCase, minEnclosingCircle_sequential,
|
||||
SANITY_CHECK_NOTHING();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// findTRUContours performance tests
|
||||
// ============================================================
|
||||
|
||||
typedef TestBaseWithParam< tuple<Size, int, int> > TestFindTRUContours;
|
||||
|
||||
PERF_TEST_P(TestFindTRUContours, findTRUContours,
|
||||
Combine(
|
||||
Values(sz1080p, sz2160p), // image size
|
||||
Values(128, 512, 2048), // circle count
|
||||
Values(1, 0) // nthreads: 1=single-thread baseline, 0=all available
|
||||
)
|
||||
)
|
||||
{
|
||||
Size img_size = get<0>(GetParam());
|
||||
int num_circles = get<1>(GetParam());
|
||||
int nthreads = get<2>(GetParam());
|
||||
|
||||
RNG rng(12345);
|
||||
Mat img = Mat::zeros(img_size, CV_8UC1);
|
||||
for (int i = 0; i < num_circles; ++i)
|
||||
{
|
||||
Point center(rng.uniform(50, img_size.width - 50),
|
||||
rng.uniform(50, img_size.height - 50));
|
||||
int radius = rng.uniform(10, 200);
|
||||
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>> contours;
|
||||
int prev_nthreads=cv::getNumThreads();
|
||||
cv::setNumThreads(nthreads);
|
||||
TEST_CYCLE() findContours(binary, contours, RETR_LIST, CHAIN_APPROX_NONE);
|
||||
cv::setNumThreads(prev_nthreads);
|
||||
|
||||
SANITY_CHECK_NOTHING();
|
||||
}
|
||||
|
||||
// Baseline: same image, findContours(RETR_LIST, CHAIN_APPROX_NONE) for direct comparison
|
||||
typedef TestBaseWithParam< tuple<Size, int> > TestFindContoursBaseline;
|
||||
|
||||
PERF_TEST_P(TestFindContoursBaseline, findContours_baseline_for_TRUCO,
|
||||
Combine(
|
||||
Values(sz1080p, sz2160p),
|
||||
Values(128, 512, 2048)
|
||||
)
|
||||
)
|
||||
{
|
||||
Size img_size = get<0>(GetParam());
|
||||
int num_circles = get<1>(GetParam());
|
||||
|
||||
RNG rng(12345);
|
||||
Mat img = Mat::zeros(img_size, CV_8UC1);
|
||||
for (int i = 0; i < num_circles; ++i)
|
||||
{
|
||||
Point center(rng.uniform(50, img_size.width - 50),
|
||||
rng.uniform(50, img_size.height - 50));
|
||||
int radius = rng.uniform(10, 200);
|
||||
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>> contours;
|
||||
vector<Vec4i> hierarchy;
|
||||
|
||||
TEST_CYCLE() findContours(binary, contours, hierarchy, RETR_LIST, CHAIN_APPROX_NONE);
|
||||
|
||||
SANITY_CHECK_NOTHING();
|
||||
}
|
||||
|
||||
} } // namespace
|
||||
|
||||
@@ -303,6 +303,8 @@ void contourTreeToResults(CTree& tree,
|
||||
void approximateChainTC89(const ContourCodesStorage& chain, const Point& origin, const int method,
|
||||
ContourPointsStorage& output);
|
||||
|
||||
void findTRUContours(InputArray _src, OutputArrayOfArrays _contours, int minSize=0, bool binarize=false, int method=CHAIN_APPROX_NONE);
|
||||
|
||||
} // namespace cv
|
||||
|
||||
#endif // OPENCV_CONTOURS_COMMON_HPP
|
||||
|
||||
@@ -648,6 +648,32 @@ void cv::findContours(InputArray _image,
|
||||
return;
|
||||
}
|
||||
|
||||
// Fast path: RETR_LIST without hierarchy → findTRUContours (parallel contour extraction)
|
||||
if (mode == RETR_LIST && !_hierarchy.needed())
|
||||
{
|
||||
// findTRUContours requires FOREGROUND=255; binarize=true thresholds the padded
|
||||
// image in-place, avoiding an extra allocation (findContours accepts any non-zero value)
|
||||
findTRUContours(_image, _contours, 0, true,method);
|
||||
if (offset != Point())
|
||||
{
|
||||
if (_contours.kind() == _InputArray::STD_VECTOR_VECTOR)
|
||||
{
|
||||
auto& vv = *reinterpret_cast<std::vector<std::vector<Point>>*>(_contours.getObj());
|
||||
for (auto& c : vv)
|
||||
for (auto& p : c)
|
||||
p += offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
const Scalar shift(offset.x, offset.y);
|
||||
const int n = (int)_contours.size().height;
|
||||
for (int i = 0; i < n; i++)
|
||||
_contours.getMat(i) += shift;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: need enum value, need way to return contour starting points with chain codes
|
||||
if (method == 0 /*CV_CHAIN_CODE*/)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
|
||||
#include "precomp.hpp"
|
||||
#include "contours_common.hpp"
|
||||
#include "opencv2/core/hal/intrin.hpp"
|
||||
#include <map>
|
||||
|
||||
namespace{
|
||||
|
||||
// Tunable block size. 1024 points = 8KB (Fits easily in L1 Cache)
|
||||
template <size_t BLOCK_SIZE = 2048>
|
||||
class TRUCOPagedContour {
|
||||
public:
|
||||
struct Block {
|
||||
cv::Point data[BLOCK_SIZE];
|
||||
};
|
||||
|
||||
TRUCOPagedContour() {
|
||||
allocateBlock();
|
||||
// Initialize pointers to the start of the first block
|
||||
curr_ptr_ = all_blocks_[0]->data;
|
||||
end_ptr_ = curr_ptr_ + BLOCK_SIZE;
|
||||
}
|
||||
|
||||
~TRUCOPagedContour() {
|
||||
for (Block* b : all_blocks_) cv::fastFree(b);
|
||||
}
|
||||
|
||||
// --- HOT PATH: Minimal instructions ---
|
||||
// No counter updates, just raw pointer arithmetic.
|
||||
inline void push_back(const cv::Point& pt) {
|
||||
if (curr_ptr_ == end_ptr_) {
|
||||
current_block_idx_++;
|
||||
if (current_block_idx_ == all_blocks_.size()) {
|
||||
allocateBlock();
|
||||
}
|
||||
curr_ptr_ = all_blocks_[current_block_idx_]->data;
|
||||
end_ptr_ = curr_ptr_ + BLOCK_SIZE;
|
||||
}
|
||||
*curr_ptr_++ = pt;
|
||||
}
|
||||
|
||||
inline void pop_back() {
|
||||
// Safety check: do nothing if completely empty
|
||||
if (current_block_idx_ == 0 && curr_ptr_ == all_blocks_[0]->data) return;
|
||||
|
||||
// Check if we are at the start of the current block
|
||||
if (curr_ptr_ == all_blocks_[current_block_idx_]->data) {
|
||||
// Move to the previous block
|
||||
current_block_idx_--;
|
||||
// Point to the end of the previous block
|
||||
curr_ptr_ = all_blocks_[current_block_idx_]->data + BLOCK_SIZE;
|
||||
end_ptr_ = curr_ptr_;
|
||||
}
|
||||
curr_ptr_--;
|
||||
}
|
||||
|
||||
inline const cv::Point& back() const {
|
||||
// Handle case where back() crosses block boundary
|
||||
if (curr_ptr_ == all_blocks_[current_block_idx_]->data) {
|
||||
return all_blocks_[current_block_idx_ - 1]->data[BLOCK_SIZE - 1];
|
||||
}
|
||||
return *(curr_ptr_ - 1);
|
||||
}
|
||||
|
||||
inline const cv::Point& front() const {
|
||||
return all_blocks_[0]->data[0];
|
||||
}
|
||||
|
||||
// Calculated on demand (O(1) arithmetic, but slightly more math than reading a variable)
|
||||
size_t size() const {
|
||||
size_t elements_in_last = curr_ptr_ - all_blocks_[current_block_idx_]->data;
|
||||
return (current_block_idx_ * BLOCK_SIZE) + elements_in_last;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
current_block_idx_ = 0;
|
||||
if (!all_blocks_.empty()) {
|
||||
curr_ptr_ = all_blocks_[0]->data;
|
||||
end_ptr_ = curr_ptr_ + BLOCK_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized Copy: Uses block-wise memcpy
|
||||
void copyTo(std::vector<cv::Point>& out) const {
|
||||
size_t total = size();
|
||||
out.resize(total);
|
||||
if (total == 0) return;
|
||||
|
||||
cv::Point* dst = out.data();
|
||||
|
||||
// 1. Copy full blocks
|
||||
for (size_t i = 0; i < current_block_idx_; ++i) {
|
||||
std::memcpy(dst, all_blocks_[i]->data, BLOCK_SIZE * sizeof(cv::Point));
|
||||
dst += BLOCK_SIZE;
|
||||
}
|
||||
|
||||
// 2. Copy partial last block
|
||||
size_t last_block_count = curr_ptr_ - all_blocks_[current_block_idx_]->data;
|
||||
if (last_block_count > 0) {
|
||||
std::memcpy(dst, all_blocks_[current_block_idx_]->data, last_block_count * sizeof(cv::Point));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void grow() {
|
||||
current_block_idx_++;
|
||||
if (current_block_idx_ == all_blocks_.size()) {
|
||||
allocateBlock();
|
||||
}
|
||||
curr_ptr_ = all_blocks_[current_block_idx_]->data;
|
||||
end_ptr_ = curr_ptr_ + BLOCK_SIZE;
|
||||
}
|
||||
|
||||
void allocateBlock() {
|
||||
Block* b = (Block*)cv::fastMalloc(sizeof(Block));
|
||||
all_blocks_.push_back(b);
|
||||
}
|
||||
|
||||
std::vector<Block*> all_blocks_;
|
||||
size_t current_block_idx_ = 0;
|
||||
|
||||
// Fast pointers for the hot loop
|
||||
cv::Point* curr_ptr_ = nullptr;
|
||||
cv::Point* end_ptr_ = nullptr;
|
||||
};
|
||||
|
||||
|
||||
////IMPLEMENTATION
|
||||
|
||||
struct AccumulatorT:public std::vector<std::vector<cv::Point>>{
|
||||
std::vector<int> idx_internal_lastLine,idx_external_firstLine;
|
||||
};
|
||||
|
||||
|
||||
class TRUCOntourTracer : public cv::ParallelLoopBody
|
||||
{
|
||||
public:
|
||||
|
||||
// We use a pointer to the accumulator to avoid passing huge objects
|
||||
// Accumulator: Vector of (Vector of Contours), where Contour is Vector of Points
|
||||
using AccumulatorType = std::vector<AccumulatorT>;
|
||||
|
||||
TRUCOntourTracer(const cv::Mat& img,
|
||||
const std::vector<cv::Range>& stripRanges,
|
||||
AccumulatorType& accumulator,
|
||||
size_t minSize)
|
||||
: padded_(img), ranges_(stripRanges), accumulator_(accumulator), minSize_(minSize)
|
||||
{
|
||||
step_ = padded_.step;
|
||||
int istep = (int)step_;
|
||||
// 0: East (Right)
|
||||
offsets_[0] = 1;
|
||||
// 1: NE (Up-Right)
|
||||
offsets_[1] = -istep + 1;
|
||||
// 2: North (Up)
|
||||
offsets_[2] = -istep;
|
||||
// 3: NW (Up-Left)
|
||||
offsets_[3] = -istep - 1;
|
||||
// 4: West (Left)
|
||||
offsets_[4] = -1;
|
||||
// 5: SW (Down-Left)
|
||||
offsets_[5] = istep - 1;
|
||||
// 6: South (Down)
|
||||
offsets_[6] = istep;
|
||||
// 7: SE (Down-Right)
|
||||
offsets_[7] = istep + 1;
|
||||
|
||||
memcpy(offsets_ + 8, offsets_, 8 * sizeof(int));
|
||||
|
||||
}
|
||||
|
||||
//trace external contour marking EAST pixels (VISITED_OUTER_RIGHT) only so that later the analysis of the internal contour is exactly as expected
|
||||
void traceExternalContourMock( int r,int c,uchar *row_ptr, const cv::Range& rowRange)const{
|
||||
int curr_x = c , curr_y = r;
|
||||
int start_dir = -1 ;
|
||||
int search_idx = 5;
|
||||
uchar* curr_ptr = row_ptr + c , * start_ptr = curr_ptr;
|
||||
int dir=-1;
|
||||
bool is_first_move = true;
|
||||
// 3. TRACING LOOP
|
||||
while(true)
|
||||
{
|
||||
// Check neighbors
|
||||
for (int n = 0; n < 8; ++n)
|
||||
{
|
||||
int idx = search_idx + n;
|
||||
// Use offset cache
|
||||
uchar* neighbor = curr_ptr + offsets_[idx];
|
||||
if (*neighbor == BACKGROUND) continue;
|
||||
dir = idx & 7;
|
||||
if (((search_idx <= 1) || (dir <= search_idx - 2)) && (curr_x!=c && curr_y!=r))//do nt apply to first pixel in the way back
|
||||
*curr_ptr = VISITED_OUTER_RIGHT;
|
||||
// --- EXECUTE MOVE ---
|
||||
curr_y += dy_[dir];
|
||||
curr_x += dx_[dir];
|
||||
// Check bounds //we need to move out of the range //if first line, and internal contour, we let it go, but no further from this line
|
||||
if( curr_y < rowRange.start )
|
||||
return ;
|
||||
// Short-circuit Jacob's Check
|
||||
if (curr_ptr == start_ptr) {
|
||||
if (!is_first_move && dir == start_dir) {
|
||||
return ;//done
|
||||
}
|
||||
}
|
||||
curr_ptr = neighbor;//move ptr
|
||||
// Reset search index for Moore neighbor
|
||||
search_idx = (dir +6) & 7;
|
||||
break;
|
||||
|
||||
}
|
||||
if (is_first_move) {
|
||||
if(dir==-1){//single pixel
|
||||
break;//not moved
|
||||
}
|
||||
start_dir = dir;
|
||||
is_first_move = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
bool traceContour( TRUCOPagedContour<4096>* buffer, int r,int c,uchar *row_ptr, const cv::Range& rowRange,bool isExternal)const{
|
||||
|
||||
buffer->clear();
|
||||
|
||||
int curr_x = c , curr_y = r;
|
||||
int start_dir = -1 ;
|
||||
int search_idx = isExternal ? 5 :1;
|
||||
uchar* curr_ptr = row_ptr + c , * start_ptr = curr_ptr;
|
||||
int dir=-1;
|
||||
// int sign=isExternal?1:-1;
|
||||
|
||||
bool is_first_move = true;
|
||||
|
||||
|
||||
// QUick test to find the first element of an internal contour. We know it should be NE
|
||||
if(!isExternal){
|
||||
int n=0;
|
||||
for ( n = 0; n < 8; ++n)
|
||||
{
|
||||
int idx = search_idx + n;
|
||||
if ( *(curr_ptr + offsets_[idx]) == BACKGROUND) continue;
|
||||
if(curr_x+ dx_[ idx & 7]!=c+1 || curr_y+ dy_[ idx & 7]!=r-1) return false;
|
||||
break;
|
||||
}
|
||||
if(n==8) return false;//isolated pixels must not be considered as internal
|
||||
}
|
||||
// 3. TRACING LOOP
|
||||
while(true)
|
||||
{
|
||||
buffer->push_back({ (curr_x - 1), (curr_y - 1)});
|
||||
// Check neighbors
|
||||
for (int n = 0; n < 8; ++n)
|
||||
{
|
||||
int idx = search_idx + n;
|
||||
// Use offset cache
|
||||
uchar* neighbor = curr_ptr + offsets_[idx];
|
||||
if (*neighbor == BACKGROUND) continue;
|
||||
|
||||
dir = idx & 7;
|
||||
// --- EXECUTE MOVE ---
|
||||
curr_y += dy_[dir];
|
||||
curr_x += dx_[dir];
|
||||
|
||||
|
||||
// Check bounds //we need to move out of the range //if first line, and internal contour, we let it go, but no further from this line
|
||||
if( curr_y < rowRange.start ){
|
||||
return false;
|
||||
}
|
||||
if ((search_idx <= 1) || (dir <= search_idx - 2))
|
||||
{
|
||||
*curr_ptr = VISITED_OUTER_RIGHT;
|
||||
}
|
||||
else if (*curr_ptr == FOREGROUND)
|
||||
{
|
||||
*curr_ptr = VISITED_;
|
||||
}
|
||||
|
||||
// Short-circuit Jacob's Check
|
||||
if (curr_ptr == start_ptr) {
|
||||
if (!is_first_move && dir == start_dir) {
|
||||
return true;//done
|
||||
}
|
||||
}
|
||||
|
||||
curr_ptr = neighbor;//move ptr
|
||||
|
||||
// Reset search index for Moore neighbor
|
||||
search_idx = (dir +6) & 7;
|
||||
break;
|
||||
|
||||
}
|
||||
if (is_first_move) {
|
||||
if(dir==-1){//single pixel
|
||||
*curr_ptr = VISITED_OUTER_RIGHT;
|
||||
break;//not moved
|
||||
}
|
||||
start_dir = dir;
|
||||
is_first_move = false;
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void operator()(const cv::Range& range) const CV_OVERRIDE
|
||||
{
|
||||
// Pre-allocate buffer to avoid re-allocation during moves
|
||||
TRUCOPagedContour<4096> buffer;
|
||||
|
||||
int cols = padded_.cols;
|
||||
|
||||
for (int i = range.start; i < range.end; ++i)
|
||||
{
|
||||
const cv::Range& rowRange = ranges_[i];
|
||||
auto& local_contours = accumulator_[i];
|
||||
|
||||
// Hint for result vector size
|
||||
local_contours.reserve(2048);
|
||||
|
||||
for (int r = rowRange.start; r <= rowRange.end; ++r)
|
||||
{
|
||||
uchar* row_ptr = padded_.data + r * step_;
|
||||
|
||||
// "c" is updated by the find* functions
|
||||
for (int c = 1; c < cols - 1; )
|
||||
{
|
||||
// 1. FAST SCAN: Skip background pixels
|
||||
if ((c = findStartContourPoint(row_ptr, cols, c)) == cols) break;
|
||||
|
||||
// 2. CHECK: Only process if actually FOREGROUND (redundancy check)
|
||||
if (row_ptr[c] == FOREGROUND && r<rowRange.end )
|
||||
{
|
||||
if( traceContour(&buffer,r,c,row_ptr,rowRange,true)){
|
||||
// Post-processing
|
||||
if (buffer.size() > 1 && buffer.back() == buffer.front()) {
|
||||
buffer.pop_back();
|
||||
}
|
||||
if (buffer.size() >= minSize_) {
|
||||
// Instead of copying the vector, we move it.
|
||||
local_contours.emplace_back();
|
||||
buffer.copyTo(local_contours.back());
|
||||
if( r==rowRange.start){//register this so we can zip them as Suzuki&Abe method
|
||||
local_contours.idx_external_firstLine.push_back((int)(local_contours.size()-1) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if( row_ptr[c] == FOREGROUND && r==rowRange.end ){//extra step to mark east pixels only so that internal contours can be correctly extracted in this line
|
||||
traceExternalContourMock(r,c,row_ptr,rowRange);
|
||||
}
|
||||
|
||||
// 3. FAST SCAN: Find end of current component to skip processing it again
|
||||
c = findEndContourPoint(row_ptr, cols, c + 1);
|
||||
if(c>=cols)break;//end of row
|
||||
//internal contour
|
||||
if(row_ptr[c-1]>VISITED_OUTER_RIGHT && r>rowRange.start){//inner contours of first line are handled by the thread above
|
||||
|
||||
if(traceContour(&buffer,r,c-1,row_ptr,rowRange,false)){
|
||||
// Post-processing
|
||||
if (buffer.size() > 1 && buffer.back() == buffer.front()) {
|
||||
buffer.pop_back();
|
||||
}
|
||||
if (buffer.size() >= minSize_) {
|
||||
local_contours.emplace_back();
|
||||
buffer.copyTo(local_contours.back());
|
||||
if( r==rowRange.end){//register this so we can zip them as Suzuki&Abe method
|
||||
local_contours.idx_internal_lastLine.push_back((int)(local_contours.size() -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline int findStartContourPoint(uchar* src_data, int width, int j)
|
||||
{
|
||||
#if (CV_SIMD || CV_SIMD_SCALABLE)
|
||||
cv::v_uint8 v_zero = cv::vx_setzero_u8();
|
||||
for (; j <= width - cv::VTraits<cv::v_uint8>::vlanes(); j += cv::VTraits<cv::v_uint8>::vlanes())
|
||||
{
|
||||
cv::v_uint8 vmask = (cv::v_ne(cv::vx_load((uchar*)(src_data + j)), v_zero));
|
||||
if (cv::v_check_any(vmask))
|
||||
{
|
||||
j += cv::v_scan_forward(vmask);
|
||||
return j;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
for (; j < width && !src_data[j]; ++j)
|
||||
;
|
||||
return j;
|
||||
}
|
||||
|
||||
inline static int findEndContourPoint(uchar* src_data,int width, int j)
|
||||
{
|
||||
#if (CV_SIMD || CV_SIMD_SCALABLE)
|
||||
if (j < width && !src_data[j])
|
||||
{
|
||||
return j;
|
||||
}
|
||||
else
|
||||
{
|
||||
cv::v_uint8 v_zero = cv::vx_setzero_u8();
|
||||
for (; j <= width - cv::VTraits<cv::v_uint8>::vlanes(); j += cv::VTraits<cv::v_uint8>::vlanes())
|
||||
{
|
||||
cv::v_uint8 vmask = (cv::v_eq(cv::vx_load((uchar*)(src_data + j)), v_zero));
|
||||
if (cv::v_check_any(vmask))
|
||||
{
|
||||
j += cv::v_scan_forward(vmask);
|
||||
return j;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
for (; j < width && src_data[j]; ++j)
|
||||
;
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
cv::Mat padded_;
|
||||
const std::vector<cv::Range>& ranges_;
|
||||
AccumulatorType& accumulator_;
|
||||
size_t minSize_;
|
||||
size_t step_;
|
||||
int offsets_[16];
|
||||
|
||||
// 0=E, 1=NE, 2=N, 3=NW, 4=W, 5=SW, 6=S, 7=SE (CCW Rotation)
|
||||
const int dx_[8] = { 1, 1, 0, -1, -1, -1, 0, 1 };
|
||||
const int dy_[8] = { 0, -1, -1, -1, 0, 1, 1, 1 };
|
||||
// Constants defined once
|
||||
const uchar FOREGROUND = 255;
|
||||
const uchar BACKGROUND = 0;
|
||||
const uchar VISITED_OUTER_RIGHT = 100;
|
||||
const uchar VISITED_ = 200;
|
||||
};
|
||||
|
||||
|
||||
void approxContour(std::vector<cv::Point> &inout,cv::ContourApproximationModes contApprox_) {
|
||||
size_t n = inout.size();
|
||||
if (n <= 1) return;
|
||||
|
||||
if (contApprox_ == cv::CHAIN_APPROX_SIMPLE) {
|
||||
std::vector<cv::Point> result;
|
||||
result.reserve(n);
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
size_t prev_i = (i == 0) ? n - 1 : i - 1;
|
||||
size_t next_i = (i == n - 1) ? 0 : i + 1;
|
||||
|
||||
cv::Point v1 = inout[i] - inout[prev_i];
|
||||
cv::Point v2 = inout[next_i] - inout[i];
|
||||
|
||||
if (v1 != v2) {
|
||||
result.push_back(inout[i]);
|
||||
}
|
||||
}
|
||||
if (result.empty() && n > 0) result.push_back(inout[0]);
|
||||
inout = std::move(result);
|
||||
} else if (contApprox_ == cv::CHAIN_APPROX_TC89_L1 || contApprox_ == cv::CHAIN_APPROX_TC89_KCOS) {
|
||||
auto getCode = [](cv::Point d) -> schar {
|
||||
if (d.x == 1) {
|
||||
if (d.y == 0) return 0;
|
||||
if (d.y == -1) return 1;
|
||||
if (d.y == 1) return 7;
|
||||
} else if (d.x == 0) {
|
||||
if (d.y == -1) return 2;
|
||||
if (d.y == 1) return 6;
|
||||
} else if (d.x == -1) {
|
||||
if (d.y == -1) return 3;
|
||||
if (d.y == 0) return 4;
|
||||
if (d.y == 1) return 5;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
cv::ContourCodesStorage::storage_t codesStorage;
|
||||
cv::ContourCodesStorage codes(&codesStorage);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
cv::Point delta = inout[(i + 1) % n] - inout[i];
|
||||
codes.push_back(getCode(delta));
|
||||
}
|
||||
|
||||
cv::ContourPointsStorage::storage_t pointsStorage;
|
||||
cv::ContourPointsStorage points(&pointsStorage);
|
||||
cv::approximateChainTC89(codes, inout[0], contApprox_, points);
|
||||
|
||||
inout.clear();
|
||||
inout.reserve(points.size());
|
||||
for (size_t i = 0; i < points.size(); ++i) {
|
||||
inout.push_back(points.at(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// 1. The Core Implementation (Operates on std::vector directly)
|
||||
// ==========================================================
|
||||
void findTRUContoursImpl(cv::Mat& padded,
|
||||
std::vector<std::vector<cv::Point>>& outContours,
|
||||
int minSize,int contApprox)
|
||||
{
|
||||
// Load Balancing Logic
|
||||
const int nstripes = cv::getNumThreads();
|
||||
std::vector<cv::Range> balancedRanges;
|
||||
if (nstripes > 1) {
|
||||
int rowsPerStripe = (padded.rows - 2) / nstripes;
|
||||
int remainingRows = (padded.rows - 2) % nstripes;
|
||||
int currentRow = 1;
|
||||
for (int t = 0; t < nstripes; ++t) {
|
||||
int startRow = currentRow;
|
||||
int endRow = startRow + rowsPerStripe + (t < remainingRows ? 1 : 0);
|
||||
balancedRanges.emplace_back(startRow, endRow);
|
||||
currentRow = endRow;
|
||||
}
|
||||
}
|
||||
else {
|
||||
balancedRanges.emplace_back(1, padded.rows - 1);
|
||||
}
|
||||
// Parallel Execution
|
||||
std::vector<AccumulatorT> threadAccumulators(balancedRanges.size());
|
||||
TRUCOntourTracer worker(padded, balancedRanges, threadAccumulators, minSize);
|
||||
cv::parallel_for_(cv::Range(0, (int)balancedRanges.size()), worker);
|
||||
|
||||
|
||||
// REORG To Match Suzuki & Abe's Contour Ordering.
|
||||
// Every adjacent strip pair (t, t+1) shares a boundary row. On that row:
|
||||
// * thread t recorded INTERNAL fragments -> idx_internal_lastLine (tail of accT)
|
||||
// * thread t+1 recorded EXTERNAL fragments -> idx_external_firstLine (head of accT1)
|
||||
// Both runs are already in X-ascending order (left-to-right raster scan),
|
||||
// so a two-pointer merge restores the sequential Suzuki & Abe ordering.
|
||||
for (size_t t = 0; t + 1 < threadAccumulators.size(); ++t) {
|
||||
auto& accT = threadAccumulators[t];
|
||||
auto& accT1 = threadAccumulators[t + 1];
|
||||
|
||||
const size_t kI = accT.idx_internal_lastLine.size();
|
||||
const size_t kE = accT1.idx_external_firstLine.size();
|
||||
if (kI == 0 && kE == 0) continue;
|
||||
|
||||
const size_t tailStart = accT.size() - kI;
|
||||
|
||||
// Merge by ascending X of each contour's first point. Contours are moved,
|
||||
// not copied — only std::vector handles (three pointers) change hands.
|
||||
std::vector<std::vector<cv::Point>> merged;
|
||||
merged.reserve(kI + kE);
|
||||
size_t i = tailStart, iEnd = accT.size();
|
||||
size_t j = 0, jEnd = kE;
|
||||
while (i < iEnd && j < jEnd) {
|
||||
if (accT[i].front().x <= accT1[j].front().x)
|
||||
merged.emplace_back(std::move(accT[i++]));
|
||||
else
|
||||
merged.emplace_back(std::move(accT1[j++]));
|
||||
}
|
||||
while (i < iEnd) merged.emplace_back(std::move(accT[i++]));
|
||||
while (j < jEnd) merged.emplace_back(std::move(accT1[j++]));
|
||||
|
||||
// Replace accT's tail with the merged run.
|
||||
accT.resize(tailStart);
|
||||
for (auto& c : merged) accT.emplace_back(std::move(c));
|
||||
|
||||
// Drop the consumed externals from the head of accT1.
|
||||
accT1.erase(accT1.begin(), accT1.begin() + kE);
|
||||
|
||||
// Any remaining bookkeeping in accT1 indexed absolute positions — fix them.
|
||||
for (auto& idx : accT1.idx_internal_lastLine)
|
||||
idx -= static_cast<int>(kE);
|
||||
|
||||
// This boundary's bookkeeping is now consumed.
|
||||
accT.idx_internal_lastLine.clear();
|
||||
accT1.idx_external_firstLine.clear();
|
||||
}
|
||||
|
||||
// ZERO-COPY MERGE
|
||||
size_t totalContours = 0;
|
||||
for (auto& tVec : threadAccumulators)
|
||||
totalContours += tVec.size();
|
||||
|
||||
|
||||
outContours.clear();
|
||||
outContours.reserve(totalContours);
|
||||
// move the contours from thread accumulators to output without copying pixel data
|
||||
for (auto& tVec : threadAccumulators) {
|
||||
// move_iterator moves the vector internals (pointers) without copying pixel data
|
||||
outContours.insert(outContours.end(),
|
||||
std::make_move_iterator(tVec.begin()),
|
||||
std::make_move_iterator(tVec.end()));
|
||||
}
|
||||
// reverse the order to match original findContours Suzuki&Abe
|
||||
std::reverse(outContours.begin(), outContours.end());
|
||||
|
||||
|
||||
//7. now, lets do the contour approximation if needed in parallel
|
||||
if(contApprox!=cv::CHAIN_APPROX_NONE){
|
||||
cv::parallel_for_(cv::Range(0, (int)outContours.size()), [&](const cv::Range& range){
|
||||
for(int i=range.start;i<range.end;i++){
|
||||
approxContour(outContours[i],(cv::ContourApproximationModes)contApprox);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
namespace cv{
|
||||
|
||||
// ==========================================================
|
||||
//
|
||||
// Public API: Handles OutputArray and dispatches to the core implementation
|
||||
// This is a modified version of the original TRUCO parallel algorithm to produce the exact same output
|
||||
// as original findContours with RETR_LIST mode (no hierarchy, all contours are external). It also supports contour approximation.
|
||||
//
|
||||
// ==========================================================
|
||||
void findTRUContours(InputArray _src, OutputArrayOfArrays _contours, int minSize, bool binarize,int method)
|
||||
{
|
||||
CV_INSTRUMENT_REGION();
|
||||
Mat src = _src.getMat();
|
||||
CV_Assert(!src.empty() && src.type() == CV_8UC1);
|
||||
|
||||
// Buffer handling
|
||||
cv::Mat padded;
|
||||
cv::copyMakeBorder(src, padded, 1, 1, 1, 1, cv::BORDER_CONSTANT, 0);
|
||||
if (binarize)
|
||||
cv::threshold(padded, padded, 0, 255, cv::THRESH_BINARY);
|
||||
|
||||
|
||||
// Fast path: caller passed std::vector<std::vector<cv::Point>> directly.
|
||||
// Write into it without any intermediate copy.
|
||||
if (_contours.kind() == _InputArray::STD_VECTOR_VECTOR) {
|
||||
auto* vec = reinterpret_cast<std::vector<std::vector<cv::Point>>*>(_contours.getObj());
|
||||
findTRUContoursImpl(padded, *vec, minSize,method);
|
||||
}
|
||||
else{ // Slow path: generic OutputArray — build in a temp vector then copy.
|
||||
std::vector<std::vector<cv::Point>> tempContours;
|
||||
findTRUContoursImpl(padded, tempContours, minSize,method);
|
||||
|
||||
_contours.create((int)tempContours.size(), 1, 0, -1, true);
|
||||
for (size_t i = 0; i < tempContours.size(); i++) {
|
||||
_contours.create((int)tempContours[i].size(), 1, CV_32SC2, (int)i, true);
|
||||
Mat m = _contours.getMat((int)i);
|
||||
std::memcpy(m.data, tempContours[i].data(), tempContours[i].size() * sizeof(cv::Point));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user