
IoT 库存盘点实战从 IoT 设备调用 Custom Vision 目标检测器清点货架库存【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本文基于 IoT-For-Beginners 零售项目的第 2 课《从 IoT 设备检查库存》5-retail 项目 Lesson 20编写讲解如何把上一课训练的stock-detector目标检测模型部署发布从 Wio Terminal / Raspberry Pi / 虚拟 IoT 设备上调用它利用预测结果与边界框Bounding Box实现货架库存清点与异常货品识别并介绍基于真实设备数据重新训练模型的完整闭环流程。本课是零售项目的收官一课。读完本篇文章你将掌握如何在 Custom Vision 门户发布模型迭代并获取预测 URL 与密钥如何把图像分类代码改造为目标检测代码含置信度阈值过滤如何理解并计算 0–1 归一化的边界框坐标以及如何通过边界框重叠检测剔除重复预测、准确统计货架商品数量并将低库存告警接入 IoT 服务。库存盘点的业务场景数得准还要找得对目标检测器在零售场景中最直接的价值就是库存检查Stock Checking——既能清点库存数量也能确认货品是否摆放正确。带有摄像头的 IoT 设备可以部署在门店各处持续监控库存优先覆盖“热点区域”例如存放少量高价值商品的区域这些区域缺货时及时补货至关重要。一个典型例子摄像头对准一组最多可放 8 罐番茄酱的货架如果目标检测器只检测到 7 罐说明有 1 罐缺失需要补货。上图中检测器在可容纳 8 罐的货架上检测出 7 罐番茄酱。IoT 设备不仅能发送“需要补货”的通知还能给出缺失商品的大致位置——如果你使用机器人补货这个位置信息至关重要。 实际上补货策略不能一概而论。对于热门商品只缺 1 罐可能无需立即补货。你需要根据商品属性、顾客流量等条件构建一个“何时补货”的判定算法。除了缺货货架上还可能存在错误的库存。这可能是补货时的人工失误也可能是顾客改变主意后随手把商品放回第一个空位。对于罐头这类耐储存商品这只是小麻烦但对于冷冻或冷藏商品一旦离开冷柜就无法确定脱离低温环境的时间商品可能因此不能再售卖。目标检测可以及时发现这类“不速之客”并通知人类员工或机器人尽快把商品归位。✅ 思考除了库存盘点还有哪些场景可以把目标检测与机器人结合起来在 Custom Vision 门户发布模型迭代上一课训练好的目标检测器需要通过 Custom Vision 门户**发布Publish**后才能从 IoT 设备调用。发布操作会为某一迭代生成一个公开的预测 API 端点。任务发布一版目标检测器打开 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如果你有图像文件区域中的信息来从 IoT 设备调用模型。复制其中显示的 URL格式类似https://location.api.cognitive.microsoft.com/customvision/v3.0/Prediction/id/detect/iterations/Iteration2/image其中location是你创建自定义视觉资源时使用的区域id是一长串由字母和数字组成的 ID。同时复制Prediction-Key预测密钥的值。这是调用模型时必须传递的安全密钥只有携带该密钥的应用程序才被允许使用模型其他应用都会被拒绝。✅ 思考当发布新一版迭代时迭代名会不同。你认为如何让 IoT 设备切换到新的迭代⚠️ 本课是本项目的最后一课。完成本课及作业后记得清理云服务资源但请先完成作业因为作业需要使用这些服务。清理步骤参考 clean-up.md仓库根目录。从 IoT 设备调用目标检测器模型发布后即可从 IoT 设备调用。整体思路是复用上一课制造项目中图像分类项目的绝大部分代码把“分类”调用改为“检测”调用再针对检测结果做处理。本课提供了两条硬件路线均包含“先检测”和“再计数”两个阶段Arduino - Wio Terminal参考 wio-terminal-object-detector.md 与 wio-terminal-count-stock.md树莓派 / 虚拟设备参考 single-board-computer-object-detector.md 与 single-board-computer-count-stock.md。复用图像分类项目Wio Terminal先按制造项目第 2 课 wio-terminal-camera.md 的步骤连接 ArduCam 摄像头可用双面胶或把线缆挂在盒子上固定摄像头位置然后用 PlatformIO 新建名为stock-counter的项目再从制造项目第 2 课 README.md 中“用 IoT 设备拍摄图像”与“从 IoT 设备分类图像”两个任务中复制代码其中大部分会被复用到目标检测。树莓派 / 虚拟设备新建stock-counter文件夹虚拟设备需配置虚拟环境树莓派需安装 PiCamera 并固定好位置虚拟设备需安装 CounterFit 与 CounterFit PyCamera shim准备模型未见过的静态图片或调整摄像头对准库存。把分类代码改造成目标检测代码分类与检测的主要区别在于调用的 URL / SDK 方法不同以及返回结果不同。图像分类每个标签只返回一个结果而目标检测会返回多个结果同一个标签可能有多处目标因此必须用置信度阈值过滤掉低概率的检测框。Python树莓派 / 虚拟设备删除原来的三行分类代码results predictor.classify_image(project_id, iteration_name, image) for prediction in results.predictions: print(f{prediction.tag_name}:\t{prediction.probability * 100:.2f}%)替换为检测代码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}%)这段代码调用predictor.detect_image运行目标检测然后收集所有概率高于阈值的预测并打印到控制台。运行后摄像头会拍摄一张图片并发送给检测器piraspberrypi:~/stock-counter $ python3 app.py tomato paste: 34.13% tomato paste: 33.95% tomato paste: 35.05% tomato paste: 32.80% 可能需要根据你的图片情况调整threshold阈值的取值。完整代码见仓库中的 code-detect/pi/stock-counter/app.py 与 code-detect/virtual-iot-device/stock-counter/app.py。CWio Terminal在main.cpp顶部添加#include vector把classifyImage函数重命名为detectStock函数名与buttonPressed中的调用处都要改在detectStock函数上方声明阈值常量const float threshold 0.3f;声明一个处理预测结果的函数把每个预测打印到串口监视器void processPredictions(std::vectorJsonVariant predictions) { for(JsonVariant prediction : predictions) { String tag prediction[tagName].asString(); float probability prediction[probability].asfloat(); char buff[32]; sprintf(buff, %s:\t%.2f%%, tag.c_str(), probability * 100.0); Serial.println(buff); } }在detectStock函数中把遍历预测的for循环内容替换为“按阈值过滤”的逻辑std::vectorJsonVariant passed_predictions; for(JsonVariant prediction : predictions) { float probability prediction[probability].asfloat(); if (probability threshold) { passed_predictions.push_back(prediction); } } processPredictions(passed_predictions);这段代码遍历所有预测将概率高于阈值的预测加入列表并交给processPredictions处理。上传运行代码把摄像头对准货架上的商品并按下 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%在仓库源码 code-detect/wio-terminal/stock-counter/src/main.cpp 中可以看到完整的调用链buttonPressed通过camera.startCapture()拍摄、camera.readImageToBuffer读取 JPEG 字节流然后detectStock使用HTTPClient以application/octet-stream的Content-Type和Prediction-Key请求头向PREDICTION_URL配置在 config.h发起POST返回的 JSON 经deserializeJson解析后取出predictions数组再按阈值过滤、打印。边界框Bounding Box预测结果的空间信息调用目标检测器时不仅会得到检测对象的标签tag和概率probability还会得到每个对象的边界框Bounding Box——它定义了检测器在图像中检测到该对象的位置区域。 边界框就是框住被检测对象区域的方框用于界定对象的边界。在 Custom Vision 的Predictions标签页中提交预测的图片上会直接绘制出边界框。如上图检测到了 4 罐番茄酱每个检测对象都被叠加了红色方框指示其边界框位置。边界框的 4 个坐标值边界框由 4 个值定义top上、left左、height高、width宽。这些值都归一化到0–1之间表示相对于图像尺寸的百分比。坐标系原点0,0位于图像左上角因此top是距顶部的距离边界框的底部 top height。以上图为例图片宽 600 像素、高 800 像素。边界框从向下 320 像素处开始top 0.4800 × 0.4 320从左侧 240 像素处开始left 0.4600 × 0.4 240框高 240 像素height 0.3800 × 0.3 240框宽 120 像素width 0.2600 × 0.2 120。坐标值数值Top0.4Left0.4Height0.3Width0.2使用 0–1 的百分比值意味着无论图像被缩放到多大边界框都从横向和纵向的 0.4 处开始高度占 0.3、宽度占 0.2。用边界框 概率评估检测质量边界框可以和概率结合用来评估一次检测的准确性。例如检测器可能对同一个对象给出多个重叠的框——比如一个框被检测在另一个框内部。你的代码可以分析边界框识别这种“不可能”的情况并忽略与其他对象显著重叠的检测结果。上例中一个边界框以 78.3% 的概率框住了一罐番茄酱第二个边界框略小位于第一个框内部概率为 64.3%。代码可以检查边界框、发现它们完全重叠然后忽略概率较低的那个因为一个罐子不可能存在于另一个罐子内部。✅ 思考你能想到某种“一个对象被检测在另一个对象内部”是合理的情形吗用 IoT 设备采集的数据重新训练模型与图像分类器一样你可以使用 IoT 设备采集的真实数据重新训练模型。真实场景数据能确保模型在设备端使用时表现良好。与图像分类不同目标检测的再训练不能只是给图片打标签而必须逐一检查模型预测出的每个边界框框错了对象就删除框的位置不对就调整。任务重新训练模型确保已用 IoT 设备采集了一批多样化的图片。在Predictions标签页选择一张图片会看到红色方框标出检测对象的边界框。逐个处理每个边界框先选中它会弹出显示标签的窗口必要时用框四角的控制点调整大小如果标签错误用X按钮移除并添加正确标签如果边界框里没有对象用垃圾桶按钮删除。完成后关闭编辑器该图片会从Predictions标签页移到Training Images标签页。对所有预测重复该过程。点击Train按钮重新训练模型。训练完成后发布新迭代并更新 IoT 设备使其使用新迭代的 URL。重新部署代码并测试 IoT 设备。清点库存结合对象数量与边界框消除重叠仅凭检测框的数量统计库存并不准确——同一个对象可能被重复检测。正确做法是把概率阈值过滤后的边界框两两比较剔除重叠超过阈值的重复框再统计剩余框的数量。边界框去重原理如果两个框的重叠面积超过“较小框面积”的一定百分比就认为它们框的是同一个对象应保留概率更高或更靠前的那个。注意重叠阈值是百分比必须乘以较小的那个边界框的面积而不是整张图的面积。在 Wio Terminal 的 C 实现code-count/wio-terminal/stock-counter/src/main.cpp中整套去重逻辑如下定义重叠阈值20% 表示允许 20% 的重叠const float overlap_threshold 0.20f;定义Point、Rect结构体并用area计算矩形面积、用overlappingArea计算两矩形重叠面积不相交时返回 0struct 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把预测中的boundingBoxleft/top/width/height转换为Rect右侧 left width底部 top height。Rect rectFromBoundingBox(JsonVariant prediction) { JsonObject bounding_box prediction[boundingBox].asJsonObject(); float left bounding_box[left].asfloat(); float top bounding_box[top].asfloat(); float width bounding_box[width].asfloat(); float height bounding_box[height].asfloat(); Point topLeft {left, top}; Point bottomRight {left width, top height}; return {topLeft, bottomRight}; }在processPredictions中两两比较所有预测内层循环从i 1开始避免重复比较1 与 2 比过后2 只需与 3、4…比较重叠面积超过overlap_threshold × 较小框面积时当前框标记为不通过全部通过则加入passed_predictionsstd::vectorJsonVariant 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]); } }打印通过检查的预测详情与统计数量for(JsonVariant prediction : passed_predictions) { String boundingBox prediction[boundingBox].asString(); String tag prediction[tagName].asString(); float probability prediction[probability].asfloat(); 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.);运行后串口输出示例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 版实现打印与绘制边界框树莓派 / 虚拟设备版本除了打印边界框还能把边界框直接画到磁盘上的image.jpg里方便调试把for循环中的print改为同时输出边界框print(f{prediction.tag_name}:\t{prediction.probability * 100:.2f}%\t{prediction.bounding_box})输出示例left/top/width/height 均为 0–1piraspberrypi:~/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}安装 Pillow 用于绘图虚拟设备请在虚拟环境中执行pip3 install pillow在app.py顶部导入from PIL import Image, ImageDraw, ImageColor在文件末尾追加绘制代码把 0–1 的框坐标乘以图像宽高换算为像素坐标如 left 为 0.5、图宽 600 像素则换算为 300用红色线条画框并保存覆盖原图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], outlineImageColor.getrgb(red), width2) im.save(image.jpg)安装 Shapely 计算多边形交集树莓派需先安装依赖sudo apt install libgeos-dev再执行pip3 install shapely导入from shapely.geometry import Polygon设置overlap_threshold 0.20并定义把边界框转换为 Shapely 多边形的函数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)])两两比较所有预测用Polygon.intersection计算重叠面积超过阈值就标记删除。由于不能在遍历列表时删除元素先把待删除项收集到to_delete最后统一移除to_delete [] for i in range(0, len(predictions)): polygon_1 create_polygon(predictions[i]) for j in range(i1, 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(fCounted {len(predictions)} stock items)这段代码应放在绘制边界框之前这样生成的图片上只会显示去重后的预测框。上图中 4 个边界框只有少量重叠。如果重叠明显更大很可能框的是同一个对象——这正是需要按重叠阈值剔除的原因。完整 Python 实现见 code-count/pi/stock-counter/app.py 与 code-count/virtual-iot-device/stock-counter/app.py。挑战检测错误库存在完成基础库存计数后可以进一步挑战检测错误库存用多个对象训练模型然后改造应用——当检测到“不该出现的货品”时发出告警。侧向摆放检测更进一步检测同一货架并排摆放的库存通过给边界框位置设定限制范围判断是否有商品被放错位置。作业把目标检测器部署到边缘上一节课你把图像分类器部署到了边缘设备。本次作业要求对目标检测器做同样的事将其导出为压缩模型compact model并在边缘运行让 IoT 设备调用边缘版本。评分标准标准优秀合格待改进将目标检测器部署到边缘能够使用正确的压缩域导出目标检测器并在边缘运行能够使用正确的压缩域并导出目标检测器但无法在边缘运行无法使用正确的压缩域无法导出目标检测器也无法在边缘运行作业细节见 assignment.md。小结本课完成了零售库存检测的最后一环在 Custom Vision 门户发布Iteration2迭代把classify_image升级为detect_image或把classifyImage改造为detectStock通过 0–1 归一化的边界框坐标理解检测结果的空间语义再用重叠面积阈值过滤重复框最终在 Wio Terminal、树莓派和虚拟设备三种平台上实现货架库存的准确清点。结合上一课的模型训练与本课的设备端调用你已经具备搭建“摄像头 边缘设备 Custom Vision 云端预测”零售库存监控方案的完整能力。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考