ARTICLE DETAIL

资讯详情

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

相机SDK与Halcon混合编程:从取流到HObject的工程化实践

相机SDK与Halcon混合编程:从取流到HObject的工程化实践

简介:面向机器视觉开发者的海康相机SDK二次开发与Halcon混合编程学习包,围绕相机控制、图像采集、格式转换与Halcon算子处理等核心环节,提供可落地的C++工程参考与配套说明,适合具备C++和Qt基础、希望把工业相机接入视觉流程的中级开发者。压缩包共204个文件,约12.46MB,主体由118个头文件、48个hh文件、20个lib库及少量cpp源码构成,配合sln/vcxproj工程文件、ui/qrc界面资源与1份pdf说明手册,便于梳理SDK接口调用、工程配置和混合编程的目录组织。已有3024人学习。读者可从中获得海康SDK与Halcon协同开发的代码骨架、库文件依赖关系、Qt界面接入参考以及说明文档支撑,用于理解相机采图到视觉处理的完整链路,并为质量检测、定位追踪等场景的开发提供可复用的工程范例与排错线索。

1. 项目定位:从「两边都能跑」到「合起来能判」的那段接缝

产线调试最尴尬的一幕,是相机 Demo 里能稳定出图,Halcon 单独跑算子也正常,可两边一连起来就花屏、内存一路涨到几个 G 然后进程没了。这套相机 SDK 开发 + Halcon 混合编程的代码与说明手册,解决的正是这段「两边都对、合起来就错」的接缝问题:它把设备枚举、参数下发、回调取流、像素格式转换、HObject 构造、算子流程调用、资源释放串成一条可以照着抄的链路。

适合的人群很明确——已经会用厂商 SDK 单拍一张图、也跑过几个 Halcon 例程,但卡在工程化这一步的开发者。手册里没有算法科普,讲的都是句柄什么时候关、驱动 buffer 什么时候能还、hdev 里的阈值怎么从外部传进去这类落地细节。

2. 工程骨架拆解:三层目录、x64 编译与运行时依赖清单

拿到这类资源,第一件事不是读代码,是先看它怎么分层。混合编程项目最常见的翻车姿势,是把相机 SDK 的头文件直接 include 进算法文件里,写到最后发现换一个相机型号要改几十处。手册给的骨架是按依赖方向切的,跟具体算法无关。

2.1 三层结构与依赖方向的硬约束

项目把代码分成三块,依赖只能是单向的:

层职责允许依赖绝对不做的事
device 层相机句柄、取流、回调、buffer 生命周期厂商 SDK不 include Halcon 头文件
vision 层HObject 构造、算子调用、hdev 加载、结果结构体Halcon不知道相机品牌和句柄类型
app 层工位流程、判定逻辑、PLC/上位机通信、UIdevice + vision不直接写 SDK API

这个约束的价值在换型时体现得最明显。device 层向上暴露的是自洽的接口,句柄被void*遮住,上层根本不知道底下是哪家的相机:

// include/cam_device.h —— device 层对外的唯一门面 class CamDevice { public: bool OpenByIndex(int index); // 按枚举序号打开,不暴露厂商设备结构体 bool SetFloat(const std::string& key, double value); // 曝光/增益/帧率统一走这里 bool SetEnum(const std::string& key, const std::string& value); bool Start(); // 注册回调并开始取流 bool Stop(); void Close(); // 停流 -> 注销回调 -> 关句柄,顺序不能反 // 回调签名固定为「裸指针 + 宽高 + 帧号」,不让厂商类型漏到上层 using FrameCb = std::function<void(const uint8_t* data, int w, int h, int frameId, uint64_t ts)>; void SetCallback(FrameCb cb); private: void* handle_ = nullptr; // 厂商句柄,仅本层可见 FrameCb cb_; std::atomic<bool> grabbing_{false}; };

SetFloat/SetEnum这种键值式接口是刻意的设计:不同厂商的参数名不一样,但语义都是曝光、增益、触发源,把名称映射压在本层内部,业务层写的代码就能跨型号复用。Close()里注释那句顺序也不是摆设,先停流再注销回调最后关句柄,反过来做在多数 SDK 上会触发回调访问已释放内存。

2.2 目录与文件清单

手册配套的代码目录大致是这么铺开的,对照着看能快速定位:

