☰
IoT 目标检测实战:树莓派 + Custom Vision 从分类到货架盘点一次跑通
2026/10/5 1:51:08 网站建设 项目流程

IoT 目标检测实战:树莓派 + Custom Vision 从分类到货架盘点一次跑通

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

货架上还剩几罐番茄酱?这是 IoT-For-Beginners 项目零售场景中一个很实际的问题,而答案交给目标检测模型来回答:把摄像头对准货架拍一张照片,图片发往已经发布的 Custom Vision 检测器,程序就能把检测到的每一件商品的标签和置信度打印出来。如果一层货架最多放 8 罐却只检测到 7 罐,说明缺了一罐,该触发补货了;如果混进来一罐不属于这排货架的婴儿玉米,检测器同样会把它暴露出来。

从"分类一张图"到"盘点一货架"

这个项目分两段走:检测器(stock-detector)的建模、训练和发布在 Custom Vision 门户里完成,属于"云端"部分;本文覆盖的是"设备端"部分——让程序调用这个检测器,并逐步加功能,直到能输出货架上的商品数量。起点是一个已经写好的图像分类(image classifier)程序:拍照、调用模型、打印结果,整套骨架都是现成的,所以改造量很小,改动集中在 SDK 方法名和结果处理上。

硬件上有两条路线可选:真机用 Raspberry Pi 加 PiCamera,没有硬件则用虚拟 IoT 设备加 CounterFit 模拟摄像头。两条路线走的是同一套代码结构,后文会分开说明各自要准备什么。

动手之前:环境与硬件

开工前需要确认两件事:检测器已经从 Custom Vision 门户发布、能拿到调用凭据;拍摄设备就位。发布stock-detector项目后,在 Prediction 对话框的If you have an image file区域可以看到 prediction URL 和 Prediction-key,程序调用模型就靠这两样:

虚拟 IoT 设备线

  • 在电脑上创建stock-counter文件夹,并建好虚拟环境(virtual environment)
  • 安装 CounterFit 和 CounterFit PyCamera shim(counterfit_shims_picamera)
  • 喂静态图片的话,准备几张检测器没见过的货架照片;用网络摄像头的话,确认摆放角度能清楚拍到要盘点的库存

树莓派线

  • 在 Pi 上创建stock-counter文件夹
  • 装好 PiCamera;建议把镜头固定在一个位置,例如让排线搭在盒子或罐子顶上,或用双面胶把相机粘在盒子上,保证每次拍摄都正对货架

拍照环节不用从零写起,制造项目里已经有"从设备取图并送模型"的完整示范,参考 制造项目第 2 课的 README 中设备拍照和调用分类器的两个步骤即可,其中大部分代码会直接复用到检测里。虚拟设备版本与树莓派版本唯一的差别在开头几行:先通过CounterFitConnection.init('127.0.0.1', 5000)连上本地 CounterFit 服务,再把真实 PiCamera 换成 CounterFit 提供的模拟摄像头;之后的模型调用、阈值过滤与结果输出和 Pi 版本完全一致。

核心改动:把分类调用换成检测调用

改动面其实只有一行:SDK 上调用的方法名。分类器用classify_image,检测器用detect_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}%')

把上面这段调用换成下面的检测调用,顺带多出一条概率过滤:

# 调用目标检测器(方法名由 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后面必须跟一个 threshold。

组合起来,改造后的完整 Raspberry Pi 程序如下(完整程序在仓库的 stock-counter 代码目录 下,分 pi 版 与 virtual-iot-device 版):

import io import time from picamera import PiCamera from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient from msrest.authentication import ApiKeyCredentials camera = PiCamera() camera.resolution = (640, 480) camera.rotation = 0 time.sleep(2) # 等镜头对准货架、光线稳定后再拍 image = io.BytesIO() camera.capture(image, 'jpeg') image.seek(0) with open('image.jpg', 'wb') as image_file: image_file.write(image.read()) # 落盘一份,方便回头查看 prediction_url = '<prediction_url>' prediction_key = '<prediction key>' # 从 URL 中拆出端点、项目 ID 和迭代名 parts = prediction_url.split('/') endpoint = 'https://' + parts[2] project_id = parts[6] iteration_name = parts[9] prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": prediction_key}) predictor = CustomVisionPredictionClient(endpoint, prediction_credentials) image.seek(0) results = predictor.detect_image(project_id, iteration_name, image) threshold = 0.3 predictions = list(prediction for prediction in results.predictions if prediction.probability > threshold) # 打印逻辑同前:遍历保留下来的预测,输出标签名与概率

