Skip to content

Commit ea5fb7b

Browse files
author
Alexander Panov
committed
Merge pull request opencv#3200 from AleksandrPanov:aruco_improvements
Aruco improvements * add writeDictionary(), dict distance, fix readDictionary(), readDetectorParameters() * add aruco_dict_utils.cpp * add py test_write_read_dict * update tutorial
1 parent 43d3584 commit ea5fb7b

18 files changed

+415
-89
lines changed

modules/aruco/include/opencv2/aruco.hpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ struct CV_EXPORTS_W DetectorParameters {
169169

170170
DetectorParameters();
171171
CV_WRAP static Ptr<DetectorParameters> create();
172-
CV_WRAP static bool readDetectorParameters(const FileNode& fn, Ptr<DetectorParameters>& params);
172+
CV_WRAP bool readDetectorParameters(const FileNode& fn);
173173

174174
CV_PROP_RW int adaptiveThreshWinSizeMin;
175175
CV_PROP_RW int adaptiveThreshWinSizeMax;

modules/aruco/include/opencv2/aruco/dictionary.hpp

+28-23
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,20 @@ class CV_EXPORTS_W Dictionary {
9494
const Ptr<Dictionary> &baseDictionary, int randomSeed=0);
9595

9696
/**
97-
* @brief Read a new dictionary from FileNode. Format:
98-
* nmarkers: 35
99-
* markersize: 6
100-
* marker_0: "101011111011111001001001101100000000"
101-
* ...
97+
* @brief Read a new dictionary from FileNode. Format:\n
98+
* nmarkers: 35\n
99+
* markersize: 6\n
100+
* maxCorrectionBits: 5\n
101+
* marker_0: "101011111011111001001001101100000000"\n
102+
* ...\n
102103
* marker_34: "011111010000111011111110110101100101"
103104
*/
104-
CV_WRAP static bool readDictionary(const cv::FileNode& fn, cv::Ptr<cv::aruco::Dictionary> &dictionary);
105+
CV_WRAP bool readDictionary(const cv::FileNode& fn);
105106

107+
/**
108+
* @brief Write a dictionary to FileStorage. Format is the same as in readDictionary().
109+
*/
110+
CV_WRAP void writeDictionary(Ptr<FileStorage>& fs);
106111
/**
107112
* @see getPredefinedDictionary
108113
*/
@@ -149,23 +154,23 @@ class CV_EXPORTS_W Dictionary {
149154
distance
150155
*/
151156
enum PREDEFINED_DICTIONARY_NAME {
152-
DICT_4X4_50 = 0,
153-
DICT_4X4_100,
154-
DICT_4X4_250,
155-
DICT_4X4_1000,
156-
DICT_5X5_50,
157-
DICT_5X5_100,
158-
DICT_5X5_250,
159-
DICT_5X5_1000,
160-
DICT_6X6_50,
161-
DICT_6X6_100,
162-
DICT_6X6_250,
163-
DICT_6X6_1000,
164-
DICT_7X7_50,
165-
DICT_7X7_100,
166-
DICT_7X7_250,
167-
DICT_7X7_1000,
168-
DICT_ARUCO_ORIGINAL,
157+
DICT_4X4_50 = 0, ///< 4x4 bits, minimum hamming distance between any two codes = 4, 50 codes
158+
DICT_4X4_100, ///< 4x4 bits, minimum hamming distance between any two codes = 3, 100 codes
159+
DICT_4X4_250, ///< 4x4 bits, minimum hamming distance between any two codes = 3, 250 codes
160+
DICT_4X4_1000, ///< 4x4 bits, minimum hamming distance between any two codes = 2, 1000 codes
161+
DICT_5X5_50, ///< 5x5 bits, minimum hamming distance between any two codes = 8, 50 codes
162+
DICT_5X5_100, ///< 5x5 bits, minimum hamming distance between any two codes = 7, 100 codes
163+
DICT_5X5_250, ///< 5x5 bits, minimum hamming distance between any two codes = 6, 250 codes
164+
DICT_5X5_1000, ///< 5x5 bits, minimum hamming distance between any two codes = 5, 1000 codes
165+
DICT_6X6_50, ///< 6x6 bits, minimum hamming distance between any two codes = 13, 50 codes
166+
DICT_6X6_100, ///< 6x6 bits, minimum hamming distance between any two codes = 12, 100 codes
167+
DICT_6X6_250, ///< 6x6 bits, minimum hamming distance between any two codes = 11, 250 codes
168+
DICT_6X6_1000, ///< 6x6 bits, minimum hamming distance between any two codes = 9, 1000 codes
169+
DICT_7X7_50, ///< 7x7 bits, minimum hamming distance between any two codes = 19, 50 codes
170+
DICT_7X7_100, ///< 7x7 bits, minimum hamming distance between any two codes = 18, 100 codes
171+
DICT_7X7_250, ///< 7x7 bits, minimum hamming distance between any two codes = 17, 250 codes
172+
DICT_7X7_1000, ///< 7x7 bits, minimum hamming distance between any two codes = 14, 1000 codes
173+
DICT_ARUCO_ORIGINAL, ///< 6x6 bits, minimum hamming distance between any two codes = 3, 1024 codes
169174
DICT_APRILTAG_16h5, ///< 4x4 bits, minimum hamming distance between any two codes = 5, 30 codes
170175
DICT_APRILTAG_25h9, ///< 5x5 bits, minimum hamming distance between any two codes = 9, 35 codes
171176
DICT_APRILTAG_36h10, ///< 6x6 bits, minimum hamming distance between any two codes = 10, 2320 codes

modules/aruco/misc/python/test/test_aruco.py

+29
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,34 @@ def test_drawCharucoDiamond(self):
3535
img = cv.aruco.drawCharucoDiamond(aruco_dict, np.array([0, 1, 2, 3]), 100, 80)
3636
self.assertTrue(img is not None)
3737

38+
def test_write_read_dict(self):
39+
40+
try:
41+
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_5X5_50)
42+
markers_gold = aruco_dict.bytesList
43+
44+
# write aruco_dict
45+
filename = "test_dict.yml"
46+
fs_write = cv.FileStorage(filename, cv.FileStorage_WRITE)
47+
aruco_dict.writeDictionary(fs_write)
48+
fs_write.release()
49+
50+
# reset aruco_dict
51+
aruco_dict = cv.aruco.getPredefinedDictionary(cv.aruco.DICT_6X6_250)
52+
53+
# read aruco_dict
54+
fs_read = cv.FileStorage(filename, cv.FileStorage_READ)
55+
aruco_dict.readDictionary(fs_read.root())
56+
fs_read.release()
57+
58+
# check equal
59+
self.assertEqual(aruco_dict.markerSize, 5)
60+
self.assertEqual(aruco_dict.maxCorrectionBits, 3)
61+
np.testing.assert_array_equal(aruco_dict.bytesList, markers_gold)
62+
63+
finally:
64+
if os.path.exists(filename):
65+
os.remove(filename)
66+
3867
if __name__ == '__main__':
3968
NewOpenCVTests.bootstrap()
+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#include <opencv2/core/hal/hal.hpp>
2+
#include <opencv2/aruco.hpp>
3+
#include <iostream>
4+
5+
using namespace cv;
6+
using namespace std;
7+
8+
static inline int _getSelfDistance(const Mat &marker) {
9+
Mat bytes = aruco::Dictionary::getByteListFromBits(marker);
10+
int minHamming = (int)marker.total() + 1;
11+
for(int r = 1; r < 4; r++) {
12+
int currentHamming = cv::hal::normHamming(bytes.ptr(), bytes.ptr() + bytes.cols*r, bytes.cols);
13+
if(currentHamming < minHamming) minHamming = currentHamming;
14+
}
15+
Mat b;
16+
flip(marker, b, 0);
17+
Mat flipBytes = aruco::Dictionary::getByteListFromBits(b);
18+
for(int r = 0; r < 4; r++) {
19+
int currentHamming = cv::hal::normHamming(flipBytes.ptr(), bytes.ptr() + bytes.cols*r, bytes.cols);
20+
if(currentHamming < minHamming) minHamming = currentHamming;
21+
}
22+
flip(marker, b, 1);
23+
flipBytes = aruco::Dictionary::getByteListFromBits(b);
24+
for(int r = 0; r < 4; r++) {
25+
int currentHamming = cv::hal::normHamming(flipBytes.ptr(), bytes.ptr() + bytes.cols*r, bytes.cols);
26+
if(currentHamming < minHamming) minHamming = currentHamming;
27+
}
28+
return minHamming;
29+
}
30+
31+
static inline int getFlipDistanceToId(Ptr<aruco::Dictionary> dict, InputArray bits, int id, bool allRotations = true) {
32+
Mat bytesList = dict->bytesList;
33+
CV_Assert(id >= 0 && id < bytesList.rows);
34+
35+
unsigned int nRotations = 4;
36+
if(!allRotations) nRotations = 1;
37+
38+
Mat candidateBytes = aruco::Dictionary::getByteListFromBits(bits.getMat());
39+
int currentMinDistance = int(bits.total() * bits.total());
40+
for(unsigned int r = 0; r < nRotations; r++) {
41+
int currentHamming = cv::hal::normHamming(
42+
bytesList.ptr(id) + r*candidateBytes.cols,
43+
candidateBytes.ptr(),
44+
candidateBytes.cols);
45+
46+
if(currentHamming < currentMinDistance) {
47+
currentMinDistance = currentHamming;
48+
}
49+
}
50+
Mat b;
51+
flip(bits.getMat(), b, 0);
52+
candidateBytes = aruco::Dictionary::getByteListFromBits(b);
53+
for(unsigned int r = 0; r < nRotations; r++) {
54+
int currentHamming = cv::hal::normHamming(
55+
bytesList.ptr(id) + r * candidateBytes.cols,
56+
candidateBytes.ptr(),
57+
candidateBytes.cols);
58+
if (currentHamming < currentMinDistance) {
59+
currentMinDistance = currentHamming;
60+
}
61+
}
62+
63+
flip(bits.getMat(), b, 1);
64+
candidateBytes = aruco::Dictionary::getByteListFromBits(b);
65+
for(unsigned int r = 0; r < nRotations; r++) {
66+
int currentHamming = cv::hal::normHamming(
67+
bytesList.ptr(id) + r * candidateBytes.cols,
68+
candidateBytes.ptr(),
69+
candidateBytes.cols);
70+
if (currentHamming < currentMinDistance) {
71+
currentMinDistance = currentHamming;
72+
}
73+
}
74+
return currentMinDistance;
75+
}
76+
77+
static inline Ptr<aruco::Dictionary> generateCustomAsymmetricDictionary(int nMarkers, int markerSize,
78+
const Ptr<aruco::Dictionary> &baseDictionary, int randomSeed) {
79+
RNG rng((uint64)(randomSeed));
80+
81+
Ptr<aruco::Dictionary> out = makePtr<aruco::Dictionary>();
82+
out->markerSize = markerSize;
83+
84+
// theoretical maximum intermarker distance
85+
// See S. Garrido-Jurado, R. Muñoz-Salinas, F. J. Madrid-Cuevas, and M. J. Marín-Jiménez. 2014.
86+
// "Automatic generation and detection of highly reliable fiducial markers under occlusion".
87+
// Pattern Recogn. 47, 6 (June 2014), 2280-2292. DOI=10.1016/j.patcog.2014.01.005
88+
int C = (int)std::floor(float(markerSize * markerSize) / 4.f);
89+
int tau = 2 * (int)std::floor(float(C) * 4.f / 3.f);
90+
91+
// if baseDictionary is provided, calculate its intermarker distance
92+
if(baseDictionary->bytesList.rows > 0) {
93+
CV_Assert(baseDictionary->markerSize == markerSize);
94+
out->bytesList = baseDictionary->bytesList.clone();
95+
96+
int minDistance = markerSize * markerSize + 1;
97+
for(int i = 0; i < out->bytesList.rows; i++) {
98+
Mat markerBytes = out->bytesList.rowRange(i, i + 1);
99+
Mat markerBits = aruco::Dictionary::getBitsFromByteList(markerBytes, markerSize);
100+
minDistance = min(minDistance, _getSelfDistance(markerBits));
101+
for(int j = i + 1; j < out->bytesList.rows; j++) {
102+
minDistance = min(minDistance, getFlipDistanceToId(out, markerBits, j));
103+
}
104+
}
105+
tau = minDistance;
106+
}
107+
108+
// current best option
109+
int bestTau = 0;
110+
Mat bestMarker;
111+
112+
// after these number of unproductive iterations, the best option is accepted
113+
const int maxUnproductiveIterations = 5000;
114+
int unproductiveIterations = 0;
115+
116+
while(out->bytesList.rows < nMarkers) {
117+
Mat currentMarker(markerSize, markerSize, CV_8UC1, Scalar::all(0));
118+
rng.fill(currentMarker, RNG::UNIFORM, 0, 2);
119+
120+
int selfDistance = _getSelfDistance(currentMarker);
121+
int minDistance = selfDistance;
122+
123+
// if self distance is better or equal than current best option, calculate distance
124+
// to previous accepted markers
125+
if(selfDistance >= bestTau) {
126+
for(int i = 0; i < out->bytesList.rows; i++) {
127+
int currentDistance = getFlipDistanceToId(out, currentMarker, i);
128+
minDistance = min(currentDistance, minDistance);
129+
if(minDistance <= bestTau) {
130+
break;
131+
}
132+
}
133+
}
134+
135+
// if distance is high enough, accept the marker
136+
if(minDistance >= tau) {
137+
unproductiveIterations = 0;
138+
bestTau = 0;
139+
Mat bytes = aruco::Dictionary::getByteListFromBits(currentMarker);
140+
out->bytesList.push_back(bytes);
141+
} else {
142+
unproductiveIterations++;
143+
144+
// if distance is not enough, but is better than the current best option
145+
if(minDistance > bestTau) {
146+
bestTau = minDistance;
147+
bestMarker = currentMarker;
148+
}
149+
150+
// if number of unproductive iterarions has been reached, accept the current best option
151+
if(unproductiveIterations == maxUnproductiveIterations) {
152+
unproductiveIterations = 0;
153+
tau = bestTau;
154+
bestTau = 0;
155+
Mat bytes = aruco::Dictionary::getByteListFromBits(bestMarker);
156+
out->bytesList.push_back(bytes);
157+
}
158+
}
159+
}
160+
161+
// update the maximum number of correction bits for the generated dictionary
162+
out->maxCorrectionBits = (tau - 1) / 2;
163+
164+
return out;
165+
}
166+
167+
static inline int getMinDistForDict(const Ptr<aruco::Dictionary>& dict) {
168+
const int dict_size = dict->bytesList.rows;
169+
const int marker_size = dict->markerSize;
170+
int minDist = marker_size * marker_size;
171+
for (int i = 0; i < dict_size; i++) {
172+
Mat row = dict->bytesList.row(i);
173+
Mat marker = dict->getBitsFromByteList(row, marker_size);
174+
for (int j = 0; j < dict_size; j++) {
175+
if (j != i) {
176+
minDist = min(dict->getDistanceToId(marker, j), minDist);
177+
}
178+
}
179+
}
180+
return minDist;
181+
}
182+
183+
static inline int getMinAsymDistForDict(const Ptr<aruco::Dictionary>& dict) {
184+
const int dict_size = dict->bytesList.rows;
185+
const int marker_size = dict->markerSize;
186+
int minDist = marker_size * marker_size;
187+
for (int i = 0; i < dict_size; i++)
188+
{
189+
Mat row = dict->bytesList.row(i);
190+
Mat marker = dict->getBitsFromByteList(row, marker_size);
191+
for (int j = 0; j < dict_size; j++)
192+
{
193+
if (j != i)
194+
{
195+
minDist = min(getFlipDistanceToId(dict, marker, j), minDist);
196+
}
197+
}
198+
}
199+
return minDist;
200+
}
201+
202+
const char* keys =
203+
"{@outfile |<none> | Output file with custom dict }"
204+
"{r | false | Calculate the metric considering flipped markers }"
205+
"{d | | Dictionary: DICT_4X4_50=0, DICT_4X4_100=1, DICT_4X4_250=2,"
206+
"DICT_4X4_1000=3, DICT_5X5_50=4, DICT_5X5_100=5, DICT_5X5_250=6, DICT_5X5_1000=7, "
207+
"DICT_6X6_50=8, DICT_6X6_100=9, DICT_6X6_250=10, DICT_6X6_1000=11, DICT_7X7_50=12,"
208+
"DICT_7X7_100=13, DICT_7X7_250=14, DICT_7X7_1000=15, DICT_ARUCO_ORIGINAL = 16}"
209+
"{nMarkers | | Number of markers in the dictionary }"
210+
"{markerSize | | Marker size }"
211+
"{cd | | Input file with custom dictionary }";
212+
213+
const char* about =
214+
"This program can be used to calculate the ArUco dictionary metric.\n"
215+
"To calculate the metric considering flipped markers use -'r' flag.\n"
216+
"This program can be used to create and write the custom ArUco dictionary.\n";
217+
218+
int main(int argc, char *argv[])
219+
{
220+
CommandLineParser parser(argc, argv, keys);
221+
parser.about(about);
222+
223+
if(argc < 2) {
224+
parser.printMessage();
225+
return 0;
226+
}
227+
string outputFile = parser.get<String>(0);
228+
int nMarkers = parser.get<int>("nMarkers");
229+
int markerSize = parser.get<int>("markerSize");
230+
bool checkFlippedMarkers = parser.get<bool>("r");
231+
232+
Ptr<aruco::Dictionary> dictionary = aruco::getPredefinedDictionary(0);
233+
if (parser.has("d")) {
234+
int dictionaryId = parser.get<int>("d");
235+
dictionary = aruco::getPredefinedDictionary(aruco::PREDEFINED_DICTIONARY_NAME(dictionaryId));
236+
}
237+
else if (parser.has("cd")) {
238+
FileStorage fs(parser.get<std::string>("cd"), FileStorage::READ);
239+
bool readOk = dictionary->aruco::Dictionary::readDictionary(fs.root());
240+
if(!readOk) {
241+
cerr << "Invalid dictionary file" << endl;
242+
return 0;
243+
}
244+
}
245+
else if (outputFile.empty() || nMarkers == 0 || markerSize == 0) {
246+
cerr << "Dictionary not specified" << endl;
247+
return 0;
248+
}
249+
250+
if (!outputFile.empty() && nMarkers > 0 && markerSize > 0)
251+
{
252+
Ptr<FileStorage> fs = makePtr<FileStorage>(outputFile, FileStorage::WRITE);
253+
if (checkFlippedMarkers)
254+
dictionary = generateCustomAsymmetricDictionary(nMarkers, markerSize, makePtr<aruco::Dictionary>(), 0);
255+
else
256+
dictionary = aruco::generateCustomDictionary(nMarkers, markerSize, makePtr<aruco::Dictionary>(), 0);
257+
dictionary->writeDictionary(fs);
258+
}
259+
260+
if (checkFlippedMarkers) {
261+
cout << getMinAsymDistForDict(dictionary) << endl;
262+
}
263+
else {
264+
cout << getMinDistForDict(dictionary) << endl;
265+
}
266+
return 0;
267+
}

0 commit comments

Comments
 (0)