mirror of
https://github.com/opencv/opencv.git
synced 2026-07-21 19:33:03 +04:00
Merge pull request #29252 from JonasPerolini:pr-aruco-bit-threshold-in-refine
Use validBitIdThreshold for Aruco refineDetectedMarkers #29252 The goal of this PR is to solve the issue raised by @vrabaud in https://github.com/opencv/opencv/pull/28289 (comment: https://github.com/opencv/opencv/pull/28289#discussion_r3355812646). **Issue:** `refineDetectedMarkers()` converted the extracted cell ratios with `convertTo(CV_8UC1)` (an implicit 0.5 threshold) before computing the code distance, ignoring `detectorParams.validBitIdThreshold`. **Solution:** Make the refine path consistent with the main detection path `Dictionary::identify`. **Changes:** - Add a `Dictionary::getDistanceToId()` overload that takes the float cell pixel ratio matrix and `validBitIdThreshold` (similar to how it's done for the `identify()` overload. - Move the per cell distance computation into a private `getDistanceToIdImpl` helper used by both `identify()` and the new overload of `getDistanceToId()` to avoid repetitions. - `refineDetectedMarkers()` now calls the new overload. **Tests:** - `CV_ArucoRefine.validBitIdThreshold`: a marker with one degraded cell is recovered at threshold 0.7 but not at 0.49. - `CV_ArucoDictionary.getDistanceToIdCellPixelRatio`: unit-tests both `getDistanceToId` overloads. All passed
This commit is contained in:
@@ -65,7 +65,7 @@ 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.
|
||||
/** @brief Given a matrix of pixel ratio ranging from 0 to 1. Returns whether the marker is identified or not.
|
||||
*
|
||||
* Returns reference to the marker id in the dictionary (if any) and its rotation.
|
||||
*/
|
||||
@@ -77,6 +77,22 @@ class CV_EXPORTS_W_SIMPLE Dictionary {
|
||||
*/
|
||||
CV_WRAP int getDistanceToId(InputArray bits, int id, bool allRotations = true) const;
|
||||
|
||||
/** @brief Returns number of cells that differ from the specific id.
|
||||
*
|
||||
* For each cell, the distance is increased when the difference between the detected
|
||||
* cell pixel ratio and the dictionary bit value is greater than `validBitIdThreshold`.
|
||||
* If `allRotations` is set, the four possible marker rotations are considered.
|
||||
*
|
||||
* @param onlyCellPixelRatio markerSize x markerSize matrix (CV_32FC1) holding, for each cell,
|
||||
* the ratio of white pixels ranging from 0 to 1
|
||||
* @param id marker id in the dictionary to compute the distance to
|
||||
* @param allRotations if set, the four possible marker rotations are considered and the
|
||||
* smallest distance is returned
|
||||
* @param validBitIdThreshold maximum allowed difference between a cell pixel ratio and the
|
||||
* dictionary bit value; cells exceeding it are counted as differing
|
||||
*/
|
||||
CV_WRAP int getDistanceToId(InputArray onlyCellPixelRatio, int id, bool allRotations, float validBitIdThreshold) const;
|
||||
|
||||
/** @brief Generate a canonical marker image
|
||||
*/
|
||||
CV_WRAP void generateImageMarker(int id, int sidePixels, OutputArray _img, int borderBits = 1) const;
|
||||
|
||||
@@ -144,6 +144,32 @@ class aruco_objdetect_test(NewOpenCVTests):
|
||||
|
||||
self.assertEqual(dist, 0)
|
||||
|
||||
def test_getDistanceToId_cell_pixel_ratio(self):
|
||||
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_50)
|
||||
idx = 7
|
||||
valid_bit_id_threshold = 0.49
|
||||
bit_marker = np.array([[0, 1, 0, 1], [0, 1, 1, 1], [1, 1, 0, 0], [0, 1, 0, 0]], dtype=np.uint8)
|
||||
ratio_marker = bit_marker.astype(np.float32)
|
||||
|
||||
# Same marker as test_getDistanceToId, but passed as float cell ratios.
|
||||
dist = aruco_dict.getDistanceToId(ratio_marker, idx, True, valid_bit_id_threshold)
|
||||
self.assertEqual(dist, 0)
|
||||
|
||||
# A small drift stays within the threshold.
|
||||
accepted_ratio = ratio_marker.copy()
|
||||
accepted_ratio[0, 0] = 0.4
|
||||
dist = aruco_dict.getDistanceToId(accepted_ratio, idx, True, valid_bit_id_threshold)
|
||||
self.assertEqual(dist, 0)
|
||||
|
||||
# A full flip crosses the threshold and counts as one bad cell.
|
||||
erroneous_ratio = ratio_marker.copy()
|
||||
erroneous_ratio[0, 0] = 1.0 - erroneous_ratio[0, 0]
|
||||
dist = aruco_dict.getDistanceToId(onlyCellPixelRatio=erroneous_ratio,
|
||||
id=idx,
|
||||
allRotations=True,
|
||||
validBitIdThreshold=valid_bit_id_threshold)
|
||||
self.assertEqual(dist, 1)
|
||||
|
||||
def test_aruco_detector(self):
|
||||
aruco_params = cv.aruco.DetectorParameters()
|
||||
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_4X4_250)
|
||||
|
||||
@@ -466,7 +466,6 @@ static float _getMarkerConfidence(const Mat& groundTruthbits, const Mat &cellPix
|
||||
return std::max(0.f, std::min(1.f, normalizedMarkerConfidence));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Tries to identify one candidate given the dictionary
|
||||
* @return candidate typ. zero if the candidate is not valid,
|
||||
@@ -510,10 +509,10 @@ static uint8_t _identifyOneCandidate(const Dictionary& dictionary, const Mat& _i
|
||||
if(borderErrors > maximumErrorsInBorder) return 0; // border is wrong
|
||||
|
||||
// take only inner bits
|
||||
Mat onlyCellPixelRatio =
|
||||
cellPixelRatio.rowRange(params.markerBorderBits,
|
||||
cellPixelRatio.rows - params.markerBorderBits)
|
||||
.colRange(params.markerBorderBits, cellPixelRatio.cols - params.markerBorderBits);
|
||||
Mat onlyCellPixelRatio = cellPixelRatio(
|
||||
Rect(params.markerBorderBits, params.markerBorderBits,
|
||||
cellPixelRatio.cols - 2 * params.markerBorderBits,
|
||||
cellPixelRatio.rows - 2 * params.markerBorderBits));
|
||||
|
||||
// try to identify the marker
|
||||
if(!dictionary.identify(onlyCellPixelRatio, idx, rotation, params.errorCorrectionRate, params.validBitIdThreshold))
|
||||
@@ -1405,15 +1404,13 @@ void ArucoDetector::refineDetectedMarkers(InputArray _image, const Board& _board
|
||||
detectorParams.perspectiveRemovePixelPerCell,
|
||||
detectorParams.perspectiveRemoveIgnoredMarginPerCell, detectorParams.minOtsuStdDev);
|
||||
|
||||
Mat bits;
|
||||
cellPixelRatio.convertTo(bits, CV_8UC1);
|
||||
Mat onlyCellPixelRatio = cellPixelRatio(
|
||||
Rect(detectorParams.markerBorderBits, detectorParams.markerBorderBits,
|
||||
cellPixelRatio.cols - 2 * detectorParams.markerBorderBits,
|
||||
cellPixelRatio.rows - 2 * detectorParams.markerBorderBits));
|
||||
|
||||
Mat onlyBits =
|
||||
bits.rowRange(detectorParams.markerBorderBits, bits.rows - detectorParams.markerBorderBits)
|
||||
.colRange(detectorParams.markerBorderBits, bits.rows - detectorParams.markerBorderBits);
|
||||
|
||||
codeDistance =
|
||||
dictionary.getDistanceToId(onlyBits, undetectedMarkersIds[i], false);
|
||||
codeDistance = dictionary.getDistanceToId(onlyCellPixelRatio, undetectedMarkersIds[i],
|
||||
false, detectorParams.validBitIdThreshold);
|
||||
}
|
||||
|
||||
// if everythin is ok, assign values to current best match
|
||||
|
||||
@@ -15,6 +15,86 @@ namespace aruco {
|
||||
|
||||
using namespace std;
|
||||
|
||||
struct CellBitMasks {
|
||||
CellBitMasks(const Mat &onlyCellPixelRatio, int markerSize, float validBitIdThreshold)
|
||||
: s((markerSize * markerSize + 8 - 1) / 8),
|
||||
totalCells(markerSize * markerSize),
|
||||
temp(4 * s),
|
||||
not0(temp.data()), not1(not0 + s), notXor(not1 + s), temp0(temp.data() + 3 * s) {
|
||||
uint8_t* not0Writable = temp.data();
|
||||
uint8_t* not1Writable = not0Writable + s;
|
||||
uint8_t* notXorWritable = not1Writable + s;
|
||||
|
||||
// Fill bit masks of cells that are not black (not0) and not white (not1).
|
||||
unsigned char not0Byte = 0, not1Byte = 0;
|
||||
int currentByte = 0, currentBit = 0;
|
||||
for(int j = 0; j < markerSize; j++) {
|
||||
const float* cellPixelRatioRow = onlyCellPixelRatio.ptr<float>(j);
|
||||
for(int i = 0; i < markerSize; i++) {
|
||||
not0Byte <<= 1; not1Byte <<= 1;
|
||||
if(cellPixelRatioRow[i] > validBitIdThreshold) not0Byte |= 1;
|
||||
if(cellPixelRatioRow[i] < 1 - validBitIdThreshold) not1Byte |= 1;
|
||||
++currentBit;
|
||||
if(currentBit == 8) {
|
||||
not0Writable[currentByte] = not0Byte;
|
||||
not1Writable[currentByte] = not1Byte;
|
||||
not0Byte = not1Byte = 0;
|
||||
++currentByte;
|
||||
currentBit = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(currentBit != 0) {
|
||||
not0Writable[currentByte] = not0Byte;
|
||||
not1Writable[currentByte] = not1Byte;
|
||||
}
|
||||
|
||||
// Computing: notXor = not0 ^ not1
|
||||
hal::xor8u(not0, s, not1, s, notXorWritable, s, s, 1, nullptr);
|
||||
}
|
||||
|
||||
CellBitMasks(const CellBitMasks&) = delete;
|
||||
CellBitMasks& operator=(const CellBitMasks&) = delete;
|
||||
|
||||
// Smallest Hamming distance between these cell masks and dictionary marker `id`,
|
||||
// searching the tested rotations; `rotation` returns the best one.
|
||||
// Mutates the internal buffer (temp0).
|
||||
int hammingDistanceToId(const Mat& bytesList, int id, bool allRotations, int& rotation) {
|
||||
CV_Assert(id >= 0 && id < bytesList.rows);
|
||||
|
||||
const unsigned int nRotations = allRotations ? 4u : 1u;
|
||||
int currentMinDistance = totalCells + 1;
|
||||
rotation = -1;
|
||||
|
||||
const uchar* bytesRot = bytesList.ptr(id);
|
||||
for(unsigned int r = 0; r < nRotations; r++, bytesRot += s) {
|
||||
// Error if (marker is 0 and input is not 0) or (marker is 1 and input is not 1)
|
||||
// i.e.: (!bytesRot && not0) || (bytesRot && not1)
|
||||
// This is equivalent to: not0 ^ ((not0 ^ not1) & bytesRot)
|
||||
// Computing: temp0 = (not0 ^ not1) & bytesRot
|
||||
hal::and8u(notXor, s, bytesRot, s, temp0, s, s, 1, nullptr);
|
||||
// Computing the final result (xor is performed internally).
|
||||
int currentHamming = cv::hal::normHamming(not0, temp0, s);
|
||||
|
||||
if(currentHamming < currentMinDistance) {
|
||||
currentMinDistance = currentHamming;
|
||||
rotation = static_cast<int>(r);
|
||||
// Break for perfect distance.
|
||||
if(currentMinDistance == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
return currentMinDistance;
|
||||
}
|
||||
|
||||
const int s; // bytes per rotation
|
||||
const int totalCells;
|
||||
std::vector<uint8_t> temp;
|
||||
const uint8_t *not0, *not1, *notXor;
|
||||
uint8_t *temp0; // internal scratch workspace
|
||||
};
|
||||
|
||||
|
||||
Dictionary::Dictionary(): markerSize(0), maxCorrectionBits(0) {}
|
||||
|
||||
|
||||
@@ -46,6 +126,7 @@ bool Dictionary::readDictionary(const cv::FileNode& fn) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void Dictionary::writeDictionary(FileStorage& fs, const String &name)
|
||||
{
|
||||
CV_Assert(fs.isOpened());
|
||||
@@ -75,36 +156,9 @@ void Dictionary::writeDictionary(FileStorage& fs, const String &name)
|
||||
|
||||
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);
|
||||
CV_Assert(onlyCellPixelRatio.type() == CV_32FC1);
|
||||
|
||||
// Fill bit masks of cells that are not black (not0) and not white (not1).
|
||||
const int s = (markerSize * markerSize + 8 - 1) / 8;
|
||||
AutoBuffer<uint8_t> temp(4 * s);
|
||||
uint8_t* not0 = temp.data(), * not1 = not0 + s;
|
||||
uint8_t not0Byte = 0, not1Byte = 0;
|
||||
int currentByte = 0, currentBit = 0;
|
||||
for(int j = 0; j < markerSize; j++) {
|
||||
const float* cellPixelRatioRow = onlyCellPixelRatio.ptr<float>(j);
|
||||
for(int i = 0; i < markerSize; i++) {
|
||||
not0Byte <<= 1; not1Byte <<= 1;
|
||||
if(cellPixelRatioRow[i] > validBitIdThreshold) not0Byte |= 1;
|
||||
if(cellPixelRatioRow[i] < 1 - validBitIdThreshold) not1Byte |= 1;
|
||||
++currentBit;
|
||||
if(currentBit == 8) {
|
||||
not0[currentByte] = not0Byte;
|
||||
not1[currentByte] = not1Byte;
|
||||
not0Byte = not1Byte = 0;
|
||||
++currentByte;
|
||||
currentBit = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentBit != 0) {
|
||||
not0[currentByte] = not0Byte;
|
||||
not1[currentByte] = not1Byte;
|
||||
}
|
||||
uint8_t* notXor = not1 + s, * temp0 = notXor + s;
|
||||
// Computing: notXor = not0 ^ not1
|
||||
hal::xor8u(not0, s, not1, s, notXor, s, s, 1, nullptr);
|
||||
CellBitMasks cellBitMasks(onlyCellPixelRatio, markerSize, validBitIdThreshold);
|
||||
|
||||
int maxCorrectionRecalculed = int(double(maxCorrectionBits) * maxCorrectionRate);
|
||||
|
||||
@@ -112,25 +166,8 @@ bool Dictionary::identify(const Mat &onlyCellPixelRatio, CV_OUT int &idx, CV_OUT
|
||||
|
||||
// search closest marker in dict
|
||||
for(int m = 0; m < bytesList.rows; m++) {
|
||||
int currentMinDistance = markerSize * markerSize + 1;
|
||||
int currentRotation = -1;
|
||||
const uchar* bytesRot = bytesList.ptr(m);
|
||||
for(int r = 0; r < 4; r++, bytesRot += s) {
|
||||
// Error if: (marker is 0 and input is not 0) or (marker is 1 and input is not 1)
|
||||
// i.e. if: (!bytesRot && not0) || (bytesRot && not1)
|
||||
// This is actually: not0 ^ ((not0 ^ not1) & bytesRot)
|
||||
// Computing: temp0 = (not0 ^ not1) & bytesRot
|
||||
hal::and8u(notXor, s, bytesRot, s, temp0, s, s, 1, nullptr);
|
||||
// Computing the final result (xor is performed internally).
|
||||
int currentHamming = cv::hal::normHamming(not0, temp0, s);
|
||||
|
||||
if(currentHamming < currentMinDistance) {
|
||||
currentMinDistance = currentHamming;
|
||||
currentRotation = r;
|
||||
// Break for perfect distance.
|
||||
if (currentMinDistance == 0) break;
|
||||
}
|
||||
}
|
||||
int currentMinDistance = cellBitMasks.hammingDistanceToId(bytesList, m, true, currentRotation);
|
||||
|
||||
// if maxCorrection is fulfilled, return this one
|
||||
if(currentMinDistance <= maxCorrectionRecalculed) {
|
||||
@@ -148,9 +185,8 @@ bool Dictionary::identify(const Mat &onlyBits, CV_OUT int &idx, CV_OUT int &rota
|
||||
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);
|
||||
Mat(onlyBits > 0).convertTo(candidateBitRatio, CV_32F, 1.0 / 255.0);
|
||||
return identify(candidateBitRatio, idx, rotation, maxCorrectionRate, DEFAULT_VALID_BIT_ID_THRESHOLD);
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +213,19 @@ int Dictionary::getDistanceToId(InputArray bits, int id, bool allRotations) cons
|
||||
}
|
||||
|
||||
|
||||
int Dictionary::getDistanceToId(InputArray onlyCellPixelRatio, int id, bool allRotations, float validBitIdThreshold) const {
|
||||
|
||||
Mat onlyCellPixelRatioMat = onlyCellPixelRatio.getMat();
|
||||
CV_Assert(onlyCellPixelRatioMat.rows == markerSize && onlyCellPixelRatioMat.cols == markerSize);
|
||||
CV_Assert(onlyCellPixelRatioMat.type() == CV_32FC1);
|
||||
CV_Assert(id >= 0 && id < bytesList.rows);
|
||||
|
||||
int rotation = -1;
|
||||
CellBitMasks cellBitMasks(onlyCellPixelRatioMat, markerSize, validBitIdThreshold);
|
||||
return cellBitMasks.hammingDistanceToId(bytesList, id, allRotations, rotation);
|
||||
}
|
||||
|
||||
|
||||
void Dictionary::generateImageMarker(int id, int sidePixels, OutputArray _img, int borderBits) const {
|
||||
CV_Assert(sidePixels >= (markerSize + 2*borderBits));
|
||||
CV_Assert(id < bytesList.rows);
|
||||
@@ -405,6 +454,7 @@ static Mat _generateRandomMarker(int markerSize, RNG &rng) {
|
||||
return marker;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Calculate selfDistance of the codification of a marker Mat. Self distance is the Hamming
|
||||
* distance of the marker to itself in the other rotations.
|
||||
|
||||
@@ -206,6 +206,94 @@ void CV_ArucoRefine::run(int) {
|
||||
}
|
||||
}
|
||||
|
||||
// Find the position of a given marker id in the detection results, or -1 if absent.
|
||||
static int findMarkerIndex(const vector<int>& ids, int markerId) {
|
||||
for(size_t i = 0; i < ids.size(); i++) {
|
||||
if(ids[i] == markerId)
|
||||
return (int)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Warp a marker image onto an arbitrary quad in the scene and paint it over the
|
||||
// background. A neutral grey (127) is used as the "background", the marker
|
||||
// only contains black/white pixels, so everything that stays 127 after the warp
|
||||
// is background and is left untouched.
|
||||
static void drawMarkerAtCorners(Mat& image, const Mat& marker, const vector<Point2f>& corners) {
|
||||
vector<Point2f> originalCorners = {
|
||||
Point2f(0.f, 0.f),
|
||||
Point2f((float)marker.cols - 1.f, 0.f),
|
||||
Point2f((float)marker.cols - 1.f, (float)marker.rows - 1.f),
|
||||
Point2f(0.f, (float)marker.rows - 1.f)
|
||||
};
|
||||
Mat transformation = getPerspectiveTransform(originalCorners, corners);
|
||||
|
||||
Mat warped(image.size(), image.type(), Scalar::all(127));
|
||||
warpPerspective(marker, warped, transformation, image.size(), INTER_NEAREST, BORDER_CONSTANT, Scalar::all(127));
|
||||
|
||||
Mat mask = warped != 127;
|
||||
warped.copyTo(image, mask);
|
||||
}
|
||||
|
||||
// Degrade the marker image: find its first black inner cell and partially fill it with white so
|
||||
// that the cell's white-pixel ratio becomes ~whiteRatio. This lets the test control how far a
|
||||
// single cell drifts from its ground-truth bit, which is what validBitIdThreshold gates.
|
||||
static bool setFirstBlackInnerCellWhiteRatio(Mat& marker, const aruco::Dictionary& dictionary,
|
||||
int markerId, int markerBorderBits, float whiteRatio) {
|
||||
const int markerSizeWithBorders = dictionary.markerSize + 2 * markerBorderBits;
|
||||
const int cellSize = marker.rows / markerSizeWithBorders;
|
||||
if(marker.cols != marker.rows || cellSize * markerSizeWithBorders != marker.rows)
|
||||
return false;
|
||||
|
||||
Mat markerBits = dictionary.getMarkerBits(markerId);
|
||||
for(int y = 0; y < dictionary.markerSize; y++) {
|
||||
for(int x = 0; x < dictionary.markerSize; x++) {
|
||||
if(markerBits.ptr<float>(y)[x] != 0.f)
|
||||
continue; // skip white cells
|
||||
|
||||
Rect cell((x + markerBorderBits) * cellSize, (y + markerBorderBits) * cellSize,
|
||||
cellSize, cellSize);
|
||||
marker(cell).setTo(Scalar::all(0));
|
||||
|
||||
// A centred white square of side sqrt(whiteRatio)*cellSize covers ~whiteRatio of the cell.
|
||||
int whiteSide = cvRound(cellSize * std::sqrt(whiteRatio));
|
||||
whiteSide = std::max(1, std::min(cellSize, whiteSide));
|
||||
const int offset = (cellSize - whiteSide) / 2;
|
||||
marker(Rect(cell.x + offset, cell.y + offset, whiteSide, whiteSide)).setTo(Scalar::all(255));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drop a marker from the detection results and move its corners to the rejected list, so that
|
||||
// refineDetectedMarkers() has a rejected candidate to try to recover.
|
||||
static bool removeMarkerAndMakeRejected(int markerId, vector<vector<Point2f>>& corners,
|
||||
vector<int>& ids, vector<vector<Point2f>>& rejected) {
|
||||
const int markerIndex = findMarkerIndex(ids, markerId);
|
||||
if(markerIndex < 0)
|
||||
return false;
|
||||
|
||||
rejected.clear();
|
||||
rejected.push_back(corners[(size_t)markerIndex]);
|
||||
corners.erase(corners.begin() + markerIndex);
|
||||
ids.erase(ids.begin() + markerIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Render a flat board image and detect its markers.
|
||||
// Returns true only when every board marker was found.
|
||||
static bool generateBoardForRefine(const aruco::GridBoard& board, int markerBorderBits,
|
||||
Mat& image, const aruco::ArucoDetector& detector,
|
||||
vector<vector<Point2f>>& corners, vector<int>& ids) {
|
||||
board.generateImage(Size(760, 760), image, 50, markerBorderBits);
|
||||
|
||||
vector<vector<Point2f>> rejected;
|
||||
detector.detectMarkers(image, corners, ids, rejected);
|
||||
return board.getIds().size() == ids.size();
|
||||
}
|
||||
|
||||
TEST(CV_ArucoBoardPose, accuracy) {
|
||||
CV_ArucoBoardPose test(ArucoAlgParams::USE_DEFAULT);
|
||||
test.safe_run();
|
||||
@@ -229,6 +317,75 @@ TEST(CV_Aruco3Refine, accuracy) {
|
||||
test.safe_run();
|
||||
}
|
||||
|
||||
// refineDetectedMarkers() must use detectorParams.validBitIdThreshold when matching a rejected
|
||||
// candidate's cell ratios against the expected marker code. Both cases below refine the very same
|
||||
// image: a board whose dropped marker 0 is redrawn with one black cell brightened to a 0.6 white
|
||||
// ratio and differ only in the threshold: the strict default (0.49) treats that cell as a bit
|
||||
// error and leaves the marker rejected, while a relaxed 0.7 tolerates the deviation and recovers it.
|
||||
class CV_ArucoRefineValidBitIdThreshold : public testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
const int markerBorderBits = 1;
|
||||
const int markerSidePixels = 300;
|
||||
|
||||
dictionary = aruco::getPredefinedDictionary(aruco::DICT_4X4_50);
|
||||
board = aruco::GridBoard(Size(2, 2), 1.f, 0.2f, dictionary);
|
||||
|
||||
detectorParameters.markerBorderBits = markerBorderBits;
|
||||
detectorParameters.perspectiveRemovePixelPerCell = 20;
|
||||
detectorParameters.perspectiveRemoveIgnoredMarginPerCell = 0.;
|
||||
|
||||
const aruco::ArucoDetector detector(dictionary, detectorParameters, refineParameters);
|
||||
|
||||
// Start from a fully detected board (clean markers, so the threshold is irrelevant here).
|
||||
ASSERT_TRUE(generateBoardForRefine(board, markerBorderBits, image, detector, corners, ids));
|
||||
|
||||
// Drop marker 0 so it becomes a rejected candidate for refinement.
|
||||
ASSERT_TRUE(removeMarkerAndMakeRejected(markerId, corners, ids, rejected));
|
||||
|
||||
// Draw a degraded version of marker 0 (one black cell at 0.6 white ratio) at its location.
|
||||
Mat marker;
|
||||
dictionary.generateImageMarker(markerId, markerSidePixels, marker, markerBorderBits);
|
||||
ASSERT_TRUE(setFirstBlackInnerCellWhiteRatio(marker, dictionary, markerId, markerBorderBits, 0.6f));
|
||||
drawMarkerAtCorners(image, marker, rejected[0]);
|
||||
}
|
||||
|
||||
// Refine the shared image with a given threshold and report whether marker 0 was recovered.
|
||||
// refineDetectedMarkers() mutates its inputs, so each attempt runs on its own copy.
|
||||
bool isMarkerRecovered(float validBitIdThreshold) const {
|
||||
aruco::DetectorParameters attemptParameters = detectorParameters;
|
||||
attemptParameters.validBitIdThreshold = validBitIdThreshold;
|
||||
const aruco::ArucoDetector attemptDetector(dictionary, attemptParameters, refineParameters);
|
||||
|
||||
vector<vector<Point2f>> attemptCorners = corners;
|
||||
vector<int> attemptIds = ids;
|
||||
vector<vector<Point2f>> attemptRejected = rejected;
|
||||
attemptDetector.refineDetectedMarkers(image, board, attemptCorners, attemptIds, attemptRejected);
|
||||
return findMarkerIndex(attemptIds, markerId) >= 0;
|
||||
}
|
||||
|
||||
const int markerId = 0;
|
||||
aruco::Dictionary dictionary;
|
||||
aruco::GridBoard board;
|
||||
aruco::DetectorParameters detectorParameters;
|
||||
aruco::RefineParameters refineParameters{10.f, 1.f, true};
|
||||
|
||||
Mat image;
|
||||
vector<vector<Point2f>> corners;
|
||||
vector<int> ids;
|
||||
vector<vector<Point2f>> rejected;
|
||||
};
|
||||
|
||||
// Strict threshold: the 0.6 white cell is treated as a bit error, so the marker is not recovered.
|
||||
TEST_F(CV_ArucoRefineValidBitIdThreshold, strictThresholdKeepsMarkerRejected) {
|
||||
EXPECT_FALSE(isMarkerRecovered(0.49f));
|
||||
}
|
||||
|
||||
// Relaxed threshold: the deviation is tolerated, so the marker is recovered.
|
||||
TEST_F(CV_ArucoRefineValidBitIdThreshold, relaxedThresholdRecoversMarker) {
|
||||
EXPECT_TRUE(isMarkerRecovered(0.7f));
|
||||
}
|
||||
|
||||
TEST(CV_ArucoBoardPose, CheckNegativeZ)
|
||||
{
|
||||
double matrixData[9] = { -3.9062571886921410e+02, 0., 4.2350000000000000e+02,
|
||||
@@ -329,6 +486,85 @@ TEST(CV_ArucoDictionary, extendDictionary) {
|
||||
ASSERT_EQ(custom_dictionary.bytesList.rows, 150);
|
||||
ASSERT_EQ(cv::norm(custom_dictionary.bytesList, base_dictionary.bytesList.rowRange(0, 150)), 0.);
|
||||
}
|
||||
|
||||
// Unit-test both getDistanceToId() overloads on a known marker: the existing bit-based overload
|
||||
// must keep its exact Hamming behaviour, and the new ratio-based overload must count a cell as an
|
||||
// error only when it deviates from the expected bit by more than validBitIdThreshold.
|
||||
TEST(CV_ArucoDictionary, getDistanceToIdCellPixelRatio) {
|
||||
const int markerId = 0;
|
||||
const float validBitIdThreshold = 0.49f;
|
||||
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_4X4_50);
|
||||
|
||||
// Bit overload: the exact marker bits are at distance 0 from their own id.
|
||||
Mat bits = aruco::Dictionary::getBitsFromByteList(dictionary.bytesList.rowRange(markerId, markerId + 1),
|
||||
dictionary.markerSize);
|
||||
EXPECT_EQ(0, dictionary.getDistanceToId(bits, markerId, false));
|
||||
|
||||
// Bit overload: flipping a single bit yields a Hamming distance of exactly 1.
|
||||
Mat erroneousBits = bits.clone();
|
||||
erroneousBits.ptr<uchar>(0)[0] = (uchar)!erroneousBits.ptr<uchar>(0)[0];
|
||||
EXPECT_EQ(1, dictionary.getDistanceToId(erroneousBits, markerId, false));
|
||||
|
||||
// Ground-truth bit values (0.f or 1.f) for the ratio overload checks below.
|
||||
Mat markerRatio = dictionary.getMarkerBits(markerId);
|
||||
const float expectedBit = markerRatio.ptr<float>(0)[0];
|
||||
|
||||
// Ratio overload: a 0.4 drift toward the wrong value stays within the 0.49 tolerance -> no error.
|
||||
Mat acceptedRatio = markerRatio.clone();
|
||||
acceptedRatio.ptr<float>(0)[0] = expectedBit > 0.5f ? 0.6f : 0.4f;
|
||||
EXPECT_EQ(0, dictionary.getDistanceToId(acceptedRatio, markerId, false, validBitIdThreshold));
|
||||
|
||||
// Ratio overload: a 0.6 drift exceeds the 0.49 tolerance -> the cell counts as one error.
|
||||
Mat rejectedRatio = markerRatio.clone();
|
||||
rejectedRatio.ptr<float>(0)[0] = expectedBit > 0.5f ? 0.4f : 0.6f;
|
||||
EXPECT_EQ(1, dictionary.getDistanceToId(rejectedRatio, markerId, false, validBitIdThreshold));
|
||||
}
|
||||
|
||||
// 5x5 markers leave one meaningful bit in the final packed byte. Flip only that cell
|
||||
// far enough from its expected value and verify that the ratio distance counts it.
|
||||
TEST(CV_ArucoDictionary, getDistanceToIdCellPixelRatioPartialByte) {
|
||||
const int markerId = 15;
|
||||
const float validBitIdThreshold = 0.49f;
|
||||
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_5X5_50);
|
||||
|
||||
Mat markerRatio = dictionary.getMarkerBits(markerId);
|
||||
EXPECT_EQ(0, dictionary.getDistanceToId(markerRatio, markerId, false, validBitIdThreshold));
|
||||
|
||||
Mat rotatedMarkerRatio = dictionary.getMarkerBits(markerId, 1);
|
||||
EXPECT_EQ(0, dictionary.getDistanceToId(rotatedMarkerRatio, markerId, true, validBitIdThreshold));
|
||||
|
||||
Mat rejectedRatio = markerRatio.clone();
|
||||
float& lastCellRatio = rejectedRatio.ptr<float>(dictionary.markerSize - 1)[dictionary.markerSize - 1];
|
||||
lastCellRatio = lastCellRatio > 0.5f ? 0.4f : 0.6f;
|
||||
EXPECT_EQ(1, dictionary.getDistanceToId(rejectedRatio, markerId, false, validBitIdThreshold));
|
||||
}
|
||||
|
||||
TEST(CV_ArucoDictionary, identifyBitMask) {
|
||||
const int markerId = 7;
|
||||
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(aruco::DICT_4X4_50);
|
||||
|
||||
// Start with a 0/1 bit matrix for the marker and confirm that the bit-based
|
||||
// identify overload handles it without any ratio threshold input.
|
||||
Mat bits = aruco::Dictionary::getBitsFromByteList(dictionary.bytesList.rowRange(markerId, markerId + 1),
|
||||
dictionary.markerSize);
|
||||
|
||||
int idx = -1;
|
||||
int rotation = -1;
|
||||
ASSERT_TRUE(dictionary.identify(bits, idx, rotation, 0.0));
|
||||
EXPECT_EQ(markerId, idx);
|
||||
EXPECT_EQ(0, rotation);
|
||||
|
||||
// OpenCV comparisons produce masks with values 0 and 255, not 0 and 1. The raw-bit
|
||||
// identify overload must normalize those masks before delegating to the ratio path.
|
||||
Mat bitMask;
|
||||
bits.convertTo(bitMask, CV_8U, 255.0);
|
||||
idx = -1;
|
||||
rotation = -1;
|
||||
ASSERT_TRUE(dictionary.identify(bitMask, idx, rotation, 0.0));
|
||||
EXPECT_EQ(markerId, idx);
|
||||
EXPECT_EQ(0, rotation);
|
||||
}
|
||||
|
||||
TEST(CV_ArucoBoardGenerateImage_RotationTest, HandlesRotatedMarkersWithoutBoundingBoxError)
|
||||
{
|
||||
using namespace cv;
|
||||
|
||||
Reference in New Issue
Block a user