IoT 零售库存盘点实战:基于 Azure Custom Vision 目标检测的货架计数指南

IoT 零售库存盘点实战:基于 Azure Custom Vision 目标检测的货架计数指南 IoT 零售库存盘点实战基于 Azure 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/en/5-retail/README.md关联文档为捷克语译本 translations/cs/5-retail/README.md系统讲解如何利用运行在 IoT 设备上的 AI 模型自动统计货架库存。读者将掌握两类核心技术能力一是在云端用 Azure Custom Vision 训练**目标检测Object Detection**模型来识别并定位货架商品二是把已发布的模型集成到 Raspberry Pi或虚拟 IoT 设备与 Wio Terminal 上结合边界框做去重处理最终输出准确的库存数量。模块概览零售补货痛点与 IoT 解决方案食品从生产端到达消费者手中之前最后一道环节是零售——农贸市场、果蔬店、超市与各类门店都需要保证货架上有顾客看得见、买得到的商品。在大型超市中保持货架满货是最耗费人力与时间的任务之一员工需要逐个检查货架发现空位后再从仓库取货补上。IoT 可以显著简化这一过程在 IoT 设备上运行 AI 模型来计数库存。这类机器学习模型不止于对整张图片做分类而是能够识别单个商品对象并逐一计数从而自动判断哪些货架缺货、缺了多少甚至定位缺货的具体位置。本模块共两节课完整内容位于 translations/en/5-retail/lessons/1-train-stock-detector/README.md 与 translations/en/5-retail/lessons/2-check-stock-device/README.md训练库存检测器——训练一个基于图片的 AI 模型来识别货架上的商品从 IoT 设备检查库存——把模型部署到 IoT 设备上并完成计数。 本模块会使用部分云资源。如果不会完成该项目内的所有课程记得按照 clean-up.md 清理你的云资源。第一课训练库存检测器目标检测与图像分类的区别上一项目制造模块中训练的是图像分类器——判断整张图片里是否包含某类东西。目标检测则不同模型不只按标签给整图分类而是被训练去识别图片中的对象并定位它们。这意味着它可以同时输出对象的类别、概率以及对象在图片中的位置从而支持计数。以训练好的腰果罐 vs 番茄酱罐分类器为例当输入一张腰果罐图片时分类器返回如下概率标签概率cashew nuts98.4%tomato paste1.6%输入一张番茄酱罐图片时标签概率cashew nuts0.7%tomato paste99.3%虽然可以通过设置阈值来判断图片内容但如果一张图里同时有多个番茄酱罐或既有腰果又有番茄酱分类结果就无法回答有几个。这正是目标检测的用武之地。目标检测的训练方式与分类不同不是给整张图片打一个标签而是高亮图片中包含对象的区域并为其打标签一张图可以标注一个或多个对象。这样模型学到的是对象本身长什么样。做预测时返回的不是标签概率列表而是一组被检测对象、其边界框以及每个对象匹配其标签的概率。边界框bounding box是画在对象周围的矩形。上图中同时出现了腰果罐和三个番茄酱罐检测器为腰果返回一个边界框及 97.6% 的置信度同时为每个番茄酱罐分别返回独立的边界框与置信度。目标检测的工作原理目标检测使用复杂的 ML 模型模型把图片划分为多个单元格检查某个边界框的中心是否与训练图片中对象相似可以理解为在图片的不同区域上反复运行图像分类器来寻找匹配。模型有多种实现著名代表是YOLOYou Only Look Once它速度极快可检测人、狗、瓶子、汽车等 20 类对象。目标检测模型可通过**迁移学习transfer learning**重新训练以识别自定义对象。目标检测在零售中的应用零售场景中目标检测的典型应用包括库存检查与计数——识别货架何时库存偏低库存过低时向员工或补货机器人发送通知口罩检测——公共卫生事件期间识别佩戴口罩与未佩戴口罩的人员自动结账——在无人商店中检测顾客从货架取走的商品并自动计费危险检测——识别地上的破损商品或洒落的液体提醒清洁人员处理。创建 Custom Vision 目标检测项目与训练图像分类器类似可以使用Custom Vision训练目标检测器具体步骤如下为该模块创建一个名为stock-detector的资源组在stock-detector资源组中创建免费的 Custom Vision 训练资源与预测资源分别命名为stock-detector-training与stock-detector-prediction。 免费层训练与预测资源各只能有一个请确保已清理此前课程的项目。 ⚠️ 资源创建的具体步骤可参考 创建认知服务资源。打开 Custom Vision 门户customvision.ai用与 Azure 账号关联的 Microsoft 账户登录新建一个 Custom Vision 项目命名为stock-detector。创建时选择之前创建的stock-detector-training资源项目类型选择Object Detection域选择Products on Shelves。✅Products on Shelves域是专门为检测商店货架上的库存设计的。收集与标注训练数据训练模型需要一组包含待检测对象的图片每个对象至少准备 15 张图片覆盖不同角度与光照条件数量越多越好由于使用了 Products on Shelves 域建议把对象按货架陈列的方式摆放拍摄另外准备几张测试图片用于验证如果检测多个对象测试图片应包含全部对象。包含多个对象的图片其训练数量计入其中所有对象的最低 15 张要求图片需为PNG 或 JPEG 格式小于 6MB。iPhone 拍摄的高分辨率 HEIC 图片需要先转换甚至缩放各类对象的图片数量应尽量均衡仓库中提供了腰果与番茄酱罐的示例图片位于 5-retail/lessons/1-train-stock-detector/imagestraining与testing两个子目录分别存放训练与测试图片。上传图片并创建标签时为待检测对象创建相应标签绘制边界框时尽量紧贴对象。标注全部图片比较耗时但工具会自动建议边界框以加速流程 如果每类对象超过 15 张可以在标注 15 张后先训练再使用Suggested tags建议标签功能由已训练模型在未标注图片上自动检测对象你只需确认或拒绝建议的边界框可节省大量时间。训练与快速测试在已标注图片上训练目标检测器时选择Quick Training快速训练作为训练类型。训练会持续几分钟。训练完成后通过Quick Test按钮上传测试图片验证检测效果务必使用之前预留的测试图片而不是训练图片。下图是某次测试结果3 个番茄酱罐被检测出来置信度分别为 38%、35.5% 与 34.6%。逐一测试所有测试图片并观察各次预测的置信度。用预测结果重训练模型与上一项目的图像分类器一样目标检测器在测试中可能表现不佳。改进方式是用模型出错的图片重训练。每次使用快速测试做预测时图片与结果都会被保存可在Predictions标签页找到这些测试图片确认正确的检测、删除错误的检测、补上漏检的对象然后重新训练并再次测试。第二课从 IoT 设备检查库存库存计数与错放检测场景目标检测器可用于检查库存——计数商品或确认商品在正确位置。可在门店各处部署带摄像头的 IoT 设备监控库存优先覆盖补货最关键的货位例如存放少量高价值商品、必须保持满货的货架。假设某货架设计容量为 8 罐番茄酱若目标检测器只识别出 7 罐说明缺了一罐需要补货IoT 设备可通知员工补货甚至给出缺货位置——这对使用机器人补货的场景尤为有用。是否补货还取决于门店与商品热度仅缺一罐时可能不需要补货需要你根据商品、客流量等因素设计专门的补货判定算法。另一种场景是错放商品补货时的人为失误或顾客把商品放回错误位置会导致错误库存出现在货架上。对于罐头等非易腐品这只是不便但对冷冻、冷藏等易腐品脱离冷藏环境时间不明会让商品无法销售。目标检测可以识别错放商品并提醒人工或机器人将其放回正确位置发布模型迭代并获取预测端点在 Custom Vision 门户中打开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 设备以使用新迭代从 Raspberry Pi / 虚拟设备调用检测器Python对象检测代码与图像分类代码非常相似主要区别在于调用的 SDK 方法与返回结果的处理。仓库中完整代码见 5-retail/lessons/2-check-stock-device/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}%)替换为检测代码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方法运行目标检测器与图像分类器每个标签只返回一个结果不同目标检测器会返回多个结果因此必须用阈值过滤掉低概率的检测。实际运行输出示例piraspberrypi:~/stock-counter $ python3 app.py tomato paste: 34.13% tomato paste: 33.95% tomato paste: 35.05% tomato paste: 32.80% 可能需要根据图片情况调整threshold值。拍摄的图片与这些数值也会出现在 Custom Vision 的Predictions标签页中。从 Wio Terminal 调用检测器Arduino CWio Terminal 端的完整实现见 code-detect/wio-terminal/stock-counter/src/main.cpp关键改动包括在main.cpp顶部加入#include vector把classifyImage函数改名为detectStock并同步更新buttonPressed中的调用定义过滤低概率检测的阈值const float threshold 0.3f;定义处理预测结果的函数processPredictions把每个预测的标签与概率打印到串口监视器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中用阈值过滤预测只保留高于阈值的项再交给processPredictions。在detectStock中图片以application/octet-stream内容类型 POST 到预测 URL并携带Prediction-Key请求头收到 HTTP 200 后用 ArduinoJson 解析响应体中的predictions数组。WiFi 凭据、预测 URL、预测密钥与 TLS 根证书配置在 config.h 中项目依赖与编译参数见 platformio.ini使用seeed_wio_terminal开发板、Arduino 框架ArduinoJson 版本 6.17.3。上传运行后把摄像头对准货架并按下 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%理解边界框坐标调用目标检测器时除了标签与概率还会返回边界框——即检测到对象的区域矩形。Custom Vision 的Predictions标签页会把边界框直接画在提交分析的图片上边界框由四个值定义top上、left左、height高、width宽。这些值的范围是 01表示相对于图片尺寸的比例原点 (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使用百分比值可以保证边界框随图片大小等比缩放。边界框与概率还能用来评估检测准确性例如当检测到重叠对象时代码可以分析边界框并忽略低概率的检测上图中一个边界框以 78.3% 置信度预测番茄酱罐另一个更小的框以 64.3% 预测同一对象代码可识别重叠并丢弃概率较低的检测。用设备采集的真实数据重训练与图像分类器一样可以使用IoT 设备采集的真实数据重训练模型让模型在实际场景中表现更好。与分类器不同的是必须逐一检查模型检测出的每个边界框在 Predictions 标签页选中图片红色矩形即检测到的边界框选中某个框查看其标签用角落手柄调整尺寸标签错误时用X按钮删除并重新指派正确标签用垃圾桶按钮删除不包含对象的边界框。完成编辑后图片会从 Predictions 标签页移到 Training Images 标签页重复处理所有预测然后点击Train重训发布新迭代并更新设备端 URL。计数库存处理重叠边界框把检测到的对象数量与边界框结合即可统计货架库存。核心难点是重叠去重同一对象可能被检测出多个有重叠的边界框若重叠面积过大说明这些框指向同一对象需要忽略多余的框。Python打印与绘制边界框在for循环中把print语句改为显示边界框print(f{prediction.tag_name}:\t{prediction.probability * 100:.2f}%\t{prediction.bounding_box})输出示例left、top、width、height 取值 01tomato paste: 33.42% {additional_properties: {}, left: 0.3455171, top: 0.09916268, width: 0.14175442, height: 0.29405564} tomato paste: 34.41% {additional_properties: {}, left: 0.48283678, top: 0.10242918, width: 0.11782813, height: 0.27467814} tomato paste: 31.25% {additional_properties: {}, left: 0.4923783, top: 0.35007596, width: 0.13668466, height: 0.28304994} tomato paste: 31.05% {additional_properties: {}, left: 0.36416405, top: 0.37494493, width: 0.14024884, height: 0.26880276}用Pillow把边界框画到保存的图片上作为调试手段pip3 install pillowfrom PIL import Image, ImageDraw, ImageColorwith Image.open(image.jpg) as im: draw ImageDraw.Draw(im) for prediction in predictions: 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 left scale_left * im.width top scale_top * im.height right scale_right * im.width bottom scale_bottom * im.height draw.rectangle([left, top, right, bottom], outlineImageColor.getrgb(red), width2) im.save(image.jpg)这段代码把 01 的归一化坐标乘以图片实际宽高转换为像素坐标再用红色线条绘制每个边界框并保存覆盖原图。Python用 Shapely 消除重叠上面的示例中边界框有轻微重叠若重叠过大这些框可能代表同一个对象需在计数时忽略。使用Shapely计算交集sudo apt install libgeos-dev # Raspberry Pi 需先安装依赖 pip3 install shapelyfrom shapely.geometry import Polygon定义允许的重叠比例阈值与多边形转换函数overlap_threshold 0.20def 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)])随后两两比较所有预测的边界框用Polygon.intersection计算重叠多边形面积因为阈值是最小边界框面积的百分比而非绝对面积所以取两个框中面积较小者计算最大允许重叠面积若超过则标记删除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)注意两点实现细节一旦某个预测被标记删除内层循环立即break无需再比较由于不能在遍历列表时删除元素先收集到to_delete列表再统一移除。这段重叠过滤逻辑在绘制边界框之前执行因此最终生成的图片只显示去重后的预测。完整实现见 5-retail/lessons/2-check-stock-device/code-count/pi/stock-counter/app.py。 说明课程文档中overlap_threshold取 0.20即 20% 重叠作为示例仓库 code-count/pi/stock-counter/app.py 中实际取值为 0.002需要根据图片与货架场景调优。这只是一个非常基础的重叠处理方式直接删除重叠对中的第一个生产代码应加入更复杂的逻辑例如考虑多对象之间的重叠或处理一个边界框完全包含在另一个之内的情况。Wio Terminal手写矩形重叠算法Wio Terminal 没有 Shapely需自行实现矩形重叠计算完整代码见 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/stock-counter/src/main.cpp。首先定义阈值、点与矩形结构体const float overlap_threshold 0.20f; 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; }overlappingArea计算两个矩形的交集面积不相交时返回 0。再把检测结果中的boundingBox对象转换为Rect右 left width底 top heightRect 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}; }在processPredictions中两两比较外层循环从 i0 开始内层循环从 ji1 开始避免同一对预测被重复比较当重叠面积超过最小边界框面积 × 阈值时标记该预测不通过全部比较通过后才加入passed_predictionsstd::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]); } }最后把通过的预测打印到串口监视器并输出计数值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.);上传运行后把摄像头对准货架、按下 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.这个计数值可以进一步发送到 IoT 服务在库存偏低时触发告警。Wio Terminal 端的摄像头驱动封装见 5-retail/lessons/2-check-stock-device/code-count/wio-terminal/stock-counter/src/camera.h它基于 ArduCAM 与 OV2640 传感器将 JPEG 数据从 FIFO 读入缓冲区后交给detectStock发送。仓库源码速览本模块所有可运行代码均位于 5-retail/lessons/2-check-stock-device 下便于对照学习检测无计数code-detect/pi/stock-counter/app.py、code-detect/virtual-iot-device/stock-counter/app.py、code-detect/wio-terminal/stock-counter/src/main.cpp计数含重叠去重code-count/pi/stock-counter/app.py、code-count/virtual-iot-device/stock-counter/app.py、code-count/wio-terminal/stock-counter/src/main.cpp配置Wio Terminal 的 WiFi/密钥/TLS 证书配置在config.h项目依赖在platformio.iniPython 端把预测 URL 解析为 endpoint、project_id 与 iteration_name 的逻辑parts prediction_url.split(/)可直接复用示例数据集训练/测试图片在 5-retail/lessons/1-train-stock-detector/images作业与挑战两节课各有一个配套作业评分标准见各assignment.md第一课作业assignment.md对比不同域。创建目标检测器时可选的多个域训练并对比它们在你库存场景下的表现在顶部菜单点 Settings 选择新域并 Save changes重训后用新迭代测试描述哪个域效果更好。第二课作业assignment.md把目标检测器部署到边缘。与上一项目把分类器部署到边缘一样将目标检测器导出为紧凑模型compact model在边缘运行并让 IoT 设备调用边缘版本。课程还提供了两个进阶挑战一是检测错放库存——训练模型识别多种对象当检测到错误商品时提醒你更进一步检测同一货架上的并列商品通过限制边界框范围识别错放商品的位置。小结本模块完整呈现了一条云训练 端侧推理的零售库存检测流水线在 Custom Vision 中用 Products on Shelves 域训练目标检测模型发布迭代后通过 REST API 集成到 Raspberry Pi或 CounterFit 虚拟设备与 Wio Terminal 上端侧代码用阈值过滤低置信度预测、用归一化边界框绘制检测结果并通过矩形/多边形重叠计算消除同一对象的重复检测最终得到可靠的货架库存数量。这套方案可以扩展到口罩检测、自动结账、危险检测等更多零售场景也可以配合补货机器人实现货架自动巡检。完成课程后请按照 clean-up.md 清理免费的云资源避免产生不必要的费用。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考