基于 Wio Terminal 的库存盘点:用重叠边界框过滤实现货架商品计数 基于 Wio Terminal 的库存盘点用重叠边界框过滤实现货架商品计数【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners导读本文围绕 IoT-For-Beginners 零售项目5-retail中“用 IoT 设备盘点库存”这一课聚焦 Wio Terminal 硬件平台上的核心算法实现当 Custom Vision 目标检测返回的多个边界框bounding box高度重叠时如何判定它们实际指向同一个商品并通过“非极大值抑制”式的重叠过滤逻辑得到准确的商品数量。读完本文你将掌握归一化边界框坐标的解析、矩形相交面积的计算、基于最小框面积百分比的重叠去重算法并能在 Wio Terminal 上独立完成一次真实的货架库存计数。为什么需要过滤重叠的边界框目标检测器对一张货架照片返回的不只是标签和置信度还会为每个检测到的物体绘制一个边界框bounding box。当货架上的商品彼此紧挨着摆放时检测器可能对一个商品产生多个相互重叠的框——比如 78.3% 置信度的外框内部又套了一个 64.3% 的内框。物理上一个罐头不可能被装进另一个罐头里因此这些“几乎完全重合”的框必然指向同一物体。此时如果直接按框的数量计数库存就会被重复统计。正确的做法是计算两两边界框的重叠面积当重叠程度超过阈值时将其中一方视为重复检测并丢弃。下图是仓库教程原图4 罐番茄酱被检测器框出且各边界框之间只有少量重叠恰好可以作为“无需去重”的基准情形。若重叠显著加大就必须忽略其中重复的框。这正是本课wio-terminal-count-stock.md要解决的完整工程问题。前置条件边界框的归一化坐标在动手写代码前需要先理解 Custom Vision 返回的边界框数据结构。边界框由 4 个值定义left、top、width、height取值均归一化在01 之间表示相对于图像宽高的百分比。坐标原点位于图像左上角top框顶边距图像顶部的距离left框左边距图像左侧的距离框的右边界 left width框的底边界 top height。例如一张 600×800 像素的图像上框从距顶 320 像素处开始则top 0.4800 × 0.4 320。因为采用百分比无论图像缩放成什么尺寸框的相对位置与相对大小都不会改变。本课算法中需要频繁进行“归一化坐标 → 矩形”的换算这与仓库 code-count/virtual-iot-device 的 Python 实现 中create_polygon的处理思路完全一致。完整实现Wio Terminal 上的重叠过滤计数以下步骤对应 wio-terminal-count-stock.md 的全部操作最终完整代码可在仓库的 code-count/wio-terminal/stock-counter/src/main.cpp 中查看本文关键代码行号均以该文件为准。第 1 步定义重叠阈值在processPredictions函数上方添加常量const float overlap_threshold 0.20f;它定义“两个边界框被视为同一物体”之前允许的重叠比例。0.20f表示 20%。该值越大去重越宽松越小则越敏感容易把紧邻的不同商品误判为同一物体。对应 main.cpp 中threshold置信度阈值与overlap_threshold相邻声明两者共同决定最终计数的准确性。第 2 步定义点、矩形结构体与面积计算在processPredictions上方继续添加struct Point { float x, y; }; struct Rect { Point topLeft, bottomRight; }; float area(Rect rect) { return abs(rect.bottomRight.x - rect.topLeft.x) * abs(rect.bottomRight.y - rect.topLeft.y); } float overlappingArea(Rect rect1, Rect rect2) { float left max(rect1.topLeft.x, rect2.topLeft.x); float right min(rect1.bottomRight.x, rect2.bottomRight.x); float top max(rect1.topLeft.y, rect2.topLeft.y); float bottom min(rect1.bottomRight.y, rect2.bottomRight.y); if ( right left bottom top ) { return (right-left)*(bottom-top); } return 0.0f; }代码逻辑分三层Point存储图像上的坐标点Rect用左上、右下两个角点定义矩形area通过两角点的横纵坐标差之积计算矩形面积overlappingArea是核心先取两矩形左边界较大者、右边界较小者、上边界较大者、下边界较小者构成“交叠矩形”若交叠矩形的宽高仍为正right left bottom top说明确实相交返回交叠面积否则返回0.0f即两框不重叠。这正是经典的矩形相交检测算法对应 main.cpp。第 3 步把 JSON 预测转换为矩形Custom Vision 返回的预测结果由 ArduinoJson 解析为JsonVariant。添加转换函数Rect rectFromBoundingBox(JsonVariant prediction) { JsonObject bounding_box prediction[boundingBox].asJsonObject(); float left bounding_box[left].asfloat(); float top bounding_box[top].asfloat(); float width bounding_box[width].asfloat(); float height bounding_box[height].asfloat(); Point topLeft {left, top}; Point bottomRight {left width, top height}; return {topLeft, bottomRight}; }该函数从预测对象中取出boundingBox的四个字段按“右 左 宽、下 上 高”构造出Rect供后续面积与重叠计算使用。见 main.cpp。这里的字段名与 Custom Vision 检测 API 的响应体predictions[].boundingBox.{left,top,width,height}一一对应。第 4 步重写 processPredictions 完成去重先清空原processPredictions函数内容然后填入以下双重循环std::vectorJsonVariant passed_predictions; for (int i 0; i predictions.size(); i) { Rect prediction_1_rect rectFromBoundingBox(predictions[i]); float prediction_1_area area(prediction_1_rect); bool passed true; for (int j i 1; j predictions.size(); j) { Rect prediction_2_rect rectFromBoundingBox(predictions[j]); float prediction_2_area area(prediction_2_rect); float overlap overlappingArea(prediction_1_rect, prediction_2_rect); float smallest_area min(prediction_1_area, prediction_2_area); if (overlap (overlap_threshold * smallest_area)) { passed false; break; } } if (passed) { passed_predictions.push_back(predictions[i]); } }算法要点如下passed_predictions向量保存所有“通过检查”的预测外层循环取当前预测内层循环只从j i 1开始保证每对预测只比较一次——1 与 2 比较过之后2 无需再与 1 比较只需与 3、4…比较对每一对预测计算重叠面积再取两个框中面积较小者作为基准关键判断overlap overlap_threshold * smallest_area。这里将阈值按最小框面积缩放因为overlap_threshold是百分比必须乘以“框的面积”而非“整张图的面积”才能表达“重叠面积超过较小框面积的 20%”这一物理含义一旦重叠超标当前预测被标记为passed false并break不再与后续框比较全部通过后才加入passed_predictions。见 main.cpp。 提示这是一种非常简化的去重方式——重叠对中直接丢弃先出现的一个。生产级代码通常需要更精细的策略例如处理多个物体彼此重叠的复杂场景或判断一个边界框是否完全包含在另一个框内可结合 README 中的 overlap-object-detection 场景 理解。第 5 步输出通过检查的预测详情在去重循环之后遍历passed_predictions并将每条预测打印到串口监视器for(JsonVariant prediction : passed_predictions) { String boundingBox prediction[boundingBox].asString(); String tag prediction[tagName].asString(); float probability prediction[probability].asfloat(); char buff[32]; sprintf(buff, %s:\t%.2f%%\t%s, tag.c_str(), probability * 100.0, boundingBox.c_str()); Serial.println(buff); }每条输出包含标签名、置信度百分比形式两位小数和原始边界框 JSON。char buff[32]用于格式化字符串%.2f%%将 01 的概率转换为百分数。第 6 步输出最终计数最后打印本次共清点了多少件商品Serial.print(Counted ); Serial.print(passed_predictions.size()); Serial.println( stock items.);passed_predictions.size()即去重后的有效商品数。原文档特别指出这个数值完全可以再封装成消息发送给上游 IoT 服务如 IoT Hub / 函数应用用于在库存水平偏低时触发补货告警。运行与验证将代码上传到 Wio TerminalPlatformIO 环境seeed_wio_terminal依赖配置见 platformio.ini把 ArduCam 镜头对准货架上的商品按下 Wio Terminal 的C 按钮源码中WIO_KEY_C配置为INPUT_PULLUP见 main.cpp串口输出示例如下Connecting to WiFi.. Connected! Image captured Image read to buffer with length 17416 tomato paste: 35.84% {left:0.395631,top:0.215897,width:0.180768,height:0.359364} tomato paste: 35.87% {left:0.378554,top:0.583012,width:0.14824,height:0.359382} tomato paste: 34.11% {left:0.699024,top:0.592617,width:0.124411,height:0.350456} tomato paste: 35.16% {left:0.513006,top:0.647853,width:0.187472,height:0.325817} Counted 4 stock items.可以观察到4 个边界框的left/top/width/height均为 01 之间的归一化坐标彼此没有显著重叠因此全部通过过滤最终输出Counted 4 stock items。调试建议尝试把overlap_threshold调小如0.10f或调大如0.40f观察哪些预测被忽略、计数如何变化从而为你的货架场景找到最合适的取值。从调用链看整体数据流要理解这段计数代码在整个检测流程中的位置需要回溯调用链见 main.cpp 全文件loop()轮询WIO_KEY_C按下时调用buttonPressed()L214-L222buttonPressed()通过Camera类camera.h 基于 ArduCAM 驱动 OV2640 传感器以 JPEG、640×480 分辨率捕获图像完成拍照并把图像字节读入缓冲区detectStock()用WiFiClientSecure携带config.h中配置的 CA 证书见 config.h向 Custom Vision 的PREDICTION_URL发起application/octet-stream的 POST 请求请求头带Prediction-Key响应中的predictions数组先按置信度阈值thresholdconst float threshold 0.0f本课示例设为过滤低概率检测筛一遍再交给processPredictions()做重叠过滤与计数。也就是说本课新增的Point/Rect/area/overlappingArea/rectFromBoundingBox及重写后的processPredictions是“低置信度过滤”之后、串口输出之前的第二道也是最后一道数据关卡。前置的“把图像分类器改造成检测器、解析 predictions 数组”步骤参见同目录的 wio-terminal-object-detector.md其输出样例4 条tomato paste预测正是本课示例输入的来源。扩展重新训练模型与生产化考虑本课的计数结果直接依赖检测模型的精度。仓库 README 建议用 IoT 设备采集的真实照片重新训练模型并逐一人工校正每个边界框删除框住错误对象的框、调整错位框、修正标签。每次重新训练并发布新迭代后需同步更新设备端config.h中的PREDICTION_URL迭代名称会变化如Iteration2。另外本课去重只处理了“一对多”的最简单情形。若要应对“一个框完全包含另一个框”“多个框连环重叠”等情况需要像 virtual-iot-device 版本 那样引入几何库Python 侧使用shapely的Polygon.intersection计算相交面积并补充更完善的取舍策略这也是把原型算法推向生产环境时的主要优化方向。小结通过本课你在 Wio Terminal 上完成了一条完整的“拍照 → 云端目标检测 → 置信度过滤 → 重叠去重 → 计数输出”链路用归一化边界框坐标构造矩形用矩形相交算法计算重叠面积再用“重叠面积 阈值 × 最小框面积”的判定剔除重复检测最终把去重后的数量打印到串口并具备上报 IoT 服务的能力。这套思路既是本零售项目库存盘点的核心也是边缘侧轻量目标计数应用的通用范式。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考