3. 相机对象生命周期与异常处理

在 Basler pylon SDK 的 C++ 接口中,CInstantCamera 是应用层最常用的相机对象。它并不是一个简单的“句柄”,而是一个把设备、传输层、抓取引擎、参数节点树和事件回调封装在一起的状态机。理解它的生命周期,是构建稳定采集程序的基础。很多现场问题——程序退出时崩溃、拔掉网线后卡死、重连后取不到图——根源都不在算法,而在对象创建、打开、关闭、销毁的顺序,以及异常没有被正确区分和处理。

一、CInstantCamera 的构造与销毁语义

CInstantCamera 的构造方式决定了它绑定哪台设备。常见有三种:

  • 默认构造 CInstantCamera():不绑定任何设备,后续通过 Attach() 挂载。
  • 按设备信息构造 CInstantCamera(CTlFactory::GetInstance().CreateDevice(info)):直接绑定枚举到的某台相机。
  • 按序列号构造 CInstantCamera(CTlFactory::GetInstance().CreateFirstDevice()):绑定第一台可用设备,适合单相机场景。

构造本身通常不会打开设备,只是建立对象与设备描述之间的关联。真正与硬件建立通信发生在 Open()。因此构造失败一般意味着设备信息无效或传输层未初始化,而 Open() 失败才代表物理连接、权限或占用问题。

销毁时,CInstantCamera 的析构函数会自动调用 Close(),并释放内部资源。但依赖析构做清理有两个风险:一是析构发生在异常栈展开过程中,若此时再抛异常会导致程序终止;二是如果抓取线程仍在运行,析构可能等待超时。因此工程上应显式管理生命周期,而不是把清理全部交给析构。

二、Open/Close 的语义与状态约束

Open() 的语义是:向传输层申请设备、建立控制通道、加载 GenICam 节点树,使相机进入可配置状态。此时可以读写 Width、Height、ExposureTime、Gain 等参数,但还没有开始取流。

Close() 的语义是:停止抓取、释放传输层资源、断开控制通道。关闭后对象仍存在,可以再次 Open(),但之前设置的参数是否保留取决于相机是否断电以及是否使用了用户参数集(User Set)。

关键约束是:StartGrabbing() 之前必须已 Open();Close() 之前必须先 StopGrabbing()。如果直接对未打开的相机调用 StartGrabbing(),pylon 会抛出 GenericException,提示设备未打开。反过来,在抓取过程中直接 Close(),不同版本行为不一致,可能阻塞或抛异常。稳妥的顺序是:

camera.Open();
// 配置参数
camera.StartGrabbing(GrabStrategy_LatestImageOnly);
// 取流循环
camera.StopGrabbing();
camera.Close();

三、设备移除与重连

工业现场最常见的中断是网线松动、交换机断电或相机掉电。pylon 通过设备移除事件通知应用:可以注册 RegisterRemovalCallback(),也可以使用 RegisterConfiguration() 配合 GrabLoop 检测。当设备被移除时,正在进行的 RetrieveResult() 会返回错误或抛出异常,抓取循环必须能够退出。

重连不能简单地在原对象上反复 Open()。正确做法是:先 StopGrabbing(),再 Close(),然后销毁旧对象,重新枚举设备,用新的设备信息构造新的 CInstantCamera。因为设备移除后,原对象内部的传输层句柄已经失效,继续使用会持续报错。

重连策略建议采用退避重试:首次失败后等待 500 ms,之后 1 s、2 s、4 s,上限 10 s,避免在相机未恢复时高频枚举占用 CPU。同时应记录重连次数和最后一次错误码,便于现场判断是网络问题还是相机故障。

四、异常类型与捕获策略

pylon 的异常体系以 GenericException 为基类,常见派生类包括:

  • GenericException:通用错误,如设备未打开、参数越界、节点不可写。
  • TimeoutException:取流或等待事件超时,通常由 RetrieveResult 的超时参数触发。
  • LogicalException:调用顺序错误,如重复打开。
  • RuntimeException:运行时错误,如传输层中断。

