mirror of
https://github.com/opencv/opencv.git
synced 2025-08-06 14:36:36 +08:00
add new contour filtering, test, refactoring
This commit is contained in:
parent
e202116b56
commit
84590f96e5
@ -34,7 +34,7 @@ class aruco_objdetect_test(NewOpenCVTests):
|
||||
aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type[aruco_type_i])
|
||||
board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict)
|
||||
charuco_detector = cv.aruco.CharucoDetector(board)
|
||||
from_cv_img = board.generateImage((cols*square_size*10, rows*square_size*10))
|
||||
from_cv_img = board.generateImage((cols*square_size, rows*square_size))
|
||||
|
||||
#draw desk using svg
|
||||
fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg")
|
||||
@ -50,15 +50,20 @@ class aruco_objdetect_test(NewOpenCVTests):
|
||||
pm.make_charuco_board()
|
||||
pm.save()
|
||||
drawing = svg2rlg(filesvg)
|
||||
renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=720)
|
||||
renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=72)
|
||||
from_svg_img = cv.imread(filepng)
|
||||
_charucoCorners, _charuco_ids_svg, marker_corners_svg, marker_ids_svg = charuco_detector.detectBoard(from_svg_img)
|
||||
_charucoCorners, _charuco_ids_cv, marker_corners_cv, marker_ids_cv = charuco_detector.detectBoard(from_cv_img)
|
||||
marker_corners_svg_map, marker_corners_cv_map = {}, {}
|
||||
for i in range(len(marker_ids_svg)):
|
||||
marker_corners_svg_map[int(marker_ids_svg[i][0])] = marker_corners_svg[i]
|
||||
for i in range(len(marker_ids_cv)):
|
||||
marker_corners_cv_map[int(marker_ids_cv[i][0])] = marker_corners_cv[i]
|
||||
|
||||
#test
|
||||
_charucoCorners, _charucoIds, markerCorners_svg, markerIds_svg = charuco_detector.detectBoard(from_svg_img)
|
||||
_charucoCorners, _charucoIds, markerCorners_cv, markerIds_cv = charuco_detector.detectBoard(from_cv_img)
|
||||
|
||||
np.testing.assert_allclose(markerCorners_svg, markerCorners_cv, 0.1, 0.1)
|
||||
np.testing.assert_allclose(markerIds_svg, markerIds_cv, 0.1, 0.1)
|
||||
for key_svg in marker_corners_svg_map.keys():
|
||||
marker_svg = marker_corners_svg_map[key_svg]
|
||||
marker_cv = marker_corners_cv_map[key_svg]
|
||||
np.testing.assert_allclose(marker_svg, marker_cv, 0.1, 0.1)
|
||||
finally:
|
||||
if os.path.exists(filesvg):
|
||||
os.remove(filesvg)
|
||||
@ -87,7 +92,7 @@ class aruco_objdetect_test(NewOpenCVTests):
|
||||
aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type)
|
||||
board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict)
|
||||
charuco_detector = cv.aruco.CharucoDetector(board)
|
||||
from_cv_img = board.generateImage((cols*square_size*10, rows*square_size*10))
|
||||
from_cv_img = board.generateImage((cols*square_size, rows*square_size))
|
||||
|
||||
#draw desk using svg
|
||||
fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg")
|
||||
@ -102,17 +107,24 @@ class aruco_objdetect_test(NewOpenCVTests):
|
||||
pm.make_charuco_board()
|
||||
pm.save()
|
||||
drawing = svg2rlg(filesvg)
|
||||
renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=720)
|
||||
renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=72)
|
||||
from_svg_img = cv.imread(filepng)
|
||||
|
||||
#test
|
||||
_charucoCorners, _charucoIds, markerCorners_svg, markerIds_svg = charuco_detector.detectBoard(from_svg_img)
|
||||
_charucoCorners, _charucoIds, markerCorners_cv, markerIds_cv = charuco_detector.detectBoard(from_cv_img)
|
||||
_charucoCorners, _charuco_ids_svg, marker_corners_svg, marker_ids_svg = charuco_detector.detectBoard(from_svg_img)
|
||||
_charucoCorners, _charuco_ids_cv, marker_corners_cv, marker_ids_cv = charuco_detector.detectBoard(from_cv_img)
|
||||
marker_corners_svg_map, marker_corners_cv_map = {}, {}
|
||||
for i in range(len(marker_ids_svg)):
|
||||
marker_corners_svg_map[int(marker_ids_svg[i][0])] = marker_corners_svg[i]
|
||||
for i in range(len(marker_ids_cv)):
|
||||
marker_corners_cv_map[int(marker_ids_cv[i][0])] = marker_corners_cv[i]
|
||||
|
||||
np.testing.assert_allclose(markerCorners_svg, markerCorners_cv, 0.1, 0.1)
|
||||
np.testing.assert_allclose(markerIds_svg, markerIds_cv, 0.1, 0.1)
|
||||
for key_svg in marker_corners_svg_map.keys():
|
||||
marker_svg = marker_corners_svg_map[key_svg]
|
||||
marker_cv = marker_corners_cv_map[key_svg]
|
||||
np.testing.assert_allclose(marker_svg, marker_cv, 0.1, 0.1)
|
||||
finally:
|
||||
if os.path.exists(filesvg):
|
||||
os.remove(filesvg)
|
||||
if os.path.exists(filepng):
|
||||
os.remove(filepng)
|
||||
os.remove(filepng)
|
@ -33,7 +33,7 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters {
|
||||
polygonalApproxAccuracyRate = 0.03;
|
||||
minCornerDistanceRate = 0.05;
|
||||
minDistanceToBorder = 3;
|
||||
minMarkerDistanceRate = 0.05;
|
||||
minMarkerDistanceRate = 0.125;
|
||||
cornerRefinementMethod = (int)CORNER_REFINE_NONE;
|
||||
cornerRefinementWinSize = 5;
|
||||
relativeCornerRefinmentWinSize = 0.3f;
|
||||
@ -100,12 +100,26 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters {
|
||||
/// minimum distance of any corner to the image border for detected markers (in pixels) (default 3)
|
||||
CV_PROP_RW int minDistanceToBorder;
|
||||
|
||||
/** @brief minimum mean distance beetween two marker corners to be considered imilar, so that the smaller one is removed.
|
||||
/** @brief minimum average distance between the corners of the two markers to be grouped (default 0.125).
|
||||
*
|
||||
* The rate is relative to the smaller perimeter of the two markers (default 0.05).
|
||||
* The rate is relative to the smaller perimeter of the two markers.
|
||||
* Two markers are grouped if average distance between the corners of the two markers is less than
|
||||
* min(MarkerPerimeter1, MarkerPerimeter2)*minMarkerDistanceRate.
|
||||
*
|
||||
* default value is 0.125 because 0.125*MarkerPerimeter = (MarkerPerimeter / 4) * 0.5 = half the side of the marker.
|
||||
*
|
||||
* @note default value was changed from 0.05 after 4.8.1 release, because the filtering algorithm has been changed.
|
||||
* Now a few candidates from the same group can be added to the list of candidates if they are far from each other.
|
||||
* @sa minGroupDistance.
|
||||
*/
|
||||
CV_PROP_RW double minMarkerDistanceRate;
|
||||
|
||||
/** @brief minimum average distance between the corners of the two markers in group to add them to the list of candidates
|
||||
*
|
||||
* The average distance between the corners of the two markers is calculated relative to its module size (default 0.21).
|
||||
*/
|
||||
CV_PROP_RW float minGroupDistance = 0.21f;
|
||||
|
||||
/** @brief default value CORNER_REFINE_NONE */
|
||||
CV_PROP_RW int cornerRefinementMethod;
|
||||
|
||||
|
@ -50,6 +50,7 @@ static inline bool readWrite(DetectorParameters ¶ms, const FileNode* readNod
|
||||
readNode, writeStorage);
|
||||
check |= readWriteParameter("minOtsuStdDev", params.minOtsuStdDev, readNode, writeStorage);
|
||||
check |= readWriteParameter("errorCorrectionRate", params.errorCorrectionRate, readNode, writeStorage);
|
||||
check |= readWriteParameter("minGroupDistance", params.minGroupDistance, readNode, writeStorage);
|
||||
// new aruco 3 functionality
|
||||
check |= readWriteParameter("useAruco3Detection", params.useAruco3Detection, readNode, writeStorage);
|
||||
check |= readWriteParameter("minSideLengthCanonicalImg", params.minSideLengthCanonicalImg, readNode, writeStorage);
|
||||
@ -212,149 +213,65 @@ static void _reorderCandidatesCorners(vector<vector<Point2f> > &candidates) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief to make sure that the corner's order of both candidates (default/white) is the same
|
||||
*/
|
||||
static vector<Point2f> alignContourOrder(Point2f corner, vector<Point2f> candidate) {
|
||||
uint8_t r=0;
|
||||
double min = norm( Vec2f( corner - candidate[0] ), NORM_L2SQR);
|
||||
for(uint8_t pos=1; pos < 4; pos++) {
|
||||
double nDiff = norm( Vec2f( corner - candidate[pos] ), NORM_L2SQR);
|
||||
if(nDiff < min){
|
||||
r = pos;
|
||||
min =nDiff;
|
||||
}
|
||||
static float getAverageModuleSize(const vector<Point2f>& markerCorners, int markerSize, int markerBorderBits) {
|
||||
float averageArucoModuleSize = 0.f;
|
||||
for (size_t i = 0ull; i < 4ull; i++) {
|
||||
averageArucoModuleSize += sqrt(normL2Sqr<float>(Point2f(markerCorners[i] - markerCorners[(i+1ull) % 4ull])));
|
||||
}
|
||||
std::rotate(candidate.begin(), candidate.begin() + r, candidate.end());
|
||||
return candidate;
|
||||
int numModules = markerSize + markerBorderBits * 2;
|
||||
averageArucoModuleSize /= ((float)markerCorners.size()*numModules);
|
||||
return averageArucoModuleSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check candidates that are too close to each other, save the potential candidates
|
||||
* (i.e. biggest/smallest contour) and remove the rest
|
||||
*/
|
||||
static void _filterTooCloseCandidates(const vector<vector<Point2f> > &candidatesIn,
|
||||
vector<vector<vector<Point2f> > > &candidatesSetOut,
|
||||
const vector<vector<Point> > &contoursIn,
|
||||
vector<vector<vector<Point> > > &contoursSetOut,
|
||||
double minMarkerDistanceRate, bool detectInvertedMarker) {
|
||||
static bool checkMarker1InMarker2(const vector<Point2f>& marker1, const vector<Point2f>& marker2) {
|
||||
return pointPolygonTest(marker2, marker1[0], false) >= 0 && pointPolygonTest(marker2, marker1[1], false) >= 0 &&
|
||||
pointPolygonTest(marker2, marker1[2], false) >= 0 && pointPolygonTest(marker2, marker1[3], false) >= 0;
|
||||
}
|
||||
|
||||
CV_Assert(minMarkerDistanceRate >= 0);
|
||||
vector<int> candGroup;
|
||||
candGroup.resize(candidatesIn.size(), -1);
|
||||
vector<vector<unsigned int> > groupedCandidates;
|
||||
for(unsigned int i = 0; i < candidatesIn.size(); i++) {
|
||||
bool isSingleContour = true;
|
||||
for(unsigned int j = i + 1; j < candidatesIn.size(); j++) {
|
||||
struct MarkerCandidate {
|
||||
vector<Point2f> corners;
|
||||
vector<Point> contour;
|
||||
float perimeter = 0.f;
|
||||
};
|
||||
|
||||
int minimumPerimeter = min((int)contoursIn[i].size(), (int)contoursIn[j].size() );
|
||||
struct MarkerCandidateTree : MarkerCandidate{
|
||||
int parent = -1;
|
||||
int depth = 0;
|
||||
vector<MarkerCandidate> closeContours;
|
||||
|
||||
// fc is the first corner considered on one of the markers, 4 combinations are possible
|
||||
for(int fc = 0; fc < 4; fc++) {
|
||||
double distSq = 0;
|
||||
for(int c = 0; c < 4; c++) {
|
||||
// modC is the corner considering first corner is fc
|
||||
int modC = (c + fc) % 4;
|
||||
distSq += (candidatesIn[i][modC].x - candidatesIn[j][c].x) *
|
||||
(candidatesIn[i][modC].x - candidatesIn[j][c].x) +
|
||||
(candidatesIn[i][modC].y - candidatesIn[j][c].y) *
|
||||
(candidatesIn[i][modC].y - candidatesIn[j][c].y);
|
||||
}
|
||||
distSq /= 4.;
|
||||
MarkerCandidateTree() {}
|
||||
|
||||
// if mean square distance is too low, remove the smaller one of the two markers
|
||||
double minMarkerDistancePixels = double(minimumPerimeter) * minMarkerDistanceRate;
|
||||
if(distSq < minMarkerDistancePixels * minMarkerDistancePixels) {
|
||||
isSingleContour = false;
|
||||
// i and j are not related to a group
|
||||
if(candGroup[i]<0 && candGroup[j]<0){
|
||||
// mark candidates with their corresponding group number
|
||||
candGroup[i] = candGroup[j] = (int)groupedCandidates.size();
|
||||
|
||||
// create group
|
||||
vector<unsigned int> grouped;
|
||||
grouped.push_back(i);
|
||||
grouped.push_back(j);
|
||||
groupedCandidates.push_back( grouped );
|
||||
}
|
||||
// i is related to a group
|
||||
else if(candGroup[i] > -1 && candGroup[j] == -1){
|
||||
int group = candGroup[i];
|
||||
candGroup[j] = group;
|
||||
|
||||
// add to group
|
||||
groupedCandidates[group].push_back( j );
|
||||
}
|
||||
// j is related to a group
|
||||
else if(candGroup[j] > -1 && candGroup[i] == -1){
|
||||
int group = candGroup[j];
|
||||
candGroup[i] = group;
|
||||
|
||||
// add to group
|
||||
groupedCandidates[group].push_back( i );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSingleContour && candGroup[i] < 0)
|
||||
{
|
||||
candGroup[i] = (int)groupedCandidates.size();
|
||||
vector<unsigned int> grouped;
|
||||
grouped.push_back(i);
|
||||
grouped.push_back(i); // step "save possible candidates" require minimum 2 elements
|
||||
groupedCandidates.push_back(grouped);
|
||||
MarkerCandidateTree(vector<Point2f>&& corners_, vector<Point>&& contour_) {
|
||||
corners = std::move(corners_);
|
||||
contour = std::move(contour_);
|
||||
perimeter = 0.f;
|
||||
for (size_t i = 0ull; i < 4ull; i++) {
|
||||
perimeter += sqrt(normL2Sqr<float>(Point2f(corners[i] - corners[(i+1ull) % 4ull])));
|
||||
}
|
||||
}
|
||||
|
||||
// save possible candidates
|
||||
candidatesSetOut.clear();
|
||||
contoursSetOut.clear();
|
||||
|
||||
vector<vector<Point2f> > biggerCandidates;
|
||||
vector<vector<Point> > biggerContours;
|
||||
vector<vector<Point2f> > smallerCandidates;
|
||||
vector<vector<Point> > smallerContours;
|
||||
|
||||
// save possible candidates
|
||||
for(unsigned int i = 0; i < groupedCandidates.size(); i++) {
|
||||
unsigned int smallerIdx = groupedCandidates[i][0];
|
||||
unsigned int biggerIdx = smallerIdx;
|
||||
double smallerArea = contourArea(candidatesIn[smallerIdx]);
|
||||
double biggerArea = smallerArea;
|
||||
|
||||
// evaluate group elements
|
||||
for(unsigned int j = 1; j < groupedCandidates[i].size(); j++) {
|
||||
unsigned int currIdx = groupedCandidates[i][j];
|
||||
double currArea = contourArea(candidatesIn[currIdx]);
|
||||
|
||||
// check if current contour is bigger
|
||||
if(currArea >= biggerArea) {
|
||||
biggerIdx = currIdx;
|
||||
biggerArea = currArea;
|
||||
}
|
||||
|
||||
// check if current contour is smaller
|
||||
if(currArea < smallerArea && detectInvertedMarker) {
|
||||
smallerIdx = currIdx;
|
||||
smallerArea = currArea;
|
||||
}
|
||||
}
|
||||
|
||||
// add contours and candidates
|
||||
biggerCandidates.push_back(candidatesIn[biggerIdx]);
|
||||
biggerContours.push_back(contoursIn[biggerIdx]);
|
||||
if(detectInvertedMarker) {
|
||||
smallerCandidates.push_back(alignContourOrder(candidatesIn[biggerIdx][0], candidatesIn[smallerIdx]));
|
||||
smallerContours.push_back(contoursIn[smallerIdx]);
|
||||
}
|
||||
bool operator<(const MarkerCandidateTree& m) const {
|
||||
// sorting the contors in descending order
|
||||
return perimeter > m.perimeter;
|
||||
}
|
||||
// to preserve the structure :: candidateSet< defaultCandidates, whiteCandidates >
|
||||
// default candidates
|
||||
candidatesSetOut.push_back(biggerCandidates);
|
||||
contoursSetOut.push_back(biggerContours);
|
||||
// white candidates
|
||||
candidatesSetOut.push_back(smallerCandidates);
|
||||
contoursSetOut.push_back(smallerContours);
|
||||
};
|
||||
|
||||
|
||||
// returns the average distance between the marker points
|
||||
float static inline getAverageDistance(const std::vector<Point2f>& marker1, const std::vector<Point2f>& marker2) {
|
||||
float minDistSq = std::numeric_limits<float>::max();
|
||||
// fc is the first corner considered on one of the markers, 4 combinations are possible
|
||||
for(int fc = 0; fc < 4; fc++) {
|
||||
float distSq = 0;
|
||||
for(int c = 0; c < 4; c++) {
|
||||
// modC is the corner considering first corner is fc
|
||||
int modC = (c + fc) % 4;
|
||||
distSq += normL2Sqr<float>(marker1[modC] - marker2[c]);
|
||||
}
|
||||
distSq /= 4.f;
|
||||
minDistSq = min(minDistSq, distSq);
|
||||
}
|
||||
return sqrt(minDistSq);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -403,29 +320,6 @@ static void _detectInitialCandidates(const Mat &grey, vector<vector<Point2f> > &
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Detect square candidates in the input image
|
||||
*/
|
||||
static void _detectCandidates(InputArray _grayImage, vector<vector<vector<Point2f> > >& candidatesSetOut,
|
||||
vector<vector<vector<Point> > >& contoursSetOut, const DetectorParameters &_params) {
|
||||
Mat grey = _grayImage.getMat();
|
||||
CV_DbgAssert(grey.total() != 0);
|
||||
CV_DbgAssert(grey.type() == CV_8UC1);
|
||||
|
||||
/// 1. DETECT FIRST SET OF CANDIDATES
|
||||
vector<vector<Point2f> > candidates;
|
||||
vector<vector<Point> > contours;
|
||||
_detectInitialCandidates(grey, candidates, contours, _params);
|
||||
/// 2. SORT CORNERS
|
||||
_reorderCandidatesCorners(candidates);
|
||||
|
||||
/// 3. FILTER OUT NEAR CANDIDATE PAIRS
|
||||
// save the outter/inner border (i.e. potential candidates)
|
||||
_filterTooCloseCandidates(candidates, candidatesSetOut, contours, contoursSetOut,
|
||||
_params.minMarkerDistanceRate, _params.detectInvertedMarker);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Given an input image and candidate corners, extract the bits of the candidate, including
|
||||
* the border bits
|
||||
@ -527,12 +421,10 @@ static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) {
|
||||
* 1 if the candidate is a black candidate (default candidate)
|
||||
* 2 if the candidate is a white candidate
|
||||
*/
|
||||
static uint8_t _identifyOneCandidate(const Dictionary& dictionary, InputArray _image,
|
||||
static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _image,
|
||||
const vector<Point2f>& _corners, int& idx,
|
||||
const DetectorParameters& params, int& rotation,
|
||||
const float scale = 1.f) {
|
||||
CV_DbgAssert(_corners.size() == 4);
|
||||
CV_DbgAssert(_image.getMat().total() != 0);
|
||||
CV_DbgAssert(params.markerBorderBits > 0);
|
||||
uint8_t typ=1;
|
||||
// get bits
|
||||
@ -610,87 +502,6 @@ static size_t _findOptPyrImageForCanonicalImg(
|
||||
return optLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Identify square candidates according to a marker dictionary
|
||||
*/
|
||||
|
||||
static void _identifyCandidates(InputArray grey,
|
||||
const vector<Mat>& image_pyr,
|
||||
vector<vector<vector<Point2f> > >& _candidatesSet,
|
||||
vector<vector<vector<Point> > >& _contoursSet, const Dictionary &_dictionary,
|
||||
vector<vector<Point2f> >& _accepted, vector<vector<Point> >& _contours, vector<int>& ids,
|
||||
const DetectorParameters ¶ms,
|
||||
OutputArrayOfArrays _rejected = noArray()) {
|
||||
CV_DbgAssert(grey.getMat().total() != 0);
|
||||
CV_DbgAssert(grey.getMat().type() == CV_8UC1);
|
||||
int ncandidates = (int)_candidatesSet[0].size();
|
||||
vector<vector<Point2f> > accepted;
|
||||
vector<vector<Point2f> > rejected;
|
||||
vector<vector<Point> > contours;
|
||||
|
||||
vector<int> idsTmp(ncandidates, -1);
|
||||
vector<int> rotated(ncandidates, 0);
|
||||
vector<uint8_t> validCandidates(ncandidates, 0);
|
||||
|
||||
//// Analyze each of the candidates
|
||||
parallel_for_(Range(0, ncandidates), [&](const Range &range) {
|
||||
const int begin = range.start;
|
||||
const int end = range.end;
|
||||
|
||||
vector<vector<Point2f> >& candidates = params.detectInvertedMarker ? _candidatesSet[1] : _candidatesSet[0];
|
||||
vector<vector<Point> >& contourS = params.detectInvertedMarker ? _contoursSet[1] : _contoursSet[0];
|
||||
|
||||
for(int i = begin; i < end; i++) {
|
||||
int currId = -1;
|
||||
// implements equation (4)
|
||||
if (params.useAruco3Detection) {
|
||||
const int perimeterOfContour = static_cast<int>(contourS[i].size());
|
||||
const int min_perimeter = params.minSideLengthCanonicalImg * 4;
|
||||
const size_t nearestImgId = _findOptPyrImageForCanonicalImg(image_pyr, grey.cols(), perimeterOfContour, min_perimeter);
|
||||
const float scale = image_pyr[nearestImgId].cols / static_cast<float>(grey.cols());
|
||||
|
||||
validCandidates[i] = _identifyOneCandidate(_dictionary, image_pyr[nearestImgId], candidates[i], currId, params, rotated[i], scale);
|
||||
}
|
||||
else {
|
||||
validCandidates[i] = _identifyOneCandidate(_dictionary, grey, candidates[i], currId, params, rotated[i]);
|
||||
}
|
||||
|
||||
if(validCandidates[i] > 0)
|
||||
idsTmp[i] = currId;
|
||||
}
|
||||
});
|
||||
|
||||
for(int i = 0; i < ncandidates; i++) {
|
||||
if(validCandidates[i] > 0) {
|
||||
// to choose the right set of candidates :: 0 for default, 1 for white markers
|
||||
uint8_t set = validCandidates[i]-1;
|
||||
|
||||
// shift corner positions to the correct rotation
|
||||
correctCornerPosition(_candidatesSet[set][i], rotated[i]);
|
||||
|
||||
if( !params.detectInvertedMarker && validCandidates[i] == 2 )
|
||||
continue;
|
||||
|
||||
// add valid candidate
|
||||
accepted.push_back(_candidatesSet[set][i]);
|
||||
ids.push_back(idsTmp[i]);
|
||||
|
||||
contours.push_back(_contoursSet[set][i]);
|
||||
|
||||
} else {
|
||||
rejected.push_back(_candidatesSet[0][i]);
|
||||
}
|
||||
}
|
||||
|
||||
// parse output
|
||||
_accepted = accepted;
|
||||
|
||||
_contours= contours;
|
||||
|
||||
if(_rejected.needed()) {
|
||||
_copyVector2Output(rejected, _rejected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Line fitting A * B = C :: Called from function refineCandidateLines
|
||||
@ -846,16 +657,210 @@ struct ArucoDetector::ArucoDetectorImpl {
|
||||
ArucoDetectorImpl(const Dictionary &_dictionary, const DetectorParameters &_detectorParams,
|
||||
const RefineParameters& _refineParams): dictionary(_dictionary),
|
||||
detectorParams(_detectorParams), refineParams(_refineParams) {}
|
||||
/**
|
||||
* @brief Detect square candidates in the input image
|
||||
*/
|
||||
void detectCandidates(const Mat& grey, vector<vector<Point2f> >& candidates, vector<vector<Point> >& contours) {
|
||||
/// 1. DETECT FIRST SET OF CANDIDATES
|
||||
_detectInitialCandidates(grey, candidates, contours, detectorParams);
|
||||
/// 2. SORT CORNERS
|
||||
_reorderCandidatesCorners(candidates);
|
||||
}
|
||||
|
||||
float getAverageArucoPinSize(vector<Point2f> markerCorners) {
|
||||
float averageArucoModuleSize = 0.f;
|
||||
int numPins = dictionary.markerSize + detectorParams.markerBorderBits * 2;
|
||||
for (size_t i = 0ull; i < markerCorners.size(); i++) {
|
||||
averageArucoModuleSize += sqrt(normL2Sqr<float>(Point2f(markerCorners[i] - markerCorners[(i+1ull)%markerCorners.size()])));
|
||||
/**
|
||||
* @brief FILTER OUT NEAR CANDIDATE PAIRS
|
||||
*
|
||||
* save the outter/inner border (i.e. potential candidates) to vector<MarkerCandidateTree>,
|
||||
* clear candidates and contours
|
||||
*/
|
||||
vector<MarkerCandidateTree>
|
||||
filterTooCloseCandidates(vector<vector<Point2f> > &candidates, vector<vector<Point> > &contours) {
|
||||
CV_Assert(detectorParams.minMarkerDistanceRate >= 0.);
|
||||
vector<MarkerCandidateTree> candidateTree(candidates.size());
|
||||
for(size_t i = 0ull; i < candidates.size(); i++) {
|
||||
candidateTree[i] = MarkerCandidateTree(std::move(candidates[i]), std::move(contours[i]));
|
||||
}
|
||||
averageArucoModuleSize /= ((float)markerCorners.size()*numPins);
|
||||
return averageArucoModuleSize;
|
||||
}
|
||||
candidates.clear();
|
||||
contours.clear();
|
||||
|
||||
// sort candidates from big to small
|
||||
std::sort(candidateTree.begin(), candidateTree.end());
|
||||
// group index for each candidate
|
||||
vector<int> groupId(candidateTree.size(), -1);
|
||||
vector<vector<size_t> > groupedCandidates;
|
||||
vector<bool> isSelectedContours(candidateTree.size(), true);
|
||||
|
||||
size_t countSelectedContours = 0ull;
|
||||
for (size_t i = 0ull; i < candidateTree.size(); i++) {
|
||||
for (size_t j = i + 1ull; j < candidateTree.size(); j++) {
|
||||
float minDist = getAverageDistance(candidateTree[i].corners, candidateTree[j].corners);
|
||||
// if mean distance is too low, group markers
|
||||
// the distance between the points of two independent markers should be more than half the side of the marker
|
||||
// half the side of the marker = (perimeter / 4) * 0.5 = perimeter * 0.125
|
||||
if(minDist < candidateTree[j].perimeter*(float)detectorParams.minMarkerDistanceRate) {
|
||||
isSelectedContours[i] = false;
|
||||
isSelectedContours[j] = false;
|
||||
// i and j are not related to a group
|
||||
if(groupId[i] < 0 && groupId[j] < 0){
|
||||
// mark candidates with their corresponding group number
|
||||
groupId[i] = groupId[j] = (int)groupedCandidates.size();
|
||||
// create group
|
||||
groupedCandidates.push_back({i, j});
|
||||
}
|
||||
// i is related to a group
|
||||
else if(groupId[i] > -1 && groupId[j] == -1) {
|
||||
int group = groupId[i];
|
||||
groupId[j] = group;
|
||||
// add to group
|
||||
groupedCandidates[group].push_back(j);
|
||||
}
|
||||
// j is related to a group
|
||||
else if(groupId[j] > -1 && groupId[i] == -1) {
|
||||
int group = groupId[j];
|
||||
groupId[i] = group;
|
||||
// add to group
|
||||
groupedCandidates[group].push_back(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
countSelectedContours += isSelectedContours[i];
|
||||
}
|
||||
|
||||
for (vector<size_t>& grouped : groupedCandidates) {
|
||||
if (detectorParams.detectInvertedMarker) // if detectInvertedMarker choose smallest contours
|
||||
std::sort(grouped.begin(), grouped.end(), [](const size_t &a, const size_t &b) {
|
||||
return a > b;
|
||||
});
|
||||
else // if detectInvertedMarker==false choose largest contours
|
||||
std::sort(grouped.begin(), grouped.end());
|
||||
size_t currId = grouped[0];
|
||||
isSelectedContours[currId] = true;
|
||||
for (size_t i = 1ull; i < grouped.size(); i++) {
|
||||
size_t id = grouped[i];
|
||||
float dist = getAverageDistance(candidateTree[id].corners, candidateTree[currId].corners);
|
||||
float moduleSize = getAverageModuleSize(candidateTree[id].corners, dictionary.markerSize, detectorParams.markerBorderBits);
|
||||
if (dist > detectorParams.minGroupDistance*moduleSize) {
|
||||
currId = id;
|
||||
candidateTree[grouped[0]].closeContours.push_back(candidateTree[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vector<MarkerCandidateTree> selectedCandidates(countSelectedContours + groupedCandidates.size());
|
||||
countSelectedContours = 0ull;
|
||||
for (size_t i = 0ull; i < candidateTree.size(); i++) {
|
||||
if (isSelectedContours[i]) {
|
||||
selectedCandidates[countSelectedContours] = std::move(candidateTree[i]);
|
||||
countSelectedContours++;
|
||||
}
|
||||
}
|
||||
|
||||
// find hierarchy in the candidate tree
|
||||
for (int i = (int)selectedCandidates.size()-1; i >= 0; i--) {
|
||||
for (int j = i - 1; j >= 0; j--) {
|
||||
if (checkMarker1InMarker2(selectedCandidates[i].corners, selectedCandidates[j].corners)) {
|
||||
selectedCandidates[i].parent = j;
|
||||
selectedCandidates[j].depth = max(selectedCandidates[j].depth, selectedCandidates[i].depth + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedCandidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Identify square candidates according to a marker dictionary
|
||||
*/
|
||||
void identifyCandidates(const Mat& grey, const vector<Mat>& image_pyr, vector<MarkerCandidateTree>& selectedContours,
|
||||
vector<vector<Point2f> >& accepted, vector<vector<Point> >& contours,
|
||||
vector<int>& ids, OutputArrayOfArrays _rejected = noArray()) {
|
||||
size_t ncandidates = selectedContours.size();
|
||||
vector<vector<Point2f> > rejected;
|
||||
|
||||
vector<int> idsTmp(ncandidates, -1);
|
||||
vector<int> rotated(ncandidates, 0);
|
||||
vector<uint8_t> validCandidates(ncandidates, 0);
|
||||
vector<bool> was(ncandidates, false);
|
||||
bool checkCloseContours = true;
|
||||
|
||||
int maxDepth = 0;
|
||||
for (size_t i = 0ull; i < selectedContours.size(); i++)
|
||||
maxDepth = max(selectedContours[i].depth, maxDepth);
|
||||
vector<vector<size_t>> depths(maxDepth+1);
|
||||
for (size_t i = 0ull; i < selectedContours.size(); i++) {
|
||||
depths[selectedContours[i].depth].push_back(i);
|
||||
}
|
||||
|
||||
//// Analyze each of the candidates
|
||||
int depth = 0;
|
||||
size_t counter = 0;
|
||||
while (counter < ncandidates) {
|
||||
parallel_for_(Range(0, (int)depths[depth].size()), [&](const Range& range) {
|
||||
const int begin = range.start;
|
||||
const int end = range.end;
|
||||
for (int i = begin; i < end; i++) {
|
||||
size_t v = depths[depth][i];
|
||||
was[v] = true;
|
||||
Mat img = grey;
|
||||
// implements equation (4)
|
||||
if (detectorParams.useAruco3Detection) {
|
||||
const int minPerimeter = detectorParams.minSideLengthCanonicalImg * 4;
|
||||
const size_t nearestImgId = _findOptPyrImageForCanonicalImg(image_pyr, grey.cols, static_cast<int>(selectedContours[v].contour.size()), minPerimeter);
|
||||
img = image_pyr[nearestImgId];
|
||||
}
|
||||
const float scale = detectorParams.useAruco3Detection ? img.cols / static_cast<float>(grey.cols) : 1.f;
|
||||
|
||||
validCandidates[v] = _identifyOneCandidate(dictionary, img, selectedContours[v].corners, idsTmp[v], detectorParams, rotated[v], scale);
|
||||
|
||||
if (validCandidates[v] == 0 && checkCloseContours) {
|
||||
for (const MarkerCandidate& closeMarkerCandidate: selectedContours[v].closeContours) {
|
||||
validCandidates[v] = _identifyOneCandidate(dictionary, img, closeMarkerCandidate.corners, idsTmp[v], detectorParams, rotated[v], scale);
|
||||
if (validCandidates[v] > 0) {
|
||||
selectedContours[v].corners = closeMarkerCandidate.corners;
|
||||
selectedContours[v].contour = closeMarkerCandidate.contour;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// visit the parent vertices of the detected markers to skip identify parent contours
|
||||
for(size_t v : depths[depth]) {
|
||||
if(validCandidates[v] > 0) {
|
||||
int parent = selectedContours[v].parent;
|
||||
while (parent != -1) {
|
||||
if (!was[parent]) {
|
||||
was[parent] = true;
|
||||
counter++;
|
||||
}
|
||||
parent = selectedContours[parent].parent;
|
||||
}
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
|
||||
for (size_t i = 0ull; i < selectedContours.size(); i++) {
|
||||
if (validCandidates[i] > 0) {
|
||||
// shift corner positions to the correct rotation
|
||||
correctCornerPosition(selectedContours[i].corners, rotated[i]);
|
||||
|
||||
accepted.push_back(selectedContours[i].corners);
|
||||
contours.push_back(selectedContours[i].contour);
|
||||
ids.push_back(idsTmp[i]);
|
||||
}
|
||||
else {
|
||||
rejected.push_back(selectedContours[i].corners);
|
||||
}
|
||||
}
|
||||
|
||||
// parse output
|
||||
if(_rejected.needed()) {
|
||||
_copyVector2Output(rejected, _rejected);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -929,23 +934,21 @@ void ArucoDetector::detectMarkers(InputArray _image, OutputArrayOfArrays _corner
|
||||
vector<vector<Point> > contours;
|
||||
vector<int> ids;
|
||||
|
||||
vector<vector<vector<Point2f> > > candidatesSet;
|
||||
vector<vector<vector<Point> > > contoursSet;
|
||||
|
||||
/// STEP 2.a Detect marker candidates :: using AprilTag
|
||||
if(detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_APRILTAG){
|
||||
_apriltag(grey, detectorParams, candidates, contours);
|
||||
|
||||
candidatesSet.push_back(candidates);
|
||||
contoursSet.push_back(contours);
|
||||
}
|
||||
/// STEP 2.b Detect marker candidates :: traditional way
|
||||
else
|
||||
_detectCandidates(grey, candidatesSet, contoursSet, detectorParams);
|
||||
else {
|
||||
arucoDetectorImpl->detectCandidates(grey, candidates, contours);
|
||||
}
|
||||
|
||||
/// STEP 2.c FILTER OUT NEAR CANDIDATE PAIRS
|
||||
auto selectedCandidates = arucoDetectorImpl->filterTooCloseCandidates(candidates, contours);
|
||||
|
||||
/// STEP 2: Check candidate codification (identify markers)
|
||||
_identifyCandidates(grey, grey_pyramid, candidatesSet, contoursSet, dictionary,
|
||||
candidates, contours, ids, detectorParams, _rejectedImgPoints);
|
||||
arucoDetectorImpl->identifyCandidates(grey, grey_pyramid, selectedCandidates, candidates, contours,
|
||||
ids, _rejectedImgPoints);
|
||||
|
||||
/// STEP 3: Corner refinement :: use corner subpix
|
||||
if (detectorParams.cornerRefinementMethod == (int)CORNER_REFINE_SUBPIX) {
|
||||
@ -963,7 +966,7 @@ void ArucoDetector::detectMarkers(InputArray _image, OutputArrayOfArrays _corner
|
||||
}
|
||||
else {
|
||||
int cornerRefinementWinSize = std::max(1, cvRound(detectorParams.relativeCornerRefinmentWinSize*
|
||||
arucoDetectorImpl->getAverageArucoPinSize(candidates[i])));
|
||||
getAverageModuleSize(candidates[i], dictionary.markerSize, detectorParams.markerBorderBits)));
|
||||
cornerRefinementWinSize = min(cornerRefinementWinSize, detectorParams.cornerRefinementWinSize);
|
||||
cornerSubPix(grey, Mat(candidates[i]), Size(cornerRefinementWinSize, cornerRefinementWinSize), Size(-1, -1),
|
||||
TermCriteria(TermCriteria::MAX_ITER | TermCriteria::EPS,
|
||||
@ -1238,7 +1241,7 @@ void ArucoDetector::refineDetectedMarkers(InputArray _image, const Board& _board
|
||||
|
||||
std::vector<Point2f> marker(closestRotatedMarker.begin<Point2f>(), closestRotatedMarker.end<Point2f>());
|
||||
int cornerRefinementWinSize = std::max(1, cvRound(detectorParams.relativeCornerRefinmentWinSize*
|
||||
arucoDetectorImpl->getAverageArucoPinSize(marker)));
|
||||
getAverageModuleSize(marker, dictionary.markerSize, detectorParams.markerBorderBits)));
|
||||
cornerRefinementWinSize = min(cornerRefinementWinSize, detectorParams.cornerRefinementWinSize);
|
||||
cornerSubPix(grey, closestRotatedMarker,
|
||||
Size(cornerRefinementWinSize, cornerRefinementWinSize),
|
||||
|
@ -314,7 +314,9 @@ struct CharucoDetector::CharucoDetectorImpl {
|
||||
vector<vector<Point2f> > rejectedMarkers;
|
||||
arucoDetector.detectMarkers(image, _markerCorners, _markerIds, rejectedMarkers);
|
||||
if (charucoParameters.tryRefineMarkers)
|
||||
arucoDetector.refineDetectedMarkers(image, board, _markerCorners, _markerIds, rejectedMarkers);
|
||||
arucoDetector.refineDetectedMarkers(image, board, _markerCorners, _markerIds, rejectedMarkers);
|
||||
if (_markerCorners.empty() && _markerIds.empty())
|
||||
return;
|
||||
}
|
||||
// if camera parameters are avaible, use approximated calibration
|
||||
if(!charucoParameters.cameraMatrix.empty())
|
||||
|
@ -613,6 +613,32 @@ TEST(CV_ArucoDetectMarkers, regression_2492)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(CV_ArucoDetectMarkers, regression_contour_24220)
|
||||
{
|
||||
aruco::ArucoDetector detector;
|
||||
vector<int> markerIds;
|
||||
vector<vector<Point2f> > markerCorners;
|
||||
string imgPath = cvtest::findDataFile("aruco/failmask9.png");
|
||||
Mat image = imread(imgPath);
|
||||
|
||||
const size_t N = 1ull;
|
||||
const int goldCorners[8] = {392,175, 99,257, 117,109, 365,44};
|
||||
const int goldCornersId = 0;
|
||||
|
||||
detector.detectMarkers(image, markerCorners, markerIds);
|
||||
|
||||
ASSERT_EQ(N, markerIds.size());
|
||||
ASSERT_EQ(4ull, markerCorners[0].size());
|
||||
ASSERT_EQ(goldCornersId, markerIds[0]);
|
||||
for (int j = 0; j < 4; j++)
|
||||
{
|
||||
EXPECT_NEAR(static_cast<float>(goldCorners[j * 2]), markerCorners[0][j].x, 1.f);
|
||||
EXPECT_NEAR(static_cast<float>(goldCorners[j * 2 + 1]), markerCorners[0][j].y, 1.f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ArucoThreading: public testing::TestWithParam<aruco::CornerRefineMethod>
|
||||
{
|
||||
struct NumThreadsSetter {
|
||||
|
@ -650,7 +650,7 @@ TEST(Charuco, issue_14014)
|
||||
EXPECT_EQ(Size(4, 1), corners[0].size()); // check dimension of detected corners
|
||||
|
||||
size_t numRejPoints = rejectedPoints.size();
|
||||
ASSERT_EQ(rejectedPoints.size(), 26ull); // optional check to track regressions
|
||||
ASSERT_EQ(rejectedPoints.size(), 24ull); // optional check to track regressions
|
||||
EXPECT_EQ(Size(4, 1), rejectedPoints[0].size()); // check dimension of detected corners
|
||||
|
||||
detector.refineDetectedMarkers(img, board, corners, ids, rejectedPoints);
|
||||
|
Loading…
Reference in New Issue
Block a user