
IoT-For-Beginners 实战基于 Wio Terminal 的边界框重叠过滤与货架库存计数【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本篇文章是 IoT-For-Beginners 开源课程中「零售项目Retail」第二课《使用 IoT 设备检查库存》的 Wio Terminal 实战篇。它讲解如何将上一课训练好的目标检测模型返回的**预测结果predictions与边界框bounding boxes**结合起来在单片机端用 C 实现重叠过滤算法从而准确统计货架上的商品件数。读完本文你将掌握边界框坐标系的含义、Point/Rect几何结构的定义、area与overlappingArea面积计算方法以及如何在 main.cpp 中实现一套可运行、可调参的库存计数流程并理解将其接入云端 IoT 服务以触发补货告警的完整思路。为什么需要统计库存从目标检测到商品计数在零售场景中目标检测不仅能回答「货架上有没有商品」还能回答「货架上有多少商品」。本课 README5-retail/lessons/2-check-stock-device/README.md给出一个典型例子假设货架容量为 8 罐番茄酱而目标检测器只检测出 7 罐说明缺货 1 罐、需要补货。更进一步检测结果携带的边界框位置信息还能指示「缺货的商品大概在货架的哪个位置」这对使用机器人自动补货的场景尤为重要。但检测结果直接用于计数有一个陷阱同一商品可能被输出多个高度重叠的边界框。例如某一罐番茄酱同时被检测为 78.3% 和 64.3% 两个框其中较小的框完全落在较大框内部此时若把两个框都当作两件商品就会虚增库存。因此计数的关键一步是识别并忽略掉重叠面积显著过大的边界框只保留互不重叠的预测。下面的图片展示了一个理想情形4 个边界框彼此只有很小重叠因此可以直接判为 4 件商品。边界框Bounding Box坐标系基础要编写重叠过滤代码首先必须理解 Custom Vision 返回的边界框数据格式。目标检测接口的每条预测prediction大致形如{ tagName: tomato paste, probability: 0.3584, boundingBox: { left: 0.395631, top: 0.215897, width: 0.180768, height: 0.359364 } }边界框由 4 个值定义top、left、height、width。这些值的取值范围均为0~1表示相对于图像尺寸的百分比坐标如0.4即 40%。坐标系原点0, 0位于图像的左上角top是边界框顶部到图像顶部的距离left是边界框左边缘到图像左边缘的距离边界框底部 top height右边缘 left width。以 bounding-box.png 中的例子说明图像宽 600 像素、高 800 像素边界框距离顶部 320 像素top 0.4、距离左侧 240 像素left 0.4、高 240 像素height 0.3、宽 120 像素width 0.2坐标值Top0.4Left0.4Height0.3Width0.2使用 0~1 的百分比值的最大好处是与图像实际分辨率无关无论图像被缩放成多大边界框始终位于图像 40% 处、占据 30% 高度与 20% 宽度。这也意味着在代码中你完全可以用 0~1 的相对坐标直接做几何运算而不必关心摄像头实际输出的 640×480 分辨率。项目准备stock-counter工程的构成在进入计数算法之前先明确代码所处的位置与运行环境。本课完整代码位于 code-count/wio-terminal虚拟设备与树莓派版本分别在code-count/virtual-iot-device与code-count/pi目录其工程结构如下code-count/wio-terminal/stock-counter/ ├── include/ # 空占位目录 ├── lib/ # 空占位目录 ├── src/ │ ├── camera.h # ArduCAM 摄像头封装OV2640 │ ├── config.h # WiFi、预测 URL、预测密钥、TLS 证书 │ └── main.cpp # 主程序拍照 → 检测 → 计数 ├── test/ └── platformio.ini # PlatformIO 工程配置该工程是直接基于上一课wio-terminal-object-detector.md的目标检测工程改造而来因此继承了其中四要素摄像头采集camera.h封装 ArduCAM 摄像头以Camera(JPEG, OV2640_640x480)初始化buttonPressed()中按下 Wio Terminal 的C 键后通过startCapture()、captureReady()、readImageToBuffer()读取 JPEG 数据到内存缓冲区。WiFi 连接与 TLSconnectWiFi()使用WiFi.begin(SSID, PASSWORD)连接网络并通过client.setCACert(CERTIFICATE)配置 Microsoft Azure TLS 根证书config.h中的SSID、PASSWORD、PREDICTION_URL、PREDICTION_KEY均为需要替换的占位符。调用云端检测接口detectStock()通过HTTPClient向 Custom Vision 的预测 URL 发送application/octet-stream的 JPEG 二进制数据请求头携带Prediction-Key返回 JSON 后用 ArduinoJson 解析出predictions数组。概率阈值过滤const float threshold 0.0f;用于滤除低置信度检测与分类器不同目标检测会返回多条结果必须过滤低概率项。platformio.ini使用seeed_wio_terminal开发板关键依赖包括Seeed Arduino rpcWiFi、Seeed Arduino FS、Seeed_Arduino_mbedtls与bblanchon/ArduinoJson 6.17.3并通过-DARDUCAM_SHIELD_V2、-DOV2640_CAM编译宏启用 ArduCAM 驱动。上一课的processPredictions只负责把每条预测的tagName与probability打印到串口本课要做的就是把这个函数改造为「先按边界框重叠度去重、再计数输出」。核心实现基于重叠阈值的库存计数算法本课的全部代码改动围绕processPredictions函数展开分四步完成设定重叠阈值、定义几何结构与面积计算、将边界框转换为Rect、两两比较并过滤重叠预测。第一步设定重叠阈值在processPredictions函数上方加入const float overlap_threshold 0.20f;overlap_threshold定义的是「两个边界框重叠面积占比较小框面积的百分比阈值」。0.20即 20%当两个框的重叠面积超过较小框面积的 20% 时就认为它们指向的是同一个物体。这个值是经验性的实际运行时需要根据你的货架布局与拍摄角度反复调整。第二步定义Point、Rect结构及面积计算函数在阈值下方、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存储图像上的一个坐标点(x, y)Rect用左上角topLeft与右下角bottomRight两个点定义一个矩形area()由右下角与左上角的坐标差宽 × 高计算矩形面积overlappingArea()是标准的轴对齐矩形交集面积算法交集矩形的左边界取两者left的较大值、右边界取两者right的较小值、上边界取top的较大值、下边界取bottom的较小值若right left bottom top说明确实存在交集返回(right-left)*(bottom-top)否则返回0.0f。这套几何代码可以直接在 main.cpp 中找到对应实现。第三步把 JSON 边界框转换为Rect在overlappingArea下方添加转换函数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对象读出left、top、width、height四个 0~1 坐标转换为Rect左上角为(left, top)右下角为(left width, top height)——即右边缘 左 宽下边缘 上 高与上一节边界框坐标系的定义完全一致。这条转换链JSON →Rect对应 main.cpp。第四步两两比较预测并过滤重叠首先清空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]); } }算法要点外层循环遍历所有预测为每条预测生成Rect并计算其面积初始假定其通过检查passed true。内层循环从j i 1开始只与当前预测之后的预测比较。这避免了重复比较1 与 2 比较过后无需再比较 2 与 1只需继续与 3、4…比较把比较次数从 O(n²) 降为 O(n²/2)。对每一对预测计算重叠面积overlap并与两框中较小框的面积乘以阈值后的结果比较overlap overlap_threshold * smallest_area。注意这里的关键点在于——阈值是百分比必须乘以较小边界框的面积用来衡量「重叠占该框本身面积的比例」而不是占整幅图像面积的比例否则同一件商品的大框之间永远算不出显著重叠。一旦某条预测与任何后续预测重叠超限立即break退出内层循环并标记为未通过通过全部检查的预测被加入passed_predictions集合。 课程文档明确提示这是一种非常简化的去重叠方式——直接丢弃重叠对中的第一个预测。生产级代码通常需要更精细的逻辑例如考虑多个物体之间的重叠关系、处理一个边界框完全包含另一个边界框的情况以及结合置信度选择保留哪一个框。这段代码的完整实现位于 main.cpp。第五步输出通过预测与最终计数在过滤逻辑之后加入将通过的预测打印到串口监视器的代码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」的格式输出。随后打印最终统计结果Serial.print(Counted ); Serial.print(passed_predictions.size()); Serial.println( stock items.);passed_predictions.size()即过滤掉重叠框后的去重商品件数也就是货架上的库存数量。正如课程文档所述这个数字完全可以进一步发送到 IoT 服务例如 Azure IoT Hub在库存低于阈值时触发补货告警。烧录运行与结果验证完成上述改造后用 PlatformIO 将程序烧录到 Wio Terminal。运行时将摄像头对准货架上的商品按下C 键串口监视器输出如下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 条预测均保留了各自的边界框坐标最终计数为 4 件商品。请务必动手调整overlap_threshold的值例如0.05f、0.50f并重新烧录观察哪些预测会被过滤掉阈值越小两个框越容易被认为是「同一个物体」计数越保守阈值越大则越容易把重叠框误判为多件商品。上图展示了典型的重叠误检一个框报告 78.3% 置信度另一个更小的框完全嵌套其中、置信度 64.3%。借助本文的过滤算法这类检测会被自动剔除。延伸把计数结果接入 IoT 服务与模型迭代接入云端告警passed_predictions.size()是现成的业务指标。参考本课 README 中的 IoT 参考架构可将其发布到 Azure IoT Hub由云端的 Azure Functions 或流处理任务在数量低于阈值时触发补货通知。重新训练模型与图像分类不同目标检测模型的迭代需要人工逐框审核——在 Custom Vision 的Predictions标签页选中图片后逐个检查红框标签错误用X删除后重打框的位置与大小用角点手柄调整未包含物体的框用垃圾桶按钮删除审核完成后点击Train重新训练并发布新迭代再更新设备端PREDICTION_URL即可。课程提供了Assignmentassignment.md供进一步练习。其他硬件平台本文算法思路与硬件无关Raspberry Pi / 虚拟设备版本请参见 single-board-computer-count-stock.md。小结至此你完成了从「目标检测返回原始预测」到「Wio Terminal 本地过滤重叠并统计库存」的完整闭环。核心收获有三点一是理解边界框left/top/width/height的 0~1 归一化坐标系二是掌握基于Point/Rect的交集面积算法与「阈值 × 较小框面积」的百分比判据三是能够独立调整overlap_threshold以适应真实货架场景。下一步不妨尝试课程挑战——用多标签模型检测「错放商品」或结合边界框位置定义货架分区构建更智能的货架巡检系统。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考