Basler Pylon .NET SDK 外部触发(External Trigger)采集示例
C# 工业相机开发:实战 Basler Pylon 外部触发与高速缓存
在工业自动化视觉检测中,外部触发(External Trigger) 是最常用的模式。它能确保相机快门与传送带上的工件同步。但在高频触发下,磁盘 IO 往往跟不上采集速度。
本文将解析一个基于 Basler Pylon SDK 的 C# 示例,重点讲解如何实现异步触发、内存深拷贝缓存以及 TIFF 格式的高质量保存。
核心设计思路
本示例采用了生产-消费的分离思想:
-
采集层:相机监听外部电平信号(Line 4),一旦触发即压入内存。
-
缓存层:使用
List<byte[]>进行深拷贝存储,避免缓冲区被 SDK 自动覆盖。 -
持久化层:通过控制台交互,在空闲时段将内存数据批量写入磁盘。
源码如下
using System; using System.Collections.Generic; using System.Drawing.Imaging; using System.Drawing; using System.Runtime.InteropServices; using System.Threading; using Basler.Pylon; namespace 巴斯勒外部触发示例 { class Program { // SFNC 2.0.0 版本,用于判断相机是否支持新的浮点曝光时间参数 static Version sfnc2_0_0 = new Version(2, 0, 0); // 用于存储抓取到的图像原始字节数据,最多缓存1000张 static readonly List<byte[]> imageBuffer = new List<byte[]>(1000); // 线程锁对象,确保多线程下对 imageBuffer 的访问安全 static readonly object bufferLock = new object(); // 记录第一张图像的元数据(宽、高、像素格式),后续保存图像时需要用到 static int _width = 0; static int _height = 0; static PixelType _pixelType = PixelType.Undefined; static void Main(string[] args) { int exitCode = 0; // 启动一个后台线程用于监听键盘输入(Ctrl+S 保存,Ctrl+Q 清空) Thread keyboardThread = new Thread(KeyboardListener); keyboardThread.IsBackground = true; // 后台线程,主线程结束时自动退出 keyboardThread.Start(); try { // 创建相机对象,使用 using 确保资源正确释放 using (Camera camera = new Camera()) { // 相机打开后自动应用连续采集配置(Basler 提供的 helper) camera.CameraOpened += Configuration.AcquireContinuous; // 打开相机 camera.Open(); // 设置抓取缓冲区最大数量为10,提高抓取效率 camera.Parameters[PLCameraInstance.MaxNumBuffer].SetValue(10); // 关闭自动曝光,手动控制曝光时间 camera.Parameters[PLCamera.ExposureAuto].TrySetValue(PLCamera.ExposureAuto.Off); // 根据相机支持的 SFNC 版本设置曝光时间 if (camera.GetSfncVersion() < sfnc2_0_0) { // 老版本使用整数型 ExposureTimeRaw,取最小值(最短曝光) camera.Parameters[PLCamera.ExposureTimeRaw].SetValue(camera.Parameters[PLCamera.ExposureTimeRaw].GetMinimum()); } else { // 新版本支持浮点曝光时间,设置为 20ms(20000 μs) camera.Parameters[PLCamera.ExposureTime].SetValue(20000); } // ==================== 外部触发关键设置 ==================== // 选择触发类型为帧开始触发 camera.Parameters[PLCamera.TriggerSelector].SetValue("FrameStart"); // 开启触发模式 camera.Parameters[PLCamera.TriggerMode].SetValue(PLCamera.TriggerMode.On); // 设置触发信号来源为 Line4(硬件引脚) camera.Parameters[PLCamera.TriggerSource].SetValue(PLCamera.TriggerSource.Line4); // 采集模式设为连续采集(外部触发信号到来时才实际抓图) camera.Parameters[PLCamera.AcquisitionMode].SetValue(PLCamera.AcquisitionMode.Continuous); // ============================================================= // 启动数据流抓取 camera.StreamGrabber.Start(); Console.WriteLine("相机已启动外部触发模式(无限触发)。"); Console.WriteLine("Ctrl + S : 保存当前累计的所有图像到本地"); Console.WriteLine("Ctrl + Q : 清空已累计图像,重新开始累计"); Console.WriteLine("Ctrl + C 或关闭窗口 : 退出程序"); // 主循环:持续从相机抓取图像 while (true) { // 等待抓取结果,最多等待 5000ms,超时返回 null IGrabResult grabResult = camera.StreamGrabber.RetrieveResult(5000, TimeoutHandling.Return); if (grabResult != null && grabResult.GrabSucceeded) { lock (bufferLock) // 加锁保护 imageBuffer { // 缓冲区满时移除最早的一张,保持最多1000张 if (imageBuffer.Count >= 1000) { imageBuffer.RemoveAt(0); } // 第一次抓到图像时记录宽、高和像素格式(后续保存需要) if (_width == 0) { _width = grabResult.Width; _height = grabResult.Height; _pixelType = grabResult.PixelTypeValue; // 关键:获取实际像素格式(如 Mono8) } // 获取原始像素数据(byte[]) byte[] buffer = grabResult.PixelData as byte[]; if (buffer != null) { // 深拷贝一份数据存入缓冲区(防止原缓冲区被驱动复用) byte[] pixelData = new byte[buffer.Length]; Buffer.BlockCopy(buffer, 0, pixelData, 0, buffer.Length); imageBuffer.Add(pixelData); } } // 实时显示当前累计图像数量 Console.Write($"\r已累计图像: {imageBuffer.Count,4} 张"); // 释放抓取结果,归还给驱动的缓冲区 grabResult.Dispose(); } else if (grabResult != null) { // 抓取失败也需要释放资源 grabResult.Dispose(); } } } } catch (Exception e) { // 捕获异常并输出错误信息 Console.Error.WriteLine("Exception: {0}", e.Message); exitCode = 1; } finally { // 程序结束前清空缓冲区,释放内存 lock (bufferLock) { imageBuffer.Clear(); } } // 程序退出时返回退出码(正常为0,异常为1) Environment.Exit(exitCode); } /// <summary> /// 后台线程:监听键盘输入,实现 Ctrl+S 保存、Ctrl+Q 清空功能 /// </summary> static void KeyboardListener() { while (true) { // 拦截按键,不在控制台显示 ConsoleKeyInfo keyInfo = Console.ReadKey(true); // 判断是否按下了 Ctrl 组合键 if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) { if (keyInfo.Key == ConsoleKey.S) { SaveAllImages(); // Ctrl + S 保存所有图像 } else if (keyInfo.Key == ConsoleKey.Q) { ClearBuffer(); // Ctrl + Q 清空缓冲区 } } } } /// <summary> /// 将缓冲区中所有图像保存为 8-bit 灰度 TIFF 文件 /// </summary> static void SaveAllImages() { lock (bufferLock) { if (imageBuffer.Count == 0) { Console.WriteLine("\n当前没有累计的图像可保存。"); return; } // 创建保存文件夹 string folder = "CapturedImages"; if (!System.IO.Directory.Exists(folder)) System.IO.Directory.CreateDirectory(folder); Console.WriteLine($"\n正在以 TIFF 格式保存 {imageBuffer.Count} 张图像..."); int index = 0; foreach (byte[] data in imageBuffer) { string fileName = System.IO.Path.Combine(folder, $"image_{index:D4}.tif"); try { // 创建一个 8-bit 索引颜色格式的位图(灰度图) using (Bitmap bmp = new Bitmap(_width, _height, PixelFormat.Format8bppIndexed)) { // 设置标准的 256 级灰度调色板(非常重要,否则 TIFF 显示会出错) ColorPalette palette = bmp.Palette; for (int i = 0; i < 256; i++) { palette.Entries[i] = Color.FromArgb(i, i, i); } bmp.Palette = palette; // 锁定位图内存,直接将原始字节数据拷贝进去 BitmapData bmpData = bmp.LockBits( new Rectangle(0, 0, _width, _height), ImageLockMode.WriteOnly, bmp.PixelFormat); Marshal.Copy(data, 0, bmpData.Scan0, data.Length); bmp.UnlockBits(bmpData); // 保存为无压缩的 TIFF 格式(保留原始像素值) bmp.Save(fileName, ImageFormat.Tiff); } index++; } catch (Exception ex) { Console.WriteLine($"保存第 {index} 张失败: {ex.Message}"); } } Console.WriteLine($"保存完成!共 {index} 张 TIFF 图像已存入: {folder}"); } } /// <summary> /// 清空图像缓冲区 /// </summary> static void ClearBuffer() { lock (bufferLock) { imageBuffer.Clear(); Console.WriteLine("\n缓冲区已清空。"); } } } }

浙公网安备 33010602011771号