从零打造 C# 工业视觉检测系统(六):OpenCV 图像处理与像素级比例尺标定

从零打造 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 = 1503
  • Stride = 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 标定方法

本项目采用已知尺寸参照物法

  1. 在相机视野中放置一个已知精确尺寸的标定板(或标准块)

  2. 用算法检测标定板边缘,计算像素距离

  3. 比例尺 = 物理尺寸 / 像素尺寸

    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 中,但了解图像处理的基本原理有助于:

  1. 调试问题:当检测结果异常时,能判断是图像质量问题还是算法问题
  2. 优化参数:曝光、增益、光源角度直接影响边缘清晰度
  3. 二次开发:在算法结果上叠加显示(如在原图上画检测框)

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;
}

七、总结

本文从像素层面讲解了图像处理的基础:

  1. BGR 格式:理解像素在内存中的排列方式
  2. LockBits + unsafe:高性能像素操作
  3. 比例尺标定:像素到物理尺寸的关键转换
  4. 坐标转换:UI 坐标与图像坐标的映射

下篇预告:《从零打造 C# 工业视觉检测系统(七):串口通信 SerialPort 封装与调试工具开发》

我们将进入工业通信领域,讲解 RS232/RS485 和 C# 串口编程。


欢迎点赞、收藏、关注,你的支持是我持续输出的动力!

相关推荐
深视智能科技1 小时前
PIV流场测量相机怎么选?深视智能三类高速成像方案解析
视觉检测
小金子会发光11 小时前
C# WinForms 手搓 Modbus RTU 调试助手:不依赖第三方 Modbus 库,从 CRC16 到 8 种功能码完整实现
c#·modbusrtu·串口通讯
深视智能科技13 小时前
DIC 高速应变测量系统解决方案
视觉检测
上海安当技术15 小时前
C/S 桌面软件怎么接 UKey 强认证?C# SDK 与 C 动态库集成 WinForms/Qt/工控实战
c语言·qt·c#
明如正午16 小时前
【C#】跨线程更新进度条_UpdateProgress 方法解析
c#
等一朵映山红17 小时前
基于 OpenCV 实现摄像头实时人脸检测完整流程
人工智能·opencv·计算机视觉
曹牧18 小时前
C#:`JsonConvert.DeserializeObject`
windows·c#
望天hous18 小时前
C#中masstranist快速入门
开发语言·c#
冷咖啡离 我的笨笨18 小时前
数据去重:通过 C# 删除 Excel 中的重复行
开发语言·c#·excel