#include #include using namespace cv; using namespace std; static int _getSelfDistance(const Mat &marker) { Mat bytes = aruco::Dictionary::getByteListFromBits(marker); double minHamming = (double)marker.total() + 1; for(int r = 1; r < 4; r++) { cv::Mat tmp1(1, bytes.cols, CV_8UC1, Scalar::all(0)); cv::Mat tmp2(1, bytes.cols, CV_8UC1, Scalar::all(0)); uchar* rot0 = tmp1.ptr(); uchar* rot1 = tmp2.ptr(); for (int i = 0; i < bytes.cols; ++i) { rot0[i] = bytes.ptr()[i]; rot1[i] = bytes.ptr()[bytes.cols*r + i]; } double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING); if (currentHamming < minHamming) minHamming = currentHamming; } Mat b; flip(marker, b, 0); Mat flipBytes = aruco::Dictionary::getByteListFromBits(b); for(int r = 0; r < 4; r++) { cv::Mat tmp1(1, flipBytes.cols, CV_8UC1, Scalar::all(0)); cv::Mat tmp2(1, bytes.cols, CV_8UC1, Scalar::all(0)); uchar* rot0 = tmp1.ptr(); uchar* rot1 = tmp2.ptr(); for (int i = 0; i < bytes.cols; ++i) { rot0[i] = flipBytes.ptr()[i]; rot1[i] = bytes.ptr()[bytes.cols*r + i]; } double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING); if(currentHamming < minHamming) minHamming = currentHamming; } flip(marker, b, 1); flipBytes = aruco::Dictionary::getByteListFromBits(b); for(int r = 0; r < 4; r++) { cv::Mat tmp1(1, flipBytes.cols, CV_8UC1, Scalar::all(0)); cv::Mat tmp2(1, bytes.cols, CV_8UC1, Scalar::all(0)); uchar* rot0 = tmp1.ptr(); uchar* rot1 = tmp2.ptr(); for (int i = 0; i < bytes.cols; ++i) { rot0[i] = flipBytes.ptr()[i]; rot1[i] = bytes.ptr()[bytes.cols*r + i]; } double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING); if(currentHamming < minHamming) minHamming = currentHamming; } return cvRound(minHamming); } static inline int getFlipDistanceToId(const aruco::Dictionary& dict, InputArray bits, int id, bool allRotations = true) { Mat bytesList = dict.bytesList; CV_Assert(id >= 0 && id < bytesList.rows); unsigned int nRotations = 4; if(!allRotations) nRotations = 1; Mat candidateBytes = aruco::Dictionary::getByteListFromBits(bits.getMat()); double currentMinDistance = int(bits.total() * bits.total()); for(unsigned int r = 0; r < nRotations; r++) { cv::Mat tmp1(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); cv::Mat tmp2(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); uchar* rot0 = tmp1.ptr(); uchar* rot1 = tmp2.ptr(); for (int i = 0; i < candidateBytes.cols; ++i) { rot0[i] = bytesList.ptr(id)[r*candidateBytes.cols + i]; rot1[i] = candidateBytes.ptr()[i]; } double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING); if(currentHamming < currentMinDistance) { currentMinDistance = currentHamming; } } Mat b; flip(bits.getMat(), b, 0); candidateBytes = aruco::Dictionary::getByteListFromBits(b); for(unsigned int r = 0; r < nRotations; r++) { cv::Mat tmp1(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); cv::Mat tmp2(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); uchar* rot0 = tmp1.ptr(); uchar* rot1 = tmp2.ptr(); for (int i = 0; i < candidateBytes.cols; ++i) { rot0[i] = bytesList.ptr(id)[r*candidateBytes.cols + i]; rot1[i] = candidateBytes.ptr()[i]; } double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING); if (currentHamming < currentMinDistance) { currentMinDistance = currentHamming; } } flip(bits.getMat(), b, 1); candidateBytes = aruco::Dictionary::getByteListFromBits(b); for(unsigned int r = 0; r < nRotations; r++) { cv::Mat tmp1(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); cv::Mat tmp2(1, candidateBytes.cols, CV_8UC1, Scalar::all(0)); uchar* rot0 = tmp1.ptr(); uchar* rot1 = tmp2.ptr(); for (int i = 0; i < candidateBytes.cols; ++i) { rot0[i] = bytesList.ptr(id)[r*candidateBytes.cols + i]; rot1[i] = candidateBytes.ptr()[i]; } double currentHamming = cv::norm(tmp1, tmp2, cv::NORM_HAMMING); if (currentHamming < currentMinDistance) { currentMinDistance = currentHamming; } } return cvRound(currentMinDistance); } static inline aruco::Dictionary generateCustomAsymmetricDictionary(int nMarkers, int markerSize, const aruco::Dictionary &baseDictionary, int randomSeed) { RNG rng((uint64)(randomSeed)); aruco::Dictionary out; out.markerSize = markerSize; // theoretical maximum intermarker distance // See S. Garrido-Jurado, R. Muñoz-Salinas, F. J. Madrid-Cuevas, and M. J. Marín-Jiménez. 2014. // "Automatic generation and detection of highly reliable fiducial markers under occlusion". // Pattern Recogn. 47, 6 (June 2014), 2280-2292. DOI=10.1016/j.patcog.2014.01.005 int C = (int)std::floor(float(markerSize * markerSize) / 4.f); int tau = 2 * (int)std::floor(float(C) * 4.f / 3.f); // if baseDictionary is provided, calculate its intermarker distance if(baseDictionary.bytesList.rows > 0) { CV_Assert(baseDictionary.markerSize == markerSize); out.bytesList = baseDictionary.bytesList.clone(); int minDistance = markerSize * markerSize + 1; for(int i = 0; i < out.bytesList.rows; i++) { Mat markerBytes = out.bytesList.rowRange(i, i + 1); Mat markerBits = aruco::Dictionary::getBitsFromByteList(markerBytes, markerSize); minDistance = min(minDistance, _getSelfDistance(markerBits)); for(int j = i + 1; j < out.bytesList.rows; j++) { minDistance = min(minDistance, getFlipDistanceToId(out, markerBits, j)); } } tau = minDistance; } // current best option int bestTau = 0; Mat bestMarker; // after these number of unproductive iterations, the best option is accepted const int maxUnproductiveIterations = 5000; int unproductiveIterations = 0; while(out.bytesList.rows < nMarkers) { Mat currentMarker(markerSize, markerSize, CV_8UC1, Scalar::all(0)); rng.fill(currentMarker, RNG::UNIFORM, 0, 2); int selfDistance = _getSelfDistance(currentMarker); int minDistance = selfDistance; // if self distance is better or equal than current best option, calculate distance // to previous accepted markers if(selfDistance >= bestTau) { for(int i = 0; i < out.bytesList.rows; i++) { int currentDistance = getFlipDistanceToId(out, currentMarker, i); minDistance = min(currentDistance, minDistance); if(minDistance <= bestTau) { break; } } } // if distance is high enough, accept the marker if(minDistance >= tau) { unproductiveIterations = 0; bestTau = 0; Mat bytes = aruco::Dictionary::getByteListFromBits(currentMarker); out.bytesList.push_back(bytes); } else { unproductiveIterations++; // if distance is not enough, but is better than the current best option if(minDistance > bestTau) { bestTau = minDistance; bestMarker = currentMarker; } // if number of unproductive iterarions has been reached, accept the current best option if(unproductiveIterations == maxUnproductiveIterations) { unproductiveIterations = 0; tau = bestTau; bestTau = 0; Mat bytes = aruco::Dictionary::getByteListFromBits(bestMarker); out.bytesList.push_back(bytes); } } } // update the maximum number of correction bits for the generated dictionary out.maxCorrectionBits = (tau - 1) / 2; return out; } static inline int getMinDistForDict(const aruco::Dictionary& dict) { const int dict_size = dict.bytesList.rows; const int marker_size = dict.markerSize; int minDist = marker_size * marker_size; for (int i = 0; i < dict_size; i++) { Mat row = dict.bytesList.row(i); Mat marker = dict.getBitsFromByteList(row, marker_size); for (int j = 0; j < dict_size; j++) { if (j != i) { minDist = min(dict.getDistanceToId(marker, j), minDist); } } } return minDist; } static inline int getMinAsymDistForDict(const aruco::Dictionary& dict) { const int dict_size = dict.bytesList.rows; const int marker_size = dict.markerSize; int minDist = marker_size * marker_size; for (int i = 0; i < dict_size; i++) { Mat row = dict.bytesList.row(i); Mat marker = dict.getBitsFromByteList(row, marker_size); for (int j = 0; j < dict_size; j++) { if (j != i) { minDist = min(getFlipDistanceToId(dict, marker, j), minDist); } } } return minDist; } const char* keys = "{@outfile | | Output file with custom dict }" "{r | false | Calculate the metric considering flipped markers }" "{d | | Dictionary Name: DICT_4X4_50, DICT_4X4_100, DICT_4X4_250," "DICT_4X4_1000, DICT_5X5_50, DICT_5X5_100, DICT_5X5_250, DICT_5X5_1000, " "DICT_6X6_50, DICT_6X6_100, DICT_6X6_250, DICT_6X6_1000, DICT_7X7_50," "DICT_7X7_100, DICT_7X7_250, DICT_7X7_1000, DICT_ARUCO_ORIGINAL," "DICT_APRILTAG_16h5, DICT_APRILTAG_25h9, DICT_APRILTAG_36h10," "DICT_APRILTAG_36h11}" "{nMarkers | | Number of markers in the dictionary }" "{markerSize | | Marker size }" "{cd | | Input file with custom dictionary }"; const char* about = "This program can be used to calculate the ArUco dictionary metric.\n" "To calculate the metric considering flipped markers use -'r' flag.\n" "This program can be used to create and write the custom ArUco dictionary.\n"; int main(int argc, char *argv[]) { CommandLineParser parser(argc, argv, keys); parser.about(about); if(argc < 2) { parser.printMessage(); return 0; } string outputFile = parser.get(0); int nMarkers = parser.get("nMarkers"); int markerSize = parser.get("markerSize"); bool checkFlippedMarkers = parser.get("r"); aruco::Dictionary dictionary = aruco::getPredefinedDictionary(cv::aruco::DICT_4X4_50); if (parser.has("d")) { string arucoDictName = parser.get("d"); cv::aruco::PredefinedDictionaryType arucoDict; if (arucoDictName == "DICT_4X4_50") { arucoDict = cv::aruco::DICT_4X4_50; } else if (arucoDictName == "DICT_4X4_100") { arucoDict = cv::aruco::DICT_4X4_100; } else if (arucoDictName == "DICT_4X4_250") { arucoDict = cv::aruco::DICT_4X4_250; } else if (arucoDictName == "DICT_4X4_1000") { arucoDict = cv::aruco::DICT_4X4_1000; } else if (arucoDictName == "DICT_5X5_50") { arucoDict = cv::aruco::DICT_5X5_50; } else if (arucoDictName == "DICT_5X5_100") { arucoDict = cv::aruco::DICT_5X5_100; } else if (arucoDictName == "DICT_5X5_250") { arucoDict = cv::aruco::DICT_5X5_250; } else if (arucoDictName == "DICT_5X5_1000") { arucoDict = cv::aruco::DICT_5X5_1000; } else if (arucoDictName == "DICT_6X6_50") { arucoDict = cv::aruco::DICT_6X6_50; } else if (arucoDictName == "DICT_6X6_100") { arucoDict = cv::aruco::DICT_6X6_100; } else if (arucoDictName == "DICT_6X6_250") { arucoDict = cv::aruco::DICT_6X6_250; } else if (arucoDictName == "DICT_6X6_1000") { arucoDict = cv::aruco::DICT_6X6_1000; } else if (arucoDictName == "DICT_7X7_50") { arucoDict = cv::aruco::DICT_7X7_50; } else if (arucoDictName == "DICT_7X7_100") { arucoDict = cv::aruco::DICT_7X7_100; } else if (arucoDictName == "DICT_7X7_250") { arucoDict = cv::aruco::DICT_7X7_250; } else if (arucoDictName == "DICT_7X7_1000") { arucoDict = cv::aruco::DICT_7X7_1000; } else if (arucoDictName == "DICT_ARUCO_ORIGINAL") { arucoDict = cv::aruco::DICT_ARUCO_ORIGINAL; } else if (arucoDictName == "DICT_APRILTAG_16h5") { arucoDict = cv::aruco::DICT_APRILTAG_16h5; } else if (arucoDictName == "DICT_APRILTAG_25h9") { arucoDict = cv::aruco::DICT_APRILTAG_25h9; } else if (arucoDictName == "DICT_APRILTAG_36h10") { arucoDict = cv::aruco::DICT_APRILTAG_36h10; } else if (arucoDictName == "DICT_APRILTAG_36h11") { arucoDict = cv::aruco::DICT_APRILTAG_36h11; } else { cout << "incorrect name of aruco dictionary \n"; return 1; } dictionary = aruco::getPredefinedDictionary(arucoDict); } else if (parser.has("cd")) { FileStorage fs(parser.get("cd"), FileStorage::READ); bool readOk = dictionary.readDictionary(fs.root()); if(!readOk) { cerr << "Invalid dictionary file" << endl; return 0; } } else if (outputFile.empty() || nMarkers == 0 || markerSize == 0) { cerr << "Dictionary not specified" << endl; return 0; } if (!outputFile.empty() && nMarkers > 0 && markerSize > 0) { FileStorage fs(outputFile, FileStorage::WRITE); if (checkFlippedMarkers) dictionary = generateCustomAsymmetricDictionary(nMarkers, markerSize, aruco::Dictionary(), 0); else dictionary = aruco::extendDictionary(nMarkers, markerSize, aruco::Dictionary(), 0); dictionary.writeDictionary(fs); } if (checkFlippedMarkers) { cout << "Hamming distance: " << getMinAsymDistForDict(dictionary) << endl; } else { cout << "Hamming distance: " << getMinDistForDict(dictionary) << endl; } return 0; }