mirror of
https://github.com/opencv/opencv.git
synced 2026-09-15 07:29:07 -05:00
Merge branch 4.x
This commit is contained in:
@@ -20,6 +20,8 @@ enum CornerRefineMethod{
|
||||
CORNER_REFINE_APRILTAG, ///< Tag and corners detection based on the AprilTag 2 approach @cite wang2016iros
|
||||
};
|
||||
|
||||
static constexpr float DEFAULT_VALID_BIT_ID_THRESHOLD{0.49f};
|
||||
|
||||
/** @brief struct DetectorParameters is used by ArucoDetector
|
||||
*/
|
||||
struct CV_EXPORTS_W_SIMPLE DetectorParameters {
|
||||
@@ -49,7 +51,7 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters {
|
||||
aprilTagQuadSigma = 0.0;
|
||||
aprilTagMinClusterPixels = 5;
|
||||
aprilTagMaxNmaxima = 10;
|
||||
aprilTagCriticalRad = (float)(10* CV_PI /180);
|
||||
aprilTagCriticalRad = (float)(10 * CV_PI / 180);
|
||||
aprilTagMaxLineFitMse = 10.0;
|
||||
aprilTagMinWhiteBlackDiff = 5;
|
||||
aprilTagDeglitch = 0;
|
||||
@@ -57,6 +59,7 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters {
|
||||
useAruco3Detection = false;
|
||||
minSideLengthCanonicalImg = 32;
|
||||
minMarkerLengthRatioOriginalImg = 0.0;
|
||||
validBitIdThreshold = DEFAULT_VALID_BIT_ID_THRESHOLD;
|
||||
}
|
||||
|
||||
/** @brief Read a new set of DetectorParameters from FileNode (use FileStorage.root()).
|
||||
@@ -168,12 +171,12 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters {
|
||||
*/
|
||||
CV_PROP_RW double maxErroneousBitsInBorderRate;
|
||||
|
||||
/** @brief minimun standard deviation in pixels values during the decodification step to apply Otsu
|
||||
/** @brief minimum standard deviation in pixels values during the decodification step to apply Otsu
|
||||
* thresholding (otherwise, all the bits are set to 0 or 1 depending on mean higher than 128 or not) (default 5.0)
|
||||
*/
|
||||
CV_PROP_RW double minOtsuStdDev;
|
||||
|
||||
/// error correction rate respect to the maximun error correction capability for each dictionary (default 0.6).
|
||||
/// error correction rate respect to the maximum error correction capability for each dictionary (default 0.6).
|
||||
CV_PROP_RW double errorCorrectionRate;
|
||||
|
||||
/** @brief April :: User-configurable parameters.
|
||||
@@ -231,6 +234,9 @@ struct CV_EXPORTS_W_SIMPLE DetectorParameters {
|
||||
|
||||
/// range [0,1], eq (2) from paper. The parameter tau_i has a direct influence on the processing speed.
|
||||
CV_PROP_RW float minMarkerLengthRatioOriginalImg;
|
||||
|
||||
/// range [0,1], define the acceptable threshold when comparing the detected marker to the dictionary during marker identification.
|
||||
CV_PROP_RW float validBitIdThreshold;
|
||||
};
|
||||
|
||||
/** @brief struct RefineParameters is used by ArucoDetector
|
||||
|
||||
@@ -65,6 +65,12 @@ class CV_EXPORTS_W_SIMPLE Dictionary {
|
||||
*/
|
||||
CV_WRAP bool identify(const Mat &onlyBits, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate) const;
|
||||
|
||||
/** @brief Given a matrix of pixel ratio raging from 0 to 1. Returns whether if marker is identified or not.
|
||||
*
|
||||
* Returns reference to the marker id in the dictionary (if any) and its rotation.
|
||||
*/
|
||||
CV_WRAP bool identify(const Mat &onlyCellPixelRatio, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate, float validBitIdThreshold) const;
|
||||
|
||||
/** @brief Returns Hamming distance of the input bits to the specific id.
|
||||
*
|
||||
* If `allRotations` flag is set, the four possible marker rotations are considered
|
||||
@@ -84,6 +90,10 @@ class CV_EXPORTS_W_SIMPLE Dictionary {
|
||||
/** @brief Transform list of bytes to matrix of bits
|
||||
*/
|
||||
CV_WRAP static Mat getBitsFromByteList(const Mat &byteList, int markerSize, int rotationId = 0);
|
||||
|
||||
/** @brief Get ground truth bits float
|
||||
*/
|
||||
CV_WRAP Mat getMarkerBits(int markerId, int rotationId = 0) const;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -323,6 +323,7 @@ class aruco_objdetect_test(NewOpenCVTests):
|
||||
imgSize = (500, 500)
|
||||
params = cv.aruco.DetectorParameters()
|
||||
params.minDistanceToBorder = 3
|
||||
params.validBitIdThreshold = 0.5
|
||||
|
||||
board = cv.aruco.CharucoBoard((4, 4), 0.03, 0.015, cv.aruco.getPredefinedDictionary(cv.aruco.DICT_6X6_250))
|
||||
detector = cv.aruco.CharucoDetector(board, detectorParams=params)
|
||||
|
||||
@@ -310,12 +310,11 @@ static void _detectInitialCandidates(const Mat &grey, vector<vector<Point2f> > &
|
||||
|
||||
|
||||
/**
|
||||
* @brief Given an input image and candidate corners, extract the bits of the candidate, including
|
||||
* @brief Given an input image and candidate corners, extract the cell pixel ratio of the candidate, including
|
||||
* the border bits
|
||||
*/
|
||||
static Mat _extractBits(InputArray _image, const vector<Point2f>& corners, int markerSize,
|
||||
int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu,
|
||||
OutputArray _cellPixelRatio = noArray()) {
|
||||
static Mat _extractCellPixelRatio(InputArray _image, const vector<Point2f>& corners, int markerSize,
|
||||
int markerBorderBits, int cellSize, double cellMarginRate, double minStdDevOtsu) {
|
||||
CV_Assert(_image.getMat().channels() == 1);
|
||||
CV_Assert(corners.size() == 4ull);
|
||||
CV_Assert(markerBorderBits > 0 && cellSize > 0 && cellMarginRate >= 0 && cellMarginRate <= 0.5);
|
||||
@@ -339,11 +338,11 @@ static Mat _extractBits(InputArray _image, const vector<Point2f>& corners, int m
|
||||
warpPerspective(_image, resultImg, transformation, Size(resultImgSize, resultImgSize),
|
||||
INTER_NEAREST);
|
||||
|
||||
// output image containing the bits
|
||||
Mat bits(markerSizeWithBorders, markerSizeWithBorders, CV_8UC1, Scalar::all(0));
|
||||
// output image containing the ratio of white pixels in each cell
|
||||
Mat cellPixelRatio(markerSizeWithBorders, markerSizeWithBorders, CV_32FC1, Scalar::all(0));
|
||||
|
||||
// check if standard deviation is enough to apply Otsu
|
||||
// if not enough, it probably means all bits are the same color (black or white)
|
||||
// if not enough, it probably means all pixels are the same color (black or white)
|
||||
Mat mean, stddev;
|
||||
// Remove some border just to avoid border noise from perspective transformation
|
||||
Mat innerRegion = resultImg.colRange(cellSize / 2, resultImg.cols - cellSize / 2)
|
||||
@@ -351,18 +350,13 @@ static Mat _extractBits(InputArray _image, const vector<Point2f>& corners, int m
|
||||
meanStdDev(innerRegion, mean, stddev);
|
||||
if(stddev.ptr< double >(0)[0] < minStdDevOtsu) {
|
||||
// all black or all white, depending on mean value
|
||||
if(mean.ptr< double >(0)[0] > 127)
|
||||
bits.setTo(1);
|
||||
else
|
||||
bits.setTo(0);
|
||||
if(_cellPixelRatio.needed()) bits.convertTo(_cellPixelRatio, CV_32F);
|
||||
return bits;
|
||||
}
|
||||
if(mean.ptr< double >(0)[0] > 127){
|
||||
cellPixelRatio.setTo(1);
|
||||
} else {
|
||||
cellPixelRatio.setTo(0);
|
||||
}
|
||||
|
||||
Mat cellPixelRatio;
|
||||
if (_cellPixelRatio.needed()) {
|
||||
_cellPixelRatio.create(markerSizeWithBorders, markerSizeWithBorders, CV_32FC1);
|
||||
cellPixelRatio = _cellPixelRatio.getMatRef();
|
||||
return cellPixelRatio;
|
||||
}
|
||||
|
||||
// now extract code, first threshold using Otsu
|
||||
@@ -377,38 +371,40 @@ static Mat _extractBits(InputArray _image, const vector<Point2f>& corners, int m
|
||||
cellSize - 2 * cellMarginPixels));
|
||||
// count white pixels on each cell to assign its value
|
||||
size_t nZ = (size_t) countNonZero(square);
|
||||
if(nZ > square.total() / 2) bits.at<unsigned char>(y, x) = 1;
|
||||
|
||||
// define the cell pixel ratio as the ratio of the white pixels. For inverted markers, the ratio will be inverted.
|
||||
if(_cellPixelRatio.needed()) cellPixelRatio.at<float>(y, x) = (nZ / (float)square.total());
|
||||
cellPixelRatio.at<float>(y, x) = (nZ / (float)square.total());
|
||||
}
|
||||
}
|
||||
|
||||
return bits;
|
||||
return cellPixelRatio;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Return number of erroneous bits in border, i.e. number of white bits in border.
|
||||
* @brief Return number of erroneous bits in border, i.e. bits for which pixel ratio > validBitIdThreshold.
|
||||
*/
|
||||
static int _getBorderErrors(const Mat &bits, int markerSize, int borderSize) {
|
||||
static int _getBorderErrors(const Mat &cellPixelRatio, int markerSize, int borderSize, float validBitIdThreshold) {
|
||||
|
||||
int sizeWithBorders = markerSize + 2 * borderSize;
|
||||
|
||||
CV_Assert(markerSize > 0 && bits.cols == sizeWithBorders && bits.rows == sizeWithBorders);
|
||||
CV_Assert(markerSize > 0 && cellPixelRatio.cols == sizeWithBorders && cellPixelRatio.rows == sizeWithBorders);
|
||||
|
||||
// Get border error. cellPixelRatio has the opposite color as the borders.
|
||||
int totalErrors = 0;
|
||||
for(int y = 0; y < sizeWithBorders; y++) {
|
||||
for(int k = 0; k < borderSize; k++) {
|
||||
if(bits.ptr<unsigned char>(y)[k] != 0) totalErrors++;
|
||||
if(bits.ptr<unsigned char>(y)[sizeWithBorders - 1 - k] != 0) totalErrors++;
|
||||
// Left and right vertical sides
|
||||
if(cellPixelRatio.ptr<float>(y)[k] > validBitIdThreshold) totalErrors++;
|
||||
if(cellPixelRatio.ptr<float>(y)[sizeWithBorders - 1 - k] > validBitIdThreshold) totalErrors++;
|
||||
}
|
||||
}
|
||||
for(int x = borderSize; x < sizeWithBorders - borderSize; x++) {
|
||||
for(int k = 0; k < borderSize; k++) {
|
||||
if(bits.ptr<unsigned char>(k)[x] != 0) totalErrors++;
|
||||
if(bits.ptr<unsigned char>(sizeWithBorders - 1 - k)[x] != 0) totalErrors++;
|
||||
// Top and bottom horizontal sides
|
||||
if(cellPixelRatio.ptr<float>(k)[x] > validBitIdThreshold) totalErrors++;
|
||||
if(cellPixelRatio.ptr<float>(sizeWithBorders - 1 - k)[x] > validBitIdThreshold) totalErrors++;
|
||||
}
|
||||
}
|
||||
return totalErrors;
|
||||
@@ -482,49 +478,43 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i
|
||||
scaled_corners[i].y = _corners[i].y * scale;
|
||||
}
|
||||
|
||||
Mat cellPixelRatio;
|
||||
Mat candidateBits =
|
||||
_extractBits(_image, scaled_corners, dictionary.markerSize, params.markerBorderBits,
|
||||
params.perspectiveRemovePixelPerCell,
|
||||
params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev,
|
||||
cellPixelRatio);
|
||||
Mat cellPixelRatio =
|
||||
_extractCellPixelRatio(_image, scaled_corners, dictionary.markerSize, params.markerBorderBits,
|
||||
params.perspectiveRemovePixelPerCell,
|
||||
params.perspectiveRemoveIgnoredMarginPerCell, params.minOtsuStdDev);
|
||||
|
||||
// analyze border bits
|
||||
int maximumErrorsInBorder =
|
||||
int(dictionary.markerSize * dictionary.markerSize * params.maxErroneousBitsInBorderRate);
|
||||
int(dictionary.markerSize * dictionary.markerSize * params.maxErroneousBitsInBorderRate);
|
||||
int borderErrors =
|
||||
_getBorderErrors(candidateBits, dictionary.markerSize, params.markerBorderBits);
|
||||
_getBorderErrors(cellPixelRatio, dictionary.markerSize, params.markerBorderBits, params.validBitIdThreshold);
|
||||
|
||||
// check if it is a white marker
|
||||
if(params.detectInvertedMarker){
|
||||
// to get from 255 to 1
|
||||
Mat invertedImg = ~candidateBits-254;
|
||||
int invBError = _getBorderErrors(invertedImg, dictionary.markerSize, params.markerBorderBits);
|
||||
Mat invCellPixelRatio = 1.f - cellPixelRatio;
|
||||
int invBError = _getBorderErrors(invCellPixelRatio, dictionary.markerSize, params.markerBorderBits, params.validBitIdThreshold);
|
||||
// white marker
|
||||
if(invBError<borderErrors){
|
||||
cellPixelRatio = 1.f - cellPixelRatio;
|
||||
borderErrors = invBError;
|
||||
invertedImg.copyTo(candidateBits);
|
||||
invCellPixelRatio.copyTo(cellPixelRatio);
|
||||
typ=2;
|
||||
}
|
||||
}
|
||||
if(borderErrors > maximumErrorsInBorder) return 0; // border is wrong
|
||||
|
||||
// take only inner bits
|
||||
Mat onlyBits =
|
||||
candidateBits.rowRange(params.markerBorderBits,
|
||||
candidateBits.rows - params.markerBorderBits)
|
||||
.colRange(params.markerBorderBits, candidateBits.cols - params.markerBorderBits);
|
||||
Mat onlyCellPixelRatio =
|
||||
cellPixelRatio.rowRange(params.markerBorderBits,
|
||||
cellPixelRatio.rows - params.markerBorderBits)
|
||||
.colRange(params.markerBorderBits, cellPixelRatio.cols - params.markerBorderBits);
|
||||
|
||||
// try to indentify the marker
|
||||
if(!dictionary.identify(onlyBits, idx, rotation, params.errorCorrectionRate))
|
||||
// try to identify the marker
|
||||
if(!dictionary.identify(onlyCellPixelRatio, idx, rotation, params.errorCorrectionRate, params.validBitIdThreshold))
|
||||
return 0;
|
||||
|
||||
// compute the candidate's confidence
|
||||
if(confidenceNeeded) {
|
||||
Mat groundTruthbits;
|
||||
Mat bitsUints = dictionary.getBitsFromByteList(dictionary.bytesList.rowRange(idx, idx + 1), dictionary.markerSize, rotation);
|
||||
bitsUints.convertTo(groundTruthbits, CV_32F);
|
||||
Mat groundTruthbits = dictionary.getMarkerBits(idx, rotation);
|
||||
markerConfidence = _getMarkerConfidence(groundTruthbits, cellPixelRatio, dictionary.markerSize, params.markerBorderBits);
|
||||
}
|
||||
|
||||
@@ -1403,11 +1393,14 @@ void ArucoDetector::refineDetectedMarkers(InputArray _image, const Board& _board
|
||||
if(refineParams.errorCorrectionRate >= 0) {
|
||||
|
||||
// extract bits
|
||||
Mat bits = _extractBits(
|
||||
Mat cellPixelRatio = _extractCellPixelRatio(
|
||||
grey, rotatedMarker, dictionary.markerSize, detectorParams.markerBorderBits,
|
||||
detectorParams.perspectiveRemovePixelPerCell,
|
||||
detectorParams.perspectiveRemoveIgnoredMarginPerCell, detectorParams.minOtsuStdDev);
|
||||
|
||||
Mat bits;
|
||||
cellPixelRatio.convertTo(bits, CV_8UC1);
|
||||
|
||||
Mat onlyBits =
|
||||
bits.rowRange(detectorParams.markerBorderBits, bits.rows - detectorParams.markerBorderBits)
|
||||
.colRange(detectorParams.markerBorderBits, bits.rows - detectorParams.markerBorderBits);
|
||||
|
||||
@@ -73,25 +73,32 @@ void Dictionary::writeDictionary(FileStorage& fs, const String &name)
|
||||
}
|
||||
|
||||
|
||||
bool Dictionary::identify(const Mat &onlyBits, int &idx, int &rotation, double maxCorrectionRate) const {
|
||||
CV_Assert(onlyBits.rows == markerSize && onlyBits.cols == markerSize);
|
||||
bool Dictionary::identify(const Mat &onlyCellPixelRatio, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate, float validBitIdThreshold) const {
|
||||
CV_Assert(onlyCellPixelRatio.rows == markerSize && onlyCellPixelRatio.cols == markerSize);
|
||||
|
||||
int maxCorrectionRecalculed = int(double(maxCorrectionBits) * maxCorrectionRate);
|
||||
|
||||
// get as a byte list
|
||||
Mat candidateBytes = getByteListFromBits(onlyBits);
|
||||
|
||||
idx = -1; // by default, not found
|
||||
|
||||
// search closest marker in dict
|
||||
for(int m = 0; m < bytesList.rows; m++) {
|
||||
int currentMinDistance = markerSize * markerSize + 1;
|
||||
int currentRotation = -1;
|
||||
for(unsigned int r = 0; r < 4; r++) {
|
||||
int currentHamming = cv::hal::normHamming(
|
||||
bytesList.ptr(m)+r*candidateBytes.cols,
|
||||
candidateBytes.ptr(),
|
||||
candidateBytes.cols);
|
||||
for(int r = 0; r < 4; r++) {
|
||||
|
||||
Mat bitsRot = getBitsFromByteList(bytesList.rowRange(m, m + 1), markerSize, r);
|
||||
bitsRot.convertTo(bitsRot, CV_32F);
|
||||
|
||||
// Loop over all bits dictBitsList [m, markerSize * markerSize, 4]; onlyCellPixelRatio [markerSize, markerSize]
|
||||
int currentHamming = 0;
|
||||
for(int i = 0; i < markerSize; i++) {
|
||||
for(int j = 0; j < markerSize; j++) {
|
||||
// If detected bit is too far from the ground truth, consider it false.
|
||||
if(fabs(onlyCellPixelRatio.at<float>(i, j) - static_cast<float>(bitsRot.at<float>(i, j))) > validBitIdThreshold){
|
||||
currentHamming++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(currentHamming < currentMinDistance) {
|
||||
currentMinDistance = currentHamming;
|
||||
@@ -111,6 +118,16 @@ bool Dictionary::identify(const Mat &onlyBits, int &idx, int &rotation, double m
|
||||
}
|
||||
|
||||
|
||||
bool Dictionary::identify(const Mat &onlyBits, CV_OUT int &idx, CV_OUT int &rotation, double maxCorrectionRate) const {
|
||||
CV_Assert(onlyBits.rows == markerSize && onlyBits.cols == markerSize);
|
||||
|
||||
Mat candidateBitRatio;
|
||||
onlyBits.convertTo(candidateBitRatio, CV_32F);
|
||||
const float validBitIdThreshold = DEFAULT_VALID_BIT_ID_THRESHOLD;
|
||||
return identify(candidateBitRatio, idx, rotation, maxCorrectionRate, validBitIdThreshold);
|
||||
}
|
||||
|
||||
|
||||
int Dictionary::getDistanceToId(InputArray bits, int id, bool allRotations) const {
|
||||
|
||||
CV_Assert(id >= 0 && id < bytesList.rows);
|
||||
@@ -147,7 +164,8 @@ void Dictionary::generateImageMarker(int id, int sidePixels, OutputArray _img, i
|
||||
Mat innerRegion = tinyMarker.rowRange(borderBits, tinyMarker.rows - borderBits)
|
||||
.colRange(borderBits, tinyMarker.cols - borderBits);
|
||||
// put inner bits
|
||||
Mat bits = 255 * getBitsFromByteList(bytesList.rowRange(id, id + 1), markerSize);
|
||||
Mat bits = getMarkerBits(id);
|
||||
bits.convertTo(bits, CV_8U, 255.0);
|
||||
CV_Assert(innerRegion.total() == bits.total());
|
||||
bits.copyTo(innerRegion);
|
||||
|
||||
@@ -194,12 +212,28 @@ Mat Dictionary::getByteListFromBits(const Mat &bits) {
|
||||
}
|
||||
|
||||
|
||||
Mat Dictionary::getMarkerBits(int markerId, int rotationId) const {
|
||||
|
||||
const int nbRotations = 4;
|
||||
CV_Assert(markerId < bytesList.rows);
|
||||
CV_Assert(rotationId < nbRotations);
|
||||
|
||||
Mat bits(markerSize, markerSize, CV_32F, Scalar::all(0));
|
||||
Mat bitsUints = getBitsFromByteList(bytesList.rowRange(markerId, markerId + 1), markerSize, rotationId);
|
||||
bitsUints.convertTo(bits, CV_32F);
|
||||
|
||||
CV_Assert(bits.rows == markerSize && bits.cols == markerSize);
|
||||
return bits;
|
||||
}
|
||||
|
||||
|
||||
Mat Dictionary::getBitsFromByteList(const Mat &byteList, int markerSize, int rotationId) {
|
||||
CV_Assert(byteList.total() > 0 &&
|
||||
byteList.total() >= (unsigned int)markerSize * markerSize / 8 &&
|
||||
byteList.total() <= (unsigned int)markerSize * markerSize / 8 + 1);
|
||||
|
||||
CV_Assert(rotationId >=0 && rotationId < 4);
|
||||
CV_Assert(byteList.channels() >= 4);
|
||||
CV_Assert(rotationId >= 0 && rotationId < 4);
|
||||
|
||||
Mat bits(markerSize, markerSize, CV_8UC1, Scalar::all(0));
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ TEST(CV_ArucoTutorial, can_find_diamondmarkers)
|
||||
aruco::DetectorParameters detectorParams;
|
||||
detectorParams.readDetectorParameters(fs.root());
|
||||
detectorParams.cornerRefinementMethod = aruco::CORNER_REFINE_APRILTAG;
|
||||
detectorParams.validBitIdThreshold = 0.5f;
|
||||
|
||||
aruco::CharucoBoard charucoBoard(Size(3, 3), 0.4f, 0.25f, dictionary);
|
||||
aruco::CharucoDetector detector(charucoBoard, aruco::CharucoParameters(), detectorParams);
|
||||
|
||||
@@ -473,7 +473,7 @@ markerDetectionGT applyTemperingToMarkerCells(cv::Mat &marker,
|
||||
++cellsTempered;
|
||||
|
||||
// cell too tempered, no detection expected
|
||||
if(cellTempConfig.cellRatioToTemper > 0.5f) {
|
||||
if(cellTempConfig.cellRatioToTemper > params.validBitIdThreshold) {
|
||||
if(isBorder){
|
||||
++borderErrors;
|
||||
} else {
|
||||
@@ -556,7 +556,7 @@ static void runArucoDetectionConfidence(ArucoAlgParams arucoAlgParam) {
|
||||
// make sure there are no bits have any detection errors
|
||||
params.maxErroneousBitsInBorderRate = 0.0;
|
||||
params.errorCorrectionRate = 0.0;
|
||||
params.perspectiveRemovePixelPerCell = 8; // esnsure that there is enough resolution to properly handle distortions
|
||||
params.perspectiveRemovePixelPerCell = 8; // ensure that there is enough resolution to properly handle distortions
|
||||
aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_6X6_250), params);
|
||||
|
||||
const bool detectInvertedMarker = (arucoAlgParam == ArucoAlgParams::DETECT_INVERTED_MARKER);
|
||||
@@ -674,6 +674,144 @@ static void runArucoDetectionConfidence(ArucoAlgParams arucoAlgParam) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper struc and functions for CV_ArucoDetectionUnc
|
||||
struct ArucoThresholdTestConfig {
|
||||
MarkerTemperingConfig markerTemperingConfig; // Configuration of cells to invert (percentage, number and markerRegionToTemper)
|
||||
float validBitIdThreshold; // range [0,1], define the acceptable threshold when comparing the detected marker to the dictionary during marker identification.
|
||||
float perspectiveRemoveIgnoredMarginPerCell; // Width of the margin of pixels on each cell not considered for the marker identification
|
||||
int markerBorderBits; // Number of bits of the marker border
|
||||
float distortionRatio; // Percentage of offset used for perspective distortion, bigger means more distorted
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test the param validBitIdThreshold
|
||||
* Loops over a set of detector configurations (validBitIdThreshold, distortion, DetectorParameters such as markerBorderBits)
|
||||
* For each configuration, it creates a synthetic image containing four markers arranged in a 2x2 grid.
|
||||
* Each marker is generated with its own configuration (id, size, rotation).
|
||||
* Make sure that markers are detected or not based on validBitIdThreshold and percentage of tempering.
|
||||
* Finally, it runs the detector and checks that each marker is detected or not based on the threshold.
|
||||
*
|
||||
*/
|
||||
static void runArucoDetectionThreshold(ArucoAlgParams arucoAlgParam) {
|
||||
|
||||
aruco::DetectorParameters params;
|
||||
// make sure there are no bits have any detection errors
|
||||
params.perspectiveRemovePixelPerCell = 20; // ensure that there is enough resolution to properly handle distortions
|
||||
params.maxErroneousBitsInBorderRate = 0.f;
|
||||
params.errorCorrectionRate = 0.f;
|
||||
aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_5X5_250), params); // Max correction: 6bits
|
||||
|
||||
const bool detectInvertedMarker = (arucoAlgParam == ArucoAlgParams::DETECT_INVERTED_MARKER);
|
||||
|
||||
// define several detector configurations to test different settings
|
||||
// {{MarkerTemperingConfig}, validBitIdThreshold, perspectiveRemoveIgnoredMarginPerCell, markerBorderBits, distortionRatio}
|
||||
|
||||
vector<ArucoThresholdTestConfig> detectorConfigs = {
|
||||
// No tempering, expect detection for every threshold
|
||||
{{0.f, 0, MarkerRegionToTemper::ALL}, 0.3f, 0.f, 1, 0.f},
|
||||
{{0.f, 0, MarkerRegionToTemper::ALL}, 0.5f, 0.f, 1, 0.f},
|
||||
{{0.f, 0, MarkerRegionToTemper::ALL}, 0.9f, 0.f, 1, 0.f},
|
||||
// Include distortions
|
||||
{{0.f, 0, MarkerRegionToTemper::ALL}, 0.3f, 0.f, 1, 0.05f},
|
||||
{{0.f, 0, MarkerRegionToTemper::ALL}, 0.5f, 0.f, 1, 0.1f},
|
||||
{{0.f, 0, MarkerRegionToTemper::ALL}, 0.9f, 0.f, 1, 0.2f},
|
||||
|
||||
// 20% temper, expect detection with threshold above 0.2
|
||||
{{0.2f, 5, MarkerRegionToTemper::BORDER}, 0.30f, 0.f, 1, 0.f}, // Detection
|
||||
|
||||
{{0.2f, 1, MarkerRegionToTemper::BORDER}, 0.18f, 0.f, 1, 0.f}, // No detection
|
||||
{{0.2f, 1, MarkerRegionToTemper::BORDER}, 0.18f, 0.f, 1, 0.f}, // No detection
|
||||
{{0.2f, 10, MarkerRegionToTemper::INNER}, 0.22f, 0.f, 1, 0.f}, // Detection
|
||||
{{0.2f, 1, MarkerRegionToTemper::INNER}, 0.18f, 0.f, 1, 0.f} // No detection
|
||||
|
||||
// distortions
|
||||
};
|
||||
|
||||
// define marker configurations for the 4 markers in each image
|
||||
const int markerSidePixels = 700; // To simplify the cell division, markerSidePixels is a multiple of 7. (5x5 dict + 2 border bits)
|
||||
vector<MarkerCreationConfig> markerCreationConfig = {
|
||||
{0, markerSidePixels, markerRot::ROT_90}, // {id, markerSidePixels, rotation}
|
||||
{1, markerSidePixels, markerRot::ROT_270},
|
||||
{2, markerSidePixels, markerRot::NONE},
|
||||
{3, markerSidePixels, markerRot::ROT_180}
|
||||
};
|
||||
|
||||
// loop over each detector configuration
|
||||
for (size_t cfgIdx = 0; cfgIdx < detectorConfigs.size(); cfgIdx++) {
|
||||
ArucoThresholdTestConfig detCfg = detectorConfigs[cfgIdx];
|
||||
|
||||
// update detector parameters
|
||||
params.validBitIdThreshold =detCfg.validBitIdThreshold;
|
||||
params.perspectiveRemoveIgnoredMarginPerCell = detCfg.perspectiveRemoveIgnoredMarginPerCell;
|
||||
params.markerBorderBits = detCfg.markerBorderBits;
|
||||
params.detectInvertedMarker = detectInvertedMarker;
|
||||
detector.setDetectorParameters(params);
|
||||
|
||||
// create a blank image large enough to hold 4 markers in a 2x2 grid
|
||||
const int margin = markerSidePixels / 2;
|
||||
const int imageSize = (markerSidePixels * 2) + margin * 3;
|
||||
Mat img(imageSize, imageSize, CV_8UC1, Scalar(255));
|
||||
|
||||
vector<markerDetectionGT> groundTruths;
|
||||
const aruco::Dictionary &dictionary = detector.getDictionary();
|
||||
|
||||
// place each marker into the image
|
||||
for (int row = 0; row < 2; row++) {
|
||||
for (int col = 0; col < 2; col++) {
|
||||
int index = row * 2 + col;
|
||||
MarkerCreationConfig markerCfg = markerCreationConfig[index];
|
||||
// adjust marker id to be unique for each detector configuration
|
||||
markerCfg.id += static_cast<int>(cfgIdx * markerCreationConfig.size());
|
||||
|
||||
// generate img
|
||||
Mat markerImg;
|
||||
markerDetectionGT gt = generateTemperedMarkerImage(markerImg, markerCfg, detCfg.markerTemperingConfig, params, dictionary, detCfg.distortionRatio);
|
||||
|
||||
groundTruths.push_back(gt);
|
||||
|
||||
// place marker in the image
|
||||
Point2f topLeft(static_cast<float>(margin + col * (markerSidePixels + margin)),
|
||||
static_cast<float>(margin + row * (markerSidePixels + margin)));
|
||||
placeMarker(img, markerImg, topLeft);
|
||||
}
|
||||
}
|
||||
|
||||
// if testing inverted markers globally, invert the whole image
|
||||
if (detectInvertedMarker) {
|
||||
bitwise_not(img, img);
|
||||
}
|
||||
|
||||
// run detection.
|
||||
vector<vector<Point2f>> corners, rejected;
|
||||
vector<int> ids;
|
||||
vector<float> markerConfidence;
|
||||
detector.detectMarkersWithConfidence(img, corners, ids, markerConfidence, rejected);
|
||||
|
||||
ASSERT_EQ(ids.size(), corners.size());
|
||||
ASSERT_EQ(ids.size(), markerConfidence.size());
|
||||
|
||||
std::map<int, float> confidenceById;
|
||||
for (size_t i = 0; i < ids.size(); i++) {
|
||||
confidenceById[ids[i]] = markerConfidence[i];
|
||||
}
|
||||
|
||||
// verify that every marker is detected and its confidence is within tolerance
|
||||
for (const auto& currentGT : groundTruths) {
|
||||
const auto it = confidenceById.find(currentGT.id);
|
||||
const bool detected = it != confidenceById.end();
|
||||
EXPECT_EQ(currentGT.expectDetection, detected)
|
||||
<< "Marker id: " << currentGT.id << " (detector config " << cfgIdx << ")";
|
||||
|
||||
if (currentGT.expectDetection && detected) {
|
||||
EXPECT_NEAR(currentGT.confidence, it->second, 0.05)
|
||||
<< "Marker id: " << currentGT.id << " (detector config " << cfgIdx << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Check max and min size in marker detection parameters
|
||||
*/
|
||||
@@ -981,6 +1119,14 @@ TEST(CV_InvertedFlagArucoDetectionConfidence, algorithmic) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CV_ArucoDetectionThreshold, algorithmic) {
|
||||
runArucoDetectionThreshold(ArucoAlgParams::USE_DEFAULT);
|
||||
}
|
||||
|
||||
TEST(CV_InvertedArucoDetectionThreshold, algorithmic) {
|
||||
runArucoDetectionThreshold(ArucoAlgParams::DETECT_INVERTED_MARKER);
|
||||
}
|
||||
|
||||
TEST(CV_ArucoDetectMarkers, regression_3192)
|
||||
{
|
||||
aruco::ArucoDetector detector(aruco::getPredefinedDictionary(aruco::DICT_4X4_50));
|
||||
@@ -1304,6 +1450,7 @@ TEST_P(ArucoThreading, number_of_threads_does_not_change_results)
|
||||
|
||||
aruco::DetectorParameters detectorParameters = detector.getDetectorParameters();
|
||||
detectorParameters.cornerRefinementMethod = (int)GetParam();
|
||||
detectorParameters.validBitIdThreshold = 0.5f;
|
||||
detector.setDetectorParameters(detectorParameters);
|
||||
|
||||
vector<vector<Point2f> > original_corners;
|
||||
|
||||
@@ -66,6 +66,7 @@ void CV_ArucoBoardPose::run(int) {
|
||||
vector<vector<Point2f> > corners;
|
||||
vector<int> ids;
|
||||
detectorParameters.markerBorderBits = markerBorder;
|
||||
detectorParameters.validBitIdThreshold = 0.5f;
|
||||
detector.setDetectorParameters(detectorParameters);
|
||||
detector.detectMarkers(img, corners, ids);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user