project/ ├─ include/ │ ├─ cam_device.h # 相机门面接口 │ ├─ frame_queue.h # 无锁/有界帧队列 │ └─ vision_tool.h # Halcon 工具封装 ├─ src/ │ ├─ device/ │ │ ├─ cam_device.cpp # SDK 调用集中在这一个文件 │ │ └─ pixel_convert.cpp # Bayer/Mono 转换 │ ├─ vision/ │ │ ├─ vision_tool.cpp # HObject 构造、算子、hdev │ │ └─ calib_param.cpp # 标定结果与像素当量 │ └─ app/ │ └─ station_flow.cpp # 工位时序:触发->采集->判定->回信号 ├─ halcon_proc/ │ ├─ detect_defect.hdev # 算法工程师交付的流程 │ └─ locate.hdev ├─ config/ │ └─ station.ini # 曝光、阈值、ROI 全在这里 └─ third_party/ ├─ cam_sdk/ # 厂商头文件 + lib + dll └─ halcon/ # halcon.lib / halconcpp.lib / dll

halcon_proc/独立于源码目录是有意为之。算法工程师改完 hdev 只需要覆盖这一个文件夹,不用重新编译 C++ 工程——这在实际协作里省掉大量来回。

2.3 编译配置与 DLL 落地位置

位数不匹配是这类项目排第一的启动失败原因。相机 SDK 和 Halcon 运行库必须同为 x64(或同为 x86),C++ 工程也必须切到对应平台。CMake 里可以这样写死,避免有人手滑:

# CMakeLists.txt 关键片段 set(CMAKE_CXX_STANDARD 17) if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8) message(FATAL_ERROR "本工程必须 64 位编译,请检查生成器平台") endif() # Halcon 头文件与库,路径按实际安装位置调整 set(HALCON_ROOT "third_party/halcon") target_include_directories(${PROJECT_NAME} PRIVATE ${HALCON_ROOT}/include) target_link_libraries(${PROJECT_NAME} PRIVATE ${HALCON_ROOT}/lib/x64/halconcpp.lib ${HALCON_ROOT}/lib/x64/halcon.lib) # 相机 SDK 同理,注意它的 lib 通常也分 win32/x64 子目录 target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/third_party/cam_sdk/lib/x64/CameraApi.lib) # 源文件统一按 UTF-8 处理,避免中文路径在日志里变乱码 if(MSVC) add_compile_options(/utf-8) endif()

CMAKE_SIZEOF_VOID_P那个检查看着多余,但在多人协作的机器上确实救过场——有人用 32 位生成器打开工程,编译能过,运行时报「找不到指定的模块」,实际上是因为 32 位进程去加载 x64 的 dll 了,错误码还特别不直观。

运行时 dll 的放置位置也要说清楚:相机 SDK 的运行时库和 Halcon 的halcon.dll、halconcpp.dll统一复制到 exe 同目录,不要指望系统 PATH。工控机上经常同时装了好几个版本的视觉软件,PATH 里谁先命中完全看运气,这种问题排查起来就是黑匣子。手册里给的做法是在 CMake 里加add_custom_command(TARGET ... POST_BUILD ...)做拷贝,把依赖收敛到输出目录。

3. 采集链路:从枚举设备到一帧回调的完整代码

采集这一段是最容易「照抄能跑、改一行就崩」的部分。下面按初始化、参数、回调、转换四步拆,代码里的函数名按常见厂商 SDK 的形态做了等价改写,语义和实际接口一致,迁移时把手上的头文件对着替换即可。

3.1 初始化与设备枚举:先拿句柄再谈参数

顺序是固定的:初始化运行时 → 枚举设备 → 创建句柄 → 打开设备。任何一步失败都要往回退,不能跳。

// src/device/cam_device.cpp bool CamDevice::OpenByIndex(int index) { // 1) 运行时初始化,多数 SDK 内部会做引用计数,重复调用是安全的 if (Cam_Initialize() != CAM_OK) { LogError("SDK 运行时初始化失败"); return false; } // 2) 枚举。先传 nullptr 拿数量,再按数量分配,避免固定数组不够用 uint32_t count = 0; if (Cam_EnumDevices(nullptr, &count) != CAM_OK || count == 0) { LogError("未枚举到设备,检查网线/供电/网卡巨帧"); return false; } if (index < 0 || index >= static_cast<int>(count)) { LogError("设备序号越界, count=%u", count); return false; } std::vector<CamDevInfo> infos(count); Cam_EnumDevices(infos.data(), &count); // 第二次调用才填内容 // 3) 创建句柄 -> 打开设备。句柄创建失败和打开失败要分开报,方便定位 if (Cam_CreateHandle(&handle_) != CAM_OK || handle_ == nullptr) { LogError("句柄创建失败"); return false; } if (Cam_OpenDevice(handle_, infos[index].serial) != CAM_OK) { LogError("打开设备失败, sn=%s", infos[index].serial); Cam_DestroyHandle(handle_); handle_ = nullptr; return false; } return true; }

