3. Basler相机SDK与采集配置

在精密测量系统中,相机能否稳定、可重复地输出图像,直接决定后续标定与测量的可信度。Basler 相机通过 pylon SDK 对外提供统一的编程接口,把“发现设备—配置参数—取流—回调处理—异常恢复”串成一条可工程化的采集链路。本节围绕 pylon API 的实际使用展开,重点说明相机枚举、参数设置、流取图、事件回调以及如何搭建一个可长期运行的采集框架。

pylon API 的层次与选型

pylon SDK 提供 C++、C#、Python 等多语言绑定,底层统一封装 GenICam 标准。工程中常用的对象层次为:PylonInitialize() 初始化运行时,CTlFactory 负责枚举与创建相机,InstantCamera 或其派生类承载参数与取流,CGrabResultPtr 承载一帧图像及其元数据。对于 Basler 面阵相机,通常使用 CBaslerUniversalInstantCamera,它同时兼容 ace、dart、boost 等系列,并允许通过 GetNodeMap() 访问 GenICam 节点。

选型上,若只需单相机同步采集,InstantCamera 足够;若需要多相机、硬件触发或复杂事件处理,建议直接使用 CBaslerUniversalInstantCamera 配合 PylonDevice 层操作。Python 侧对应 pypylon,接口命名与 C++ 基本一致,便于快速验证。

相机枚举与设备选择

枚举是采集的第一步。通过 CTlFactory::GetInstance().EnumerateDevices() 可获得设备列表,每个 CDeviceInfo 包含型号、序列号、用户自定义名称、接口类型(GigE、USB3、Camera Link)等信息。工程中不应依赖枚举顺序,而应通过序列号或用户自定义名称定位目标相机,避免多机环境下取错设备。

from pypylon import pylon

tl_factory = pylon.TlFactory.GetInstance()
devices = tl_factory.EnumerateDevices()
for dev in devices:
    print(dev.GetModelName(), dev.GetSerialNumber(),
          dev.GetUserDefinedName(), dev.GetDeviceClass())

camera = pylon.InstantCamera(tl_factory.CreateDevice(devices[0]))
camera.Open()

若相机已被其他进程占用,Open() 会抛出异常。此时应检查是否残留进程,或使用 camera.IsOpen() 判断状态。对于 GigE 相机,还需确认网卡巨帧、IP 配置与带宽是否满足要求。

参数设置:从像素格式到曝光

参数设置的核心是让图像质量与测量需求匹配。常用节点包括:PixelFormat(Mono8、Mono12、BayerRG8 等)、Width/Height、OffsetX/OffsetY、ExposureTime、Gain、AcquisitionFrameRate、TriggerMode 与 TriggerSource。设置时应遵循“先关流、后改参、再开流”的顺序,避免运行中修改导致参数不生效或丢帧。

camera.PixelFormat.SetValue("Mono8")
camera.Width.SetValue(1920)
camera.Height.SetValue(1080)
camera.ExposureTime.SetValue(8000.0)   # 单位 us
camera.Gain.SetValue(2.0)              # 单位 dB
camera.AcquisitionFrameRateEnable.SetValue(True)
camera.AcquisitionFrameRate.SetValue(30.0)
camera.TriggerMode.SetValue("Off")     # 自由运行

参数设置后应回读校验,例如 camera.ExposureTime.GetValue(),确认实际生效值。对于需要高精度测量的场景,建议固定曝光与增益,避免自动曝光引入亮度波动;若必须使用自动曝光,应限制其目标灰度范围并记录每次变化。

流取图:连续采集与单帧抓取

pylon 提供 StartGrabbing() 与 RetrieveResult() 组合实现取流。连续采集时,RetrieveResult 会阻塞等待一帧,超时参数需根据帧率设置。单帧抓取可使用 GrabOne(),适合调试或低频测量。

camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly)
while camera.IsGrabbing():
    grab_result = camera.RetrieveResult(5000, pylon.TimeoutHandling_ThrowException)
    if grab_result.GrabSucceeded():
        img = grab_result.Array
        # 处理 img
    grab_result.Release()

GrabStrategy_LatestImageOnly 只保留最新帧,适合实时显示;GrabStrategy_OneByOne 按顺序取帧,适合需要完整序列的分析。取流过程中应关注 GrabResult 的 GetTimeStamp()、GetFrameNumber() 与 GetBlockID(),用于判断丢帧与时间对齐。

事件回调:把采集与处理解耦

在工程化框架中,不建议在主循环里做重处理。pylon 支持注册事件回调,例如 RegisterImageEventHandler 或 RegisterConfigurationEventHandler,在图像到达、相机打开、关闭、丢帧等时刻触发。回调运行在 SDK 内部线程,处理应尽量轻量,只做入队或标记,重处理交给独立线程。

class ImageHandler(pylon.ImageEventHandler):
    def OnImageGrabbed(self, camera, grab_result):
        if grab_result.GrabSucceeded():
            queue.put(grab_result.Array.copy())
        grab_result.Release()

camera.RegisterImageEventHandler(ImageHandler(),
                                 pylon.RegistrationMode_Append,
                                 pylon.Cleanup_Delete)

回调中必须调用 Release(),否则缓冲区耗尽会导致采集停止。若处理耗时较长,应使用有界队列并设置丢弃策略,避免内存无限增长。

工程化采集框架搭建

一个可长期运行的采集框架通常包含:设备管理层(枚举、打开、参数校验)、采集层(取流、回调、缓冲)、处理层(队列、算法)、监控层(帧率、丢帧、温度、异常日志)与恢复层(断线重连、参数重下发)。建议将相机配置写入 YAML 或 JSON,启动时加载并逐项校验,失败则明确报错而非静默使用默认值。

Basler pylon 工程化采集框架数据流 设备管理层 EnumerateDevices 序列号/名称匹配 Open / 参数校验 采集层 StartGrabbing RetrieveResult GrabResult 元数据 事件回调层 OnImageGrabbed 轻量入队 Release 缓冲 处理层 有界队列 算法线程 结果输出 监控与恢复层 帧率 / 丢帧计数 / 时间戳连续性 / 相机温度 / 异常日志 断线重连 → 重新枚举 → 参数重下发 → 恢复取流 配置输入(YAML / JSON) 相机序列号 · PixelFormat · Width/Height · ExposureTime · Gain · FrameRate · TriggerMode 启动时逐项校验,失败即报错;运行中禁止随意改参,需先 StopGrabbing

框架落地时,建议把“参数校验”做成独立函数:读取配置后逐项 SetValue 并 GetValue 回读,记录实际生效值。若某项不支持,应明确提示而非忽略。采集线程与处理线程通过有界队列连接,队列满时按策略丢弃最旧帧并累加丢帧计数,监控层定期输出统计。断线恢复时,先 Close() 再重新枚举,避免句柄泄漏。

调试阶段可先用 GrabOne() 验证参数与图像质量,再切换到连续采集;正式运行前应连续采集 10 分钟以上,观察帧率稳定性与丢帧计数是否为零。
回调函数中不要执行耗时算法或阻塞操作,否则会拖慢 SDK 内部线程,导致丢帧甚至采集停止。所有重处理必须转移到独立线程。

综上,pylon API 的使用并不复杂,难点在于把枚举、参数、取流、回调与监控组织成可恢复、可观测的工程结构。只要坚持“配置显式化、回调轻量化、监控常态化、恢复自动化”,就能为后续标定与精密测量提供稳定可靠的图像输入。