IoT-For-Beginners 实战:将库存目标检测器以紧凑模型部署到 Azure IoT Edge 并接入你的 IoT 设备
2026/9/15 12:45:59 网站建设 项目流程

IoT-For-Beginners 实战:将库存目标检测器以紧凑模型部署到 Azure IoT Edge 并接入你的 IoT 设备

【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners

导读

本篇文章围绕 IoT-For-Beginners 零售项目中“在边缘使用目标检测器”这一实战任务展开,目标是把上一课在 Custom Vision 云端训练好的库存目标检测器(stock-detector)导出为紧凑模型(compact model),打包成容器部署到Azure IoT Edge边缘设备上,并让 Wio Terminal、Raspberry Pi 或虚拟 IoT 设备改为调用边缘版推理接口,从而在本地网络内完成货架库存检测与计数。读完本文,你将掌握从“云端 API 调用”迁移到“边缘容器推理”的完整链路:切换紧凑域、导出 DockerFile、构建并推送容器、编写部署清单、验证边缘模块运行,以及基于边界框(bounding box)的库存计数算法。

任务目标:把上一次的“云端部署”复制到边缘

本任务(translations/en 版 assignment 原文,孟加拉语版见 translations/bn/5-retail/lessons/2-check-stock-device/assignment.md)的要求非常明确:

上一个项目中,你已经把图像分类器部署到了边缘。现在对你的目标检测器做同样的事情:将其导出为紧凑模型,在边缘运行它,并让 IoT 设备访问边缘版本。

也就是说,零售项目第五课(5-retail/lessons/2-check-stock-device/README.md)教你从 IoT 设备调用云端Custom Vision 预测端点来识别货架上的商品;而本任务则要求你把同样的能力整体“下沉”到边缘设备,这与制造项目中第 3 课《在边缘运行水果检测器》(4-manufacturing/lessons/3-run-fruit-detector-edge/README.md)的做法一脉相承——只是把“图像分类器”替换为“目标检测器”。

任务自带的评分标准(Rubric)给出了三个递进层级,也是验收时的主要依据:

标准优秀(Exemplary)合格(Adequate)需要改进(Needs Improvement)
将目标检测器部署到边缘正确使用紧凑域(compact domain),成功导出目标检测器,并在边缘上成功运行正确使用紧凑域并导出了目标检测器,但未能在边缘上运行无法使用正确的紧凑域、无法导出目标检测器、也无法在边缘运行

可见,要拿到“优秀”,三个环节缺一不可:正确的紧凑域成功导出模型在边缘成功运行并被设备访问

为什么要部署到边缘:从云端调用到边缘推理

在零售项目第 2 课中,库存检测的默认架构是:IoT 设备拍照 → 把图片上传到 Custom Vision 云端预测端点 → 返回标签、概率与边界框。这种模式直观但有几个固有代价,而边缘计算(Edge Computing)正是针对这些问题的解法。边缘计算的核心思想在制造项目第 3 课中有系统阐述:把处理放在离数据产生位置最近的本地网络设备上,而不是跨互联网调用云端服务。它的主要优势包括:

  • 速度:同网段内的调用远快于跨互联网的往返,尤其适合对时延敏感的场景;
  • 低流量与低成本:图片不再反复上传,云端资源消耗下降;
  • 隐私与安全:图片数据始终停留在本地网络,不会被上传到云端存储,适合监控画面等敏感数据;
  • 离线可用:在弱网甚至断网环境中仍然可以完成推理。

因此,本任务的完整链路是:在云端用 Custom Vision 训练目标检测器 → 用紧凑域重新训练 → 导出为容器镜像 → 推送到 Azure Container Registry → 通过 IoT Edge 部署到边缘设备 → IoT 设备把预测请求指向边缘设备而非云端端点。

第一步:切换到正确的紧凑域并重新训练

Custom Vision 可以生成两类模型:标准模型(standard)紧凑模型(compact)。紧凑模型通过多种技术压缩模型体积,使其小到可以被下载并部署在 IoT 设备/边缘设备上。训练目标检测器时,第五课第 1 课(5-retail/lessons/1-train-stock-detector/README.md)要求项目域选择Products on Shelves(货架商品),这是专门针对货架库存检测优化的域。部署到边缘前,需要把项目域切换为Products on Shelves (compact)

  1. 打开 CustomVision.ai 并登录,进入stock-detector项目;
  2. 点击Settings(⚙ 齿轮图标);
  3. Domains列表中选择Products on Shelves (compact)(对应分类器项目则选如Food (compact),可参考制造项目第 3 课中的切换操作);
  4. Export Capabilities下确保选中Basic platforms (Tensorflow, CoreML, ONNX, ...)
  5. 点击页面底部的Save Changes保存;
  6. 点击Train按钮重新训练,选择Quick training(快速训练)。