捕获时应先捕获派生类,再捕获基类,避免 TimeoutException 被 GenericException 提前吞掉。典型结构:

try {
    const CGrabResultPtr& ptr = camera.RetrieveResult(5000, TimeoutHandling_ThrowException);
    if (ptr->GrabSucceeded()) {
        // 处理图像
    }
} catch (const TimeoutException& e) {
    // 超时:可能是无触发、曝光过长或设备移除
} catch (const GenericException& e) {
    // 其他 pylon 错误
    std::cerr << e.GetDescription() << std::endl;
}

注意 RetrieveResult 的超时参数与 TimeoutHandling 配合使用:TimeoutHandling_ThrowException 会在超时时抛 TimeoutException;TimeoutHandling_Return 则返回空指针。前者适合需要明确区分超时与其他错误的场景,后者适合轮询式循环。

五、健壮连接与断开模块的设计

把上述内容封装成一个可复用的模块,核心是三个方法:Connect()、Disconnect()、IsConnected()。内部维护一个 PylonAutoInitTerm 或手动 PylonInitialize()/PylonTerminate(),并保证引用计数正确。

下面给出一个可直接参考的模块骨架,参数为示例值,实际使用需按现场调整:

class CameraModule {
public:
    bool Connect(const std::string& serial, int timeoutMs = 5000) {
        try {
            PylonInitialize();
            CDeviceInfo info;
            info.SetSerialNumber(serial.c_str());
            camera_ = new CInstantCamera(CTlFactory::GetInstance().CreateDevice(info));
            camera_->Open();
            camera_->StartGrabbing(GrabStrategy_LatestImageOnly);
            connected_ = true;
            return true;
        } catch (const GenericException& e) {
            lastError_ = e.GetDescription();
            Cleanup();
            return false;
        }
    }

    void Disconnect() {
        if (camera_) {
            try {
                if (camera_->IsGrabbing()) camera_->StopGrabbing();
                if (camera_->IsOpen()) camera_->Close();
            } catch (...) {}
            delete camera_;
            camera_ = nullptr;
        }
        PylonTerminate();
        connected_ = false;
    }

    bool IsConnected() const { return connected_; }
    std::string LastError() const { return lastError_; }

private:
    CInstantCamera* camera_ = nullptr;
    bool connected_ = false;
    std::string lastError_;
};

该模块的关键点:Connect() 中任何一步失败都调用 Cleanup() 释放已分配资源,避免半开状态;Disconnect() 中先停抓取再关闭,且用 try/catch 包裹,保证即使设备已移除也能安全退出;IsConnected() 只反映逻辑状态,不代替实际探测。

CInstantCamera 生命周期与异常处理路径 构造对象 Open() 配置参数 StartGrabbing() RetrieveResult() StopGrabbing() Close() 销毁对象 异常捕获分支 TimeoutException / GenericException 设备移除/重连 退避重试 0.5s→10s 顺序约束:Open → 配置 → StartGrabbing → RetrieveResult → StopGrabbing → Close → 销毁 异常约束:先捕获 TimeoutException,再捕获 GenericException;重连必须重建对象

六、工程实践中的几个判断依据

判断连接是否真正健康,不能只看 IsConnected() 标志。建议在取流循环中统计连续超时次数:若连续 3 次 TimeoutException,且相机 IsOpen() 仍为真,应主动触发重连流程。若 GenericException 的描述中包含 “device removed” 或 “no device”,则直接进入重连,不必等待超时。

另外,PylonInitialize() 与 PylonTerminate() 必须成对出现。多次初始化需要对应多次终止,否则传输层资源不会释放。在多相机场景中,建议在应用启动时初始化一次,退出时终止一次,而不是每台相机各自初始化。

调试阶段可打开 pylon 的日志输出,观察设备打开、抓取启动、设备移除等事件的时间点。日志中 Grab loop 的退出原因,往往比异常描述更能说明问题。
不要在 catch 块中直接调用 delete camera_ 后继续使用该指针。应先置空,再在统一清理函数中释放,避免二次释放或悬空访问。