ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

臻识相机OpenSDK集成实战:从车牌识别到MQTT上报

臻识相机OpenSDK集成实战:从车牌识别到MQTT上报 简介面向需要基于成都臻识相机开发车牌识别系统的嵌入式或应用工程师这份R3版开放SDK开发包集成了车牌识别相关算法适用于智能交通、停车出入口、安防监控等场景可帮助开发者跳过底层硬件适配直接聚焦业务功能。资源包共14个文件整体约865KB以C/C头文件、动态链接库为核心辅以编译脚本、帮助文档和可运行的示例程序覆盖接口声明、算法实现、编译构建与调用演示四大环节结构清晰、便于按需查阅。目前已有2736人学习下载适合具备一定Linux或嵌入式开发经验、希望快速接入车牌识别能力的开发者。借助包内示例和文档可以快速理解相机初始化、图像抓取、车牌识别及结果回调等关键流程同时目录将接口、文档、示例分开组织方便后续在此基础上二次开发和集成。 停车场项目的开发群里经常有人问“臻识相机的 SDK 怎么跑通”“有没有现成的对接例子”。我最近正好拿到一个包名字就叫成都臻识相机 opensdk_r3_20210414.rar一看就是 2021 年 4 月 14 日构建的 r3 版本。这个包看起来不起眼但对做车场出入口、门禁道闸、车牌识别自助缴费这些场景的开发者来说它就是整套业务的核心入口。成都臻识的车牌识别相机在停车场项目里出现频率很高跟海康、大华并列识别触发、车牌回调、图片抓取全靠这一套 OpenSDK 来完成。这篇文章不聊理论只讲我从解压这个 RAR 到真正把识别结果推上云端 MQTT 的全过程包括工程配置、回调解析、线程处理、常见的坑给正在接触臻识相机的朋友一份可以直接照着做的参考。1. 打开 opensdk_r3_20210414.rar先看包内布局拿到这类 OEM SDK 压缩包我的习惯是不要急着解压扔进工程先把包内目录完整看一遍。r3_20210414这个命名其实是两层含义r3是 SDK 的接口演进版本20210414是构建日期。不同批次相机固件可能对应不同 SDK 版本解压后先翻文档总没错。正常解压后包内大致是这么几个目录doc/SDK 使用手册和接口变更说明里面有 PDF 或 CHM这是必读文件。include/C/C 头文件定义所有导出接口和结构体。lib/动态库和导入库一般分Win32和x64两个子目录。demo/官方示例工程常见是 C 版也有部分厂家会附带 C# 示例。tools/设备搜索工具、固件升级程序、IP 修改工具等。很多开发者的第一步就栽在“库文件混用”上。lib目录下的 x86 库不能给 x64 工程用尤其是 C# 程序如果编译成了AnyCPU在 64 位系统上会默认以 64 位运行这时加载 32 位原生 dll 会直接抛异常。建议项目一开始就锁定目标平台C 明确选x64C# 明确选x64发布不要用AnyCPU。1.1 工程配置里的三个关键点新建一个 Win32 控制台工程或者空 C 工程后需要重点配置三处附加包含目录指到include/目录。附加库目录指到lib/x64目录。附加依赖项把 SDK 提供的.lib文件名填进去。例如// VS 工程配置 // C/C - 常规 - 附加包含目录 // D:\third_party\vzenith_sdk\include // 链接器 - 常规 - 附加库目录 // D:\third_party\vzenith_sdk\lib\x64 // 链接器 - 输入 - 附加依赖项 // VZ_LPR.lib链接器配置完成后还要记得把VZ_LPR.dll拷贝到 exe 的输出目录或者放进PATH环境变量里。不少人在这里把 dll 放错位置运行时提示“找不到 VZ_LPR.dll”折腾半天。1.2 先把 demo 编译通过再动自己的代码SDK 包里往往带一个能直接编译运行的 demo。我的建议很直接先原封不动把官方 demo 编译通过连上相机跑出识别结果然后再去读代码、改代码。跳过 demo 直接写业务出了问题你分不清是自己代码的问题还是 SDK 的问题排查成本会高很多。demo 里通常已经写好了“搜索设备、登录、注册回调、开始识别”这条主线你只需要从这条主线里拆出自己需要的部分。下面就从设备接入开始讲链路。2. 设备接入局域网搜索、登录与回调注册臻识相机的接入方式跟很多网络相机类似SDK 会提供设备发现接口通过 UDP 广播在局域网里找设备。要注意相机与运行 SDK 的 PC 必须在同一个网段。如果你用手提电脑直连相机先把电脑网卡 IP 改成和相机同网段例如相机默认 IP 是192.168.1.100网卡就配192.168.1.50子网掩码255.255.255.0。2.1 搜索设备时返回 0 台的问题我排查过很多次“设备搜索不到”的现场大部分都不是代码问题而是这三类Windows 防火墙默认拦截了 UDP 广播包SDK 收不到相机的回应。电脑开了多个网卡搜索接口绑定到了错误的网卡上广播发不出去。相机和电脑不在同一网段中间隔了路由器或者 VLAN。在用代码搜索之前建议先用tools/目录里的设备搜索工具验证网络是否通。工具能看到设备代码看不到那基本就是接口用法或者网卡绑定问题。如果工具也看不到先检查物理链路和防火墙。搜索接口的用法大同小异核心逻辑是拉取设备列表// 假设 SDK 导出接口中有一个搜索函数 VZ_NET_DEVICE_INFO devices[16]; int count 0; int ret VZ_SearchDevices(devices, count); if (ret 0 count 0) { for (int i 0; i count; i) { printf(ip%s port%d sn%s\n, devices[i].ip, devices[i].port, devices[i].serialNo); } }拿到设备 IP 后下一步就是登录并注册回调。2.2 登录成功不代表立刻有数据登录接口一般是传入 IP、端口、用户名、密码返回一个设备句柄后续所有操作都通过这个句柄来调用。登录成功后别急着等数据还需要做两件事注册抓拍结果回调和开启识别功能。VZ_HANDLE handle NULL; VZ_Login(192.168.1.100, 8080, admin, admin123, handle); // 注册识别结果回调 VZ_SetPlateCallBack(handle, onPlateResult, NULL); // 开启抓拍识别 VZ_StartCapture(handle);这里我要重点解释一下“回调”的触发逻辑。臻识相机不是像普通监控相机那样把每一帧视频都推到 SDK 客户端而是在相机内部完成车辆检测和车牌识别后只把“有效结果”推给上层也就是常见的“事件型”数据流。所谓有效结果可能是压线触发、视频检测到车、或者外部信号触发。因此你注册回调之后如果现场没车、没有触发回调里一直收不到数据是正常的不是程序卡死。3. 车牌识别结果结构体从内存里抠出每辆车的关键数据当回调真正触发时SDK 会给你一个结果结构体指针。这个结构体里包含车牌号码、车牌颜色、识别置信度、车辆类型、抓拍时间戳以及两张图片全景图和车牌特写图。以官方头文件常见定义为例结构体大致长这样typedef struct { char plate[32]; // 车牌号码可能带汉字 int plateColor; // 车牌颜色0-蓝牌 1-黄牌 2-绿牌 3-黑牌 4-白牌 5-其他 int confidence; // 识别置信度范围 0-100 int vehicleType; // 车辆类型小型车、大型车等 unsigned long long timeStamp; // 抓拍时间戳单位毫秒 unsigned char* fullImage; // 全景图 JPEG 数据 int fullImageLen; // 全景图长度 unsigned char* plateImage; // 车牌特写图 JPEG 数据 int plateImageLen; // 车牌特写图长度 } VZ_PLATE_RESULT;3.1 车牌字符串的编码坑在中文场景里车牌号一定是带汉字的比如“京A·12345”。SDK 返回的plate字符串通常使用的是 GBK 或 GB2312 编码。如果你的业务系统需要将结果转成 UTF-8 再通过 JSON 上传不做转码就会变成乱码。在 Windows 上我一般封装一个小函数来做 GBK 到 UTF-8 的转换std::string gbkToUtf8(const char* gbkStr) { int wLen MultiByteToWideChar(CP_ACP, 0, gbkStr, -1, NULL, 0); wchar_t* wBuf new wchar_t[wLen]; MultiByteToWideChar(CP_ACP, 0, gbkStr, -1, wBuf, wLen); int uLen WideCharToMultiByte(CP_UTF8, 0, wBuf, -1, NULL, 0, NULL, NULL); char* uBuf new char[uLen]; WideCharToMultiByte(CP_UTF8, 0, wBuf, -1, uBuf, uLen, NULL, NULL); std::string utf8Str(uBuf); delete[] wBuf; delete[] uBuf; return utf8Str; }车牌颜色字段的枚举值在不同 SDK 版本里可能不一样有版本用 0 代表蓝牌也有版本用 1 代表蓝牌。接新包时先打印一条日志确认实际枚举值再写判断逻辑。3.2 两张图片的保存方法fullImage和plateImage指向的内存是 JPEG 数据的起始地址长度由对应 Len 字段给出。保存时直接用二进制方式写入文件即可void saveImage(const char* path, unsigned char* data, int len) { FILE* fp fopen(path, wb); if (fp) { fwrite(data, 1, len, fp); fclose(fp); } }我见过不少人把图片数据当作字符串处理在末尾自动补\0这是错误思路。JPEG 是二进制文件里面可能包含0x00字节必须按长度写文件。4. 回调线程与跨线程处理别在回调函数里做耗时操作这是整个集成过程中最影响稳定性的一个点。SDK 的回调是在它自己创建的接收线程里触发的。如果你直接在回调函数里写文件、发 MQTT、查数据库一旦这些操作超过几十毫秒SDK 的接收缓冲区就可能积压导致后续识别结果被丢弃严重时还会阻塞网络接收线程让相机与主机的连接断开。我通常的做法是在回调函数里只做一件事——把结果对象拷贝一份塞进一个线程安全队列立刻返回。另外起一个工作线程从队列里取数据再慢慢处理。4.1 回调里必须深拷贝特别注意结构体里的fullImage和plateImage是指针字段。如果只做浅拷贝只是复制了指针地址回调返回后 SDK 可能会释放或复用这块内存导致悬垂指针。所以必须把图片数据单独malloc一块内存再memcpy完整复制过来。std::mutex g_mtx; std::queueResultData* g_queue; void __stdcall onPlateResult(VZ_PLATE_RESULT* result, void* userData) { if (!result) return; ResultData* data new ResultData(); memset(data, 0, sizeof(ResultData)); // 拷贝基础字段 memcpy(data-plate, result-plate, sizeof(result-plate)); >void workerThread() { while (true) { ResultData* data nullptr; { std::lock_guardstd::mutex lock(g_mtx); if (g_queue.empty()) { continue; } data g_queue.front(); g_queue.pop(); } if (data) { // 这里写文件、发 MQTT、打印日志都可以 processPlateData(data); delete[]>#include mqtt/async_client.h const std::string SERVER_ADDRESS tcp://192.168.1.100:1883; const std::string CLIENT_ID parking_gateway_1; const std::string TOPIC parking/lpr/events; void publishPlate(const std::string plate, int color, unsigned long long ts, const std::string imageBase64) { mqtt::async_client cli(SERVER_ADDRESS, CLIENT_ID); mqtt::connect_options connOpts; connOpts.set_clean_session(false); auto tok cli.connect(connOpts); tok-wait(); std::string payload { \plate\:\ plate \, \plateColor\: std::to_string(color) , \timestamp\: std::to_string(ts) , \image\:\ imageBase64 \ }; mqtt::message_ptr msg mqtt::message::create(TOPIC, payload); msg-set_qos(1); cli.publish(msg)-wait(); cli.disconnect()-wait(); }这里我特意把图片转成了 Base64 字符串。如果图片比较大或者是高清全景图建议把图片存到本地或对象存储MQTT 消息里只带图片 URL。否则每个过车事件都传大图Broker 和带宽都会很吃力。5.3 接 MQTT 前先做一张表对齐字段很多时候你在消息里定义的字段和后端同学预期的字段对不上等联调时才发现。我整理对接表时通常会这样设计字段名类型说明deviceIdstring设备 SN 或自定义设备编号platestring车牌号码UTF-8plateColorint车牌颜色枚举confidenceint置信度 0-100timestamplong抓拍时间戳毫秒imageUrlstring全景图地址plateImageUrlstring车牌小图地址这个表在后端对接、数据库设计、报表统计时都可以直接使用建议一开始就固定下来避免后期返工。6. 集成阶段最常翻车的几个问题最后把我这段时间遇到的高频问题按排查顺序列出来遇到问题可以照着走一遍。6.1 搜索不到设备先查网络再查代码排查顺序是设备工具能否发现相机。不能就是网络不通检查网线、IP、防火墙。能就是代码问题重点看搜索接口绑定的网卡。一些 SDK 会在初始化时读取网卡配置如果机器有虚拟机网卡可能绑定到虚拟网卡上导致广播发不出去。进一步说确定相机 IP 合法后用 telnet 测一下端口telnet 192.168.1.100 8080端口不通多半是相机固件开启了防火墙或者设备处于异常状态。6.2 登录失败且没有明确报错码第一种情况是 SDK 与固件版本不匹配。r3版本的 SDK 可能不支持某批老固件设备的登录逻辑。遇到这种情况优先联系设备厂商售后确认固件和 SDK 的对应关系而不是反复试密码。第二种情况是端口填错SDK 默认端口不一定是相机 Web 后台的端口要查看头文件里定义的默认端口号。6.3 有车通过但回调不触发先确认是否开启了识别开关比如VZ_StartCapture是否调用某些相机还用“启用识别”和“启用抓拍”两个开关分别控制。再确认相机的触发模式。相机通常有“视频触发”“线圈触发”“外部信号触发”几种模式。如果现场只装了视频触发但相机配置成了线圈触发而线圈又没有接线回调永远不触发。这个可以在相机 Web 后台查看实时视频流观察画面上是否有识别框。有识别框但回调不到 PC 端再检查 SDK 侧。6.4 图片数据花屏或程序崩溃大概率是内存拷贝长度错误。有人用strlen(buf)来获取 JPEG 长度而不是用 SDK 返回的len字段这是典型的错误。JPEG 是二进制流长度必须由字段明确给出不能依赖字符串函数。6.5 同一个车牌被连续上报多次视频触发模式下相机可能在车辆驶入到驶离的几秒内连续触发多次识别。业务上有时候需要“一条过车记录”就一定要做去重。我常用的策略是以车牌号码为主键配合时间窗口。比如 5 秒内同一车牌只保留第一条识别结果其余丢弃。如果现场有地感线圈还要以线圈触发对应的事件为准。下面是简化版去重逻辑// 记录每个车牌最近一次上报时间 std::mapstd::string, unsigned long long lastReportMap; bool shouldReport(const std::string plate, unsigned long long ts) { auto it lastReportMap.find(plate); if (it ! lastReportMap.end() ts - it-second 5000) { return false; } lastReportMap[plate] ts; return true; }去重阈值要根据实际场景调整出入口的位置比较窄车辆通过时间短阈值可以短一点如果在露天停车场车辆在画面内停留时间长阈值需要更灵活否则可能漏掉同一辆车进场的第二次识别。最后再说一个我的习惯每次拿到新版本 SDK我都先不直接替换到正式工程而是在demo目录里做一次最小复现把“登录、回调、取图、上报”这条主链路重新跑通再切换正式程序。这个过程一般只花半小时却能避免很多因为接口变更导致的低级问题。臻识这套 SDK 本身不算复杂核心思路就是“搜索设备、注册回调、消费队列、上云转发”把每一步都做稳停车场的车牌识别对接也就没那么玄乎了。本文还有配套的精品资源点击获取
返回列表