1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-31 08:13:04 +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:
Jonas Perolini
2026-06-29 12:37:43 +02:00
committed by GitHub
parent 3c10324630
commit 37f3c7539b
5 changed files with 389 additions and 64 deletions
+10 -13
View File
@@ -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
+100 -50
View File
@@ -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.