两次调用枚举函数是这类 SDK 的通用套路,第一次拿数量第二次填数据。用固定长度数组、只调一次,在设备数变化时会读到脏数据。另外注意index是枚举序号而不是序列号——多相机场景下序号可能因为枚举顺序变化而漂移,正式代码里应该按序列号绑定,序号只用于首次配置。手册里在这段旁边专门标了一句:上线前把序列号写进配置,别信序号。

3.2 参数下发:曝光、增益与触发模式的坑

参数配置最容易出的问题是「设了但没生效」。原因通常是节点类型搞错了——曝光是浮点节点、像素格式是枚举节点,用错函数返回值可能还是成功。

bool CamDevice::ApplyProfile(const StationConfig& cfg) { // 连续采集还是软触发,取决于工位时序 if (!SetEnum("TriggerMode", cfg.softTrigger ? "On" : "Off")) return false; if (cfg.softTrigger) { // 触发源必须是 Software,有些 SDK 默认 Line0,不设就永远等外部信号 if (!SetEnum("TriggerSource", "Software")) return false; } // 曝光单位是微秒,配置里如果按毫秒填,这里要乘 1000 if (!SetFloat("ExposureTime", cfg.exposureUs)) return false; if (!SetFloat("Gain", cfg.gainDb)) return false; // 像素格式决定后面转换分支,先定死再取流 if (!SetEnum("PixelFormat", cfg.pixelFormat)) return false; // 如 BayerRG8 return true; }

三个参数里最值得盯的是曝光单位。不同厂商的 SDK 有毫秒也有微秒,配置表里写 10 到底代表 10ms 还是 10us,直接决定图像是过曝还是全黑。我的习惯是配置项名字里直接带单位,exposure_us、gain_db,让错误在命名阶段就暴露。

触发源那一行也是个隐蔽坑:TriggerMode打开了但TriggerSource留在默认的线路触发上,程序会一直阻塞在取流处,日志里什么都没有,看起来像是相机没出图。

3.3 回调注册与取流:为什么回调里不能干活

回调是驱动线程调进来的,它对你的代码一无所知。在这条线程里做任何耗时操作,代价都会直接反映成丢帧。

// 静态回调,user 指针把 this 传回来 static void CAM_CALL OnFrame(const CamFrame* f, void* user) { auto* self = static_cast<CamDevice*>(user); if (!self || !f) return; // 只做一件事:拷进队列。绝不在回调里调 Halcon、写文件、发网络包 self->queue_.Push(CopyFrame(f)); // 函数返回即代表 buffer 归还,之后不能再碰 f->data } bool CamDevice::Start() { if (grabbing_.exchange(true)) return true; // 防重入 Cam_RegisterCallback(handle_, &OnFrame, this); if (Cam_StartGrabbing(handle_) != CAM_OK) { grabbing_ = false; Cam_RegisterCallback(handle_, nullptr, nullptr); return false; } return true; }

CopyFrame这个名字起得很直白:回调里必须拷贝。原因在 3.4 节展开——驱动给的 buffer 在回调返回后就会被回收,指针还在但内容随时可能被下一帧覆盖。见过不少代码把裸指针塞进队列就返回了,跑单帧测试一切正常,一旦帧率上来就是间歇性花屏,排查起来相当玄学。

grabbing_那个原子变量防止重复 Start,也用于 Stop 时判断状态。回调注册失败直接返回,别硬着头皮继续。

3.4 像素格式转换与缓冲区归属

一帧数据从驱动出来到能进 Halcon,中间至少要跨两道:格式转换和内存归属。

// 统一转成 Halcon 直接认的 8 位单通道或 24 位三通道 bool ConvertToMono8OrRgb(const CamFrame& src, std::vector<uint8_t>& dst, int& outW, int& outH, std::string& outType) { outW = src.width; outH = src.height; if (src.pixelFormat == PIXEL_MONO8) { dst.assign(src.data, src.data + src.size); outType = "byte"; return true; } if (src.pixelFormat == PIXEL_BAYERRG8) { dst.resize(src.width * src.height * 3); // 用 SDK 自带的转换,比手写插值稳,也不会引入 Halcon 依赖 if (Cam_ConvertPixelType(src.data, src.width, src.height, PIXEL_BAYERRG8, PIXEL_RGB8, dst.data(), dst.size()) != CAM_OK) return false; outType = "rgb"; return true; } return false; // 不认识的格式直接失败,别猜 }

Bayer 转换放在 device 层用 SDK 自带函数,比丢给 Halcon 的cfa_to_rgb更省事,也少一层格式约定。代价是每次转换都要分配三倍内存,长时间跑就得配内存池——这一点在 5.2 节展开。

缓冲区归属要记住一条:CamFrame::data指向的内存在回调返回后失效。所以队列里存的是拷贝出来的std::vector<uint8_t>,不是裸指针。这条规则看起来浪费,但它是后面零拷贝方案能安全落地的前提——先有可靠的拷贝版,再谈优化。

4. 混合编程:SDK buffer 到 HObject 的三种接法

这一段是整个项目的核心。Halcon 认的是HObject,相机给的是裸内存块,中间怎么接决定了程序的稳定性和性能。

4.1 拷贝式转换:gen_image1 最稳但不最快

最直接的写法是用GenImage1,它把外部内存整块拷进 HObject 自己的存储里,调用返回后源内存就可以随便释放。

#include "HalconCpp.h" using namespace HalconCpp; // 把一帧 mono8 数据转成 HObject,拷贝式,最安全 HObject FrameToHObjectCopy(const std::vector<uint8_t>& buf, int w, int h) { HObject img; // 第二个参数是像素类型,mono8 对应 "byte"; // 第三个参数是像素指针,GenImage1 内部会按 w*h 拷贝 GenImage1(&img, "byte", w, h, reinterpret_cast<Hlong>(const_cast<uint8_t*>(buf.data()))); return img; }

参数说明:"byte"指 8 位无符号单通道,如果是 16 位深度的相机要换成"uint2"或"int2",写错不会报错但图像会变成噪声。Hlong那个强制转换在 64 位下是必须的,32 位指针直接塞进去在 x64 上会被截断。

代价很明确:每帧一次全量拷贝。1920×1080 单通道大约 2MB,30fps 就是 60MB/s 的额外内存带宽。多数工位够用,但多相机、高帧率时会成为瓶颈。我的建议是默认用它,跑不动了再换 4.2。

4.2 零拷贝:gen_image1_extern 与生命周期红线

GenImage1Extern让 HObject 直接引用外部内存,不做拷贝。性能提升明显,但它把内存生命周期的责任完全交给了调用方。

// 不拷贝,HObject 直接指向 buf 的内存。buf 必须在 img 用完前一直有效 bool FrameToHObjectRef(const std::vector<uint8_t>& buf, int w, int h, HObject& img) { if (buf.size() < static_cast<size_t>(w) * h) return false; // 最后一个参数是清理回调:传空表示 Halcon 不负责释放这块内存 GenImage1Extern(&img, "byte", w, h, reinterpret_cast<Hlong>(const_cast<uint8_t*>(buf.data())), HTuple()); return true; }

红线只有一条:buf的生命周期必须覆盖img的所有使用过程。一旦buf被析构或者被复用写入新帧,img就指向了一块内容已经变掉的内存,算子算出来的结果是上一帧甚至半帧新半帧旧的数据,图像上表现为撕裂或错位,且不带任何报错。

实际写法一般是这样配合队列的:队列元素是std::shared_ptr<std::vector<uint8_t>>,img和这个shared_ptr绑在一起放进一个结构体,用完一起释放。手册里给了一个小 RAII 包装,思路是让HObject和它引用的内存同生共死。

注意:GenImage1Extern之后对同一块内存做memcpy覆盖,即使HObject还活着也会读到新数据。多线程下这类问题表现为随机花屏,单步调试永远复现不了。

4.3 HDevEngine 调用外部 hdev 流程

算法工程师交付.hdev,C++ 侧用HDevEngine加载调用。好处是算法改完不用重编 C++,坏处是参数传递和异常处理都得自己兜住。

class VisionTool { public: VisionTool() { // 只设一次,进程内全局生效 engine_.SetProcedurePath("halcon_proc"); // 告诉 Halcon 允许算子内部并行,多核机器上明显更快 SetSystem("parallelize_operators", "true"); } bool Detect(const HObject& img, double minArea, DefectResult& out) { try { HDevProcedure proc("detect_defect"); HDevProcedureCall call(proc); // 输入:一个图像对象 + 若干控制参数 call.SetInputIconicParamObject("Image", img); call.SetInputCtrlParamTuple("MinArea", minArea); call.Execute(); // 同步执行,耗时不设上限,需要超时得自己在外面控 HObject region = call.GetOutputIconicParamObject("DefectRegion"); HTuple area = call.GetOutputCtrlParamTuple("TotalArea"); out.area = area[0].D(); out.count = region.CountObj(); return out.area > 0; } catch (const HException& e) { // hdev 里任何算子抛错都会走到这里,错误码和文本都要落日志 LogError("hdev 执行异常: %s (code=%ld)", e.ErrorMessage().Text(), e.ErrorCode()); return false; } } private: HDevEngine engine_; };

几个关键点:SetProcedurePath指向 hdev 所在目录,路径不要用中文;HDevProcedure每次调用都构造会有开销,稳态下应该在初始化时构造一次缓存起来,但要注意HDevProcedureCall不是线程安全的,多线程得各自持有。try/catch必须包住Execute(),Halcon 的异常不会自己消失,漏掉一个就是进程直接退。

还有一个容易忽略的地方:hdev 里的输入参数名必须和 C++ 侧的字符串完全一致,包括大小写。写错了不会报「参数不存在」,而是在Execute()时抛一个不太直观的异常。

4.4 标定参数与像素当量的落地位置

像素坐标到毫米的换算,建议放在 vision 层的calib_param.cpp,不要散落在业务代码里。手册给的结构是一个小的标定参数类,保存相机内参、畸变、以及每个工位的像素当量:

struct CalibParam { double pixelSizeMm = 0.05; // 像素当量,标定板实测得到 double offsetXmm = 0.0; // 工位坐标系偏移 double offsetYmm = 0.0; // 像素 -> 毫米,注意 Y 轴方向,多数图像坐标向下为正 void ToWorld(double px, double py, double& x, double& y) const { x = px * pixelSizeMm + offsetXmm; y = py * pixelSizeMm + offsetYmm; } };

像素当量随镜头倍率变,换镜头必须重标。把pixelSizeMm写死在代码里,是后期「明明算法没改结果却不对」的常见来源,所以它应该和曝光参数一样放在配置文件里。

5. 排查与避坑:花屏、内存涨、掉线、加锁、时间戳

这一章是血泪经验的集合。下面五条都是实际会撞上的,按「现象 → 原因 → 解决」写,撞上了直接对着查。

5.1 图像偶发花屏、下半截错位

现象:连续采几百帧,中间随机出现几帧图像撕裂,上半部分是当前场景,下半部分像上一帧的残留,单帧抓图完全正常。

原因:零拷贝方案里HObject引用的内存被下一帧覆盖了。队列里存的是裸指针或者被复用的缓冲区,回调返回后驱动把 buffer 还给了采集队列,下一帧 DMA 直接写进同一块物理内存。

解决:先把 4.2 换成 4.1 的拷贝式验证,如果花屏消失就确认是生命周期问题。然后回到零拷贝方案时,队列元素改成shared_ptr持有,HObject用完再释放引用。另外检查Close()的顺序,停流之前先把队列排空,否则残留的HObject会指向已归还的 buffer。

5.2 内存持续上涨,几小时后进程被杀

现象:任务管理器里内存曲线一路向上,8 小时后进程消失,日志里没有任何异常。

原因:两类。一类是HObject没有及时释放,比如异常分支里return掉了但对象还在作用域外持有;另一类是像素转换每帧resize大块vector,频繁申请大内存导致堆碎片。

解决:用 RAII 保证HObject出作用域即析构,异常路径也不例外;转换用的目标 buffer 做成成员变量或内存池,容量够就复用,不每帧重新分配。验证方法很土但有效:跑 2 小时看内存曲线,平稳就说明对了,缓慢上升说明还有泄漏点,可以用 Halcon 自带的对象计数或者任务管理器的句柄数辅助定位。

5.3 相机掉线后无法重连,只能重启软件

现象:网线松动或者交换机抖一下,取流中断,软件里点「重连」一直失败,必须杀掉进程重开。

原因:掉线时没有走完整的关闭流程,句柄还挂在旧设备上,驱动侧认为资源仍被占用。也有的是回调还在注册状态,重连时 SDK 拒绝重复注册。

解决:把「掉线」当成一个正常状态处理,检测到取流异常就执行完整拆卸:停流 → 注销回调 → 关设备 → 销毁句柄,四步一步不能少,然后再走一遍OpenByIndex。手册里建议加一个守护逻辑,连续 N 秒没收到新帧就主动触发重连,比等人去点按钮靠谱。

5.4 多线程调 Halcon 直接崩溃

现象:单相机单线程没问题,加了一路相机或者开了 UI 预览线程后,随机崩溃在算子内部,堆栈指向 Halcon 的运行时。

原因:HDevEngine、HDevProcedureCall都不是线程安全的,多个线程共用一个实例必然出问题。另一种情况是同一个HObject被两个线程同时读取,Halcon 内部有引用计数,并发读写会破坏它。

解决:每个工作线程持有自己的HDevEngine和HDevProcedureCall实例,绝不跨线程共享。需要在线程间传递图像时用CopyObj做一份拷贝,或者序列化成HSerializedItem再反序列化。代价是内存翻倍,但换来的是确定性。

5.5 多相机时间戳对不齐,同步判定失效

现象:两路相机拍同一个工件的上下表面,判定要求两面图像是同一时刻的,但偶尔会配上相邻工件,导致良品被判成不良。

原因:直接用本机GetTickCount或system_clock打时间戳,取的是「软件收到帧」的时刻,而帧在驱动队列里排队的延迟不固定,两路相机的排队情况还不一样。

解决:优先用相机自己的硬件时间戳,SDK 一般会随帧返回设备侧时间;拿不到就用硬触发,让同一个光电信号同时触发两路相机,从物理上保证同时性。软件时间戳只能用于日志排序,不能用于同步判定。

5.6 排查工具与日志的最小配置

排查这类问题,日志里至少要能回答三个问题:这一帧是第几帧、什么时候到的、耗时多少。最省事的做法是在FrameCb里打一行带帧号和设备时间戳的 debug 日志,算法侧在Execute()前后各打一条。真出问题时把两个日志按时间排一下就清楚了。

Halcon 侧还有个小技巧:出错时把当前图像用WriteImage落盘,比对着日志猜有用得多。磁盘占用大,所以只在异常分支里写,正常流程不落。

6. 二次开发:把 Demo 改成能上产线的工位程序

拿到手的代码如果直接上机,通常会死在三个地方:参数写死、采集和算法耦在一个循环里、没有任何压测。改造思路如下。

6.1 参数外置:把写死的阈值搬进配置文件

最省事的一步,收益也最大。把曝光、增益、触发模式、MinArea、像素当量、ROI 全部挪到config/station.ini,程序启动时读一次,配合一个「热加载」按钮,调参不用重启。

[camera] exposure_us = 8000 gain_db = 0 pixel_format = BayerRG8 soft_trigger = 1 [vision] min_area = 500 pixel_size_mm = 0.05 proc_path = halcon_proc

配置读取失败要有明确报错,并且给一套内置默认值兜底——工控机上配置文件被误删的情况比想象中多。

6.2 采集与算法解耦:队列 + 双缓冲

原始 Demo 通常是「取一帧 → 算一帧」的同步循环,相机等算法、算法等相机,节拍被两头卡。改成有界队列加独立算法线程后,采集只管入队,算法只管出队,两边互不阻塞。

指标同步循环队列解耦
单帧节拍采集 + 算法之和取两者较大值
算法慢时的表现相机降帧队列堆积,需设上限
丢帧策略无队满丢最旧帧

队列长度建议设成 3~5,太小起不到缓冲作用,太大会让延迟变大。队满时丢最旧帧而不是丢最新帧,这样算法处理的永远是最接近当前的画面。

6.3 验证方法:连续跑 8 小时的三个指标

改完别急着上线,至少压一轮:连续运行 8 小时,记录内存峰值、单帧耗时 P99、丢帧率。内存峰值应该平稳不涨,P99 不超过节拍的 60%,丢帧率为 0。这三项里任何一项不达标都别上产线,尤其是 P99,平均值好看但 P99 超标时,现场表现就是「偶尔卡一下」,然后把整条线带停。

我的习惯是改造完这类混合编程的代码,上线前必须连续跑满 8 小时,中间人为拔一次网线、关一次相机电源,看它能不能自己恢复。因为产线不会给你重启软件的机会,那之后我每次交付视觉工位都强制走一遍这个自检流程。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表