将库存目标检测器部署到边缘:从 Custom Vision 紧凑模型到 IoT 设备端库存计数
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本篇技术指南基于 IoT-For-Beginners 仓库零售项目(5-retail)第 2 课《Check stock from an IoT device》的课后作业展开。该作业要求你将此前在云端训练好的库存目标检测器,像上一个项目处理图像分类器那样,以紧凑模型(compact domain)形式导出并在边缘设备上运行,然后让 Wio Terminal、Raspberry Pi 或虚拟 IoT 设备直接调用边缘版本完成货架库存检测与计数。读完本文,你将掌握从 Custom Vision 导出边缘可部署模型、通过 Azure IoT Edge 部署容器模块、从 IoT 设备以 REST/SDK 方式调用边缘推理接口,以及结合边界框(bounding box)与重叠消除算法统计库存数量的完整实战路径。
1. 作业定位:把分类器的边缘部署经验迁移到目标检测器
作业原文(translations/cs/5-retail/lessons/2-check-stock-device/assignment.md)给出的任务非常明确:在上一项目中,你已经把自己的图像分类器部署到了边缘;现在要对目标检测器做同样的事情——将其导出为紧凑模型并在边缘运行,且边缘版本必须能被你的 IoT 设备访问。
这并非从零开始的边缘部署课程,而是建立在两条既有知识链上的综合实践:
- 边缘部署链路:来自 4-manufacturing 项目的第 3 课 Run your fruit detector on the edge,其中完整讲述了边缘计算概念、Azure IoT Edge 设备注册、IoT Edge runtime 安装、模型导出、容器化部署以及从 IoT 设备调用边缘模型的流程;
- 目标检测器链路:来自零售项目第 1 课 Train a stock detector,其中完成了 Custom Vision 目标检测器的训练;以及第 2 课 Check stock from an IoT device 中从 IoT 设备调用检测器、解析边界框、统计库存的代码实践。
作业的考核点恰好落在两条链路的交叉处:能够正确使用紧凑型域、能够导出目标检测器、能够在边缘运行并被 IoT 设备访问。下面我们逐层拆解。
2. 为什么是"目标检测器"而不是"图像分类器"
在动手部署之前,先明确两者在推理结果上的本质差异,这决定了边缘调用代码的结构。
- 图像分类器:对整张图片输出每个标签的概率,每个标签只有一个置信度结果;
- 目标检测器:在图片中找出一个或多个对象,每个检测结果都附带标签、概率和边界框(bounding box),即对象在图片中的位置范围。
如上图所示,检测器在货架图片上圈出 4 个番茄酱罐头,并分别给出 35.8%、33.5%、25.7%、16.6% 的概率。正因为返回的是"多个对象 + 各自位置",你才能进一步做库存计数——这是分类器做不到的。
这个差异直接影响调用代码:分类器调用classify_image,而检测器必须调用detect_image,并且由于检测器会对同一标签返回多个候选结果,必须用阈值过滤低概率项。仓库中的完整可运行版本见 code-detect/pi/stock-counter/app.py。
3. 边缘部署的目标:把推理从云端移到本地网络
边缘计算的核心思路,是把数据处理和 AI 推理放到离数据产生地尽可能近的位置——也就是你自己的内部网络,而不是跨互联网调用云端。
在 4-manufacturing/lessons/3-run-fruit-detector-edge/README.md 中,边缘部署被总结为以下几点收益,这也正是本次作业的动机:
- 速度:推理发生在同一内部网络,数据不再跨洋往返,网络拥塞风险更低;
- 弱网可用:在连接不稳定或持续联网成本过高的场景下仍可工作;
- 降低成本:数据采集、存储、分析与触发动作在边缘完成,减少云端服务用量;
- 隐私与安全:图像数据停留在本地网络,分析完成后不必上云留存,降低泄露风险。
结合零售场景,这意味着:门店内摄像头拍摄的货架画面可以在本店边缘设备上直接完成推理,只有"库存不足""出现异常商品"这类结论或少量分析数据才需要上报。当货架可放 8 罐番茄酱而检测器只数出 7 罐时,IoT 设备即可向店员或补货机器人发出补货通知,甚至结合边界框位置信息定位空缺处——这正是本课 README 所描述的核心用例。
当然,边缘也有短板:扩容需要人工增加设备、冗余与灾备配置成本高、系统维护需要自己负责。生产系统中通常按需在云端与边缘之间做混合部署。
4. 前置条件:使用紧凑型域(compact domain)训练检测器
作业评测标准的第一条就是"能够使用正确的紧凑型域"。这是 Custom Vision 导出边缘模型的硬性前提:只有使用紧凑型域(compact domain)训练的迭代,才支持导出为可在本地运行的模型文件或容器镜像。
回顾训练过程:在零售项目第 1 课中创建stock-detector项目时,项目类型选择Object Detection,域选择Products on Shelves(针对货架商品检测优化)。如果你的训练项目使用的域不支持导出,则需要重新创建一个使用紧凑型域的项目并重新训练——这正是作业中"使用正确的紧凑型域"这一考核点的意义所在。
5. 导出紧凑模型并容器化
模型导出在 Custom Vision 门户完成,流程与上一项目导出分类器一致:
- 打开
stock-detector项目,进入顶部Performance(性能)标签页; - 在侧边 Iterations 列表中选择最新迭代;
- 点击该迭代的Export(导出)按钮,按目标平台选择导出格式(如 Dockerfile / TensorFlow 等);
- 将导出的工件(通常以容器镜像形式)推送到自己的容器仓库,供边缘设备拉取。
提示:如果你在 Raspberry Pi 这类 ARM 架构设备上运行,务必选择与 CPU 架构匹配的镜像标签,否则容器无法启动。
容器化部署的具体操作(构建镜像、推送到 Azure Container Registry、通过 IoT Edge 模块下发到设备)可回看 4-manufacturing/lessons/3-run-fruit-detector-edge/README.md 中的 "Export your model" 与 "Prepare your container for deployment" 章节。Custom Vision 导出的检测模型容器对外暴露的 REST API 与云端版本一致,区别仅在 endpoint 指向边缘设备而非 Azure 认知服务地址。
6. 注册边缘设备并部署运行
在边缘设备上跑模型前,需要先在 Azure IoT Hub 中注册一台 IoT Edge 设备。命令与注册普通设备类似,多一个--edge-enabled标志:
az iot hub device-identity create --edge-enabled \ --device-id stock-detector-edge \ --hub-name <hub_name>随后取得该设备的连接字符串,在边缘设备上安装 IoT Edge runtime 并配置连接字符串。runtime 就绪后,即可把第 5 步的检测模型容器作为 IoT Edge 模块下发部署。部署完成后,边缘设备上就运行了一个"本地版 Custom Vision 推理服务"。
之后 IoT 设备只需把推理 endpoint 从云端的https://<location>.api.cognitive.microsoft.com/customvision/...换成边缘设备的地址,其余调用方式不变。这样既保留了 Custom Vision 训练流程,又获得了边缘推理的速度、成本与隐私收益。
7. 从 IoT 设备调用边缘版目标检测器
完成边缘部署后,IoT 设备侧调用代码与调用云端检测器基本相同。下面分别给出 Python 与嵌入式两条路径。
7.1 Raspberry Pi / 虚拟设备:Python SDK
在 single-board-computer-object-detector.md 中,核心改动是把classify_image换成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运行目标检测,把所有概率高于阈值的预测收集起来打印。与分类器"每标签只返回一个结果"不同,检测器会返回大量候选框,低概率项必须过滤。完整可运行代码见 code-detect/pi/stock-counter/app.py,其中还展示了如何从预测 URL 中解析 endpoint、project_id 与 iteration_name:
parts = prediction_url.split('/') endpoint = 'https://' + parts[2] project_id = parts[6] iteration_name = parts[9]在虚拟设备(CounterFit)场景下,该代码对应 code-detect/virtual-iot-device 目录。
典型输出:
pi@raspberrypi:~/stock-counter $ python3 app.py tomato paste: 34.13% tomato paste: 33.95% tomato paste: 35.05% tomato paste: 32.80%提示:
threshold(默认 0.3)需要根据实际图片和模型表现调整,过低会混入误检,过高会漏检。
7.2 Wio Terminal:Arduino/C++
在 wio-terminal-object-detector.md 中,检测逻辑通过 HTTP POST 直接调用预测接口实现。仓库中的完整代码见 code-count/wio-terminal/stock-counter/src/main.cpp:
void detectStock(byte *buffer, uint32_t length) { HTTPClient httpClient; httpClient.begin(client, PREDICTION_URL); httpClient.addHeader("Content-Type", "application/octet-stream"); httpClient.addHeader("Prediction-Key", PREDICTION_KEY); int httpResponseCode = httpClient.POST(buffer, length); if (httpResponseCode == 200) { String result = httpClient.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, result.c_str()); JsonObject obj = doc.as<JsonObject>(); JsonArray predictions = obj["predictions"].as<JsonArray>(); 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); } httpClient.end(); }要点包括:通过Prediction-Key请求头传递访问密钥、以application/octet-stream上传 JPEG 字节流、用 ArduinoJson 解析返回的predictions数组并按阈值过滤。阈值常量声明在 main.cpp:
const float threshold = 0.0f; const float overlap_threshold = 0.20f;8. 进阶实战:用边界框完成货架库存计数
作业的隐含目标是让边缘检测结果真正可用,而边界框正是把"检测"升级为"计数"的关键。
8.1 边界框的四个值
边界框由四个 0–1 之间的比例值定义:top、left、height、width。坐标系原点在图片左上角,数值表示占图片尺寸的比例,因此无论图片缩放成什么分辨率,比例值都不会失真。例如某张 800 像素高的图片中,边界框距顶部 320 像素,则 top = 0.4。
结合概率与边界框,还可以做更聪明的判断:当两个边界框高度重叠(例如一个框完全套在另一个框里)时,物理上一个罐头不可能包含另一个罐头,代码可以据此忽略低概率的重复检测。判断规则在本课 README 的 "Bounding boxes" 一节有详细讲解。
8.2 Python:打印、绘制与重叠消除
先在打印语句中带上边界框:
print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%\t{prediction.bounding_box}')再用 Pillow 把边界框画回图片上便于调试,见 single-board-computer-count-stock.md:
with 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], outline=ImageColor.getrgb('red'), width=2) im.save('image.jpg')比例坐标乘以图片实际宽高即得像素坐标,绘制结果如下:
随后用 Shapely 计算两两边界框的交叠面积并删除重叠超限的重复检测。核心逻辑见 code-count/pi/stock-counter/app.py:
overlap_threshold = 0.002 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)]) 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')注意重叠阈值不是绝对面积,而是以两个框中较小者的面积为分母的比例(overlap_threshold * smallest_area),这样判断的是"小框被覆盖了多少比例",与图片绝对尺寸无关。教程版阈值为 0.20(20%),仓库代码中 code-count/pi/stock-counter/app.py 使用的overlap_threshold = 0.002是面向该示例图片调优后的更严格取值,你需要针对自己的摄像头画面调整。
8.3 Wio Terminal:纯 C++ 的矩形交叠计算
嵌入式端没有 Shapely,仓库用纯 C++ 实现了同样的思路,见 wio-terminal-count-stock.md 与 main.cpp:
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; }rectFromBoundingBox从 JSON 的boundingBox对象取出 left/top/width/height 并构造成矩形,右下角坐标由 left+width、top+height 计算得到。processPredictions中采用两两比较:第 i 个框只与第 i+1 个及其后的框比较(避免重复比较),重叠超过overlap_threshold * smallest_area即判定为同一对象并剔除,最后打印存活数量:
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 服务,在库存低于阈值时触发补货告警。
9. 用边缘采集的真实数据重训模型
与图像分类器一样,用 IoT 设备在真实光照、角度、货架条件下采集的图像可以重训目标检测器,使其在边缘场景下表现更稳。差异在于:目标检测器重训时不能只给图片打标签,而必须逐一核对模型输出的每个边界框——框住了错误对象就删除、位置不准就调整、漏框则补充,全部修正后再点击Train重训。重训并发布新迭代后,导出新的边缘镜像并重新部署到边缘设备,即完成一轮"边缘数据 → 云端重训 → 边缘再部署"的闭环。
10. 作业评测标准
作业的考核量表(原文见 assignment.md)如下:
| 标准 | 优秀 | 合格 | 待改进 |
|---|---|---|---|
| 将目标检测器部署到边缘 | 能够使用正确的紧凑型域,导出目标检测器并在边缘运行 | 能够使用正确的紧凑型域并导出目标检测器,但无法在边缘运行 | 无法使用正确的紧凑型域,无法导出目标检测器,也无法在边缘运行 |
对照该表自查即可确认:紧凑型域训练(第 4 节)→ 模型导出(第 5 节)→ 边缘运行与 IoT 设备访问(第 6、7 节)三个环节缺一不可。
11. 收尾:别忘了清理云资源
这是零售项目中的最后一课。按照本课 README 的提醒:完成作业前你需要保留云服务,因此务必先完成部署与验证,然后再按 clean-up.md 的指引清理 Custom Vision、IoT Hub 等云资源,避免产生持续费用。
12. 挑战延伸
完成基础作业后,教程还给出了两条延伸挑战:
- 错误商品检测:在模型上训练多种对象,当检测到不属于该货架的商品(例如番茄酱货架上的玉米罐头)时发出告警;
- 错位检测:对同一货架上并排摆放的多种商品,通过给边界框设定位置范围限制,判断商品是否被放错位置。
这两条挑战可以进一步把第 8 节的边界框运算能力(位置、重叠、坐标比例)与第 5 节起的边缘部署链路组合起来,形成一套完整的门店智能补货与理货方案。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考