【python】Printable ChArUco Board

文章目录

用 A4 纸打印一张「尺寸准确、远处也能识别」的 ChArUco 标定板

做相机标定时,很多人第一步就翻车:随手生成一张 ChArUco 板、丢给打印机、拿尺子一量------方格既不是设定的尺寸,摆远一点相机还识别不到。本文把从「生成」到「打印物理尺寸准确」再到「按相机内参反推识别距离」的完整方法整理出来,附可直接运行的脚本。整体路线如下:
#mermaid-svg-miLG2aFglpuB388U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-miLG2aFglpuB388U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-miLG2aFglpuB388U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-miLG2aFglpuB388U .error-icon{fill:#552222;}#mermaid-svg-miLG2aFglpuB388U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-miLG2aFglpuB388U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-miLG2aFglpuB388U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-miLG2aFglpuB388U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-miLG2aFglpuB388U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-miLG2aFglpuB388U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-miLG2aFglpuB388U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-miLG2aFglpuB388U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-miLG2aFglpuB388U .marker.cross{stroke:#333333;}#mermaid-svg-miLG2aFglpuB388U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-miLG2aFglpuB388U p{margin:0;}#mermaid-svg-miLG2aFglpuB388U .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-miLG2aFglpuB388U .cluster-label text{fill:#333;}#mermaid-svg-miLG2aFglpuB388U .cluster-label span{color:#333;}#mermaid-svg-miLG2aFglpuB388U .cluster-label span p{background-color:transparent;}#mermaid-svg-miLG2aFglpuB388U .label text,#mermaid-svg-miLG2aFglpuB388U span{fill:#333;color:#333;}#mermaid-svg-miLG2aFglpuB388U .node rect,#mermaid-svg-miLG2aFglpuB388U .node circle,#mermaid-svg-miLG2aFglpuB388U .node ellipse,#mermaid-svg-miLG2aFglpuB388U .node polygon,#mermaid-svg-miLG2aFglpuB388U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-miLG2aFglpuB388U .rough-node .label text,#mermaid-svg-miLG2aFglpuB388U .node .label text,#mermaid-svg-miLG2aFglpuB388U .image-shape .label,#mermaid-svg-miLG2aFglpuB388U .icon-shape .label{text-anchor:middle;}#mermaid-svg-miLG2aFglpuB388U .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-miLG2aFglpuB388U .rough-node .label,#mermaid-svg-miLG2aFglpuB388U .node .label,#mermaid-svg-miLG2aFglpuB388U .image-shape .label,#mermaid-svg-miLG2aFglpuB388U .icon-shape .label{text-align:center;}#mermaid-svg-miLG2aFglpuB388U .node.clickable{cursor:pointer;}#mermaid-svg-miLG2aFglpuB388U .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-miLG2aFglpuB388U .arrowheadPath{fill:#333333;}#mermaid-svg-miLG2aFglpuB388U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-miLG2aFglpuB388U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-miLG2aFglpuB388U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-miLG2aFglpuB388U .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-miLG2aFglpuB388U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-miLG2aFglpuB388U .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-miLG2aFglpuB388U .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-miLG2aFglpuB388U .cluster text{fill:#333;}#mermaid-svg-miLG2aFglpuB388U .cluster span{color:#333;}#mermaid-svg-miLG2aFglpuB388U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-miLG2aFglpuB388U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-miLG2aFglpuB388U rect.text{fill:none;stroke-width:0;}#mermaid-svg-miLG2aFglpuB388U .icon-shape,#mermaid-svg-miLG2aFglpuB388U .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-miLG2aFglpuB388U .icon-shape p,#mermaid-svg-miLG2aFglpuB388U .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-miLG2aFglpuB388U .icon-shape .label rect,#mermaid-svg-miLG2aFglpuB388U .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-miLG2aFglpuB388U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-miLG2aFglpuB388U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-miLG2aFglpuB388U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-miLG2aFglpuB388U .stage>*{fill:#dbeafe!important;stroke:#2563eb!important;stroke-width:1px!important;color:#1e3a8a!important;}#mermaid-svg-miLG2aFglpuB388U .stage span{fill:#dbeafe!important;stroke:#2563eb!important;stroke-width:1px!important;color:#1e3a8a!important;}#mermaid-svg-miLG2aFglpuB388U .stage tspan{fill:#1e3a8a!important;} 距离处太小则回头放大板子
① 生成 ChArUco 图像

