2. pylon C++ API核心对象解析
在 pylon C++ API 中,真正决定“程序如何发现相机、如何连接相机、如何取流”的并不是某一个函数,而是一组职责清晰的核心对象。理解它们之间的分工,比死记 API 名称更重要。本节围绕四个关键概念展开:DeviceInfo(设备描述)、TransportLayer(传输层)、InstantCamera(即时相机对象)以及相机枚举机制。它们共同构成了从“物理相机存在”到“C++ 对象可操作”的完整链路。
一、四个核心对象的职责边界
可以把 pylon 的相机访问过程理解为一条“发现—描述—创建—控制”的链路。物理相机通过 GenICam 传输层被枚举出来,枚举结果以 DeviceInfo 的形式呈现;开发者根据 DeviceInfo 选择目标相机,再通过相机工厂创建 InstantCamera 实例,最终由该实例完成参数配置、取流与事件处理。
DeviceInfo 是设备的“身份证”。它并不代表相机本身,而是对一台已发现设备的描述信息,典型字段包括:
GetFriendlyName():用户可读名称,例如acA1920-40gc;GetDeviceClass():设备类别,常见为BaslerGigE或BaslerUsb;GetSerialNumber():序列号,用于唯一标识;GetUserDefinedName():用户自定义名称,便于多相机场景区分;GetModelName():型号名称;GetInterfaceID():所属接口标识,用于判断相机挂在哪个网卡或 USB 控制器下。
TransportLayer 是“发现通道”。pylon 通过不同的传输层实现与不同物理接口的通信,例如 GigE 传输层、USB 传输层、Camera Link 传输层等。传输层负责在底层网络上广播发现请求、接收相机响应,并把响应封装成 DeviceInfo 列表。没有传输层,枚举就无从谈起。
InstantCamera 是“操作手柄”。它继承自 Pylon::CPylonInstantCamera 或通过 Pylon::CTlFactory 创建,封装了打开设备、配置节点、启动取流、抓取结果、注册事件回调等能力。开发者日常写的 StartGrabbing()、RetrieveResult()、StopGrabbing() 都发生在这一层。
四者关系可以用下图表示:
二、相机枚举机制的实际执行过程
枚举是访问相机的第一步。pylon 的枚举入口通常是 CTlFactory::GetInstance().EnumerateDevices(),它返回一个 DeviceInfoList_t。其内部逻辑可以概括为:
- 获取传输层工厂单例,遍历当前已注册的所有传输层;
- 对每个传输层调用其设备发现接口,例如 GigE 传输层会向本地网段发送发现广播;
- 收集各传输层返回的设备描述,统一转换为 DeviceInfo 对象;
- 将结果汇总为列表返回给调用者。
下面是一段典型的枚举与筛选代码,示例中假设环境中存在两台 GigE 相机和一台 USB 相机:
#include <pylon/PylonIncludes.h>
using namespace Pylon;
int main() {
PylonInitialize();
CTlFactory& tlFactory = CTlFactory::GetInstance();
DeviceInfoList_t devices;
tlFactory.EnumerateDevices(devices);
std::cout << "发现设备数量: " << devices.size() << std::endl;
for (const auto& di : devices) {
std::cout << "名称: " << di.GetFriendlyName()
<< " | 类别: " << di.GetDeviceClass()
<< " | 序列号: " << di.GetSerialNumber()
<< " | 接口: " << di.GetInterfaceID()
<< std::endl;
}
PylonTerminate();
return 0;
}
假设运行后输出如下(示例输出,非实测):
发现设备数量: 3
名称: acA1920-40gc | 类别: BaslerGigE | 序列号: 22001234 | 接口: 192.168.1.100
名称: acA1920-40gc | 类别: BaslerGigE | 序列号: 22005678 | 接口: 192.168.1.100
名称: acA1300-60um | 类别: BaslerUsb | 序列号: 23009876 | 接口: USB:0
从输出可以读出几个关键信息:两台 GigE 相机共享同一接口 ID,说明它们接在同一网卡下;USB 相机的接口 ID 以 USB: 开头,说明其传输层不同。这些信息在后续选择相机时非常关键。
三、从 DeviceInfo 到 InstantCamera 的创建
拿到 DeviceInfo 列表后,通常不会直接操作列表,而是根据业务规则筛选目标相机。常见筛选策略包括:按序列号精确匹配、按用户自定义名称匹配、按设备类别过滤、按接口 ID 限定网卡。筛选完成后,通过 CTlFactory::CreateDevice() 或 InstantCamera 构造函数创建设备对象。
DeviceInfoList_t filtered;
for (const auto& di : devices) {
if (di.GetDeviceClass() == "BaslerGigE") {
filtered.push_back(di);
}
}
if (filtered.empty()) {
std::cerr << "未找到 GigE 相机" << std::endl;
return -1;
}
CInstantCamera camera(tlFactory.CreateDevice(filtered[0]));
camera.Open();
std::cout << "已打开: " << camera.GetDeviceInfo().GetFriendlyName() << std::endl;
这里有一个容易混淆的点:CreateDevice() 返回的是 IPylonDevice*,而 CInstantCamera 接收该指针并接管其生命周期。因此不要手动 delete 这个指针,否则会造成重复释放。正确做法是让 InstantCamera 对象在析构时自动管理。
四、InstantCamera 的关键能力与生命周期
InstantCamera 之所以称为“Instant”,是因为它把取流过程封装成了接近“即时可用”的接口。其核心生命周期包括:
- 创建:通过 DeviceInfo 创建,此时尚未占用设备;
- 打开:
Open()后设备被独占,参数节点可读写; - 配置:通过
GetNodeMap()访问 GenICam 节点,设置曝光、增益、触发模式等; - 取流:
StartGrabbing()启动采集,RetrieveResult()获取CGrabResultPtr; - 停止与关闭:
StopGrabbing()、Close(),释放设备。
需要特别注意的是,Open() 之后才能访问相机节点。如果在未打开状态下调用 GetNodeMap() 并尝试写入,通常会抛出 LogicalErrorException。同样,StartGrabbing() 之前必须确保触发模式与预期一致,否则可能出现“启动了但一帧都收不到”的情况。
五、传输层注册与枚举失败的排查方向
枚举不到相机时,问题往往不在代码,而在传输层或网络配置。可按以下顺序排查:
| 现象 | 可能原因 | 检查方式 |
|---|---|---|
| 枚举列表为空 | 传输层未注册或网卡未启用 | 检查 pylon 安装时是否勾选对应传输层;确认网卡已启用 |
| 只发现部分相机 | 跨网段、防火墙拦截广播 | 确认相机与主机在同一子网;临时关闭防火墙测试 |
| DeviceInfo 存在但 Open 失败 | 设备被其他进程占用 | 关闭 pylon Viewer 或其他采集程序后重试 |
| USB 相机枚举不稳定 | USB 控制器带宽不足或供电不足 | 更换 USB 端口,避免使用无源 Hub |
GetInterfaceID() 是否与目标网卡一致,必要时在 pylon IP Configurator 中为相机分配同网段 IP。
六、小结与工程建议
DeviceInfo 负责“描述设备”,TransportLayer 负责“发现设备”,InstantCamera 负责“控制设备”,而枚举机制是把三者串起来的入口。实际工程中,建议把枚举与筛选封装成独立函数,返回目标 DeviceInfo;把 InstantCamera 的创建、打开、配置、取流封装成 RAII 风格的类,确保异常路径下也能正确释放设备。这样既能避免资源泄漏,也能让多相机程序的结构更清晰。