注意:切换域本质上是“用同一批训练数据、以新的域重新训练一个新模型”,训练完成后会得到一个新的 iteration(迭代)。

第二步:把目标检测器导出为容器

模型用紧凑域训练完成后,需要从 Custom Vision 的Performance页签导出为容器:

  1. Performance页签中找到使用紧凑域训练的最新迭代;
  2. 点击顶部的Export按钮;
  3. 选择DockerFile,并按边缘设备的架构选择版本:
    • 边缘设备是 Linux 电脑、Windows 电脑或虚拟机:选择Linux版本;
    • 边缘设备是 Raspberry Pi:选择ARM (Raspberry Pi 3)版本;
  4. 点击Export生成相关文件,再点击Download下载为 zip 压缩包;
  5. 解压到本地目录。

导出的内容是一个DockerFile(容器构建指令)加上容器内运行的应用程序代码——该应用会加载 Custom Vision 模型并暴露一个 REST API 供调用。它与云端版本的 API 形态一致,因此设备端只需把端点从云端 URL 换成边缘设备的地址即可(详见制造项目第 3 课的说明)。

第三步:构建容器并推送到容器注册中心

容器构建与分发需要 Docker 与一个容器注册中心(container registry)。作为对比参考,制造项目第 3 课给出了基于fruit-quality-detector资源组的完整命令序列,本任务直接复用同一套流程:

# 1. 创建 Azure Container Registry(Basic SKU) az acr create --resource-group stock-detector \ --sku Basic \ --name <Container registry name> # 2. 登录注册中心 az acr login --name <Container registry name> # 3. 开启 admin 模式以生成密码 az acr update --admin-enabled true \ --name <Container registry name> # 4. 生成密码并复制 PASSWORD 值 az acr credential renew --password-name password \ --output table \ --name <Container registry name>

进入解压后的模型目录,执行构建与推送:

# 构建并打标签;Raspberry Pi 用 linux/armhf,其余 Linux/Windows/VM 用 linux/amd64 docker build --platform <platform> -t <Container registry name>.azurecr.io/detector:v1 . # 推送到容器注册中心 docker push <Container registry name>.azurecr.io/detector:v1 # 验证仓库中已存在镜像 az acr repository list --output table --name <Container registry name>

提示:如果在运行 IoT Edge 的同一台设备上执行构建,可以省略--platform <platform>(默认取当前平台);在 Linux / Raspberry Pi OS 上可能需要在命令前加sudo

第四步:编写部署清单并部署到 IoT Edge 设备

要先把边缘设备注册到 IoT Hub,并为其安装 IoT Edge 运行时(Raspberry Pi 可直接运行;Windows 需借助 Linux 虚拟机;macOS 可创建带 IoT Edge 的 Linux 云虚拟机,具体可参照制造项目第 3 课的注册与安装章节)。注册命令与普通设备基本相同,只需加上--edge-enabled标志:

az iot hub device-identity create --edge-enabled \ --device-id stock-detector-edge \ --hub-name <hub_name> az iot hub device-identity connection-string show --device-id stock-detector-edge \ --output table \ --hub-name <hub_name>

然后创建一个名为deployment.json部署清单——一个列出将要部署到边缘设备的模块的 JSON 文档。把<Container registry name><Container registry password><hub_name>替换为实际值(清单结构与制造项目第 3 课中给出的deployment.json一致,区别仅在于把ImageClassifier模块换成目标检测器模块):

