mirror of
https://github.com/opencv/opencv.git
synced 2025-01-18 14:13:15 +08:00
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:
parent
f2311d1bfd
commit
9931da772d
56
apps/python_app_test.py
Executable file
56
apps/python_app_test.py
Executable 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()
|
@ -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()
|
||||
|
BIN
doc/charuco_board_pattern.png
Normal file
BIN
doc/charuco_board_pattern.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
doc/pattern_tools/DICT_4X4_100.json.gz
Normal file
BIN
doc/pattern_tools/DICT_4X4_100.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_4X4_1000.json.gz
Normal file
BIN
doc/pattern_tools/DICT_4X4_1000.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_4X4_250.json.gz
Normal file
BIN
doc/pattern_tools/DICT_4X4_250.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_4X4_50.json.gz
Normal file
BIN
doc/pattern_tools/DICT_4X4_50.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_5X5_100.json.gz
Normal file
BIN
doc/pattern_tools/DICT_5X5_100.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_5X5_1000.json.gz
Normal file
BIN
doc/pattern_tools/DICT_5X5_1000.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_5X5_250.json.gz
Normal file
BIN
doc/pattern_tools/DICT_5X5_250.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_5X5_50.json.gz
Normal file
BIN
doc/pattern_tools/DICT_5X5_50.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_6X6_100.json.gz
Normal file
BIN
doc/pattern_tools/DICT_6X6_100.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_6X6_1000.json.gz
Normal file
BIN
doc/pattern_tools/DICT_6X6_1000.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_6X6_250.json.gz
Normal file
BIN
doc/pattern_tools/DICT_6X6_250.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_6X6_50.json.gz
Normal file
BIN
doc/pattern_tools/DICT_6X6_50.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_7X7_100.json.gz
Normal file
BIN
doc/pattern_tools/DICT_7X7_100.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_7X7_1000.json.gz
Normal file
BIN
doc/pattern_tools/DICT_7X7_1000.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_7X7_250.json.gz
Normal file
BIN
doc/pattern_tools/DICT_7X7_250.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_7X7_50.json.gz
Normal file
BIN
doc/pattern_tools/DICT_7X7_50.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_APRILTAG_16h5.json.gz
Normal file
BIN
doc/pattern_tools/DICT_APRILTAG_16h5.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_APRILTAG_25h9.json.gz
Normal file
BIN
doc/pattern_tools/DICT_APRILTAG_25h9.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_APRILTAG_36h10.json.gz
Normal file
BIN
doc/pattern_tools/DICT_APRILTAG_36h10.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_APRILTAG_36h11.json.gz
Normal file
BIN
doc/pattern_tools/DICT_APRILTAG_36h11.json.gz
Normal file
Binary file not shown.
BIN
doc/pattern_tools/DICT_ARUCO_ORIGINAL.json.gz
Normal file
BIN
doc/pattern_tools/DICT_ARUCO_ORIGINAL.json.gz
Normal file
Binary file not shown.
@ -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()
|
||||
|
118
doc/pattern_tools/test_charuco_board.py
Normal file
118
doc/pattern_tools/test_charuco_board.py
Normal 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)
|
2
doc/pattern_tools/test_requirements.txt
Normal file
2
doc/pattern_tools/test_requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
svglib>=1.5.1
|
||||
reportlab>=4.0.0
|
@ -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
|
||||
|
348
samples/cpp/aruco_dict_utils.cpp
Normal file
348
samples/cpp/aruco_dict_utils.cpp
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user