generate_pattern.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. #!/usr/bin/env python
  2. """generate_pattern.py
  3. Usage example:
  4. python generate_pattern.py -o out.svg -r 11 -c 8 -T circles -s 20.0 -R 5.0 -u mm -w 216 -h 279
  5. -o, --output - output file (default out.svg)
  6. -r, --rows - pattern rows (default 11)
  7. -c, --columns - pattern columns (default 8)
  8. -T, --type - type of pattern: circles, acircles, checkerboard, radon_checkerboard, charuco_board. default circles.
  9. -s, --square_size - size of squares in pattern (default 20.0)
  10. -R, --radius_rate - circles_radius = square_size/radius_rate (default 5.0)
  11. -u, --units - mm, inches, px, m (default mm)
  12. -w, --page_width - page width in units (default 216)
  13. -h, --page_height - page height in units (default 279)
  14. -a, --page_size - page size (default A4), supersedes -h -w arguments
  15. -m, --markers - list of cells with markers for the radon checkerboard
  16. -p, --aruco_marker_size - aruco markers size for ChAruco pattern (default 10.0)
  17. -f, --dict_file - file name of custom aruco dictionary for ChAruco pattern
  18. -do, --dict_offset - index of the first ArUco index used
  19. -H, --help - show help
  20. """
  21. import argparse
  22. import numpy as np
  23. import json
  24. import gzip
  25. from svgfig import *
  26. class PatternMaker:
  27. def __init__(self, cols, rows, output, units, square_size, radius_rate, page_width, page_height, markers, aruco_marker_size, dict_file, dict_offset):
  28. self.cols = cols
  29. self.rows = rows
  30. self.output = output
  31. self.units = units
  32. self.square_size = square_size
  33. self.radius_rate = radius_rate
  34. self.width = page_width
  35. self.height = page_height
  36. self.markers = markers
  37. self.aruco_marker_size = aruco_marker_size #for charuco boards only
  38. self.dict_file = dict_file
  39. self.dict_offset = dict_offset
  40. self.g = SVG("g") # the svg group container
  41. def make_circles_pattern(self):
  42. spacing = self.square_size
  43. r = spacing / self.radius_rate
  44. pattern_width = ((self.cols - 1.0) * spacing) + (2.0 * r)
  45. pattern_height = ((self.rows - 1.0) * spacing) + (2.0 * r)
  46. x_spacing = (self.width - pattern_width) / 2.0
  47. y_spacing = (self.height - pattern_height) / 2.0
  48. for x in range(0, self.cols):
  49. for y in range(0, self.rows):
  50. dot = SVG("circle", cx=(x * spacing) + x_spacing + r,
  51. cy=(y * spacing) + y_spacing + r, r=r, fill="black", stroke="none")
  52. self.g.append(dot)
  53. def make_acircles_pattern(self):
  54. spacing = self.square_size
  55. r = spacing / self.radius_rate
  56. pattern_width = ((self.cols-1.0) * 2 * spacing) + spacing + (2.0 * r)
  57. pattern_height = ((self.rows-1.0) * spacing) + (2.0 * r)
  58. x_spacing = (self.width - pattern_width) / 2.0
  59. y_spacing = (self.height - pattern_height) / 2.0
  60. for x in range(0, self.cols):
  61. for y in range(0, self.rows):
  62. dot = SVG("circle", cx=(2 * x * spacing) + (y % 2)*spacing + x_spacing + r,
  63. cy=(y * spacing) + y_spacing + r, r=r, fill="black", stroke="none")
  64. self.g.append(dot)
  65. def make_checkerboard_pattern(self):
  66. spacing = self.square_size
  67. xspacing = (self.width - self.cols * self.square_size) / 2.0
  68. yspacing = (self.height - self.rows * self.square_size) / 2.0
  69. for x in range(0, self.cols):
  70. for y in range(0, self.rows):
  71. if x % 2 == y % 2:
  72. square = SVG("rect", x=x * spacing + xspacing, y=y * spacing + yspacing, width=spacing,
  73. height=spacing, fill="black", stroke="none")
  74. self.g.append(square)
  75. @staticmethod
  76. def _make_round_rect(x, y, diam, corners=("right", "right", "right", "right")):
  77. rad = diam / 2
  78. cw_point = ((0, 0), (diam, 0), (diam, diam), (0, diam))
  79. mid_cw_point = ((0, rad), (rad, 0), (diam, rad), (rad, diam))
  80. res_str = "M{},{} ".format(x + mid_cw_point[0][0], y + mid_cw_point[0][1])
  81. n = len(cw_point)
  82. for i in range(n):
  83. if corners[i] == "right":
  84. res_str += "L{},{} L{},{} ".format(x + cw_point[i][0], y + cw_point[i][1],
  85. x + mid_cw_point[(i + 1) % n][0], y + mid_cw_point[(i + 1) % n][1])
  86. elif corners[i] == "round":
  87. res_str += "A{},{} 0,0,1 {},{} ".format(rad, rad, x + mid_cw_point[(i + 1) % n][0],
  88. y + mid_cw_point[(i + 1) % n][1])
  89. else:
  90. raise TypeError("unknown corner type")
  91. return res_str
  92. def _get_type(self, x, y):
  93. corners = ["right", "right", "right", "right"]
  94. is_inside = True
  95. if x == 0:
  96. corners[0] = "round"
  97. corners[3] = "round"
  98. is_inside = False
  99. if y == 0:
  100. corners[0] = "round"
  101. corners[1] = "round"
  102. is_inside = False
  103. if x == self.cols - 1:
  104. corners[1] = "round"
  105. corners[2] = "round"
  106. is_inside = False
  107. if y == self.rows - 1:
  108. corners[2] = "round"
  109. corners[3] = "round"
  110. is_inside = False
  111. return corners, is_inside
  112. def make_radon_checkerboard_pattern(self):
  113. spacing = self.square_size
  114. xspacing = (self.width - self.cols * self.square_size) / 2.0
  115. yspacing = (self.height - self.rows * self.square_size) / 2.0
  116. for x in range(0, self.cols):
  117. for y in range(0, self.rows):
  118. if x % 2 == y % 2:
  119. corner_types, is_inside = self._get_type(x, y)
  120. if is_inside:
  121. square = SVG("rect", x=x * spacing + xspacing, y=y * spacing + yspacing, width=spacing,
  122. height=spacing, fill="black", stroke="none")
  123. else:
  124. square = SVG("path", d=self._make_round_rect(x * spacing + xspacing, y * spacing + yspacing,
  125. spacing, corner_types), fill="black", stroke="none")
  126. self.g.append(square)
  127. if self.markers is not None:
  128. r = self.square_size * 0.17
  129. pattern_width = ((self.cols - 1.0) * spacing) + (2.0 * r)
  130. pattern_height = ((self.rows - 1.0) * spacing) + (2.0 * r)
  131. x_spacing = (self.width - pattern_width) / 2.0
  132. y_spacing = (self.height - pattern_height) / 2.0
  133. for x, y in self.markers:
  134. color = "black"
  135. if x % 2 == y % 2:
  136. color = "white"
  137. dot = SVG("circle", cx=(x * spacing) + x_spacing + r,
  138. cy=(y * spacing) + y_spacing + r, r=r, fill=color, stroke="none")
  139. self.g.append(dot)
  140. @staticmethod
  141. def _create_marker_bits(markerSize_bits, byteList):
  142. marker = np.zeros((markerSize_bits+2, markerSize_bits+2))
  143. bits = marker[1:markerSize_bits+1, 1:markerSize_bits+1]
  144. for i in range(markerSize_bits):
  145. for j in range(markerSize_bits):
  146. bits[i][j] = int(byteList[i*markerSize_bits+j])
  147. return marker
  148. def make_charuco_board(self):
  149. if (self.aruco_marker_size>self.square_size):
  150. print("Error: Aruco marker cannot be lager than chessboard square!")
  151. return
  152. if (self.dict_file.split(".")[-1] == "gz"):
  153. with gzip.open(self.dict_file, 'r') as fin:
  154. json_bytes = fin.read()
  155. json_str = json_bytes.decode('utf-8')
  156. dictionary = json.loads(json_str)
  157. else:
  158. f = open(self.dict_file)
  159. dictionary = json.load(f)
  160. if (dictionary["nmarkers"] < int(self.cols*self.rows/2)):
  161. 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")
  162. return
  163. markerSize_bits = dictionary["markersize"]
  164. side = self.aruco_marker_size / (markerSize_bits+2)
  165. spacing = self.square_size
  166. xspacing = (self.width - self.cols * self.square_size) / 2.0
  167. yspacing = (self.height - self.rows * self.square_size) / 2.0
  168. ch_ar_border = (self.square_size - self.aruco_marker_size)/2
  169. if ch_ar_border < side*0.7:
  170. print("Marker border {} is less than 70% of ArUco pin size {}. Please increase --square_size or decrease --marker_size for stable board detection".format(ch_ar_border, int(side)))
  171. marker_id = self.dict_offset
  172. for y in range(0, self.rows):
  173. for x in range(0, self.cols):
  174. if x % 2 == y % 2:
  175. square = SVG("rect", x=x * spacing + xspacing, y=y * spacing + yspacing, width=spacing,
  176. height=spacing, fill="black", stroke="none")
  177. self.g.append(square)
  178. else:
  179. img_mark = self._create_marker_bits(markerSize_bits, dictionary["marker_"+str(marker_id)])
  180. marker_id +=1
  181. x_pos = x * spacing + xspacing
  182. y_pos = y * spacing + yspacing
  183. square = SVG("rect", x=x_pos+ch_ar_border, y=y_pos+ch_ar_border, width=self.aruco_marker_size,
  184. height=self.aruco_marker_size, fill="black", stroke="none")
  185. self.g.append(square)
  186. # BUG: https://github.com/opencv/opencv/issues/27871
  187. # The loop bellow merges white squares horizontally and vertically to exclude visible grid on the final pattern
  188. for x_ in range(len(img_mark[0])):
  189. y_ = 0
  190. while y_ < len(img_mark):
  191. y_start = y_
  192. while y_ < len(img_mark) and img_mark[y_][x_] != 0:
  193. y_ += 1
  194. if y_ > y_start:
  195. rect = SVG("rect", x=x_pos+ch_ar_border+(x_)*side, y=y_pos+ch_ar_border+(y_start)*side, width=side,
  196. height=(y_ - y_start)*side, fill="white", stroke="none")
  197. self.g.append(rect)
  198. y_ += 1
  199. for y_ in range(len(img_mark)):
  200. x_ = 0
  201. while x_ < len(img_mark[0]):
  202. x_start = x_
  203. while x_ < len(img_mark[0]) and img_mark[y_][x_] != 0:
  204. x_ += 1
  205. if x_ > x_start:
  206. rect = SVG("rect", x=x_pos+ch_ar_border+(x_start)*side, y=y_pos+ch_ar_border+(y_)*side, width=(x_-x_start)*side,
  207. height=side, fill="white", stroke="none")
  208. self.g.append(rect)
  209. x_ += 1
  210. def save(self):
  211. c = canvas(self.g, width="%d%s" % (self.width, self.units), height="%d%s" % (self.height, self.units),
  212. viewBox="0 0 %d %d" % (self.width, self.height))
  213. c.save(self.output)
  214. def main():
  215. # parse command line options
  216. parser = argparse.ArgumentParser(description="generate camera-calibration pattern", add_help=False)
  217. parser.add_argument("-H", "--help", help="show help", action="store_true", dest="show_help")
  218. parser.add_argument("-o", "--output", help="output file", default="out.svg", action="store", dest="output")
  219. parser.add_argument("-c", "--columns", help="pattern columns", default="8", action="store", dest="columns",
  220. type=int)
  221. parser.add_argument("-r", "--rows", help="pattern rows", default="11", action="store", dest="rows", type=int)
  222. parser.add_argument("-T", "--type", help="type of pattern", default="circles", action="store", dest="p_type",
  223. choices=["circles", "acircles", "checkerboard", "radon_checkerboard", "charuco_board"])
  224. parser.add_argument("-u", "--units", help="length unit", default="mm", action="store", dest="units",
  225. choices=["mm", "inches", "px", "m"])
  226. parser.add_argument("-s", "--square_size", help="size of squares in pattern", default="20.0", action="store",
  227. dest="square_size", type=float)
  228. parser.add_argument("-R", "--radius_rate", help="circles_radius = square_size/radius_rate", default="5.0",
  229. action="store", dest="radius_rate", type=float)
  230. parser.add_argument("-w", "--page_width", help="page width in units", default=argparse.SUPPRESS, action="store",
  231. dest="page_width", type=float)
  232. parser.add_argument("-h", "--page_height", help="page height in units", default=argparse.SUPPRESS, action="store",
  233. dest="page_height", type=float)
  234. parser.add_argument("-a", "--page_size", help="page size, superseded if -h and -w are set", default="A4",
  235. action="store", dest="page_size", choices=["A0", "A1", "A2", "A3", "A4", "A5"])
  236. parser.add_argument("-m", "--markers", help="list of cells with markers for the radon checkerboard. Marker "
  237. "coordinates as list of numbers: -m 1 2 3 4 means markers in cells "
  238. "[1, 2] and [3, 4]",
  239. default=argparse.SUPPRESS, action="store", dest="markers", nargs="+", type=int)
  240. parser.add_argument("-p", "--marker_size", help="aruco markers size for ChAruco pattern (default 10.0)", default="10.0",
  241. action="store", dest="aruco_marker_size", type=float)
  242. parser.add_argument("-f", "--dict_file", help="file name of custom aruco dictionary for ChAruco pattern", default="DICT_ARUCO_ORIGINAL.json",
  243. action="store", dest="dict_file", type=str)
  244. parser.add_argument("-do", "--dict_offset", help="index of the first ArUco index used", default=0,
  245. action="store", dest="dict_offset", type=int)
  246. args = parser.parse_args()
  247. show_help = args.show_help
  248. if show_help:
  249. parser.print_help()
  250. return
  251. output = args.output
  252. columns = args.columns
  253. rows = args.rows
  254. p_type = args.p_type
  255. units = args.units
  256. square_size = args.square_size
  257. radius_rate = args.radius_rate
  258. aruco_marker_size = args.aruco_marker_size
  259. dict_file = args.dict_file
  260. dict_offset = args.dict_offset
  261. if 'page_width' and 'page_height' in args:
  262. page_width = args.page_width
  263. page_height = args.page_height
  264. else:
  265. page_size = args.page_size
  266. # page size dict (ISO standard, mm) for easy lookup. format - size: [width, height]
  267. page_sizes = {"A0": [840, 1188], "A1": [594, 840], "A2": [420, 594], "A3": [297, 420], "A4": [210, 297],
  268. "A5": [148, 210]}
  269. page_width = page_sizes[page_size][0]
  270. page_height = page_sizes[page_size][1]
  271. markers = None
  272. if p_type == "radon_checkerboard" and "markers" in args:
  273. if len(args.markers) % 2 == 1:
  274. raise ValueError("The length of the markers array={} must be even".format(len(args.markers)))
  275. markers = set()
  276. for x, y in zip(args.markers[::2], args.markers[1::2]):
  277. if x in range(0, columns) and y in range(0, rows):
  278. markers.add((x, y))
  279. else:
  280. raise ValueError("The marker {},{} is outside the checkerboard".format(x, y))
  281. if p_type == "charuco_board" and aruco_marker_size >= square_size:
  282. raise ValueError("ArUco markers size must be smaller than square size")
  283. pm = PatternMaker(columns, rows, output, units, square_size, radius_rate, page_width, page_height, markers, aruco_marker_size, dict_file, dict_offset)
  284. # dict for easy lookup of pattern type
  285. mp = {"circles": pm.make_circles_pattern, "acircles": pm.make_acircles_pattern,
  286. "checkerboard": pm.make_checkerboard_pattern, "radon_checkerboard": pm.make_radon_checkerboard_pattern,
  287. "charuco_board": pm.make_charuco_board}
  288. mp[p_type]()
  289. # this should save pattern to output
  290. pm.save()
  291. if __name__ == "__main__":
  292. main()