{ "content": { "modulesContent": { "$edgeAgent": { "properties.desired": { "schemaVersion": "1.1", "runtime": { "type": "docker", "settings": { "minDockerVersion": "v1.25", "loggingOptions": "", "registryCredentials": { "ClassifierRegistry": { "username": "<Container registry name>", "password": "<Container registry password>", "address": "<Container registry name>.azurecr.io" } } } }, "systemModules": { "edgeAgent": { "type": "docker", "settings": { "image": "mcr.microsoft.com/azureiotedge-agent:1.1", "createOptions": "{}" } }, "edgeHub": { "type": "docker", "status": "running", "restartPolicy": "always", "settings": { "image": "mcr.microsoft.com/azureiotedge-hub:1.1", "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"5671/tcp\":[{\"HostPort\":\"5671\"}],\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}" } } }, "modules": { "ObjectDetector": { "version": "1.0", "type": "docker", "status": "running", "restartPolicy": "always", "settings": { "image": "<Container registry name>.azurecr.io/detector:v1", "createOptions": "{\"ExposedPorts\": {\"80/tcp\": {}},\"HostConfig\": {\"PortBindings\": {\"80/tcp\": [{\"HostPort\": \"80\"}]}}}" } } } } }, "$edgeHub": { "properties.desired": { "schemaVersion": "1.1", "routes": { "upstream": "FROM /messages/* INTO $upstream" }, "storeAndForwardConfiguration": { "timeToLiveSecs": 7200 } } } } } }

在清单所在目录执行部署:

az iot edge set-modules --device-id stock-detector-edge \ --content deployment.json \ --hub-name <hub_name>

验证边缘模块是否运行

SSH 连接到边缘设备后执行以下命令确认模块状态与日志:

iotedge list iotedge logs ObjectDetector

iotedge list的输出中应能看到ObjectDetectoredgeAgentedgeHub三个模块均为runningiotedge logs则能看到模型加载成功与 Flask 服务在0.0.0.0:80上监听的日志。之后可以用 curl 做一次冒烟测试(注意:边缘推理走内部网络,无需携带Prediction-Key,安全性由内部网络策略保证,而非公有端点 + API 密钥):

curl --location \ --request POST 'http://<IP address or name>/image' \ --header 'Content-Type: image/png' \ --data-binary '@<file_Name>'

返回 JSON 中的predictions数组即为检测结果,其中包含tagNameprobability以及boundingBox

第五步:让 IoT 设备调用边缘版目标检测器

目标检测器与图像分类器在设备端的调用代码几乎一致,主要区别在于:请求的 URL(从云端预测端点换成边缘设备地址)以及返回结果的形式(分类器每个标签一个结果,检测器返回多个带边界框的结果,需要按概率阈值过滤)。

Raspberry Pi / 虚拟 IoT 设备(Python)

对照单板电脑目标检测指南,把原来调用classify_image的三行代码:

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}%')

这段代码调用了 SDK 的detect_image方法运行目标检测,把所有概率高于阈值(0.3)的预测收集起来打印到控制台。仓库中完整的可运行版本位于 code-detect/pi/stock-counter/app.py(虚拟设备版本见 code-detect/virtual-iot-device),其从预测 URL 中解析 endpoint、project_id、iteration_name 的方式与分类器项目完全一致。运行输出示例:

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(Arduino/C++)

对照 Wio Terminal 目标检测指南,改造要点为:

  1. main.cpp顶部加入#include <vector>
  2. classifyImage函数改名为detectStock(函数定义与buttonPressed中的调用都要改);
  3. 声明概率阈值常量:const float threshold = 0.3f;——因为检测器会返回多个结果,低概率项必须被过滤;
  4. 声明processPredictions函数,遍历预测并把tagName与概率(乘以 100 后保留两位小数)打印到串口监视器;
  5. detectStock中先遍历predictions,把probability > threshold的项收集进passed_predictions,再交给processPredictions处理。

串口输出示例:

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需要根据你的实际图片效果进行调整。检测到 4 个番茄酱罐头却只有 30%~36% 的概率,说明模型置信度偏低,此时应返回 Custom Vision 的Predictions页签检查边界框并考虑用这些真实图片重新训练模型。

第六步:结合边界框统计库存数量

仅仅打印“检测到了什么”还不够——本任务所属课程(5-retail/lessons/2-check-stock-device/README.md)的最终目标是用检测数量 + 边界框统计货架库存。

理解边界框的四元组

边界框用 4 个值定义:top、left、height、width,取值均为 0~1,表示占图像尺寸的百分比;原点 (0,0) 在图像左上角。例如一张 600×800 像素的图,若边界框左上角位于距顶部 320px、距左侧 240px,尺寸为高 240px、宽 120px,则对应top=0.4left=0.4height=0.3width=0.2

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

