news 2026/9/16 16:09:16

用 IoT 设备盘点货架库存:Custom Vision 目标检测在零售场景的端到端实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 IoT 设备盘点货架库存:Custom Vision 目标检测在零售场景的端到端实战

用 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)"发布为可用的预测接口。

操作步骤:发布检测器迭代

  1. 打开 CustomVision.ai 并登录(若尚未登录),打开stock-detector项目。

  2. 点击顶部选项卡中的Performance(性能)

  3. 从侧边Iterations(迭代)列表中选择最新迭代。

  4. 点击该迭代的Publish(发布)按钮。

  5. 在弹出的Publish Model对话框中,将Prediction resource设置为上一课创建的stock-detector-prediction资源,迭代名称保留为Iteration2,点击Publish

  6. 发布完成后,点击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。

  7. 同时复制Prediction-Key的值。这是一把安全密钥,调用模型时必须随请求发送;只有携带该密钥的应用才被允许使用模型,其他应用一律被拒绝。

✅ 思考题:当发布了一个新命名的迭代时,IoT 设备端的代码应如何切换到新迭代?(答案并不复杂——只需更新代码中拼接的迭代名与预测 URL,见下文config.h中的PREDICTION_URL。)

从 IoT 设备调用目标检测器

调用检测器与上一课调用图片分类器的代码高度相似,核心差异在于两点:调用的是目标检测专用的 URL(/detect/iterations/<name>/image),且返回结果是多条预测(每个检测对象一条),而分类器每个标签只返回一条结果。因此需要对低置信度的预测做过滤。

Wio Terminal(Arduino/C++)实现

在 Wio Terminal 上,多数代码可直接复用制造(Manufacturing)项目第 2 课的图片分类器代码,只需做如下改造(完整代码见 code-detect/wio-terminal/stock-counter/src/main.cpp):

  1. main.cpp顶部加入#include <vector>

  2. classifyImage函数改名为detectStock(函数定义和buttonPressed中的调用处都要改)。

  3. detectStock上方声明概率阈值,用于过滤低置信度结果:

    const float threshold = 0.3f;

    由于目标检测器会返回多条结果,低于阈值的预测需要被丢弃。

  4. 声明一个处理预测列表的函数,把每条预测打印到串口监视器:

    void processPredictions(std::vector<JsonVariant> &predictions) { for(JsonVariant prediction : predictions) { String tag = prediction["tagName"].as<String>(); float probability = prediction["probability"].as<float>(); char buff[32]; sprintf(buff, "%s:\t%.2f%%", tag.c_str(), probability * 100.0); Serial.println(buff); } }
  5. detectStock中,把遍历预测结果的for循环替换为带阈值过滤的逻辑:

    std::vector<JsonVariant> passed_predictions; for(JsonVariant prediction : predictions) { float probability = prediction["probability"].as<float>(); if (probability > threshold) { passed_predictions.push_back(prediction); } } processPredictions(passed_predictions);

    这段代码遍历所有预测,将概率高于阈值的预测收集到passed_predictions中,再交给processPredictions输出。

  6. 上传并运行代码,把摄像头对准货架上的商品,按下 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.cpp),WiFi 的SSID/PASSWORD、TLS 的CERTIFICATEPREDICTION_URLPREDICTION_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):

  1. 删除原有的三行分类代码:

    results = predictor.classify_image(project_id, iteration_name, image) for prediction in results.predictions: print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%')
  2. 替换为基于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_urlprediction_key从预测 URL 字符串中按/分段解析出endpointproject_iditeration_name,再用ApiKeyCredentials携带Prediction-key构造CustomVisionPredictionClient

  3. 运行代码,输出示例:

    pi@raspberrypi:~/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.4(800 × 0.4 = 320);左边距左边缘 240 像素,即left = 0.4(600 × 0.4 = 240);边界框高 240 像素,即height = 0.3(800 × 0.3 = 240);宽 120 像素,即width = 0.2(600 × 0.2 = 120)。汇总如下:

坐标
Top0.4
Left0.4
Height0.3
Width0.2

由于采用 0~1 的百分比取值,无论图片被缩放到多大,边界框始终从图片的 0.4 处开始(横向与纵向),且占据 0.3 的高度与 0.2 的宽度,天然具备尺度无关性。

边界框还可以与置信度结合,用来评估检测结果的合理性。例如检测器可能对同一对象输出多个重叠框——把一个罐子"检测"到了另一个罐子内部。代码可以检查边界框,识别出这种物理上不可能的完全重叠,并忽略与其它框重叠度显著的对象:

上例中,一个边界框以 78.3% 的置信度预测了一个番茄酱罐头,另一个更小的框以 64.3% 的置信度位于第一个框内部。代码通过检查边界框发现二者完全重叠,由于不可能存在一个罐头位于另一个罐头内部,便可丢弃置信度较低的那个结果。✅ 你也可以思考:是否存在"检测一个对象位于另一个对象内部"是合法场景的情况(例如盒装物品内含独立包装)。

用真实数据重训模型

与图片分类器一样,可以利用 IoT 设备采集的真实数据对目标检测模型进行重训,这能确保模型在实际部署环境中表现良好。但与分类器不同,目标检测的样本不能简单打标签——必须逐个人工审核模型标注的边界框:框错了对象就删除,框的位置不对就调整。

