Merge pull request #23363 from vovka643:4.x_generate_charuco

Added charuco board generation to gen_pattern.py #23363

added charuco board generation in gen_pattern.py
moved aruco_dict_utils.cpp to samples from opencv_contrib (https://github.com/opencv/opencv_contrib/pull/3464)

### Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [x] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
Vladimir Ponomarev 2023-05-19 20:25:59 +03:00 committed by GitHub
parent f2311d1bfd
commit 9931da772d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 629 additions and 14 deletions

56
apps/python_app_test.py Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env python
from __future__ import print_function
import sys
sys.dont_write_bytecode = True # Don't generate .pyc files / __pycache__ directories
import os
import sys
import unittest
# Python 3 moved urlopen to urllib.requests
try:
from urllib.request import urlopen
except ImportError:
from urllib import urlopen
basedir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(os.path.join(os.path.split(basedir)[0], "modules", "python", "test"))
from tests_common import NewOpenCVTests
def load_tests(loader, tests, pattern):
cwd = os.getcwd()
config_file = 'opencv_apps_python_tests.cfg'
locations = [cwd, basedir]
if os.path.exists(config_file):
with open(config_file, 'r') as f:
locations += [str(s).strip() for s in f.readlines()]
else:
print('WARNING: OpenCV tests config file ({}) is missing, running subset of tests'.format(config_file))
tests_pattern = os.environ.get('OPENCV_APPS_TEST_FILTER', 'test_*') + '.py'
if tests_pattern != 'test_*.py':
print('Tests filter: {}'.format(tests_pattern))
processed = set()
for l in locations:
if not os.path.isabs(l):
l = os.path.normpath(os.path.join(cwd, l))
if l in processed:
continue
processed.add(l)
print('Discovering python tests from: {}'.format(l))
sys_path_modify = l not in sys.path
if sys_path_modify:
sys.path.append(l) # Hack python loader
discovered_tests = loader.discover(l, pattern=tests_pattern, top_level_dir=l)
print(' found {} tests'.format(discovered_tests.countTestCases()))
tests.addTests(loader.discover(l, pattern=tests_pattern))
if sys_path_modify:
sys.path.remove(l)
return tests
if __name__ == '__main__':
NewOpenCVTests.bootstrap()

View File

@ -1,3 +1,8 @@
if (NOT CMAKE_CROSSCOMPILING)
file(RELATIVE_PATH __loc_relative "${OpenCV_BINARY_DIR}" "${CMAKE_CURRENT_LIST_DIR}/pattern_tools\n")
file(APPEND "${OpenCV_BINARY_DIR}/opencv_apps_python_tests.cfg" "${__loc_relative}")
endif()
if(NOT BUILD_DOCS)
return()
endif()

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,7 +6,7 @@ python gen_pattern.py -o out.svg -r 11 -c 8 -T circles -s 20.0 -R 5.0 -u mm -w 2
-o, --output - output file (default out.svg)
-r, --rows - pattern rows (default 11)
-c, --columns - pattern columns (default 8)
-T, --type - type of pattern, circles, acircles, checkerboard, radon_checkerboard (default circles)
-T, --type - type of pattern: circles, acircles, checkerboard, radon_checkerboard, charuco_board. default circles.
-s, --square_size - size of squares in pattern (default 20.0)
-R, --radius_rate - circles_radius = square_size/radius_rate (default 5.0)
-u, --units - mm, inches, px, m (default mm)
@ -14,16 +14,20 @@ python gen_pattern.py -o out.svg -r 11 -c 8 -T circles -s 20.0 -R 5.0 -u mm -w 2
-h, --page_height - page height in units (default 279)
-a, --page_size - page size (default A4), supersedes -h -w arguments
-m, --markers - list of cells with markers for the radon checkerboard
-p, --aruco_marker_size - aruco markers size for ChAruco pattern (default 10.0)
-f, --dict_file - file name of custom aruco dictionary for ChAruco pattern
-H, --help - show help
"""
import argparse
import numpy as np
import json
import gzip
from svgfig import *
class PatternMaker:
def __init__(self, cols, rows, output, units, square_size, radius_rate, page_width, page_height, markers):
def __init__(self, cols, rows, output, units, square_size, radius_rate, page_width, page_height, markers, aruco_marker_size, dict_file):
self.cols = cols
self.rows = rows
self.output = output
@ -33,6 +37,9 @@ class PatternMaker:
self.width = page_width
self.height = page_height
self.markers = markers
self.aruco_marker_size = aruco_marker_size #for charuco boards only
self.dict_file = dict_file
self.g = SVG("g") # the svg group container
def make_circles_pattern(self):
@ -140,6 +147,69 @@ class PatternMaker:
cy=(y * spacing) + y_spacing + r, r=r, fill=color, stroke="none")
self.g.append(dot)
@staticmethod
def _create_marker_bits(markerSize_bits, byteList):
marker = np.zeros((markerSize_bits+2, markerSize_bits+2))
bits = marker[1:markerSize_bits+1, 1:markerSize_bits+1]
for i in range(markerSize_bits):
for j in range(markerSize_bits):
bits[i][j] = int(byteList[i*markerSize_bits+j])
return marker
def make_charuco_board(self):
if (self.aruco_marker_size>self.square_size):
print("Error: Aruco marker cannot be lager than chessboard square!")
return
if (self.dict_file.split(".")[-1] == "gz"):
with gzip.open(self.dict_file, 'r') as fin:
json_bytes = fin.read()
json_str = json_bytes.decode('utf-8')
dictionary = json.loads(json_str)
else:
f = open(self.dict_file)
dictionary = json.load(f)
if (dictionary["nmarkers"] < int(self.cols*self.rows/2)):
print("Error: Aruco dictionary contains less markers than it needs for chosen board. Please choose another dictionary or use smaller board than required for chosen board")
return
markerSize_bits = dictionary["markersize"]
side = self.aruco_marker_size / (markerSize_bits+2)
spacing = self.square_size
xspacing = (self.width - self.cols * self.square_size) / 2.0
yspacing = (self.height - self.rows * self.square_size) / 2.0
ch_ar_border = (self.square_size - self.aruco_marker_size)/2
marker_id = 0
for y in range(0, self.rows):
for x in range(0, self.cols):
if x % 2 == y % 2:
square = SVG("rect", x=x * spacing + xspacing, y=y * spacing + yspacing, width=spacing,
height=spacing, fill="black", stroke="none")
self.g.append(square)
else:
img_mark = self._create_marker_bits(markerSize_bits, dictionary["marker_"+str(marker_id)])
marker_id +=1
x_pos = x * spacing + xspacing
y_pos = y * spacing + yspacing
square = SVG("rect", x=x_pos+ch_ar_border, y=y_pos+ch_ar_border, width=self.aruco_marker_size,
height=self.aruco_marker_size, fill="black", stroke="none")
self.g.append(square)
for x_ in range(len(img_mark[0])):
for y_ in range(len(img_mark)):
if (img_mark[y_][x_] != 0):
square = SVG("rect", x=x_pos+ch_ar_border+(x_)*side, y=y_pos+ch_ar_border+(y_)*side, width=side,
height=side, fill="white", stroke="white", stroke_width = spacing*0.01)
self.g.append(square)
def save(self):
c = canvas(self.g, width="%d%s" % (self.width, self.units), height="%d%s" % (self.height, self.units),
viewBox="0 0 %d %d" % (self.width, self.height))
@ -155,7 +225,7 @@ def main():
type=int)
parser.add_argument("-r", "--rows", help="pattern rows", default="11", action="store", dest="rows", type=int)
parser.add_argument("-T", "--type", help="type of pattern", default="circles", action="store", dest="p_type",
choices=["circles", "acircles", "checkerboard", "radon_checkerboard"])
choices=["circles", "acircles", "checkerboard", "radon_checkerboard", "charuco_board"])
parser.add_argument("-u", "--units", help="length unit", default="mm", action="store", dest="units",
choices=["mm", "inches", "px", "m"])
parser.add_argument("-s", "--square_size", help="size of squares in pattern", default="20.0", action="store",
@ -172,6 +242,10 @@ def main():
"coordinates as list of numbers: -m 1 2 3 4 means markers in cells "
"[1, 2] and [3, 4]",
default=argparse.SUPPRESS, action="store", dest="markers", nargs="+", type=int)
parser.add_argument("-p", "--marker_size", help="aruco markers size for ChAruco pattern (default 10.0)", default="10.0",
action="store", dest="aruco_marker_size", type=float)
parser.add_argument("-f", "--dict_file", help="file name of custom aruco dictionary for ChAruco pattern", default="DICT_ARUCO_ORIGINAL.json",
action="store", dest="dict_file", type=str)
args = parser.parse_args()
show_help = args.show_help
@ -185,6 +259,9 @@ def main():
units = args.units
square_size = args.square_size
radius_rate = args.radius_rate
aruco_marker_size = args.aruco_marker_size
dict_file = args.dict_file
if 'page_width' and 'page_height' in args:
page_width = args.page_width
page_height = args.page_height
@ -206,10 +283,11 @@ def main():
else:
raise ValueError("The marker {},{} is outside the checkerboard".format(x, y))
pm = PatternMaker(columns, rows, output, units, square_size, radius_rate, page_width, page_height, markers)
pm = PatternMaker(columns, rows, output, units, square_size, radius_rate, page_width, page_height, markers, aruco_marker_size, dict_file)
# dict for easy lookup of pattern type
mp = {"circles": pm.make_circles_pattern, "acircles": pm.make_acircles_pattern,
"checkerboard": pm.make_checkerboard_pattern, "radon_checkerboard": pm.make_radon_checkerboard_pattern}
"checkerboard": pm.make_checkerboard_pattern, "radon_checkerboard": pm.make_radon_checkerboard_pattern,
"charuco_board": pm.make_charuco_board}
mp[p_type]()
# this should save pattern to output
pm.save()

View File

@ -0,0 +1,118 @@
from __future__ import print_function
import os, tempfile, numpy as np
import sys
import cv2 as cv
from tests_common import NewOpenCVTests
import gen_pattern
class aruco_objdetect_test(NewOpenCVTests):
def test_aruco_dicts(self):
try:
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM
except:
raise self.skipTest("libraies svglib and reportlab not found")
else:
cols = 3
rows = 5
square_size = 100
aruco_type = [cv.aruco.DICT_4X4_1000, cv.aruco.DICT_5X5_1000, cv.aruco.DICT_6X6_1000,
cv.aruco.DICT_7X7_1000, cv.aruco.DICT_ARUCO_ORIGINAL, cv.aruco.DICT_APRILTAG_16h5,
cv.aruco.DICT_APRILTAG_25h9, cv.aruco.DICT_APRILTAG_36h10, cv.aruco.DICT_APRILTAG_36h11]
aruco_type_str = ['DICT_4X4_1000','DICT_5X5_1000', 'DICT_6X6_1000',
'DICT_7X7_1000', 'DICT_ARUCO_ORIGINAL', 'DICT_APRILTAG_16h5',
'DICT_APRILTAG_25h9', 'DICT_APRILTAG_36h10', 'DICT_APRILTAG_36h11']
marker_size = 0.8*square_size
board_width = cols*square_size
board_height = rows*square_size
for aruco_type_i in range(len(aruco_type)):
#draw desk using opencv
aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type[aruco_type_i])
board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict)
charuco_detector = cv.aruco.CharucoDetector(board)
from_cv_img = board.generateImage((cols*square_size*10, rows*square_size*10))
#draw desk using svg
fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg")
os.close(fd1)
fd2, filepng = tempfile.mkstemp(prefix="svg_marker", suffix=".png")
os.close(fd2)
try:
basedir = os.path.abspath(os.path.dirname(__file__))
pm = gen_pattern.PatternMaker(cols, rows, filesvg, "px", square_size, 0, board_width,
board_height, "charuco_checkboard", marker_size,
os.path.join(basedir, aruco_type_str[aruco_type_i]+'.json.gz'))
pm.make_charuco_board()
pm.save()
drawing = svg2rlg(filesvg)
renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=720)
from_svg_img = cv.imread(filepng)
#test
_charucoCorners, _charucoIds, markerCorners_svg, markerIds_svg = charuco_detector.detectBoard(from_svg_img)
_charucoCorners, _charucoIds, markerCorners_cv, markerIds_cv = charuco_detector.detectBoard(from_cv_img)
np.testing.assert_allclose(markerCorners_svg, markerCorners_cv, 0.1, 0.1)
np.testing.assert_allclose(markerIds_svg, markerIds_cv, 0.1, 0.1)
finally:
if os.path.exists(filesvg):
os.remove(filesvg)
if os.path.exists(filepng):
os.remove(filepng)
def test_aruco_marker_sizes(self):
try:
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM
except:
raise self.skipTest("libraies svglib and reportlab not found")
else:
cols = 3
rows = 5
square_size = 100
aruco_type = cv.aruco.DICT_5X5_1000
aruco_type_str = 'DICT_5X5_1000'
marker_sizes_rate = [0.25, 0.5, 0.75, 0.9]
board_width = cols*square_size
board_height = rows*square_size
for marker_s_rate in marker_sizes_rate:
marker_size = marker_s_rate*square_size
#draw desk using opencv
aruco_dict = cv.aruco.getPredefinedDictionary(aruco_type)
board = cv.aruco.CharucoBoard((cols, rows), square_size, marker_size, aruco_dict)
charuco_detector = cv.aruco.CharucoDetector(board)
from_cv_img = board.generateImage((cols*square_size*10, rows*square_size*10))
#draw desk using svg
fd1, filesvg = tempfile.mkstemp(prefix="out", suffix=".svg")
os.close(fd1)
fd2, filepng = tempfile.mkstemp(prefix="svg_marker", suffix=".png")
os.close(fd2)
try:
basedir = os.path.abspath(os.path.dirname(__file__))
pm = gen_pattern.PatternMaker(cols, rows, filesvg, "px", square_size, 0, board_width,
board_height, "charuco_checkboard", marker_size, os.path.join(basedir, aruco_type_str+'.json.gz'))
pm.make_charuco_board()
pm.save()
drawing = svg2rlg(filesvg)
renderPM.drawToFile(drawing, filepng, fmt='PNG', dpi=720)
from_svg_img = cv.imread(filepng)
#test
_charucoCorners, _charucoIds, markerCorners_svg, markerIds_svg = charuco_detector.detectBoard(from_svg_img)
_charucoCorners, _charucoIds, markerCorners_cv, markerIds_cv = charuco_detector.detectBoard(from_cv_img)
np.testing.assert_allclose(markerCorners_svg, markerCorners_cv, 0.1, 0.1)
np.testing.assert_allclose(markerIds_svg, markerIds_cv, 0.1, 0.1)
finally:
if os.path.exists(filesvg):
os.remove(filesvg)
if os.path.exists(filepng):
os.remove(filepng)

View File

@ -0,0 +1,2 @@
svglib>=1.5.1
reportlab>=4.0.0

View File

@ -17,6 +17,9 @@ You can find a chessboard pattern in https://github.com/opencv/opencv/blob/4.x/d
You can find a circleboard pattern in https://github.com/opencv/opencv/blob/4.x/doc/acircles_pattern.png
You can find a ChAruco board pattern in https://github.com/opencv/opencv/blob/4.x/doc/charuco_board_pattern.png
(7X5 ChAruco board, square size: 30 mm , marker size: 15 mm, aruco dict: DICT_5X5_100, page width: 210 mm, page height: 297 mm)
Create your own pattern
---------------
@ -40,13 +43,18 @@ create a radon checkerboard for findChessboardCornersSB() with markers in (7 4),
python gen_pattern.py -o radon_checkerboard.svg --rows 10 --columns 15 --type radon_checkerboard -s 12.1 -m 7 4 7 5 8 5
create a ChAruco board pattern in charuco_board.svg with 7 rows, 5 columns, square size 30 mm, aruco marker size 15 mm and using DICT_5X5_100 as dictionary for aruco markers (it contains in DICT_ARUCO.json file):
python gen_pattern.py -o charuco_board.svg --rows 7 --columns 5 -T charuco_board --square_size 30 --marker_size 15 -f DICT_5X5_100.json.gz
If you want to change unit use -u option (mm inches, px, m)
If you want to change page size use -w and -h options
@cond HAVE_opencv_aruco
If you want to create a ChArUco board read @ref tutorial_charuco_detection "tutorial Detection of ChArUco Corners" in opencv_contrib tutorial.
@endcond
@cond !HAVE_opencv_aruco
If you want to create a ChArUco board read tutorial Detection of ChArUco Corners in opencv_contrib tutorial.
@endcond
If you want to use your own dictionary for ChAruco board your should write name of file with your dictionary. For example
python gen_pattern.py -o charuco_board.svg --rows 7 --columns 5 -T charuco_board -f my_dictionary.json
You can generate your dictionary in my_dictionary.json file with number of markers 30 and markers size 5 bits by using opencv/samples/cpp/aruco_dict_utils.cpp.
bin/example_cpp_aruco_dict_utils.exe my_dict.json -nMarkers=30 -markerSize=5

View File

@ -0,0 +1,348 @@
#include <opencv2/objdetect/aruco_detector.hpp>
#include <iostream>
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 |<none> | 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<String>(0);
int nMarkers = parser.get<int>("nMarkers");
int markerSize = parser.get<int>("markerSize");
bool checkFlippedMarkers = parser.get<bool>("r");
aruco::Dictionary dictionary = aruco::getPredefinedDictionary(0);
if (parser.has("d")) {
string arucoDictName = parser.get<string>("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<std::string>("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;
}