使用 0~1 的百分比值意味着无论图像缩放成多大,边界框的相对位置与相对尺寸都不变。这一点正是计数与去重逻辑的基础。

为什么必须处理重叠的边界框

目标检测器可能对同一个物体输出多个高度重叠的边界框(例如把一个罐头同时框了两次)。判定“两个框是不是同一个物体”的常用做法是计算重叠面积:若重叠面积超过较小框面积的一定百分比阈值,就认为它们指向同一物体,只保留一个。课程采用的默认阈值为20%overlap_threshold = 0.20),即两个边界框的重叠超过较小框面积的 20% 即视为重复。

Python 版本:用 Shapely 计算重叠并画框

单板电脑计数指南给出了完整的计数方案:

  1. 先安装依赖(Raspberry Pi 需要先安装系统库libgeos-dev):

    sudo apt install libgeos-dev pip3 install shapely pip3 install pillow # 用于在图片上绘制边界框
  2. 定义overlap_threshold = 0.20,并把预测的边界框转换为 Shapely 多边形:

    from shapely.geometry import Polygon 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)])
  3. 两两比较所有预测,重叠面积超过overlap_threshold × 较小框面积的标记为待删除,最后统一删除并输出计数:

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

    注意两点设计:一是“阈值必须是较小框面积的百分比”而非整图面积的百分比,这样才与框的大小无关;二是不能在遍历列表的同时删除元素,所以先把待删除项收集到to_delete再统一移除。

  4. 作为调试辅助,还可以用 Pillow 把边界框直接画到image.jpg上(用draw.rectangle按 0~1 值乘以图像宽高换算成像素坐标,红色 2px 描边),这样能直观地核对检测质量。从源码结构看,code-count/pi 与 code-count/virtual-iot-device 目录即对应这一完成态实现。

Wio Terminal 版本:手写矩形几何计算重叠

Wio Terminal 端无法直接使用 Shapely,因此Wio Terminal 计数指南用结构体与纯函数实现了相同的几何逻辑,完整代码见 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; } 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}; }

processPredictions内采用双层循环两两比较:内层循环从i+1开始,避免同一对框被重复比较;对每一对计算重叠面积,若overlap > overlap_threshold * smallest_area则判定该预测未通过;通过全部检查的预测加入passed_predictions,最后打印明细与计数:

Serial.print("Counted "); Serial.print(passed_predictions.size()); Serial.println(" stock items.");

串口输出示例(注意其中已包含归一化后的边界框 JSON):

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.

“Counted 4 stock items.”这条输出即可进一步上报给 IoT 服务,用于在库存过低时触发补货告警。

课程源码也明确提示:这种去重方式非常朴素——只是删掉重叠对中靠前的一个。生产级代码应进一步考虑多个物体的连环重叠、某个框被另一个框完全包含等更复杂的情形。

边缘部署后的注意事项

把模型部署到边缘后有两个与云端方案显著不同的点,需要在本文任务中一并理解:

  1. 重新训练的数据回流问题:边缘推理的图片不会出现在 Custom Vision 的Predictions页签中,因为图片从未离开本地网络。这是隐私与离线能力的代价——想要继续用真实数据改进模型,就必须自己实现图片的采集、存储与人工复标流程(详见制造项目第 3 课的“Model retraining”一节)。
  2. 迭代更新机制:每次重新训练得到新 iteration 后,在云端重新导出容器、推送镜像并更新部署清单即可完成模型升级;而设备端是否要改代码,取决于新端点的 URL 是否变化。

另外,本任务属于零售项目的收尾环节。按照课程提示(5-retail/lessons/2-check-stock-device/README.md),请确保先完成本文任务再清理云资源,清理步骤可参考 clean-up.md。若在真实环境中复现,还需要留意:Azure Container Registry 并非免费服务,实验结束后应及时清理以避免持续计费。

挑战与延伸

完成基础部署后,可以进一步尝试课程附带的挑战(来自5-retail/lessons/2-check-stock-device/README.md):

  • 错放商品检测:用多种商品训练模型,然后让应用在检测到“不该出现在该货架上的商品”时发出告警;
  • 相邻错位检测:对同一货架上的并排商品定义边界框的合理范围,当某个商品被放到错误位置(边界框越界)时进行识别。

这两个方向正好可以把本任务练就的“边缘推理 + 边界框处理”能力延伸到更贴近真实零售场景的应用中。

【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询