操作步骤:重训模型

  1. 先用 IoT 设备采集一批多样化的图片。
  2. Predictions选项卡中选择一张图片,红色矩形会标出检测到的边界框。
  3. 逐一审核每个边界框:选中后弹出标签信息;用边界框四角的控制手柄调整大小;若标签错误,点击X删除并重新添加正确标签;若边界框内没有对象,用垃圾桶按钮删除。
  4. 完成审核后关闭编辑器,图片会从Predictions选项卡移动到Training Images选项卡。对所有预测图片重复该过程。
  5. 点击Train按钮重训模型。训练完成后发布新迭代,并把 IoT 设备中的预测 URL 更新为新迭代对应的地址。
  6. 重新部署代码并在设备上测试。

剔除重叠框,统计库存数量

将检测到的对象数量与边界框结合起来,就可以统计货架上的库存。但直接统计predictions的条数并不准确:同一对象常被多个重叠边界框重复命中。以下图为例,各边界框之间存在少量重叠;若重叠显著增大,很可能表示它们指向的是同一个对象,统计时必须忽略这类重叠框。

Wio Terminal 的 C++ 实现

完整代码见 code-count/wio-terminal/stock-counter/src/main.cpp,实现思路如下:

  1. processPredictions函数上方定义重叠阈值:

    const float overlap_threshold = 0.20f;

    该值定义了边界框被视为"同一对象"前允许的重叠百分比,0.20 即允许 20% 重叠。

  2. 定义点与矩形结构,以及面积与重叠面积的计算函数:

    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。

  3. 声明一个把预测结果中的边界框转换为Rect的函数:

    Rect rectFromBoundingBox(JsonVariant prediction) { JsonObject bounding_box = prediction["boundingBox"].as<JsonObject>(); float left = bounding_box["left"].as<float>(); float top = bounding_box["top"].as<float>(); float width = bounding_box["width"].as<float>(); float height = bounding_box["height"].as<float>(); Point topLeft = {left, top}; Point bottomRight = {left + width, top + height}; return {topLeft, bottomRight}; }

    右边坐标由left + width得到,下边坐标由top + height得到。

  4. 重写processPredictions:对预测两两比较,若某个预测与后续任一预测的重叠面积超过阈值,则标记丢弃。注意重叠阈值是百分比,必须乘以较小边界框的面积来判定——检查的是"重叠是否超过较小框面积的给定百分比",而不是整张图片面积的百分比:

    std::vector<JsonVariant> 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)。💁 这是去除重叠的极简实现——直接丢弃重叠对中的前者。生产代码需要更完善的逻辑,例如考虑多个对象间的重叠,或一个边界框完全包含于另一个框的情况。

  5. 将通过的预测与统计数量输出到串口:

    for(JsonVariant prediction : passed_predictions) { String boundingBox = prediction["boundingBox"].as<String>(); String tag = prediction["tagName"].as<String>(); float probability = prediction["probability"].as<float>(); 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 服务,用于库存过低告警。

Python(Shapely)实现

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)])

重叠消除与计数逻辑:两层循环两两比较(ii+1...n比较),用Polygon.intersection求出重叠多边形并计算其面积,再与较小框面积乘以阈值的结果比较,超过则标记删除;由于不能在遍历列表的同时删除元素,先收集到to_delete,最后统一移除:

to_delete = [] for i in range(0, len(predictions)): polygon_1 = create_polygon(predictions[i]) for j in range(i+1, 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(f'Counted {len(predictions)} stock items')

紧接其后的绘图代码把通过筛选的边界框绘制到图片上(left + widthtop + 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.h)
    • Wio Terminal 计数:code-count/wio-terminal/stock-counter
    • Python 检测/计数: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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:09:13

OpenMontage实战:开源视频拼接工具安装、导出参数与自动化全解析

1. OpenMontage到底是什么&#xff1a;它解决的是哪一类视频处理的痛先说个我自己的例子。前阵子我要把一块活动上拍的二十几段零散素材拼成一支完整的回顾短片&#xff0c;素材来源五花八门&#xff1a;有人用手机竖屏拍的&#xff0c;有相机横屏录的&#xff0c;还有隔了几天…

作者头像 李华
网站建设 2026/9/16 16:09:05

O2O优惠券核销预测:时空建模与三维特征工程实战

简介&#xff1a;本资源是天池新人实战赛「O2O优惠券使用预测」的完整特征工程与建模实现方案&#xff0c;面向数据挖掘初学者、竞赛入门者及推荐系统实践者&#xff0c;聚焦用户消费行为建模中的关键难点——如何量化用户领券后核销意愿。资源包含5个核心文件&#xff08;3个P…

作者头像 李华
网站建设 2026/9/16 16:08:59

OpenHarmony与Flutter动画集成实战指南

1. 项目背景与核心挑战在OpenHarmony生态中集成Flutter页面转场动画&#xff0c;开发者面临三个关键挑战&#xff1a;首先是跨平台动画性能的保障&#xff0c;需要确保在OpenHarmony的ArkUI渲染引擎与Flutter的Skia引擎间保持60fps的流畅度&#xff1b;其次是原生系统动效规范的…

作者头像 李华
网站建设 2026/9/16 16:08:23

Spring框架核心注解详解与最佳实践

1. Spring框架注解体系概述在Spring框架的实际开发中&#xff0c;注解(Annotation)已经成为现代Java开发的核心工具。相比传统的XML配置方式&#xff0c;注解提供了更直观、更简洁的代码组织方式。我刚开始接触Spring时&#xff0c;面对各种注解也是一头雾水&#xff0c;但随着…

作者头像 李华