Basler Pylon .NET SDK 外部触发(External Trigger)采集示例

C# 工业相机开发:实战 Basler Pylon 外部触发与高速缓存

在工业自动化视觉检测中,外部触发(External Trigger) 是最常用的模式。它能确保相机快门与传送带上的工件同步。但在高频触发下,磁盘 IO 往往跟不上采集速度。

本文将解析一个基于 Basler Pylon SDK 的 C# 示例,重点讲解如何实现异步触发、内存深拷贝缓存以及 TIFF 格式的高质量保存


核心设计思路

本示例采用了生产-消费的分离思想:

  1. 采集层:相机监听外部电平信号(Line 4),一旦触发即压入内存。

  2. 缓存层:使用 List<byte[]> 进行深拷贝存储,避免缓冲区被 SDK 自动覆盖。

  3. 持久化层:通过控制台交互,在空闲时段将内存数据批量写入磁盘。


 

源码如下

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缓冲区已清空。");
            }
        }
    }
}
View Code

 

posted @ 2025-12-25 16:21  阿坦  阅读(8)  评论(0)    收藏  举报