OpenCV相机标定与位姿估计实战:从棋盘格到三维空间理解

OpenCV相机标定与位姿估计实战:从棋盘格到三维空间理解 1. 项目概述从棋盘格到三维世界的钥匙在计算机视觉和机器人领域让机器“看懂”世界的第一步往往是教会它如何理解自己“眼睛”——也就是相机——看到的东西。一个未经标定的相机就像一个人戴着度数不匹配的眼镜看到的图像是扭曲的测量的距离是失真的。你无法直接从一张照片里判断一个物体离你到底有多远它的真实尺寸是多少。这就是相机标定要解决的核心问题建立图像二维像素坐标与世界三维物理坐标之间的精确数学关系。而“位姿估计”则是这个关系的直接应用。它回答的是“目标物体相对于相机在哪里姿态如何”这个问题。想象一下一个机械臂要精准抓取工作台上的零件它必须知道零件相对于自己摄像头坐标系的三维位置和旋转角度。这个项目就是通过最经典、最实用的棋盘格标定法带你一步步实现从相机内参、畸变系数标定到利用单张棋盘格图像实时估算其相对于相机位姿的全过程。整个过程完全基于OpenCV-Python代码清晰原理透彻无论是做机器人视觉引导、增强现实AR、三维重建还是简单的视觉测量这都是你必须掌握的基石技能。2. 核心原理拆解针孔模型、畸变与透视变换在动手写代码之前我们需要把背后的数学和物理模型搞清楚。这能让你在调试时心中有数而不是盲目地调参。2.1 相机成像的几何模型小孔成像与坐标系转换现代数码相机的原理源于小孔成像。OpenCV使用的也是这个模型的扩展——针孔相机模型。这个模型描述了三维空间点如何投影到二维图像平面的过程涉及四次坐标系转换世界坐标系 (World Coordinate System)这是客观世界的绝对坐标系单位是米或毫米。棋盘格角点的物理位置就在这个坐标系下定义例如我们设定棋盘格左上角为(0,0,0)每个格子宽高为30mm。相机坐标系 (Camera Coordinate System)原点在相机的光心Z轴沿光轴方向指向场景。世界坐标系下的点需要通过旋转和平移即我们最终要求解的外参转换到此坐标系。图像坐标系 (Image Coordinate System)单位是毫米原点在光轴与成像平面的交点主点。相机坐标系下的点通过透视投影与焦距f相关落在这个平面上。像素坐标系 (Pixel Coordinate System)单位是像素原点通常在图像的左上角。图像坐标系下的点通过缩放与像素尺寸dx, dy相关和平移主点cx, cy的像素坐标得到最终的像素坐标。这整个转换过程用一个矩阵乘法就可以表示s * [u, v, 1]^T A * [R|t] * [X, Y, Z, 1]^T。[u, v]是像素坐标。[X, Y, Z]是世界坐标。A是内参矩阵包含了焦距fx, fy和主点cx, cy。它由相机自身的物理特性决定标定的主要目标之一就是求出它。[R|t]是旋转矩阵和平移向量组成的外参矩阵描述了世界坐标系到相机坐标系的变换。对于单张标定板图像我们求的就是这一组特定的R和t。s是一个缩放因子。2.2 镜头畸变理想与现实的差距针孔模型是理想的但真实的镜头由于光学设计、组装工艺等因素会引入畸变导致直线变弯。主要考虑两种径向畸变和两种切向畸变径向畸变图像像素点以畸变中心为原点沿径向发生位移。k1, k2, k3是它的系数。桶形畸变负径向畸变和枕形畸变正径向畸变都属于此类。切向畸变由于镜头制造时与成像平面不平行引起。p1, p2是它的系数。标定的另一个核心目标就是求出这5个或更多畸变系数[k1, k2, p1, p2, k3]。OpenCV的cv2.calibrateCamera函数在优化过程中会同时求解内参矩阵、畸变系数和每张图片的外参。2.3 单应性矩阵与位姿估计从2D到3D的桥梁当我们用cv2.findChessboardCorners找到棋盘格的角点像素坐标[u, v]并且已知这些角点在世界坐标系下的平面坐标[X, Y, 0]因为棋盘格是平的Z坐标全为0时它们之间的投影关系可以用一个3x3的单应性矩阵H来描述s * [u, v, 1]^T H * [X, Y, 1]^T。这个H矩阵蕴含了丰富的信息。理论上H A * [r1, r2, t]其中r1, r2是旋转矩阵R的前两列。因此一旦我们通过标定得到了准确的内参矩阵A就可以从单应性矩阵H中分解出r1, r2, t再利用r3 r1 × r2叉乘得到旋转矩阵的第三列最后通过奇异值分解SVD对[r1, r2, r3]进行正交化得到最终的、最接近真实旋转的旋转矩阵R和平移向量t。OpenCV的cv2.solvePnP函数正是基于这个原理提供了更鲁棒、更通用的求解方法支持非平面点它也是我们本项目后期进行实时位姿估计的核心函数。注意这里有一个关键点cv2.solvePnP需要已知内参。这就是为什么标定必须先于位姿估计进行。没有准确的内参解出的位姿误差会非常大。3. 环境准备与工具选型工欲善其事必先利其器。一个干净、版本匹配的环境能避免很多莫名其妙的错误。3.1 Python与OpenCV版本选择我强烈建议使用Python 3.8的版本这个版本区间生态稳定兼容性好。OpenCV-Pythonopencv-python库有几个变体opencv-python只包含主模块最常用。opencv-contrib-python包含主模块和贡献库opencv_contrib里面有很多额外的、好用的模块如SIFT、SURF等专利算法以及更丰富的标定工具。对于本项目如果你后续想探索更多功能建议直接安装这个版本。opencv-python-headless无GUI依赖的版本适用于服务器或无显示环境。安装命令非常简单pip install opencv-contrib-python -i https://pypi.tuna.tsinghua.edu.cn/simple使用国内的镜像源如清华源可以大幅加快下载速度。3.2 开发环境与辅助工具IDE/编辑器VSCode是绝佳选择。轻量、插件生态强大。务必安装Python扩展和Pylance它能提供优秀的代码提示和类型检查。配置好你的Python解释器路径确保VSCode使用的是你安装了OpenCV的那个环境。标定板理论上你可以用任何已知尺寸的棋盘格图案。但为了精度请注意打印时务必确保**“实际尺寸”**。如果你设计的是30mm格子打印出来用尺子量一下误差应小于0.5mm。使用高质量的激光打印机和哑光铜版纸打印效果较好。棋盘格需要平整地贴在一个坚硬的底板上如亚克力板、铝板。拍摄时标定板的平整度对精度影响极大。棋盘格的行列数patternSize指的是内角点的数量。例如一个8x10的棋盘格其内角点就是7x9。这是cv2.findChessboardCorners函数的关键参数千万别搞错。图像采集使用你项目将要用的实际相机。手机摄像头、USB网络摄像头、工业相机均可。采集时要像摄影师一样让标定板“充满”整个相机的视野覆盖视野的各个位置中心、四个角、四条边。覆盖各种姿态平放、倾斜、旋转、远近。通常需要15-25张有效图像。图像越多、姿态越丰富标定结果越鲁棒但计算量也越大。4. 相机标定全流程实操现在我们进入实战环节。我会把每一步的代码和背后的意图都讲清楚。4.1 采集标定图像与角点检测首先我们需要写一个脚本或者手动拍摄一系列棋盘格照片保存到一个文件夹如calib_imgs/中。然后编写角点检测的代码。import cv2 import numpy as np import glob # 1. 定义棋盘格规格内角点数量 pattern_size (7, 9) # 例如棋盘格每行有8个格子每列有10个格子则内角点为7x9 square_size 0.03 # 每个格子的实际物理尺寸单位米 (30mm) # 2. 准备世界坐标系中的对象点 (0,0,0), (1,0,0), (2,0,0) ....,(8,5,0) objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size # 乘上物理尺寸 # 3. 用于存储所有图像的对象点和图像点的数组 objpoints [] # 真实世界中的3D点 imgpoints [] # 图像中的2D点 # 4. 读取所有标定图像 images glob.glob(calib_imgs/*.jpg) if not images: print(未找到图像文件请检查路径) exit() for i, fname in enumerate(images): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 5. 查找棋盘格角点 ret, corners cv2.findChessboardCorners(gray, pattern_size, None) # 6. 如果找到添加对象点和图像点细化后 if ret: objpoints.append(objp) # 角点亚像素精细化提高定位精度 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) imgpoints.append(corners_refined) # 可视化角点可选用于检查 cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow(fChessboard Corners {i}, img) cv2.waitKey(500) # 显示500毫秒 cv2.destroyWindow(fChessboard Corners {i}) else: print(f警告在图像 {fname} 中未找到棋盘格角点) cv2.destroyAllWindows() print(f成功处理了 {len(objpoints)} 张图像。)实操心得cv2.findChessboardCorners的成败很大程度上取决于图像质量、对比度和棋盘格的完整性。如果某些图片总是检测失败可以尝试对图像进行直方图均衡化 (cv2.equalizeHist) 来增强对比度。调整findChessboardCorners的flags参数例如尝试cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE。手动检查图像确保棋盘格没有被遮挡且光照均匀。4.2 执行标定与参数解读收集到足够的objpoints和imgpoints后就可以调用核心的标定函数了。# 7. 执行相机标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) if ret: print(相机标定成功) print(\n 内参矩阵 (mtx) ) print(mtx) print(\n 畸变系数 (dist) ) print(dist.ravel()) # 通常为5个参数 [k1, k2, p1, p2, k3] # 计算重投影误差评估标定质量 mean_error 0 for i in range(len(objpoints)): imgpoints2, _ cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) mean_error error print(f\n 平均重投影误差 \n{mean_error / len(objpoints):.6f} 像素) else: print(相机标定失败)参数解读与质量评估ret: 标定是否成功的布尔值。mtx: 内参矩阵。一个3x3的矩阵[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]。fx, fy是以像素为单位的焦距cx, cy是主点坐标通常接近图像中心。dist: 畸变系数向量。通常是5个或更多。[k1, k2, p1, p2, k3]。rvecs,tvecs: 每张标定图像对应的旋转向量和平移向量外参。重投影误差这是衡量标定精度的黄金标准。它计算的是检测到的角点位置与用标定参数重新投影回去的理论位置之间的平均像素距离。这个值通常应小于0.5像素越接近0越好。如果误差大于1像素说明标定质量不佳需要检查标定板平整度、角点检测精度或图像数量/姿态。4.3 标定结果验证与图像去畸变标定完了怎么知道对不对最直观的方法就是看去畸变后的图像直线是否变直了。# 读取一张测试图像可以是标定集之外的 test_img cv2.imread(test_image.jpg) h, w test_img.shape[:2] # 优化内参矩阵和 ROI (Region of Interest) newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) # 方法1使用 cv2.undistort 去畸变 dst cv2.undistort(test_img, mtx, dist, None, newcameramtx) # 方法2使用 remapping对于视频流更高效 mapx, mapy cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), 5) dst_remap cv2.remap(test_img, mapx, mapy, cv2.INTER_LINEAR) # 裁剪掉 ROI 之外的黑边 x, y, w_roi, h_roi roi dst_cropped dst[y:yh_roi, x:xw_roi] # 并排显示原图和去畸变图 cv2.imshow(Original, test_img) cv2.imshow(Undistorted, dst) cv2.imshow(Undistorted and Cropped, dst_cropped) cv2.waitKey(0) cv2.destroyAllWindows() # 保存标定参数供后续使用 np.savez(camera_calib_params.npz, mtxmtx, distdist, newcameramtxnewcameramtx, roiroi) print(标定参数已保存至 camera_calib_params.npz)注意事项cv2.getOptimalNewCameraMatrix中的最后一个参数alpha控制裁剪量。alpha0会裁剪掉所有无效像素无黑边但会损失部分视野alpha1保留所有原始像素可能有黑边视野无损。通常取0.5-1之间的值进行权衡。观察去畸变后图像边缘的直线如门窗、桌面边缘是否恢复笔直是验证畸变系数有效性的好方法。5. 基于单张图像的棋盘格位姿估计有了精确的内参和畸变系数我们现在可以“读懂”单张图片中棋盘格的空间位置了。这是AR叠加、机械臂抓取等应用的关键一步。5.1 实时视频流中的位姿解算我们将编写一个程序打开摄像头实时检测棋盘格并估算其位姿同时将坐标系绘制在图像上。import cv2 import numpy as np # 1. 加载之前保存的标定参数 calib_data np.load(camera_calib_params.npz) mtx calib_data[mtx] dist calib_data[dist] # 可以使用 newcameramtx 以获得更好的去畸变效果这里为简化直接用 mtx # newcameramtx calib_data[newcameramtx] # 2. 定义棋盘格规格必须与标定时一致 pattern_size (7, 9) square_size 0.03 # 单位米 # 3. 准备世界坐标系下的棋盘格角点3D坐标 (Z0) objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size # 4. 定义用于绘制的3D坐标系轴点 (长度约为3个格子) axis np.float32([[3*square_size, 0, 0], # X轴红色 [0, 3*square_size, 0], # Y轴绿色 [0, 0, -3*square_size]]).reshape(-1, 3) # Z轴蓝色 (指向相机外) # 5. 打开摄像头 cap cv2.VideoCapture(0) if not cap.isOpened(): print(无法打开摄像头) exit() while True: ret, frame cap.read() if not ret: print(无法获取帧) break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 ret_corners, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret_corners: # 角点亚像素精细化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) # 核心使用 solvePnP 求解旋转向量和平移向量 # 参数对象点图像点内参畸变系数初始解可选使用迭代法 ret_pnp, rvec, tvec cv2.solvePnP(objp, corners_refined, mtx, dist, flagscv2.SOLVEPNP_ITERATIVE) if ret_pnp: # 将旋转向量转换为旋转矩阵可选用于更直观的理解 R, _ cv2.Rodrigues(rvec) # print(fRotation Matrix:\n{R}) # print(fTranslation Vector (meters):\n{tvec}) # 将3D坐标系轴点投影到2D图像平面 imgpts, _ cv2.projectPoints(axis, rvec, tvec, mtx, dist) # 在图像上绘制坐标系 origin tuple(corners_refined[0].ravel().astype(int)) # 以第一个角点为原点 img cv2.line(frame, origin, tuple(imgpts[0].ravel().astype(int)), (0, 0, 255), 5) # X - 红 img cv2.line(frame, origin, tuple(imgpts[1].ravel().astype(int)), (0, 255, 0), 5) # Y - 绿 img cv2.line(frame, origin, tuple(imgpts[2].ravel().astype(int)), (255, 0, 0), 5) # Z - 蓝 # 可选在图像上显示位姿数值 cv2.putText(frame, ftvec (m): [{tvec[0][0]:.2f}, {tvec[1][0]:.2f}, {tvec[2][0]:.2f}], (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) # 也可以计算欧拉角需要从旋转矩阵R转换 # ... # 绘制棋盘格角点可选 cv2.drawChessboardCorners(frame, pattern_size, corners_refined, ret_corners) cv2.imshow(Real-time Pose Estimation, frame) # 按 q 键退出 if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()5.2 位姿结果的物理意义与应用运行上面的代码你会看到一个三维坐标系被“钉”在了棋盘格上。红色轴 (X)通常指向棋盘格的右侧。绿色轴 (Y)通常指向棋盘格的下方。蓝色轴 (Z)遵循右手定则垂直于棋盘格平面指向相机注意我们在定义axis时Z值为负所以画出来是指向相机外的这更符合“物体指向观察者”的直觉。tvec向量的三个分量[tx, ty, tz]就是棋盘格坐标系原点我们设定的第一个角点在相机坐标系下的坐标单位是米。tz就是棋盘格到相机的距离。这是非常有用的信息应用场景举例增强现实 (AR)知道了棋盘格作为标记物的精确位姿就可以将虚拟的3D模型准确地“放置”在棋盘格所在的位置并随其移动。机器人抓取如果棋盘格贴在目标物体上机械臂的视觉系统通过解算出的tvec和R可以计算出工具末端需要移动到的位置和姿态。视觉测量通过测量图像中已知尺寸物体的像素距离结合内参和位姿可以反推其他物体的实际尺寸。核心技巧cv2.solvePnP有多个求解算法通过flags指定。SOLVEPNP_ITERATIVE是基于Levenberg-Marquardt优化的迭代法最常用也最稳定。对于平面目标如棋盘格SOLVEPNP_IPPE无限平面姿态估计速度更快且精度高是较新的推荐选项。如果解算的位姿跳动很大可以尝试使用SOLVEPNP_EPNP或SOLVEPNP_DLS并结合RANSAC (cv2.solvePnPRansac) 来剔除错误的角点匹配提高鲁棒性。6. 精度提升与高级技巧基础功能实现后我们总希望它更准、更稳。下面分享几个提升精度的关键点。6.1 标定阶段的精度陷阱与规避标定板质量与平整度这是最大的误差来源。打印的纸张会弯曲。务必使用刚性底板如光学平板、厚亚克力板并确保粘贴平整。有条件可以使用出厂标定的陶瓷或玻璃标定板。角点检测精度cv2.cornerSubPix是必须的。它的窗口大小(11,11)需要根据图像中角点区域的对比度调整。对比度低则增大窗口但计算量也增大。图像数量与姿态分布至少需要10-15张不同姿态的图像。姿态要尽可能覆盖相机视野的各个角落和倾斜角度避免所有图像都集中在一个很小的姿态范围内。远近也要有变化。光照均匀性避免强光直射产生反光也避免阴影覆盖部分角点。均匀的漫射光是最理想的。重投影误差分析标定完成后逐一检查每张图像的重投影误差。如果某一张图像的误差远高于平均值很可能是该图像角点检测有误或标定板不平整应考虑剔除这张图像后重新标定。6.2 位姿估计的稳定性优化使用solvePnPRansac在实时场景中cv2.findChessboardCorners偶尔会检测到错误的角点或受到噪声干扰。使用RANSAC版本的PnP求解器可以自动剔除局外点得到更稳定、更抗干扰的位姿结果。ret_pnp, rvec, tvec, inliers cv2.solvePnPRansac( objp, corners_refined, mtx, dist, iterationsCount100, reprojectionError8.0, confidence0.99 )参数reprojectionError是判定内点的阈值单位像素根据你的精度要求调整。inliers输出的是被认为是内点的角点索引列表。滤波与平滑对于视频流直接使用单帧解算的位姿可能会抖动。可以采用简单的滑动平均滤波Moving Average或卡尔曼滤波Kalman Filter对rvec和tvec进行时序上的平滑。OpenCV也提供了cv2.KalmanFilter类可供使用。多帧融合对于静态场景可以连续采集多帧分别计算位姿后取平均值或中位数能有效抑制随机噪声。坐标系定义一致性确保你定义的世界坐标系objp和绘制的3D轴axis与你的应用逻辑一致。例如在机器人学中通常定义Z轴向前X轴向左Y轴向上。混乱的坐标系定义会导致后续应用层出现难以调试的错误。7. 常见问题排查与解决实录在实际操作中你几乎一定会遇到下面这些问题。这里是我的踩坑记录和解决方案。7.1 角点检测失败或不准现象cv2.findChessboardCorners经常返回False或者检测到的角点位置明显偏移。排查检查pattern_size这是最常出错的地方务必数清楚内角点棋盘格内部黑白方块的交点的行数和列数。检查图像清晰度和对比度图像是否模糊棋盘格黑白是否分明尝试对图像进行高斯模糊去噪 (cv2.GaussianBlur) 或对比度拉伸。调整检测参数使用flags参数。cv2.CALIB_CB_ADAPTIVE_THRESH使用自适应阈值对光照不均更有效。cv2.CALIB_CB_NORMALIZE_IMAGE在阈值化前先归一化图像亮度。手动验证用画图工具打开图片放大后数一数角点确认棋盘格是否完整出现在视野中。7.2 标定误差过大重投影误差 1像素现象标定程序能跑通但输出的平均重投影误差很高。排查检查标定板物理尺寸用游标卡尺精确测量打印的棋盘格格子尺寸确保与代码中square_size完全一致。1%的尺寸误差可能导致数像素的重投影误差。检查标定板平整度这是隐形杀手。将标定板放在绝对平整的桌面上拍摄一组对比手持拍摄的误差立刻见分晓。检查图像集合剔除那些角点检测看起来就不准的、或者标定板严重倾斜透视变形极大的图片。质量优于数量。尝试不同标定算法cv2.calibrateCamera默认使用CALIB_FIX_PRINCIPAL_POINT等标志。对于主流相机可以尝试不固定主点让算法优化所有参数flagscv2.CALIB_ZERO_TANGENT_DIST cv2.CALIB_RATIONAL_MODEL。但要注意增加自由度可能在小数据集上导致过拟合。7.3 位姿估计结果抖动或明显错误现象实时显示时坐标系乱跳或者tz距离明显不合理如几米或负值。排查确认内参正确加载最基础的错误。打印出加载的mtx和dist确认它们不是全零或默认值。检查solvePnP的输入确保传入的objp和检测到的corners顺序一一对应且数量一致。cv2.findChessboardCorners返回的角点顺序是确定的通常从左上角开始按行排列。验证角点顺序在图像上绘制角点序号观察其顺序是否与世界点定义顺序匹配。for i, corner in enumerate(corners_refined): pos tuple(corner.ravel().astype(int)) cv2.putText(frame, str(i), pos, cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1)单位一致性确保square_size的单位米与你的应用场景期望的单位一致。tvec的输出单位与此相同。使用solvePnPRansac如前所述它能有效抵抗个别错误角点的干扰。检查相机镜头是否对焦清晰失焦的模糊图像会极大降低角点定位精度。7.4 去畸变后图像出现黑边或严重变形现象调用cv2.undistort后图像四周有黑色区域或者中心物体被拉伸变形。排查理解getOptimalNewCameraMatrix黑边是因为畸变校正后有效图像区域变成了一个不规则的四边形映射回矩形时四角没有像素信息。使用roi参数可以裁剪掉这些黑边。变形则可能是畸变系数k3等高阶项标定不准确可以尝试在标定时只使用k1, k2(flagscv2.CALIB_FIX_K3)。畸变模型是否合适对于鱼眼镜头或超广角镜头标准的布朗畸变模型可能不适用需要使用cv2.fisheye模块进行鱼眼相机标定。这个从相机标定到位姿估计的完整流程是开启任何严肃的计算机视觉项目的大门。它没有太多炫酷的深度学习模型但却是所有上层应用赖以稳定的基石。我自己的经验是花在精心制作标定板和采集高质量数据上的时间会在后期节省数倍的调试时间。当你看到那个小小的坐标系稳稳地“坐”在棋盘格上随着你的移动而精准变化时你就掌握了让机器理解空间关系的第一把钥匙。