Skip to content

Commit

Permalink
Merge pull request opencv#23363 from vovka643:4.x_generate_charuco
Browse files Browse the repository at this point in the history
Added charuco board generation to gen_pattern.py opencv#23363

added charuco board generation in gen_pattern.py
moved aruco_dict_utils.cpp to samples from opencv_contrib (opencv/opencv_contrib#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
  • Loading branch information
vovka643 authored and thewoz committed May 29, 2024
1 parent bf1fe58 commit d9e3fd4
Show file tree
Hide file tree
Showing 29 changed files with 629 additions and 14 deletions.
56 changes: 56 additions & 0 deletions apps/python_app_test.py
Original file line number Diff line number Diff line change
@@ -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()
5 changes: 5 additions & 0 deletions doc/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
Binary file added doc/charuco_board_pattern.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/pattern_tools/DICT_4X4_100.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_4X4_1000.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_4X4_250.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_4X4_50.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_5X5_100.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_5X5_1000.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_5X5_250.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_5X5_50.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_6X6_100.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_6X6_1000.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_6X6_250.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_6X6_50.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_7X7_100.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_7X7_1000.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_7X7_250.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_7X7_50.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_APRILTAG_16h5.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_APRILTAG_25h9.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_APRILTAG_36h10.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_APRILTAG_36h11.json.gz
Binary file not shown.
Binary file added doc/pattern_tools/DICT_ARUCO_ORIGINAL.json.gz
Binary file not shown.
92 changes: 85 additions & 7 deletions doc/pattern_tools/gen_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,28 @@
-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)
-w, --page_width - page width in units (default 216)
-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
Expand All @@ -33,6 +37,9 @@ def __init__(self, cols, rows, output, units, square_size, radius_rate, page_wid
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):
Expand Down Expand Up @@ -124,7 +131,7 @@ def make_radon_checkerboard_pattern(self):
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
Expand All @@ -140,6 +147,69 @@ def make_radon_checkerboard_pattern(self):
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))
Expand All @@ -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",
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down
118 changes: 118 additions & 0 deletions doc/pattern_tools/test_charuco_board.py
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions doc/pattern_tools/test_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
svglib>=1.5.1
reportlab>=4.0.0
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------

Expand All @@ -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

Expand All @@ -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
Loading

0 comments on commit d9e3fd4

Please sign in to comment.