OpenCV cv2.aruco
② 打印物理尺寸准确

reportlab 锁毫米 + 实际大小 100%
③ 按相机内参反推识别距离

像素 = f × M / D


一、先搞清楚:ChArUco 是什么,两个尺寸参数指什么

ChArUco 板是棋盘格(Chessboard)+ ArUco 标记的组合:棋盘格的黑白角点提供亚像素级的精确定位,嵌在白格里的 ArUco 标记则用来识别板子朝向、定位当前可见的是哪些棋盘格角点,从而给每个角点一个全局唯一编号。即使标定板被部分遮挡,靠编号也能识别出剩余角点,比纯棋盘格鲁棒得多。

生成时有两个关键尺寸:

  • Square Length(方格边长):棋盘格一个方格的边长。
  • Marker Length(标记边长):嵌在白格里的 ArUco 标记的边长,必须 小于 Square Length,四周留出白边,检测才稳。

两者常见比例约为 Marker ≈ 0.7 ~ 0.75 × Square


二、最简单的生成(OpenCV)

OpenCV 的 cv2.aruco 模块直接能生成。注意 CharucoBoard 的尺寸参数在生成图像阶段是像素,物理尺寸由打印环节决定:

python 复制代码
import cv2
from cv2 import aruco

# (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,两者合起来才拿到正确的物理尺寸:
#mermaid-svg-oGFfpgJkWrnIdg0P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oGFfpgJkWrnIdg0P .error-icon{fill:#552222;}#mermaid-svg-oGFfpgJkWrnIdg0P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oGFfpgJkWrnIdg0P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oGFfpgJkWrnIdg0P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oGFfpgJkWrnIdg0P .marker.cross{stroke:#333333;}#mermaid-svg-oGFfpgJkWrnIdg0P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oGFfpgJkWrnIdg0P p{margin:0;}#mermaid-svg-oGFfpgJkWrnIdg0P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oGFfpgJkWrnIdg0P .cluster-label text{fill:#333;}#mermaid-svg-oGFfpgJkWrnIdg0P .cluster-label span{color:#333;}#mermaid-svg-oGFfpgJkWrnIdg0P .cluster-label span p{background-color:transparent;}#mermaid-svg-oGFfpgJkWrnIdg0P .label text,#mermaid-svg-oGFfpgJkWrnIdg0P span{fill:#333;color:#333;}#mermaid-svg-oGFfpgJkWrnIdg0P .node rect,#mermaid-svg-oGFfpgJkWrnIdg0P .node circle,#mermaid-svg-oGFfpgJkWrnIdg0P .node ellipse,#mermaid-svg-oGFfpgJkWrnIdg0P .node polygon,#mermaid-svg-oGFfpgJkWrnIdg0P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oGFfpgJkWrnIdg0P .rough-node .label text,#mermaid-svg-oGFfpgJkWrnIdg0P .node .label text,#mermaid-svg-oGFfpgJkWrnIdg0P .image-shape .label,#mermaid-svg-oGFfpgJkWrnIdg0P .icon-shape .label{text-anchor:middle;}#mermaid-svg-oGFfpgJkWrnIdg0P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oGFfpgJkWrnIdg0P .rough-node .label,#mermaid-svg-oGFfpgJkWrnIdg0P .node .label,#mermaid-svg-oGFfpgJkWrnIdg0P .image-shape .label,#mermaid-svg-oGFfpgJkWrnIdg0P .icon-shape .label{text-align:center;}#mermaid-svg-oGFfpgJkWrnIdg0P .node.clickable{cursor:pointer;}#mermaid-svg-oGFfpgJkWrnIdg0P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oGFfpgJkWrnIdg0P .arrowheadPath{fill:#333333;}#mermaid-svg-oGFfpgJkWrnIdg0P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oGFfpgJkWrnIdg0P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oGFfpgJkWrnIdg0P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oGFfpgJkWrnIdg0P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oGFfpgJkWrnIdg0P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oGFfpgJkWrnIdg0P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oGFfpgJkWrnIdg0P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oGFfpgJkWrnIdg0P .cluster text{fill:#333;}#mermaid-svg-oGFfpgJkWrnIdg0P .cluster span{color:#333;}#mermaid-svg-oGFfpgJkWrnIdg0P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oGFfpgJkWrnIdg0P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oGFfpgJkWrnIdg0P rect.text{fill:none;stroke-width:0;}#mermaid-svg-oGFfpgJkWrnIdg0P .icon-shape,#mermaid-svg-oGFfpgJkWrnIdg0P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oGFfpgJkWrnIdg0P .icon-shape p,#mermaid-svg-oGFfpgJkWrnIdg0P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oGFfpgJkWrnIdg0P .icon-shape .label rect,#mermaid-svg-oGFfpgJkWrnIdg0P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oGFfpgJkWrnIdg0P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oGFfpgJkWrnIdg0P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oGFfpgJkWrnIdg0P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-oGFfpgJkWrnIdg0P .pain>*{fill:#fee2e2!important;stroke:#dc2626!important;stroke-width:1px!important;color:#7f1d1d!important;}#mermaid-svg-oGFfpgJkWrnIdg0P .pain span{fill:#fee2e2!important;stroke:#dc2626!important;stroke-width:1px!important;color:#7f1d1d!important;}#mermaid-svg-oGFfpgJkWrnIdg0P .pain tspan{fill:#7f1d1d!important;}#mermaid-svg-oGFfpgJkWrnIdg0P .stage>*{fill:#dbeafe!important;stroke:#2563eb!important;stroke-width:1px!important;color:#1e3a8a!important;}#mermaid-svg-oGFfpgJkWrnIdg0P .stage span{fill:#dbeafe!important;stroke:#2563eb!important;stroke-width:1px!important;color:#1e3a8a!important;}#mermaid-svg-oGFfpgJkWrnIdg0P .stage tspan{fill:#1e3a8a!important;} 坑1 打印机自动缩放
打印选「实际大小 / 100%」
坑2 PDF 物理尺寸生成时写错

