diff --git a/apps/python_app_test.py b/apps/python_app_test.py new file mode 100755 index 0000000000..03e49784fa --- /dev/null +++ b/apps/python_app_test.py @@ -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() diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index c19fe967b7..64a065217c 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -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() diff --git a/doc/charuco_board_pattern.png b/doc/charuco_board_pattern.png new file mode 100644 index 0000000000..e97418d1ef Binary files /dev/null and b/doc/charuco_board_pattern.png differ diff --git a/doc/pattern_tools/DICT_4X4_100.json.gz b/doc/pattern_tools/DICT_4X4_100.json.gz new file mode 100644 index 0000000000..54a202303d Binary files /dev/null and b/doc/pattern_tools/DICT_4X4_100.json.gz differ diff --git a/doc/pattern_tools/DICT_4X4_1000.json.gz b/doc/pattern_tools/DICT_4X4_1000.json.gz new file mode 100644 index 0000000000..c4f10f84ec Binary files /dev/null and b/doc/pattern_tools/DICT_4X4_1000.json.gz differ diff --git a/doc/pattern_tools/DICT_4X4_250.json.gz b/doc/pattern_tools/DICT_4X4_250.json.gz new file mode 100644 index 0000000000..bb70d2c58f Binary files /dev/null and b/doc/pattern_tools/DICT_4X4_250.json.gz differ diff --git a/doc/pattern_tools/DICT_4X4_50.json.gz b/doc/pattern_tools/DICT_4X4_50.json.gz new file mode 100644 index 0000000000..26046ff55f Binary files /dev/null and b/doc/pattern_tools/DICT_4X4_50.json.gz differ diff --git a/doc/pattern_tools/DICT_5X5_100.json.gz b/doc/pattern_tools/DICT_5X5_100.json.gz new file mode 100644 index 0000000000..a9dae45695 Binary files /dev/null and b/doc/pattern_tools/DICT_5X5_100.json.gz differ diff --git a/doc/pattern_tools/DICT_5X5_1000.json.gz b/doc/pattern_tools/DICT_5X5_1000.json.gz new file mode 100644 index 0000000000..571ea54d4a Binary files /dev/null and b/doc/pattern_tools/DICT_5X5_1000.json.gz differ diff --git a/doc/pattern_tools/DICT_5X5_250.json.gz b/doc/pattern_tools/DICT_5X5_250.json.gz new file mode 100644 index 0000000000..4c33bd61fa Binary files /dev/null and b/doc/pattern_tools/DICT_5X5_250.json.gz differ diff --git a/doc/pattern_tools/DICT_5X5_50.json.gz b/doc/pattern_tools/DICT_5X5_50.json.gz new file mode 100644 index 0000000000..d654fda1c5 Binary files /dev/null and b/doc/pattern_tools/DICT_5X5_50.json.gz differ diff --git a/doc/pattern_tools/DICT_6X6_100.json.gz b/doc/pattern_tools/DICT_6X6_100.json.gz new file mode 100644 index 0000000000..39172ddb44 Binary files /dev/null and b/doc/pattern_tools/DICT_6X6_100.json.gz differ diff --git a/doc/pattern_tools/DICT_6X6_1000.json.gz b/doc/pattern_tools/DICT_6X6_1000.json.gz new file mode 100644 index 0000000000..1091b013ec Binary files /dev/null and b/doc/pattern_tools/DICT_6X6_1000.json.gz differ diff --git a/doc/pattern_tools/DICT_6X6_250.json.gz b/doc/pattern_tools/DICT_6X6_250.json.gz new file mode 100644 index 0000000000..4c8c54a0c4 Binary files /dev/null and b/doc/pattern_tools/DICT_6X6_250.json.gz differ diff --git a/doc/pattern_tools/DICT_6X6_50.json.gz b/doc/pattern_tools/DICT_6X6_50.json.gz new file mode 100644 index 0000000000..bb8fbd32d9 Binary files /dev/null and b/doc/pattern_tools/DICT_6X6_50.json.gz differ diff --git a/doc/pattern_tools/DICT_7X7_100.json.gz b/doc/pattern_tools/DICT_7X7_100.json.gz new file mode 100644 index 0000000000..3d26534a6e Binary files /dev/null and b/doc/pattern_tools/DICT_7X7_100.json.gz differ diff --git a/doc/pattern_tools/DICT_7X7_1000.json.gz b/doc/pattern_tools/DICT_7X7_1000.json.gz new file mode 100644 index 0000000000..a62f83c15e Binary files /dev/null and b/doc/pattern_tools/DICT_7X7_1000.json.gz differ diff --git a/doc/pattern_tools/DICT_7X7_250.json.gz b/doc/pattern_tools/DICT_7X7_250.json.gz new file mode 100644 index 0000000000..03d1f83ef9 Binary files /dev/null and b/doc/pattern_tools/DICT_7X7_250.json.gz differ diff --git a/doc/pattern_tools/DICT_7X7_50.json.gz b/doc/pattern_tools/DICT_7X7_50.json.gz new file mode 100644 index 0000000000..cb17233cad Binary files /dev/null and b/doc/pattern_tools/DICT_7X7_50.json.gz differ diff --git a/doc/pattern_tools/DICT_APRILTAG_16h5.json.gz b/doc/pattern_tools/DICT_APRILTAG_16h5.json.gz new file mode 100644 index 0000000000..4367b30ed3 Binary files /dev/null and b/doc/pattern_tools/DICT_APRILTAG_16h5.json.gz differ diff --git a/doc/pattern_tools/DICT_APRILTAG_25h9.json.gz b/doc/pattern_tools/DICT_APRILTAG_25h9.json.gz new file mode 100644 index 0000000000..142841a5a6 Binary files /dev/null and b/doc/pattern_tools/DICT_APRILTAG_25h9.json.gz differ diff --git a/doc/pattern_tools/DICT_APRILTAG_36h10.json.gz b/doc/pattern_tools/DICT_APRILTAG_36h10.json.gz new file mode 100644 index 0000000000..1ed5057b66 Binary files /dev/null and b/doc/pattern_tools/DICT_APRILTAG_36h10.json.gz differ diff --git a/doc/pattern_tools/DICT_APRILTAG_36h11.json.gz b/doc/pattern_tools/DICT_APRILTAG_36h11.json.gz new file mode 100644 index 0000000000..1928dfcf4d Binary files /dev/null and b/doc/pattern_tools/DICT_APRILTAG_36h11.json.gz differ diff --git a/doc/pattern_tools/DICT_ARUCO_ORIGINAL.json.gz b/doc/pattern_tools/DICT_ARUCO_ORIGINAL.json.gz new file mode 100644 index 0000000000..863cabe4a5 Binary files /dev/null and b/doc/pattern_tools/DICT_ARUCO_ORIGINAL.json.gz differ diff --git a/doc/pattern_tools/gen_pattern.py b/doc/pattern_tools/gen_pattern.py index 4618bc318b..bec535baf6 100755 --- a/doc/pattern_tools/gen_pattern.py +++ b/doc/pattern_tools/gen_pattern.py @@ -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): @@ -124,7 +131,7 @@ class PatternMaker: height=spacing, fill="black", stroke="none") else: square = SVG("path", d=self._make_round_rect(x * spacing + xspacing, y * spacing + yspacing, - spacing, corner_types), fill="black", stroke="none") + spacing, corner_types), fill="black", stroke="none") self.g.append(square) if self.markers is not None: r = self.square_size * 0.17 @@ -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() diff --git a/doc/pattern_tools/test_charuco_board.py b/doc/pattern_tools/test_charuco_board.py new file mode 100644 index 0000000000..83d355b7a6 --- /dev/null +++ b/doc/pattern_tools/test_charuco_board.py @@ -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) diff --git a/doc/pattern_tools/test_requirements.txt b/doc/pattern_tools/test_requirements.txt new file mode 100644 index 0000000000..0c169a71e3 --- /dev/null +++ b/doc/pattern_tools/test_requirements.txt @@ -0,0 +1,2 @@ +svglib>=1.5.1 +reportlab>=4.0.0 diff --git a/doc/tutorials/calib3d/camera_calibration_pattern/camera_calibration_pattern.markdown b/doc/tutorials/calib3d/camera_calibration_pattern/camera_calibration_pattern.markdown index 8f4c1848d2..54b0c0e1cd 100644 --- a/doc/tutorials/calib3d/camera_calibration_pattern/camera_calibration_pattern.markdown +++ b/doc/tutorials/calib3d/camera_calibration_pattern/camera_calibration_pattern.markdown @@ -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 --------------- @@ -28,7 +31,7 @@ create a checkerboard pattern in file chessboard.svg with 9 rows, 6 columns and python gen_pattern.py -o chessboard.svg --rows 9 --columns 6 --type checkerboard --square_size 20 -create a circle board pattern in file circleboard.svg with 7 rows, 5 columns and a radius of 15mm: +create a circle board pattern in file circleboard.svg with 7 rows, 5 columns and a radius of 15 mm: python gen_pattern.py -o circleboard.svg --rows 7 --columns 5 --type circles --square_size 15 @@ -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 diff --git a/samples/cpp/aruco_dict_utils.cpp b/samples/cpp/aruco_dict_utils.cpp new file mode 100644 index 0000000000..4a33f15bbf --- /dev/null +++ b/samples/cpp/aruco_dict_utils.cpp @@ -0,0 +1,348 @@ +#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(0); + + 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; +}