从零打造 C# 工业视觉检测系统(六):OpenCV 图像处理与像素级比例尺标定
系列标签 :
C#OpenCV图像处理比例尺标定像素测量本文目标:掌握图像像素级操作、BGR 通道原理,以及像素到物理尺寸的标定方法
一、前言
工业视觉检测的核心任务是测量 ------测量长度、宽度、直径、角度等物理尺寸。但相机拍到的是像素,如何把像素转换成毫米?这就是比例尺标定要解决的问题。
本文将从像素操作的基础讲起,一直讲到本项目中的标定实现。
二、图像数据在内存中的形态
2.1 BGR 格式
本项目使用的海康相机输出 24 位 BGR 图像,每个像素占 3 字节:
内存地址: 0 1 2 3 4 5 6 7 8
[B0][G0][R0][B1][G1][R1][B2][G2][R2]
└────像素0────┘└────像素1────┘└────像素2──...
为什么是 BGR 而不是 RGB?
这是历史原因。Windows GDI 和早期显卡使用 BGR 顺序,OpenCV 也沿用了这一传统。虽然命名是 Format24bppRgb,实际存储却是 BGR。
2.2 Stride(扫描行宽度)
Bitmap 的每一行在内存中占用的字节数必须是 4 的倍数(内存对齐)。因此:
Stride = ((Width * 3) + 3) / 4 * 4
例如:宽度 501 像素的图像:
Width * 3 = 1503Stride = 1504(向上取整到 4 的倍数)- 每行末尾有 1 字节的填充
在 ImageUtil.IntptrToBitmap 中,处理完一行像素后需要跳过填充:
csharp
ptr += bmpDataSrc.Stride - bmpDataSrc.Width * 3;
三、像素级操作实战
3.1 LockBits 高性能访问
Bitmap.GetPixel/SetPixel 太慢!工业图像动辄几百万像素,必须用 LockBits 直接操作内存:
csharp
BitmapData bmpData = bitmap.LockBits(
rect,
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb);
// 获取首像素地址
IntPtr ptr = bmpData.Scan0;
// ... 操作像素 ...
bitmap.UnlockBits(bmpData); // 必须解锁!
3.2 使用 unsafe 指针加速
csharp
unsafe
{
byte* ptr = (byte*)(bmpData.Scan0);
for (int y = 0; y < bmpData.Height; y++)
{
for (int x = 0; x < bmpData.Width; x++)
{
// 反转颜色(负片效果)
ptr[0] = (byte)(255 - ptr[0]); // B
ptr[1] = (byte)(255 - ptr[1]); // G
ptr[2] = (byte)(255 - ptr[2]); // R
ptr += 3;
}
ptr += bmpData.Stride - bmpData.Width * 3; // 跳过填充
}
}
注意:项目属性中必须开启"允许不安全代码"。
3.3 使用 Marshal.Copy(安全模式)
如果不希望使用 unsafe,可以先把像素拷贝到托管数组:
csharp
byte[] pixels = new byte[height * stride];
Marshal.Copy(bmpData.Scan0, pixels, 0, pixels.Length);
// 在托管数组中处理...
for (int i = 0; i < pixels.Length; i += 3)
{
pixels[i] = (byte)(255 - pixels[i]); // B
pixels[i+1] = (byte)(255 - pixels[i+1]);// G
pixels[i+2] = (byte)(255 - pixels[i+2]);// R
}
Marshal.Copy(pixels, 0, bmpData.Scan0, pixels.Length);
四、比例尺标定原理
4.1 什么是比例尺?
比例尺(Scale)表示每个像素对应的物理尺寸,单位通常是 mm/pixel。
物理尺寸 = 像素距离 × 比例尺
例如:比例尺 = 0.05 mm/pixel,图像中两点相距 100 像素,则实际距离 = 5 mm。
4.2 标定方法
本项目采用已知尺寸参照物法:
-
在相机视野中放置一个已知精确尺寸的标定板(或标准块)
-
用算法检测标定板边缘,计算像素距离
-
比例尺 = 物理尺寸 / 像素尺寸
Scale = 50.0 mm / 956 pixels = 0.0523 mm/pixel
4.3 项目中的标定实现
CameraOptionForm 中的标定按钮逻辑:
csharp
private void Bt_scale_Click(object sender, EventArgs e)
{
// 1. 构建标准产品参数结构体
SlugStruct normalSlug = new SlugStruct();
normalSlug.type = int.Parse(Config.type);
normalSlug.length = double.Parse(Config.length);
normalSlug.length_up_offset = double.Parse(Config.lengthUpOffset);
normalSlug.length_down_offset = double.Parse(Config.lengthDownOffset);
normalSlug.width = double.Parse(Config.width);
normalSlug.width_up_offset = double.Parse(Config.widthUpOffset);
normalSlug.width_down_offset = double.Parse(Config.widthDownOffset);
normalSlug.is_qualified = false;
normalSlug.scale = double.Parse(Config.scale);
// 2. 初始化算法
DllMethod.Init(CameraData.CameraIndex[0], normalSlug);
// 3. 调用标定接口,自动计算比例尺
Config.scale = DllMethod.Scale(CameraData.CameraIndex[0]).ToString();
// 4. 更新显示并保存
Tb_scale.Text = Convert.ToDouble(Config.scale).ToString("0.000");
IniConfigHelper.SaveParam();
// 5. 释放算法资源
DllMethod.UnInit();
}
流程说明:
Init告诉算法当前产品的标准规格Scale让算法拍摄标定板,自动计算并返回比例尺- 标定结果保存到
config.json,后续检测直接使用
五、坐标转换:PictureBox 到图像像素
5.1 为什么需要坐标转换?
当用户在 PictureBox 上点击或绘制 ROI 时,点击的是控件坐标,需要转换为原始图像坐标:
csharp
public static void ConvertCoordinates(PictureBox pic,
out int X0, out int Y0, int x, int y)
{
int pic_hgt = pic.ClientSize.Height;
int pic_wid = pic.ClientSize.Width;
int img_hgt = pic.Image.Height;
int img_wid = pic.Image.Width;
X0 = x;
Y0 = y;
switch (pic.SizeMode)
{
case PictureBoxSizeMode.AutoSize:
case PictureBoxSizeMode.StretchImage:
X0 = (int)(img_wid * x / (float)pic_wid);
Y0 = (int)(img_hgt * y / (float)pic_hgt);
break;
}
}
5.2 不同 SizeMode 的处理
| SizeMode | 特点 | 坐标转换 |
|---|---|---|
Normal |
1:1 显示,图像可能超出控件 | 无需转换 |
StretchImage |
拉伸填满控件 | 按比例映射 |
Zoom |
等比缩放,保持比例 | 按比例映射 |
CenterImage |
居中显示,不缩放 | 需加偏移量 |
六、图像处理在检测中的应用
虽然本项目的核心算法封装在 Algorithm.dll 中,但了解图像处理的基本原理有助于:
- 调试问题:当检测结果异常时,能判断是图像质量问题还是算法问题
- 优化参数:曝光、增益、光源角度直接影响边缘清晰度
- 二次开发:在算法结果上叠加显示(如在原图上画检测框)
6.1 常见预处理操作
csharp
// 灰度化(如果需要)
for (int i = 0; i < pixels.Length; i += 3)
{
byte gray = (byte)(0.299 * pixels[i+2] + 0.587 * pixels[i+1] + 0.114 * pixels[i]);
pixels[i] = pixels[i+1] = pixels[i+2] = gray;
}
// 二值化
byte threshold = 128;
for (int i = 0; i < pixels.Length; i += 3)
{
byte gray = (byte)(0.299 * pixels[i+2] + 0.587 * pixels[i+1] + 0.114 * pixels[i]);
byte val = gray > threshold ? (byte)255 : (byte)0;
pixels[i] = pixels[i+1] = pixels[i+2] = val;
}
七、总结
本文从像素层面讲解了图像处理的基础:
- BGR 格式:理解像素在内存中的排列方式
- LockBits + unsafe:高性能像素操作
- 比例尺标定:像素到物理尺寸的关键转换
- 坐标转换:UI 坐标与图像坐标的映射
下篇预告:《从零打造 C# 工业视觉检测系统(七):串口通信 SerialPort 封装与调试工具开发》
我们将进入工业通信领域,讲解 RS232/RS485 和 C# 串口编程。
欢迎点赞、收藏、关注,你的支持是我持续输出的动力!