PIL 回落 72 DPI
reportlab 按物理毫米摆到 A4

物理尺寸与像素解耦
坑3 分辨率过高

100% 只印中心一小块
尺子量方格 = 40mm ✓

思路:不要靠 DPI 元数据传递物理尺寸 。改用 reportlab 建一张真正的 A4 画布,把棋盘图像按物理毫米精确摆放上去。这样图像分辨率只决定清晰度,物理尺寸由「毫米坐标」直接锁死,打印选「实际大小」就一定准。

python 复制代码
import cv2
from cv2 import aruco
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
from PIL import Image

# ===== 目标物理尺寸 =====
COLS, ROWS = 5, 7
SQUARE_MM = 40.0
MARKER_MM = 30.0

# ===== 生成高分辨率棋盘图像(分辨率只影响清晰度)=====
render_dpi = 300
px_per_mm = render_dpi / 25.4
square_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 mm
board_h_mm = ROWS * SQUARE_MM      # 280 mm
page_w, page_h = A4                # 210 x 297 mm(单位 point)
x = (page_w - board_w_mm * mm) / 2
y = (page_h - board_h_mm * mm) / 2

c = 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 这种小板无妨,但大板或多板场景要留意;另外检测代码里的字典也要同步改。

七、两个必须记住的一致性要点

  1. 打印务必选「实际大小 / 100%」,否则前面所有物理尺寸的努力全白费。打印后用尺子实测方格边长核对。
  2. 改了标定板的物理尺寸,标定代码里的 squareLength / markerLength 必须同步改成对应的米数 (如 0.040 / 0.030)。这两个值决定了标定出来的尺度------它们错了,相机外参的平移量、以及基于视差换算的深度都会整体缩放错。注意区分 :标定板的物理尺寸不影响相机内参 (内参是像素单位、与标定板实际多大无关,写错也照样能标出正确的 fx/fy/畸变);它只影响外参平移量和度量深度。正因为内参不受影响,这个错误特别隐蔽------重投影误差看起来正常,只有最终的距离/尺度整体偏了。

八、小结

  • ChArUco = 棋盘格 + ArUco,Marker 要小于 Square 并留白边。
  • 打印翻车三连:自动缩放、PDF 生成时物理页面尺寸被写错(PIL 回落 72 DPI)、分辨率过高只印中心。
  • 稳妥解法:用 reportlab 把图按物理毫米摆到 A4 画布上,物理尺寸与像素解耦,打印选「实际大小」。
  • 尺寸够不够,用 像素 = f × M / D 按相机内参算,别凭感觉。
  • 物理尺寸、打印缩放、标定代码里的参数,三处必须一致。