【python】Printable ChArUco Board
文章目录
- 用 A4 纸打印一张「尺寸准确、远处也能识别」的 ChArUco 标定板
- 一、先搞清楚:ChArUco 是什么,两个尺寸参数指什么
- 二、最简单的生成(OpenCV)
- 三、三个打印大坑
- 坑 1:打印机自动缩放
- 坑 2:PDF 的物理页面尺寸在生成时就被写错
- 坑 3:分辨率太高,页面比纸大,选 100% 只印出中心一小块
- 四、稳妥解法:让物理尺寸和像素分辨率彻底解耦
- 五、关键一步:按相机内参反推「多远能识别到」
- 六、按需定尺寸:把板子放大到铺满 A4
- 七、两个必须记住的一致性要点
- 八、小结
用 A4 纸打印一张「尺寸准确、远处也能识别」的 ChArUco 标定板
做相机标定时,很多人第一步就翻车:随手生成一张 ChArUco 板、丢给打印机、拿尺子一量——方格既不是设定的尺寸,摆远一点相机还识别不到。本文把从「生成」到「打印物理尺寸准确」再到「按相机内参反推识别距离」的完整方法整理出来,附可直接运行的脚本。整体路线如下:
一、先搞清楚:ChArUco 是什么,两个尺寸参数指什么
ChArUco 板是棋盘格(Chessboard)+ ArUco 标记的组合:棋盘格的黑白角点提供亚像素级的精确定位,嵌在白格里的 ArUco 标记则用来识别板子朝向、定位当前可见的是哪些棋盘格角点,从而给每个角点一个全局唯一编号。即使标定板被部分遮挡,靠编号也能识别出剩余角点,比纯棋盘格鲁棒得多。
生成时有两个关键尺寸:
- Square Length(方格边长):棋盘格一个方格的边长。
- Marker Length(标记边长):嵌在白格里的 ArUco 标记的边长,必须小于Square Length,四周留出白边,检测才稳。
两者常见比例约为Marker ≈ 0.7 ~ 0.75 × Square。
二、最简单的生成(OpenCV)
OpenCV 的cv2.aruco模块直接能生成。注意CharucoBoard的尺寸参数在生成图像阶段是像素,物理尺寸由打印环节决定:
importcv2fromcv2importaruco# (cols, rows) = (5, 7),即 5 列 7 行aruco_dict=aruco.getPredefinedDictionary(aruco.DICT_6X6_250)board=aruco.CharucoBoard((5,7),squareLength=200,markerLength=150,dictionary=aruco_dict)img=board.generateImage((1000,1400))# 输出像素尺寸cv2.imwrite('charuco.png',img)到这里你会拿到一张图。但真正的坑在打印。
三、三个打印大坑
坑 1:打印机自动缩放
直接把图片丢进打印预览,默认往往是「适应纸张」——它会把图按纸张大小缩放,你设定的 25mm 方格印出来可能变成 40mm。打印时必须选「实际大小 / 100%」,关掉任何「适应纸张 / 自动缩放」。
坑 2:PDF 的物理页面尺寸在生成时就被写错
先厘清一个常见误解:PDF 的页面物理尺寸是绝对写死在文件里的(MediaBox,单位是 point = 1/72 英寸),阅读器不会、也无需用 DPI 去"猜"页面多大。所以如果打印对话框里显示的尺寸不对,错误不是阅读器"解读"出来的,而是生成阶段就烤进了文件。
实测就踩了这个坑:我用 PIL 把图存成 PDF 并写了dpi=(96,96),本以为得到一张 A4(21×29.7cm),结果打印对话框显示成了 28×39.6cm。原因是Pillow 的 PDF 导出在不少版本里并不真正采用这个dpi参数、而是回落到 72 DPI:于是 793px 的图被当作793 ÷ 72 × 25.4 = 279.7mm ≈ 28cm写进了页面宽度(高度 1122px →1122 ÷ 72 × 25.4 ≈ 39.6cm,两个维度都指向 72 DPI 生成)。阅读器只是忠实显示了这个被写错的尺寸。
根因:把「物理尺寸」隐式地绑定在「图像像素 + DPI 元数据」上,而这条 DPI 通路(尤其 PIL 存 PDF)不可靠——它悄悄回落到 72 时,物理尺寸就整个错位。
坑 3:分辨率太高,页面比纸大,选 100% 只印出中心一小块
如果直接用 300 DPI 生成整页图(2480×3508px),有些阅读器会把它当成一张超大纸,选「实际大小」时只印出页面中心的一小块。
四、稳妥解法:让物理尺寸和像素分辨率彻底解耦
三个坑各自对应的解法,可以先看这张对应关系图——坑 1 靠打印设置,坑 2/坑 3 靠 reportlab,两者合起来才拿到正确的物理尺寸:
思路:不要靠 DPI 元数据传递物理尺寸。改用reportlab建一张真正的 A4 画布,把棋盘图像按物理毫米精确摆放上去。这样图像分辨率只决定清晰度,物理尺寸由「毫米坐标」直接锁死,打印选「实际大小」就一定准。
importcv2fromcv2importarucofromreportlab.lib.pagesizesimportA4fromreportlab.lib.unitsimportmmfromreportlab.pdfgenimportcanvasfromreportlab.lib.utilsimportImageReaderfromPILimportImage# ===== 目标物理尺寸 =====COLS,ROWS=5,7SQUARE_MM=40.0MARKER_MM=30.0# ===== 生成高分辨率棋盘图像(分辨率只影响清晰度)=====render_dpi=300px_per_mm=render_dpi/25.4square_px=round(SQUARE_MM*px_per_mm)marker_px=round(MARKER_MM*px_per_mm)aruco_dict=aruco.getPredefinedDictionary(aruco.DICT_6X6_250)board=aruco.CharucoBoard((COLS,ROWS),squareLength=square_px,markerLength=marker_px,dictionary=aruco_dict)img=board.generateImage((COLS*square_px,ROWS*square_px),marginSize=0)img_pil=Image.fromarray(img)# ===== reportlab 在 A4 上按物理毫米精确放置、居中 =====board_w_mm=COLS*SQUARE_MM# 200 mmboard_h_mm=ROWS*SQUARE_MM# 280 mmpage_w,page_h=A4# 210 x 297 mm(单位 point)x=(page_w-board_w_mm*mm)/2y=(page_h-board_h_mm*mm)/2c=canvas.Canvas('charuco_A4.pdf',pagesize=A4)c.drawImage(ImageReader(img_pil),x,y,width=board_w_mm*mm,height=board_h_mm*mm)c.showPage()c.save()打印这张charuco_A4.pdf,选「实际大小 / 100%」,拿尺子量方格——就是 40mm。之所以能根治,正是因为 reportlab 绕开了 PIL 那条不可靠的 DPI 通路,直接把正确的物理尺寸写进 A4 页面的 MediaBox。
这里
generateImage(..., marginSize=0)让棋盘外不留白边是安全的,因为 reportlab 把它居中放在整张白色 A4 上、四周自然留出了空白(ArUco 检测所需的静默区 quiet zone 就来自这片空白)。但如果你改成贴边打印、或把图裁到刚好只剩棋盘,就会丢掉这圈静默区导致检测变差——那种场景要显式保留 margin。
五、关键一步:按相机内参反推「多远能识别到」
尺寸准了还不够。我遇到的真实问题是:25mm/18mm 的小板子,摆到 50cm 处相机就识别不到了。要不要放大、放多大,不能凭感觉,要用相机内参算。
针孔模型下,一个物理尺寸为M(米)的物体,在距离D(米)处成像的像素大小为:
像素 = f × M / D其中f是相机焦距(像素)。这是物体正对相机、且fx ≈ fy前提下的量级估算(用于判断"够不够大"足矣,不必当作图像边缘、大畸变区的精确值)。以我的相机为例(一组双目标定得到 f = 1459 px,单目分辨率 1520×1520):
| 物理尺寸 | 50cm 处成像 |
|---|---|
| Square 25mm | 1459 × 0.025 / 0.5 ≈ 73 px |
| Marker 18mm | 1459 × 0.018 / 0.5 ≈ 52 px |
一个 DICT_6X6 的 ArUco,加上四周边框共8 个模块。52px 的 marker 意味着每个模块只有52 / 8 ≈ 6.5 px——稍有离焦、运动模糊或斜视角,检测就崩了。这正是「50cm 看不见」的原因。
经验上,ArUco marker 的成像至少要几十像素、每模块 ≥ 3~4px 才比较稳;越大越好。
六、按需定尺寸:把板子放大到铺满 A4
要让 marker 在 50cm 处翻倍到 ~88px,反推物理尺寸并考虑 A4 上限。A4 竖版放 5×7,单格上限是min(210/5, 297/7) ≈ 42mm。取Square 40mm / Marker 30mm(留边距),重新算各距离:
| 距离 | Square (40mm) | Marker (30mm) |
|---|---|---|
| 0.3m | 195 px | 146 px |
| 0.5m | 117 px | 88 px |
| 0.8m | 73 px | 55 px |
| 1.0m | 58 px | 44 px |
50cm 处 marker 从 52px 提到 88px,检测就稳了。这也是单张 A4 的物理极限。
如果还需要更大(比如要在 1m 处稳),单张 A4 已到头,只能:
- 减少格数(如 4×6),单格可到 ~48mm;
- 换 A3 纸,单格可到 ~57mm;
- 换更粗的字典(DICT_5X5 或 DICT_4X4):同样的 marker 物理尺寸下模块更少、每模块更大,更抗距离和模糊。代价是可编码的 marker 数量更少、码间汉明距离更小、抗误检能力略降——对 5×7 这种小板无妨,但大板或多板场景要留意;另外检测代码里的字典也要同步改。
七、两个必须记住的一致性要点
- 打印务必选「实际大小 / 100%」,否则前面所有物理尺寸的努力全白费。打印后用尺子实测方格边长核对。
- 改了标定板的物理尺寸,标定代码里的
squareLength/markerLength必须同步改成对应的米数(如 0.040 / 0.030)。这两个值决定了标定出来的尺度——它们错了,相机外参的平移量、以及基于视差换算的深度都会整体缩放错。注意区分:标定板的物理尺寸不影响相机内参(内参是像素单位、与标定板实际多大无关,写错也照样能标出正确的 fx/fy/畸变);它只影响外参平移量和度量深度。正因为内参不受影响,这个错误特别隐蔽——重投影误差看起来正常,只有最终的距离/尺度整体偏了。
八、小结
- ChArUco = 棋盘格 + ArUco,Marker 要小于 Square 并留白边。
- 打印翻车三连:自动缩放、PDF 生成时物理页面尺寸被写错(PIL 回落 72 DPI)、分辨率过高只印中心。
- 稳妥解法:用 reportlab 把图按物理毫米摆到 A4 画布上,物理尺寸与像素解耦,打印选「实际大小」。
- 尺寸够不够,用
像素 = f × M / D按相机内参算,别凭感觉。 - 物理尺寸、打印缩放、标定代码里的参数,三处必须一致。
