IoT-For-Beginners 零售项目实战:在 Wio Terminal 上调用 Custom Vision 对象检测器完成货架库存识别
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
本篇技术指南基于 IoT-For-Beginners 仓库5-retail(零售)项目的第 2 课「从 IoT 设备检查库存」,聚焦 Wio Terminal(Arduino 平台)开发板:如何把上一课训练好的 Custom Vision 对象检测器(object detector)从云端"搬到"嵌入式设备上,将原有的图像分类(classify)代码改造为对象检测(detect)代码,并通过置信度阈值过滤低概率检测结果。完成本指南后,你将掌握detectStock函数的完整实现思路、ArduinoJson 解析预测结果、串口输出检测报告的全套实战能力,为后续的库存计数(stock counting)打下基础。
一、课程背景:为什么需要把对象检测器接入 IoT 设备
在5-retail项目的前一课中,你已经使用 Custom Vision 训练了一个能够识别货架上番茄酱罐头(tomato paste)的对象检测器,并把它发布为可调用的预测服务。本课的核心任务,就是让部署在货架旁的 Wio Terminal 拍摄照片、把图片字节流发送到该服务,再把返回的多组"标签 + 概率 + 边界框"结果在串口监视器中展示出来。
与图像分类器(image classifier)不同,对象检测器会返回一个或多个检测结果(同一张图片中可能同时识别出多个罐头),因此不能简单地把分类代码照搬过来——你需要在代码中引入阈值过滤与结果批量处理逻辑。本课英文原文档位于 5-retail/lessons/2-check-stock-device/wio-terminal-object-detector.md,对应的完整可运行代码位于 code-detect/wio-terminal。
前置要求:请先完成课程 5-retail/lessons/2-check-stock-device/README.md 中「发布对象检测器迭代」的任务,从 Custom Vision 门户获取Prediction URL与Prediction-Key。发布后的检测端点 URL 形如
https://<location>.api.cognitive.microsoft.com/customvision/v3.0/Prediction/<id>/detect/iterations/Iteration2/image。
二、复制图像分类器项目:从零搭建stock-counter
对象检测器的绝大部分代码与制造项目(4-manufacturing)中创建的图像分类器相同,因此不必从零写起,而是复制并改造。
任务:复制图像分类器项目
连接 ArduCam 摄像头:按照制造项目第 2 课的步骤把 ArduCam 接到 Wio Terminal,参见 4-manufacturing/lessons/2-check-fruit-from-device/wio-terminal-camera.md 中的"连接摄像头"任务。为了让检测结果稳定,建议把摄像头固定在一个固定位置——例如把排线挂在纸箱或罐头上,或用双面胶将摄像头固定在纸箱上,模拟真实货架监控场景。
创建 PlatformIO 工程:使用 PlatformIO 新建一个 Wio Terminal 工程,命名为
stock-counter。复现拍照流程:按照制造项目第 2 课 README.md 中"使用 IoT 设备拍摄图像"任务的步骤,实现从摄像头拍照并读取字节缓冲区的逻辑。
复现分类调用流程:按照制造项目第 2 课 README.md 中"从 IoT 设备调用图像分类器"任务的步骤实现网络请求。这部分代码绝大部分可以原样复用到对象检测中。
三、核心改造:从classifyImage到detectStock
分类代码与检测代码高度相似,主要差异有两点:调用的 URL 不同(检测端点而非分类端点),以及返回结果的形态不同(单个结果 vs 多个结果)。下面按改造步骤逐项展开,每个步骤均可在 code-detect/wio-terminal/stock-counter/src/main.cpp 中找到最终成品。
步骤 1:添加<vector>头文件
在main.cpp顶部加入以下 include:
#include <vector>对象检测会返回多个预测结果,后续需要用std::vector容器来收集"通过阈值"的预测项,因此必须引入该标准库头。
步骤 2:重命名函数为detectStock
把classifyImage函数改名为detectStock,同时把buttonPressed函数中的调用处一并改名。仓库成品代码中,该函数的签名是:
void detectStock(byte *buffer, uint32_t length)它接收拍照得到的 JPEG 字节缓冲区指针与长度,负责完成 HTTP 请求与结果解析(详见第五节源码剖析)。
步骤 3:声明置信度阈值threshold
在detectStock函数上方声明一个常量,用于过滤低概率检测:
const float threshold = 0.3f;为什么要过滤?图像分类器每个标签只返回一个结果;而对象检测器会为图片中每个疑似目标都返回一个结果,其中混有大量低置信度的误检。threshold = 0.3f表示只保留概率大于 30% 的检测。仓库成品代码code-count版本中该值被设为0.0f(code-count/wio-terminal/stock-counter/src/main.cpp 第 61 行),说明阈值需要根据实际图片与模型表现灵活调整。
步骤 4:新增processPredictions处理函数
在detectStock上方声明一个处理预测列表的函数:
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); } }该函数接收一个JsonVariant预测列表,逐条取出tagName(标签名)与probability(概率,0~1 之间的小数),乘以 100 转成百分比后通过sprintf格式化为标签:概率形式打印到串口监视器。
步骤 5:用阈值过滤逻辑替换原有 for 循环
在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);这段代码的作用是:遍历所有预测,把概率高于threshold的预测收集进passed_predictions(一个std::vector<JsonVariant>),最后统一交给processPredictions打印。这样,误检噪声被剔除,串口输出只保留可信的检测结果。
四、上传运行与结果解读
完成上述改造后:
- 通过 PlatformIO 把代码上传到 Wio Terminal;
- 将摄像头对准货架上的物品;
- 按下C 按钮(
WIO_KEY_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%可以看到:设备先完成 WiFi 连接(Connecting to WiFi..→Connected!),随后成功捕获图像(Image captured),并从 ArduCam FIFO 读取出 17416 字节的 JPEG 数据,最后对象检测器在货架图片中识别出 4 个番茄酱罐头,置信度均在 34%~36% 之间。
💁调参提示:如果出现大量无关输出或漏检,可能需要针对你的图片把
threshold调整到合适值。你拍摄的原始图片与这些数值都可以在 Custom Vision 门户的Predictions标签页中查看,便于对照验证模型表现。
下面这张图展示了课程中同一场景的检测结果:货架上 4 罐番茄酱被全部框出,4 个检测的置信度分别为 35.8%、33.5%、25.7% 和 16.6%:
五、源码级剖析:detectStock的完整调用链
光看改造步骤还不够,下面结合仓库源码把detectStock背后的完整调用链拆解清楚,便于你真正理解"嵌入式设备如何调用云端对象检测器"。
5.1 完整的detectStock实现
仓库成品代码(code-detect/wio-terminal/stock-counter/src/main.cpp 第 77-111 行)中,detectStock的完整实现为:
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(); }关键点逐条说明:
- HTTPS 请求:
httpClient.begin(client, PREDICTION_URL)使用全局的WiFiClientSecure client发起 HTTPS 请求,client的 CA 证书在connectWiFi()中通过client.setCACert(CERTIFICATE)设置(见 config.h 中的 DigiCert Global Root G2 根证书)。 - 两个请求头:
Content-Type: application/octet-stream表示请求体是原始图片字节流;Prediction-Key携带你在 Custom Vision 门户获取的密钥——只有携带合法密钥的应用才被允许调用模型,其他请求一律被拒绝。 - 二进制上传:
httpClient.POST(buffer, length)直接把 JPEG 字节缓冲区和长度作为请求体 POST 出去,无需 base64 编码。 - 响应解析:响应码为 200 时,用
ArduinoJson的DynamicJsonDocument doc(1024)解析 JSON,取出顶层predictions数组,数组中每个元素包含tagName、probability以及boundingBox等字段。 - 阈值过滤:对
predictions数组逐条判断probability > threshold,通过的放入passed_predictions。 - 资源释放:
httpClient.end()关闭连接。
5.2 拍照与触发链路:loop→buttonPressed
main.cpp的主循环(第 136-145 行)持续轮询 C 按钮:
void loop() { if (digitalRead(WIO_KEY_C) == LOW) { buttonPressed(); delay(2000); } delay(200); }setup()中通过pinMode(WIO_KEY_C, INPUT_PULLUP)将 WIO Terminal 的 C 按钮配置为内部上拉输入(main.cpp 第 59 行),按下时读到LOW即触发buttonPressed(),并在两次触发之间加入 2 秒防抖延迟。
buttonPressed完成"拍照 → 读缓冲 → 检测 → 释放内存"的完整流程:
void buttonPressed() { camera.startCapture(); while (!camera.captureReady()) delay(100); Serial.println("Image captured"); byte *buffer; uint32_t length; if (camera.readImageToBuffer(&buffer, length)) { Serial.print("Image read to buffer with length "); Serial.println(length); detectStock(buffer, length); delete (buffer); } }摄像头封装类Camera(见 camera.h)基于 ArduCAM 库实现:init()负责 SPI 总线自检、OV2640 传感器芯片识别与 JPEG 格式初始化;startCapture()冲刷 FIFO 并触发一次拍摄;captureReady()通过CAP_DONE_MASK位轮询拍摄完成标志;readImageToBuffer()从 FIFO 突发读出 JPEG 数据并以new byte[length]动态分配缓冲区,读到 JPEG 结束标记0xFF 0xD9时停止。每次检测完成后delete(buffer)释放堆内存,防止内存泄漏。
5.3 配置项与工程依赖
所有需要你填写的配置都集中在 config.h:
| 配置项 | 说明 | 填写示例 |
|---|---|---|
SSID | WiFi 网络名称 | 你的路由器 SSID |
PASSWORD | WiFi 密码 | 你的 WiFi 密码 |
PREDICTION_URL | Custom Vision 检测端点 URL | 第五节开头提到的 detect 端点 |
PREDICTION_KEY | 预测密钥 | Custom Vision 门户中获取的 Prediction-Key |
CERTIFICATE | Azure TLS 根证书 | 仓库已内置 DigiCert Global Root G2,一般无需改动 |
工程依赖由 platformio.ini 声明:目标板为seeed_wio_terminal(Atmel SAM 平台 + Arduino 框架),依赖seeed-studio系列的 rpcWiFi、FS、SFUD、rpcUnified、mbedtls、RTC 库以及bblanchon/ArduinoJson @ 6.17.3。build_flags中定义了-DARDUCAM_SHIELD_V2与-DOV2640_CAM,用于启用 ArduCAM 扩展板与 OV2640 摄像头支持——创建新工程时务必保留这两个宏,否则摄像头驱动无法编译。
六、下一步:从"检测"走向"计数"
本课完成的是检测,而课程系列的目标是计数:结合检测返回的boundingBox(边界框,由left、top、width、height四个 0~1 归一化坐标定义),可以判断两个检测框是否高度重叠、是否为同一个物体,进而统计货架上的实际库存数量。这部分进阶内容见本课下一篇指南 wio-terminal-count-stock.md,对应成品代码位于 code-count/wio-terminal。
本课配套的完整课程说明(含发布检测器迭代、边界框原理、模型重训练等内容)请参阅 5-retail/lessons/2-check-stock-device/README.md。
结语
至此,你的 Wio Terminal 已经成功从"图像分类设备"升级为"对象检测设备":按下 C 按钮即可拍摄货架照片、调用 Custom Vision 对象检测服务、按 30% 置信度阈值过滤并打印所有可信检测结果。整个改造过程只涉及少量代码差异——新增<vector>头、重命名函数、声明阈值、新增处理函数、替换遍历逻辑——印证了对象检测与图像分类在客户端调用层面的高度相似性。接下来,就可以基于边界框数据实现真正的库存计数与低库存告警了。
【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考