从零打造 C# 工业视觉检测系统(五):相机连接、连续取流与双路实时预览
系列标签 :
C#海康威视相机取流IntPtrBitmap多线程本文目标:实现相机的连接与断开、连续取流循环,以及双路相机的实时画面显示
一、前言
上篇文章我们完成了相机的枚举和选择。本文将进入更核心的环节------让相机"动"起来:
- 连接选中的相机
- 启动连续取流(Grab)
- 将每一帧图像转换为可显示的
Bitmap - 在界面上实时预览
本项目支持最多 2 路相机,我们将完整讲解双路相机的生命周期管理。
二、相机连接流程
2.1 连接参数说明
ConnectCamera 方法的参数:
csharp
int ConnectCamera(
int selectIndex, // 用户在列表中选中的相机索引
int selectCameraIndex, // 逻辑相机编号(0=正面,1=背面)
ref int CameraId, // 返回的相机句柄 ID
ref int ImgWidth, // 返回的图像宽度
ref int ImgHeight // 返回的图像高度
);
2.2 正面相机连接代码
csharp
private bool ConnectFontCamera()
{
if (!CameraData.isConnect[0])
{
// 弹出选择窗口
SelectCamera selectCamera = new SelectCamera();
selectCamera.ShowDialog();
CameraData.CameraIndex[0] = (int)selectCamera.selectCameraIndex;
selectCamera.Dispose();
if (CameraData.CameraIndex[0] != -1)
{
int ret = DllMethod.ConnectCamera(
CameraData.CameraIndex[0],
0, // 逻辑编号 0 = 正面相机
ref currentCameraCount,
ref CameraData.ImageWidthList[0],
ref CameraData.ImageHeightList[0]);
if (ret == 0)
{
CameraData.StartFrontGrabing(); // 启动取流
return true;
}
}
}
return false;
}
2.3 背面相机连接代码
与正面相机几乎一致,只是逻辑编号改为 1:
csharp
private bool ConnectBackCamera()
{
if (!CameraData.isConnect[1])
{
SelectCamera selectCamera = new SelectCamera();
selectCamera.ShowDialog();
CameraData.CameraIndex[1] = (int)selectCamera.selectCameraIndex;
selectCamera.Dispose();
if (CameraData.CameraIndex[1] != -1)
{
int ret = DllMethod.ConnectCamera(
CameraData.CameraIndex[1],
1, // 逻辑编号 1 = 背面相机
ref currentCameraCount,
ref CameraData.ImageWidthList[1],
ref CameraData.ImageHeightList[1]);
if (ret == 0)
{
CameraData.StartBackGrabing();
return true;
}
}
}
return false;
}
三、连续取流(Grab)实现
3.1 为什么用后台 Task?
相机取流是一个阻塞循环过程:不断从驱动缓冲区读取最新图像。如果在 UI 线程执行,界面会完全卡死。因此必须使用后台线程。
3.2 CameraData 中的取流任务
csharp
public static class CameraData
{
public static Task frontTask;
public static Task backTask;
public static CancellationTokenSource frontCancelTokenSource;
public static CancellationTokenSource backCancelTokenSource;
public static void StartFrontGrabing()
{
frontCancelTokenSource = new CancellationTokenSource();
frontTask = new Task(() =>
{
while (true)
{
if (frontCancelTokenSource.IsCancellationRequested)
break;
DllMethod.Photograph(0); // 触发拍照/取流
}
}, frontCancelTokenSource.Token);
frontTask.Start();
isConnect[0] = true;
ImageShowBufList[0] = DllMethod.GetImageData(0); // 获取图像缓冲区指针
}
public static void StopFrontGrabing()
{
frontCancelTokenSource.Cancel();
frontTask.Wait(); // 等待任务结束
frontCancelTokenSource.Dispose();
DllMethod.DisconnectCamera(0);
DllMethod.ClearImage(0);
isConnect[0] = false;
CameraIndex[0] = -1;
}
}
关键点:
CancellationTokenSource.Cancel()发出取消信号frontTask.Wait()等待循环退出,避免强制中断导致资源泄漏DisconnectCamera释放 SDK 资源ClearImage清空图像缓冲区
四、IntPtr 到 Bitmap 的转换
4.1 图像数据在内存中的形态
海康 SDK 返回的图像数据是 24 位 BGR 格式 (每像素 3 字节,蓝绿红顺序),存储在非托管内存中,通过 IntPtr 指针访问。
内存布局:
[B][G][R][B][G][R][B][G][R]...
像素0 像素1 像素2
4.2 ImageUtil.IntptrToBitmap 实现
csharp
public static class ImageUtil
{
public static Bitmap IntptrToBitmap(int width, int height, IntPtr pt)
{
// 创建目标 Bitmap
Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
Rectangle rect = new Rectangle(0, 0, width, height);
// 锁定 Bitmap 的内存
BitmapData bmpDataSrc = bitmap.LockBits(
rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
IntPtr ptrSrc = bmpDataSrc.Scan0;
byte[] rgbValues = new byte[width * height * 3];
// 从 SDK 缓冲区拷贝到托管数组
System.Runtime.InteropServices.Marshal.Copy(pt, rgbValues, 0, width * height * 3);
// 使用指针将像素写入 Bitmap(高性能)
unsafe
{
byte* ptr = (byte*)(bmpDataSrc.Scan0);
for (int i = 0; i < bmpDataSrc.Height; i++)
{
for (int j = 0; j < bmpDataSrc.Width; j++)
{
ptr[0] = rgbValues[(bmpDataSrc.Width * i + j) * 3 + 0]; // B
ptr[1] = rgbValues[(bmpDataSrc.Width * i + j) * 3 + 1]; // G
ptr[2] = rgbValues[(bmpDataSrc.Width * i + j) * 3 + 2]; // R
ptr += 3;
}
ptr += bmpDataSrc.Stride - bmpDataSrc.Width * 3; // 跳过对齐填充
}
}
bitmap.UnlockBits(bmpDataSrc);
return bitmap;
}
}
重要细节:
Stride是每行实际占用的字节数,可能大于width * 3(内存对齐填充)unsafe代码块需要项目开启"允许不安全代码"选项- BGR 顺序直接写入即可,Windows GDI+ 的
Format24bppRgb实际就是 BGR 存储
4.3 项目中的不安全代码配置
在 FormUI.csproj 中:
xml
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
五、双路实时预览
5.1 异步显示任务
csharp
Task frontTask;
Task backTask;
CancellationTokenSource frontCancelTokenSource;
CancellationTokenSource backCancelTokenSource;
private void ShowVideo()
{
if (CameraData.isConnect[0] == true)
{
frontCancelTokenSource = new CancellationTokenSource();
frontTask = Task.Run(async () =>
{
while (!frontCancelTokenSource.Token.IsCancellationRequested)
{
frontBitmap = ImageUtil.IntptrToBitmap(
CameraData.ImageWidthList[0],
CameraData.ImageHeightList[0],
CameraData.ImageShowBufList[0]);
this.BeginInvoke(new Action(() =>
{
FrontPic.Image = frontBitmap;
FrontPic.Refresh();
}));
await Task.Delay(50); // 20 FPS 足够预览
}
}, frontCancelTokenSource.Token);
}
if (CameraData.isConnect[1] == true)
{
backCancelTokenSource = new CancellationTokenSource();
backTask = Task.Run(async () =>
{
while (!backCancelTokenSource.Token.IsCancellationRequested)
{
backBitmap = ImageUtil.IntptrToBitmap(
CameraData.ImageWidthList[1],
CameraData.ImageHeightList[1],
CameraData.ImageShowBufList[1]);
this.BeginInvoke(new Action(() =>
{
BackPic.Image = backBitmap;
BackPic.Refresh();
}));
await Task.Delay(50);
}
}, backCancelTokenSource.Token);
}
}
5.2 停止预览
csharp
private void StopVideo()
{
if (CameraData.isConnect[0])
{
frontCancelTokenSource.Cancel();
frontTask.Wait();
frontCancelTokenSource?.Dispose();
this.Invoke(new Action(() =>
{
FrontPic.Image = null;
FrontPic.Refresh();
}));
}
if (CameraData.isConnect[1])
{
backCancelTokenSource.Cancel();
backTask.Wait();
backCancelTokenSource?.Dispose();
this.Invoke(new Action(() =>
{
BackPic.Image = null;
BackPic.Refresh();
}));
}
}
线程安全要点:
- 后台线程中不能直接接触 UI 控件,必须通过
BeginInvoke/Invoke BeginInvoke是异步的,不会阻塞取流循环;Invoke是同步的await Task.Delay(50)控制刷新频率,减轻 CPU 负担
六、内存优化建议
6.1 Bitmap 对象的生命周期
每帧都 new Bitmap() 会产生大量内存分配。优化方案:
- 复用 Bitmap:如果尺寸不变,可以只创建一次,后续更新像素数据
- 及时释放 :旧的 Bitmap 需要
Dispose(),否则内存泄漏 - 双缓冲:避免画面闪烁
6.2 本项目中的处理方式
当前实现中每帧都新建 Bitmap,但对于预览场景(50ms 一帧)来说,GC 可以承受。如果帧率要求更高(如 100+ FPS),建议采用固定缓冲区 + LockBits 更新的方案。
七、总结
本文完成了相机从连接到显示的全流程:
- 连接相机 :通过
ConnectCamera建立 SDK 会话 - 连续取流 :后台
Task+CancellationToken实现优雅启停 - 图像转换 :
IntPtr→byte[]→Bitmap的完整链路 - 实时预览:双路异步显示,线程安全更新 UI
下篇预告:《从零打造 C# 工业视觉检测系统(六):OpenCV 图像处理与像素级比例尺标定》
我们将深入讲解图像像素操作、BGR 通道,以及如何将像素距离转换为物理尺寸。
欢迎点赞、收藏、关注,你的支持是我持续输出的动力!