手头有个机械臂抓取或者移动端避障的项目,要用普通摄像头测出目标的三维坐标,你是不是心里直犯嘀咕?“这玩意儿是不是得套什么高深的三维重建算法?”“那些矩阵公式看得我脑壳疼,到底怎么落地?”
别慌,今天咱们不扯那些让人昏昏欲睡的纯数学公式。作为在工业视觉里摸爬滚打了十几年的老油条,我今天就带大家用 C# 和 OpenCvSharp 库,手把手把这个看起来高大上的“单目定位”给彻底扒光。
咱们的目标很简单:拿个几块钱的普通摄像头,贴个打印出来的二维码(ArUco 标志),就能精准实时输出它在三维空间里的具体坐标(X, Y, Z)和旋转角度。
整套方案分为两步走。相信我,只要你跟着敲一遍,你会发现:原来单目定位,真的一点都不神秘!
很多刚入行的小伙伴,一上来就急着用摄像头测距离,结果发现离得近还算凑合,稍微放远一点或者拿到画面边缘,那数据就飘得没法看。
为什么?因为畸变!
市面上几十块钱的摄像头,那镜片多多少少都有点像“哈哈镜”——中间鼓、四周缩(径向畸变),或者镜头装得不那么正(切向畸变)。
要想让计算机看懂真实的世界,我们必须先摸清这颗摄像头的“脾气”。这个摸底的过程,就叫相机标定(Calibration)。我们需要拿到两样东西:
别去网上随便下一些来路不明的图。咱们自己写个小程序,一边看着实时画面,一边变换各种角度,抓拍 15 - 20 张清晰的棋盘格照片。
避坑指南: 棋盘格规格一定要数清楚!比如我们常用 9x6 的内角点棋盘格(注意:是内角点,即黑白格交叉的内交点个数,而不是最外圈的格子数!一个 10x7 格子的纸,内角点正好是 9x6)。

1using System;
2using System.IO;
3using OpenCvSharp;
4
5class CollectCalibrationImages
6{
7 static void Main()
8 {
9 int count = 0;
10 string saveDir = "calib_images";
11 Directory.CreateDirectory(saveDir);
12
13 using var cap = new VideoCapture(0); // 打开默认摄像头
14 if (!cap.IsOpened())
15 {
16 Console.WriteLine("摄像头打不开,是不是被别的程序占用了?");
17 return;
18 }
19
20 using var frame = new Mat();
21 using var display = new Mat();
22
23 // 9x6的内角点,这玩意儿数错了后面一步都走不通!
24 var patternSize = new Size(9, 6);
25
26 Console.WriteLine("【提示】把棋盘格放到摄像头前。检测到后按【空格】保存,按【ESC】退出。");
27
28 while (true)
29 {
30 cap.Read(frame);
31 if (frame.Empty()) continue;
32 frame.CopyTo(display);
33
34 Point2f[] corners = null;
35 // 粗略检测棋盘格角点
36 bool found = Cv2.FindChessboardCorners(frame, patternSize, out corners);
37
38 if (found)
39 {
40 // 帮我们画出彩色的角点连接线,很直观
41 Cv2.DrawChessboardCorners(display, patternSize, corners, found);
42 Cv2.PutText(display, "Detected! Press SPACE to save.", new Point(10, 30),
43 HersheyFonts.HersheySimplex, 0.8, Scalar.Green, 2);
44 }
45 else
46 {
47 Cv2.PutText(display, "No Chessboard Found", new Point(10, 30),
48 HersheyFonts.HersheySimplex, 0.8, Scalar.Red, 2);
49 }
50
51 Cv2.PutText(display, #34;Saved: {count} pics", new Point(10, 65),
52 HersheyFonts.HersheySimplex, 0.7, Scalar.Yellow, 2);
53
54 Cv2.ImShow("Collector", display);
55 int key = Cv2.WaitKey(1);
56
57 if (key == 27) break; // ESC 键退出
58
59 if (key == 32 && found) // 空格键保存
60 {
61 string path = Path.Combine(saveDir, #34;calib_{count:D3}.png");
62 Cv2.ImWrite(path, frame); // 存原图,千万别存画了线的图!
63 Console.WriteLine(#34;[成功] 已保存第 {count} 张:{path}");
64 count++;
65 }
66 }
67
68 Cv2.DestroyAllWindows();
69 Console.WriteLine(#34;
搞定!共收集到 {count} 张高质量标定图,准备下一步!");
70 }
71}个人经验碎碎念: 拍这个图的时候,千万不要只是直勾勾地对着镜头。你要让棋盘格倾斜一些角度(仰角、俯角、侧角),并且尽量让棋盘格充满画面的各个角落(尤其是边缘)。因为畸变在四周最严重,如果边缘没有点,畸变系数是算不准的。
有了这一批照片,我们就可以让 OpenCvSharp 帮我们算矩阵了。我们会把每一个像素点对应的真实 3D 坐标(我们假定棋盘格处于 \(Z=0\) 的平面,每个格子长 25 毫米)塞给算法。
这个算出来的 camera_params.xml 是我们待会做三维定位的基石。丢了它,后面寸步难行。
万事俱备,东风已来!现在,我们可以把一个叫 ArUco 的黑白二维码放到摄像头前面,让系统实时告诉我:这个二维码距离我的镜头到底有多少厘米?它转了多少度?
简单来说,它是一种专门为了机器视觉定位设计的方形条码。和普通的 QR Code(比如微信付款码)相比,它的检测速度极快,定位精度极高,哪怕在极差的光照下,也能稳定地检测到四个角点。

在这段代码中,我们会通过 OpenCV 著名的 SolvePnP(Perspective-n-Point)算法,结合刚刚生成的相机内参,完美解算出标志牌在空间里的三维姿态。

在以前,很多小伙伴会说:“我用像素占比算测距不行吗?物体近了像素大,远了像素小。”
这在某些极度局限的场景下或许能用,但它的硬伤太明显了:
大家在调试相机标定时,遇到过最恶心的问题是什么?
欢迎在评论区里留言聊聊你们在视觉项目里踩过的坑,咱们一起研究研究。
今日份技术总结:
觉得有用的话,欢迎微信打赏鼓励一下,让我有动力继续输出这类实战内容。完整源码结构已在各节逐一呈现,可结合项目实际直接落地;如需完整源码,可在公众号聊天窗口获取。
#C#开发 #OpenCV #三维视觉 #机器视觉 #工业自动化
更新时间:2026-07-09
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302034903号