在树莓派上运行python3 app.py,程序会拍照、把图交给检测器,并把通过门槛的商品打印出来:

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 跑一轮,看输出再微调。

拍摄的这张图和每个检测值,都能在 Custom Vision 门户的Predictions标签页里复查。下面这张示例图里,4 罐番茄酱各被框出一个红色 bounding box,概率分别为 35.8%、33.5%、25.7%、16.6%:

边界框:四个数字怎么读、怎么用

检测器比分类器多给了一份信息——bounding box。它就是一个包住目标物体的矩形,回答"检出的东西在图的哪个位置"。框由 4 个 0 到 1 之间的相对值定义,坐标原点在图片左上角:

坐标含义
top框上边缘到图顶的相对距离
left框左边缘到图左的相对距离
height框的高占图高的比例
width框的宽占图宽的比例

框的底边位置等于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)。

用相对值而不是像素值正是关键所在:图片无论被缩放到什么尺寸,框永远落在宽高的 0.4 处、占 0.2 × 0.3 的比例,分辨率变了也不会错位。

边界框的第一个用途是调试——把框值随标签一起打到控制台,把 for 循环里的 print 行改成:

print(f'{prediction.tag_name}:\t{prediction.probability * 100:.2f}%\t{prediction.bounding_box}')

运行后,每一行结果后面都会跟上一组框坐标:

pi@raspberrypi:~/stock-counter $ python3 app.py tomato 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 把检测框画到图片上

目标:让检测结果"看得见",而不只是控制台里的一串数字。Pillow 是 Python 的图像处理库,可以在之前保存的image.jpg上画矩形。先安装:

pip3 install pillow

用虚拟 IoT 设备时,记得在已激活的虚拟环境里执行。然后在app.py顶部加一行导入:

from PIL import Image, ImageDraw, ImageColor

再把绘制代码追加到文件末尾:

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 # 2 像素宽的红色矩形描出检测框 draw.rectangle([left, top, right, bottom], outline=ImageColor.getrgb('red'), width=2) im.save('image.jpg') # 覆盖保存原图

运行效果:再跑一次程序,然后在 VS Code 资源管理器里点开image.jpg,每罐商品周围都会出现红色边界框。换算逻辑很好理解:比如 left 取 0.5、图片宽 600 像素,相乘就得到像素位置 300。

去除重叠检测框:Shapely 去重

目标:把重复框去掉,让计数准确。罐头前后排紧挨着摆放时,检测框经常会互相压住;两个框重叠得厉害,大概率是框中了同一个物体,直接相加就会多算库存。Shapely 库能算两个矩形的交集,树莓派上要先装一个系统依赖:

sudo apt install libgeos-dev pip3 install shapely

在文件顶部加导入,并在绘制边界框的代码之前定义重叠阈值、以及把框转成多边形的小函数:

from shapely.geometry import Polygon
overlap_threshold = 0.20 # 允许最多 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 # 用框的四个角点构造 Shapely 多边形 return Polygon([(scale_left, scale_top), (scale_right, scale_top), (scale_right, scale_bottom), (scale_left, scale_bottom)])

实际比较是两两配对:第 1 个框依次和它后面的每个框比,第 2 个框再和它后面的比,以此类推。重叠面积用两个多边形的 intersection 交集算出;判断标准不是绝对面积,而是相对两个框里较小者的比例——重叠面积超过overlap_threshold × 较小框面积才判为同一物体,并把当前预测记名。另一个细节:Python 不允许在遍历列表时删元素,所以代码先把要删的预测收进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')

运行效果:输出的是去重后的商品数量,这个数字可以发给 IoT 服务,库存偏低时触发补货告警。由于这段代码位于绘制边界框之前,画出来的图里只剩去重后的框。完整计数版代码见 code-count 的 pi 版 和 virtual-iot-device 版;虚拟设备版把overlap_threshold设成了 0.002,同一张图换不同阈值各跑一遍,观察哪些预测被剔除,是理解这个参数最直接的办法。

总结与下一步

拍照 →detect_image→ 阈值过滤 → 画框 → 去重 → 计数,一条链路下来,一个图像分类程序就变成了货架盘点工具,最后的商品数量就是可以上报云端的那个数字。

下一步可探索:

  • 单板计算机调用目标检测器完整指南
  • 货架库存计数完整指南
  • 进阶挑战:把检测器迁移到 Azure IoT Edge

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

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

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

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

立即咨询