
用 IoT 设备盘点货架库存Custom Vision 目标检测在零售场景的端到端实战【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本课属于 IoT-For-Beginners 课程体系中的零售Retail项目对应 translations/da/5-retail/lessons/2-check-stock-device/README.md丹麦语译文原文为英文位于 5-retail/lessons/2-check-stock-device/README.md。本文以该课为主体结合仓库内的完整示例代码展开讲解。在零售场景中摄像头与目标检测模型组合起来就是一套自动货架盘点系统当模型发现货架上本应有 8 罐番茄酱却只识别出 7 罐时IoT 设备便能自动发出补货提醒。本文完整讲解如何把上一课训练好的 Custom Vision 目标检测模型发布为可调用的预测接口从 Wio Terminal 或 Raspberry Pi 等 IoT 设备端调用它理解边界框bounding box的坐标语义结合设备采集的真实数据重训模型并最终通过重叠消除算法统计出货架上的库存数量。盘点场景从识别到计数上一课讲解了目标检测在零售业的多种用途并训练了一个能识别库存商品的检测器本课则解决如何在 IoT 设备上使用检测器来盘点库存的问题。目标检测器可用于库存检查既可以统计商品数量也可以确认商品是否放在正确的位置。带摄像头的 IoT 设备可以部署在商店各处监控库存优先覆盖需要及时补货的高优先级区域——例如存放少量高价值商品的小型货架。举例来说如果摄像头监视一个能容纳 8 罐番茄酱的货架而目标检测器只识别出 7 罐说明缺了 1 罐需要补货。上图中检测器在一个本可容纳 8 罐的货架上识别出了 7 罐番茄酱。IoT 设备不仅能发送补货通知还能给出缺失商品的大致位置——这对使用机器人补货的场景尤其有价值。 不过是否需要补货取决于商店与商品畅销程度只有 1 罐缺失时往往不会立即触发补货需要根据商品、顾客等因素设计一套判断补货时机的算法。另一种典型场景是错位商品检测。补货时的人工失误或顾客改变主意后随手把商品放回任意空位都会导致错误商品上架。对罐头这类不易腐坏的商品这只是一点麻烦但如果错位的是冷冻或冷藏商品就可能因为无法确定离开冷柜的时间而彻底无法销售。目标检测可以识别出意外出现在货架上的商品并立即提醒店员或机器人将其归位。上图中一罐玉米罐头被放到了番茄酱货架上目标检测器识别出了它使 IoT 设备能够通知店员或机器人纠正这一错误。发布模型迭代获取预测接口与密钥上一课训练的检测器模型位于 Custom Vision 门户中要让它能被 IoT 设备调用必须先将其迭代iteration发布为可用的预测接口。操作步骤发布检测器迭代打开 CustomVision.ai 并登录若尚未登录打开stock-detector项目。点击顶部选项卡中的Performance性能。从侧边Iterations迭代列表中选择最新迭代。点击该迭代的Publish发布按钮。在弹出的Publish Model对话框中将Prediction resource设置为上一课创建的stock-detector-prediction资源迭代名称保留为Iteration2点击Publish。发布完成后点击Prediction URL按钮查看预测 API 详情。找到标记为If you have an image file的区域复制其中的 URL其格式形如https://location.api.cognitive.microsoft.com/customvision/v3.0/Prediction/id/detect/iterations/Iteration2/image其中location是创建 Custom Vision 资源时选择的区域id是一长串由字母和数字组成的资源 ID。同时复制Prediction-Key的值。这是一把安全密钥调用模型时必须随请求发送只有携带该密钥的应用才被允许使用模型其他应用一律被拒绝。✅ 思考题当发布了一个新命名的迭代时IoT 设备端的代码应如何切换到新迭代答案并不复杂——只需更新代码中拼接的迭代名与预测 URL见下文config.h中的PREDICTION_URL。从 IoT 设备调用目标检测器调用检测器与上一课调用图片分类器的代码高度相似核心差异在于两点调用的是目标检测专用的 URL/detect/iterations/name/image且返回结果是多条预测每个检测对象一条而分类器每个标签只返回一条结果。因此需要对低置信度的预测做过滤。Wio TerminalArduino/C实现在 Wio Terminal 上多数代码可直接复用制造Manufacturing项目第 2 课的图片分类器代码只需做如下改造完整代码见 code-detect/wio-terminal/stock-counter/src/main.cpp在main.cpp顶部加入#include vector。把classifyImage函数改名为detectStock函数定义和buttonPressed中的调用处都要改。在detectStock上方声明概率阈值用于过滤低置信度结果const float threshold 0.3f;由于目标检测器会返回多条结果低于阈值的预测需要被丢弃。声明一个处理预测列表的函数把每条预测打印到串口监视器void processPredictions(std::vectorJsonVariant predictions) { for(JsonVariant prediction : predictions) { String tag prediction[tagName].asString(); float probability prediction[probability].asfloat(); char buff[32]; sprintf(buff, %s:\t%.2f%%, tag.c_str(), probability * 100.0); Serial.println(buff); } }在detectStock中把遍历预测结果的for循环替换为带阈值过滤的逻辑std::vectorJsonVariant passed_predictions; for(JsonVariant prediction : predictions) { float probability prediction[probability].asfloat(); if (probability threshold) { passed_predictions.push_back(prediction); } } processPredictions(passed_predictions);这段代码遍历所有预测将概率高于阈值的预测收集到passed_predictions中再交给processPredictions输出。上传并运行代码把摄像头对准货架上的商品按下 Wio Terminal 的 C 键即可在串口监视器看到输出Connecting to WiFi.. Connected! Image captured Image read to buffer with length 17416 tomato paste: 35.84% tomato paste: 35.87% tomato paste: 34.11% tomato paste: 35.16% 阈值threshold需要根据实际拍摄图片调整到合适的值。从源码看main.cppWiFi 的SSID/PASSWORD、TLS 的CERTIFICATE、PREDICTION_URL与PREDICTION_KEY都通过config.h提供HTTP POST 以application/octet-stream上传图片字节流并携带Prediction-Key请求头收到200后用 ArduinoJson 解析predictions数组。Raspberry Pi / 虚拟 IoT 设备Python实现Python 端同样复用图片分类器的工程结构stock-counter目录虚拟设备需配置虚拟环境把分类调用替换为检测调用完整代码见 code-detect/pi/stock-counter/app.py 与 code-detect/virtual-iot-device/stock-counter/app.py删除原有的三行分类代码results predictor.classify_image(project_id, iteration_name, image) for prediction in results.predictions: print(f{prediction.tag_name}:\t{prediction.probability * 100:.2f}%)替换为基于detect_image的检测代码results predictor.detect_image(project_id, iteration_name, image) threshold 0.3 predictions list(prediction for prediction in results.predictions if prediction.probability threshold) for prediction in predictions: print(f{prediction.tag_name}:\t{prediction.probability * 100:.2f}%)这段代码调用预测客户端的detect_image方法执行目标检测筛出概率高于阈值的预测并打印。从仓库中的 app.py 可以看到prediction_url、prediction_key从预测 URL 字符串中按/分段解析出endpoint、project_id和iteration_name再用ApiKeyCredentials携带Prediction-key构造CustomVisionPredictionClient。运行代码输出示例piraspberrypi:~/stock-counter $ python3 app.py tomato paste: 34.13% tomato paste: 33.95% tomato paste: 35.05% tomato paste: 32.80%这两份分步操作指南分别位于 wio-terminal-object-detector.md 与 single-board-computer-object-detector.md摄像头硬件连接与图片采集步骤可参考制造项目 4-manufacturing/lessons/2-check-fruit-from-device/README.md 中的对应任务。边界框模型输出的坐标语义调用目标检测器时返回结果不仅包含检测对象的标签tagName与置信度probability还包含边界框bounding box——它定义了模型以给定置信度检测到对象的矩形区域。在 Custom Vision 的Predictions预测选项卡中预测结果会直接把边界框绘制在提交分析的图片上上图中检测出了 4 罐番茄酱每个检测对象上都叠加了红色矩形即其边界框。边界框由 4 个值定义top顶部、left左侧、height高度、width宽度。这些值的取值范围是 0~1表示相对图片尺寸的百分比。坐标系原点 (0,0) 位于图片左上角top表示边界框上边距图片顶部的距离边界框下边则等于top height。以上图为例图片宽 600 像素、高 800 像素。边界框顶部距图片顶部 320 像素即top 0.4800 × 0.4 320左边距左边缘 240 像素即left 0.4600 × 0.4 240边界框高 240 像素即height 0.3800 × 0.3 240宽 120 像素即width 0.2600 × 0.2 120。汇总如下坐标值Top0.4Left0.4Height0.3Width0.2由于采用 0~1 的百分比取值无论图片被缩放到多大边界框始终从图片的 0.4 处开始横向与纵向且占据 0.3 的高度与 0.2 的宽度天然具备尺度无关性。边界框还可以与置信度结合用来评估检测结果的合理性。例如检测器可能对同一对象输出多个重叠框——把一个罐子检测到了另一个罐子内部。代码可以检查边界框识别出这种物理上不可能的完全重叠并忽略与其它框重叠度显著的对象上例中一个边界框以 78.3% 的置信度预测了一个番茄酱罐头另一个更小的框以 64.3% 的置信度位于第一个框内部。代码通过检查边界框发现二者完全重叠由于不可能存在一个罐头位于另一个罐头内部便可丢弃置信度较低的那个结果。✅ 你也可以思考是否存在检测一个对象位于另一个对象内部是合法场景的情况例如盒装物品内含独立包装。用真实数据重训模型与图片分类器一样可以利用 IoT 设备采集的真实数据对目标检测模型进行重训这能确保模型在实际部署环境中表现良好。但与分类器不同目标检测的样本不能简单打标签——必须逐个人工审核模型标注的边界框框错了对象就删除框的位置不对就调整。操作步骤重训模型先用 IoT 设备采集一批多样化的图片。在Predictions选项卡中选择一张图片红色矩形会标出检测到的边界框。逐一审核每个边界框选中后弹出标签信息用边界框四角的控制手柄调整大小若标签错误点击X删除并重新添加正确标签若边界框内没有对象用垃圾桶按钮删除。完成审核后关闭编辑器图片会从Predictions选项卡移动到Training Images选项卡。对所有预测图片重复该过程。点击Train按钮重训模型。训练完成后发布新迭代并把 IoT 设备中的预测 URL 更新为新迭代对应的地址。重新部署代码并在设备上测试。剔除重叠框统计库存数量将检测到的对象数量与边界框结合起来就可以统计货架上的库存。但直接统计predictions的条数并不准确同一对象常被多个重叠边界框重复命中。以下图为例各边界框之间存在少量重叠若重叠显著增大很可能表示它们指向的是同一个对象统计时必须忽略这类重叠框。Wio Terminal 的 C 实现完整代码见 code-count/wio-terminal/stock-counter/src/main.cpp实现思路如下在processPredictions函数上方定义重叠阈值const float overlap_threshold 0.20f;该值定义了边界框被视为同一对象前允许的重叠百分比0.20 即允许 20% 重叠。定义点与矩形结构以及面积与重叠面积的计算函数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计算两个矩形的交叠面积不相交时返回 0。声明一个把预测结果中的边界框转换为Rect的函数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}; }右边坐标由left width得到下边坐标由top height得到。重写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]); } }内层循环从i 1开始避免同一对预测被重复比较比较过 1 和 2 后无需再比较 2 和 1。 这是去除重叠的极简实现——直接丢弃重叠对中的前者。生产代码需要更完善的逻辑例如考虑多个对象间的重叠或一个边界框完全包含于另一个框的情况。将通过的预测与统计数量输出到串口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); } Serial.print(Counted ); Serial.print(passed_predictions.size()); Serial.println( stock items.);运行输出示例boundingBox以 JSON 形式给出 left/top/width/height 的 0~1 归一化坐标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.最终的数量可以直接上报给 IoT 服务用于库存过低告警。PythonShapely实现Python 端完整代码见 code-count/pi/stock-counter/app.py 与 code-count/virtual-iot-device/stock-counter/app.py使用两个 Pip 包完成同样的工作Pillow把边界框绘制到保存的image.jpg上作为调试手段可视化检测结果。Shapely用多边形相交计算边界框重叠面积Raspberry Pi 需先sudo apt install libgeos-dev。安装依赖pip3 install pillow pip3 install shapely导入后定义重叠阈值与多边形转换函数from PIL import Image, ImageDraw, ImageColor from shapely.geometry import Polygon overlap_threshold 0.20 def create_polygon(prediction): scale_left prediction.bounding_box.left scale_top prediction.bounding_box.top scale_right prediction.bounding_box.left prediction.bounding_box.width scale_bottom prediction.bounding_box.top prediction.bounding_box.height return Polygon([(scale_left, scale_top), (scale_right, scale_top), (scale_right, scale_bottom), (scale_left, scale_bottom)])重叠消除与计数逻辑两层循环两两比较i与i1...n比较用Polygon.intersection求出重叠多边形并计算其面积再与较小框面积乘以阈值的结果比较超过则标记删除由于不能在遍历列表的同时删除元素先收集到to_delete最后统一移除to_delete [] for i in range(0, len(predictions)): polygon_1 create_polygon(predictions[i]) for j in range(i1, len(predictions)): polygon_2 create_polygon(predictions[j]) overlap polygon_1.intersection(polygon_2).area smallest_area min(polygon_1.area, polygon_2.area) if overlap (overlap_threshold * smallest_area): to_delete.append(predictions[i]) break for d in to_delete: predictions.remove(d) print(fCounted {len(predictions)} stock items)紧接其后的绘图代码把通过筛选的边界框绘制到图片上left width、top height计算右下角再乘以图片宽高换算为像素坐标红色描边保存回image.jpg便于在 VS Code 中直接查看检测效果。挑战与课后作业挑战能否检测错误库存训练模型识别多种对象然后更新应用在检测到错位商品时发出告警。更进一步可以检测同一货架上并排摆放的商品通过给边界框定义区域界限来判断是否有商品被放错位置。课后作业参考 assignment.md——把目标检测器像上一项目部署图片分类器那样导出为紧凑模型compact model并部署到边缘edge让 IoT 设备调用边缘版本。评分标准覆盖三档能否使用正确的紧凑域并导出检测器并成功在边缘运行。注意事项与延伸阅读本课是该项目的最后一课完成课程与作业后记得清理云服务资源清理步骤见 clean-up.md。摄像头硬件连接与图片采集/分类的基线步骤来自制造项目4-manufacturing/lessons/2-check-fruit-from-device/README.md含 Wio Terminal 摄像头连接与 Python 分类调用任务。可直接运行或对照的完整工程代码Wio Terminal 检测code-detect/wio-terminal/stock-counter含 platformio.ini 与 config.hWio Terminal 计数code-count/wio-terminal/stock-counterPython 检测/计数code-detect/pi/stock-counter/app.py、code-count/pi/stock-counter/app.py分步操作指南wio-terminal-object-detector.md、single-board-computer-object-detector.md、wio-terminal-count-stock.md、single-board-computer-count-stock.md。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考