用 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 Terminal(Arduino/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::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); } }在
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输出。上传并运行代码,把摄像头对准货架上的商品,按下 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 的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。运行代码,输出示例:
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)。汇总如下:
| 坐标 | 值 |
|---|---|
| Top | 0.4 |
| Left | 0.4 |
| Height | 0.3 |
| Width | 0.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"].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得到。重写
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)。💁 这是去除重叠的极简实现——直接丢弃重叠对中的前者。生产代码需要更完善的逻辑,例如考虑多个对象间的重叠,或一个边界框完全包含于另一个框的情况。将通过的预测与统计数量输出到串口:
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)])重叠消除与计数逻辑:两层循环两两比较(i与i+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 + 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.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),仅供参考