news 2026/9/15 19:13:56

Wio Terminal 调用 Custom Vision REST API 实现水果图像分类实战(IoT-For-Beginners)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Wio Terminal 调用 Custom Vision REST API 实现水果图像分类实战(IoT-For-Beginners)

Wio Terminal 调用 Custom Vision REST API 实现水果图像分类实战(IoT-For-Beginners)

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

在 IoT-For-Beginners 的「从 IoT 设备检查水果质量」一课中,我们已训练出能够区分成熟(ripe)与未成熟(unripe)水果的图像分类模型。本篇文章聚焦于 Wio Terminal 硬件路径:如何将摄像头拍摄的 JPEG 图片通过 HTTPS 发送到 Azure Custom Vision 服务的 REST API,解析返回的 JSON 预测结果,并在串口监视器中输出每个标签的概率。读完本文,你将掌握微控制器上手动配置 TLS 根证书、使用WiFiClientSecure建立安全连接、以HTTPClient上传二进制图片并解析预测响应的完整实战流程。

一、整体架构:从相机像素到云端概率

本节课程在上一课「训练水果检测器」的基础上进行。整体消息流为:Wio Terminal 通过 SPI 接口连接的 ArduCam(基于 OV2640 传感器)拍摄 JPEG 图像,将图像字节数组通过 HTTPS POST 发送到 Custom Vision 的预测端点,Custom Vision 返回 JSON 格式的预测结果,设备解析后在串口监视器打印每个标签(ripe / unripe)的概率。该流程属于 2-check-fruit-from-device 课程 中「从 IoT 设备分类图片」的任务环节。

本课涉及的完整工程代码位于 code-classify/wio-terminal,与上一课仅保存到 SD 卡的 code-camera/wio-terminal 工程相比,核心差异是把saveToSDCard替换为classifyImage

二、HTTPS 与微控制器的证书难题

Custom Vision 服务的 REST API 通过 HTTPS(安全 HTTP)连接访问。与浏览器交互 HTTPS 端点时,客户端代码需要向被访问的服务器请求公钥证书,并用它加密发送的数据。Web 浏览器会自动完成这一过程,但微控制器不会。你必须手动请求证书,并用它建立到 REST API 的安全连接。这些证书不常变更,因此一旦取得证书,就可以硬编码在应用程序中。

证书只包含公钥,无需保密——可以放进源代码,甚至可以公开发布在 GitHub 等公共位置。

验证证书的 openssl 命令

为了确认使用的证书正确,可在 macOS 或 Linux 上运行以下命令(Windows 用户可通过 Windows Subsystem for Linux (WSL) 运行):

openssl s_client -showcerts -verify 5 -connect api.cognitive.microsoft.com:443

输出中会列出DigiCert Global Root G2证书——这是微软 Azure 在全球众多服务中使用的一个根证书,也是本课要硬编码的证书。

三、任务一:在 Wio Terminal 上搭建 SSL 客户端

步骤 1:在config.h中硬编码根证书

打开fruit-quality-detector工程中的config.h头文件,添加如下CERTIFICATE常量(这就是Microsoft Azure DigiCert Global Root G2 证书):

const char *CERTIFICATE = "-----BEGIN CERTIFICATE-----\r\n" "MIIF8zCCBNugAwIBAgIQAueRcfuAIek/4tmDg0xQwDANBgkqhkiG9w0BAQwFADBh\r\n" "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\n" "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\r\n" "MjAeFw0yMDA3MjkxMjMwMDBaFw0yNDA2MjcyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\r\n" "MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jv\r\n" "c29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwNjCCAiIwDQYJKoZIhvcNAQEBBQAD\r\n" "ggIPADCCAgoCggIBALVGARl56bx3KBUSGuPc4H5uoNFkFH4e7pvTCxRi4j/+z+Xb\r\n" "wjEz+5CipDOqjx9/jWjskL5dk7PaQkzItidsAAnDCW1leZBOIi68Lff1bjTeZgMY\r\n" "iwdRd3Y39b/lcGpiuP2d23W95YHkMMT8IlWosYIX0f4kYb62rphyfnAjYb/4Od99\r\n" "ThnhlAxGtfvSbXcBVIKCYfZgqRvV+5lReUnd1aNjRYVzPOoifgSx2fRyy1+pO1Uz\r\n" "aMMNnIOE71bVYW0A1hr19w7kOb0KkJXoALTDDj1ukUEDqQuBfBxReL5mXiu1O7WG\r\n" "0vltg0VZ/SZzctBsdBlx1BkmWYBW261KZgBivrql5ELTKKd8qgtHcLQA5fl6JB0Q\r\n" "gs5XDaWehN86Gps5JW8ArjGtjcWAIP+X8CQaWfaCnuRm6Bk/03PQWhgdi84qwA0s\r\n" "sRfFJwHUPTNSnE8EiGVk2frt0u8PG1pwSQsFuNJfcYIHEv1vOzP7uEOuDydsmCjh\r\n" "lxuoK2n5/2aVR3BMTu+p4+gl8alXoBycyLmj3J/PUgqD8SL5fTCUegGsdia/Sa60\r\n" "N2oV7vQ17wjMN+LXa2rjj/b4ZlZgXVojDmAjDwIRdDUujQu0RVsJqFLMzSIHpp2C\r\n" "Zp7mIoLrySay2YYBu7SiNwL95X6He2kS8eefBBHjzwW/9FxGqry57i71c2cDAgMB\r\n" "AAGjggGtMIIBqTAdBgNVHQ4EFgQU1cFnOsKjnfR3UltZEjgp5lVou6UwHwYDVR0j\r\n" "BBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1Ud\r\n" "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMHYG\r\n" "CCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu\r\n" "Y29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln\r\n" "aUNlcnRHbG9iYWxSb290RzIuY3J0MHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9j\r\n" "cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcmwwN6A1oDOG\r\n" "MWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5j\r\n" "cmwwHQYDVR0gBBYwFDAIBgZngQwBAgEwCAYGZ4EMAQICMBAGCSsGAQQBgjcVAQQD\r\n" "AgEAMA0GCSqGSIb3DQEBDAUAA4IBAQB2oWc93fB8esci/8esixj++N22meiGDjgF\r\n" "+rA2LUK5IOQOgcUSTGKSqF9lYfAxPjrqPjDCUPHCURv+26ad5P/BYtXtbmtxJWu+\r\n" "cS5BhMDPPeG3oPZwXRHBJFAkY4O4AF7RIAAUW6EzDflUoDHKv83zOiPfYGcpHc9s\r\n" "kxAInCedk7QSgXvMARjjOqdakor21DTmNIUotxo8kHv5hwRlGhBJwps6fEVi1Bt0\r\n" "trpM/3wYxlr473WSPUFZPgP1j519kLpWOJ8z09wxay+Br29irPcBYv0GMXlHqThy\r\n" "8y4m/HyTQeI2IMvMrQnwqPpY+rLIXyviI2vLoI+4xKE4Rn38ZZ8m\r\n" "-----END CERTIFICATE-----\r\n";

参考实现在 config.h 中。该头文件同时还定义了 WiFi 凭据SSID/PASSWORD以及稍后会用到的PREDICTION_URL/PREDICTION_KEY占位符。

步骤 2:引入WiFiClientSecure并声明实例

打开main.cpp,在文件顶部的 include 指令区添加:

#include <WiFiClientSecure.h>

然后在 include 指令下方声明一个WiFiClientSecure实例:

WiFiClientSecure client;

该类封装了通过 HTTPS 与 Web 端点通信所需的代码。在完整的 main.cpp 中可以看到,它与其他头文件(Arduino.hArduinoJson.hHTTPClient.hrpcWiFi.h、SD 相关头文件、SPI.h)一同引入,并且Camera camera = Camera(JPEG, OV2640_640x480);已在此处定义,表示相机以 JPEG 格式、640×480 分辨率工作。

步骤 3:在connectWiFi中绑定根证书

connectWiFi方法中,让WiFiClientSecure使用 DigiCert Global Root G2 证书:

client.setCACert(CERTIFICATE);

参考实现位于 main.cpp:函数首先循环调用WiFi.begin(SSID, PASSWORD)直到WiFi.status() == WL_CONNECTED,连接成功后再调用client.setCACert(CERTIFICATE)将根证书绑定到安全客户端上。

四、任务二:编写图片分类代码

步骤 1:添加 ArduinoJson 依赖

platformio.inilib_deps列表中新增一行:

bblanchon/ArduinoJson @ 6.17.3

该声明会导入 ArduinoJson:

[env:seeed_wio_terminal] platform = atmelsam board = seeed_wio_terminal framework = arduino lib_deps = seeed-studio/Seeed Arduino rpcWiFi @ 1.0.5 seeed-studio/Seeed Arduino FS @ 2.1.1 seeed-studio/Seeed Arduino SFUD @ 2.0.2 seeed-studio/Seeed Arduino rpcUnified @ 2.1.3 seeed-studio/Seeed_Arduino_mbedtls @ 3.0.1 seeed-studio/Seeed Arduino RTC @ 2.0.0 bblanchon/ArduinoJson @ 6.17.3 build_flags = -w -DARDUCAM_SHIELD_V2 -DOV2640_CAM

其中build_flags中的-DARDUCAM_SHIELD_V2-DOV2640_CAM用于编译 ArduCam 库时启用 Shield V2 与 OV2640 相机配置。

步骤 2:添加预测 URL 与 Key 常量

config.h中为 Custom Vision 服务的预测 URL 和 Key 添加常量:

const char *PREDICTION_URL = "<PREDICTION_URL>"; const char *PREDICTION_KEY = "<PREDICTION_KEY>";

<PREDICTION_URL>替换为从 Custom Vision 获得的预测 URL,将<PREDICTION_KEY>替换为预测密钥。这两项均来自上一课发布迭代后的Prediction URL对话框。预测 URL 形如:

https://<location>.api.cognitive.microsoft.com/customvision/v3.0/Prediction/<id>/classify/iterations/Iteration2/image

其中<location>是你创建 Custom Vision 资源时使用的位置,<id>是一长串字母数字组成的 ID。Prediction-Key是调用模型时必须传递的密钥,只有携带该密钥的应用才被允许使用模型,其余应用会被拒绝。

💡 如果发布了新的迭代,其名称会不同。想让 IoT 设备切换到新迭代,只需把代码中的预测 URL 替换为指向新迭代名称的端点即可(可参考 课程 README 中的说明)。

步骤 3:引入 ArduinoJson

main.cpp中添加 ArduinoJson 库的 include 指令:

#include <ArduinoJSON.h>

步骤 4:编写classifyImage函数

main.cpp中、buttonPressed函数上方添加如下函数:

void classifyImage(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>(); 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); } } httpClient.end(); }

参考实现在 main.cpp。这段代码的执行逻辑如下:

  1. 声明HTTPClient:这是一个包含与 REST API 交互方法的类。httpClient.begin(client, PREDICTION_URL)使用前面用 Azure 公钥配置好的WiFiClientSecure实例连接预测 URL,从而在 TLS 加密通道上发起请求。
  2. 发送请求头Content-Type: application/octet-stream告知 API 本次调用将发送原始二进制数据;Prediction-Key头则传递 Custom Vision 预测密钥。
  3. 发起 POST 请求httpClient.POST(buffer, length)上传一个字节数组——当本函数被调用时,该数组正包含摄像头采集到的 JPEG 图片。
  4. 检查响应状态码:POST 请求返回响应状态码。这些是明确定义的值,200表示OK,即 POST 请求成功。
  5. 读取并解析响应:若返回 200,则从 HTTP 客户端读取结果——这是 REST API 返回的、包含预测结果的 JSON 文本响应;随后用DynamicJsonDocument反序列化,遍历predictions数组。

💁 POST 请求用于发送数据并获取响应。其他请求类型如 GET 用于取回数据,你的浏览器加载网页用的就是 GET 请求。完整的 HTTP 状态码列表可参考公开的 HTTP 状态码文档。

步骤 5:理解 Custom Vision 的 JSON 响应格式

Custom Vision 分类 API 返回的 JSON 文档格式如下:

{ "id":"45d614d3-7d6f-47e9-8fa2-04f237366a16", "project":"135607e5-efac-4855-8afb-c93af3380531", "iteration":"04f1c1fa-11ec-4e59-bb23-4c7aca353665", "created":"2021-06-10T17:58:58.959Z", "predictions":[ { "probability":0.5582016, "tagId":"05a432ea-9718-4098-b14f-5f0688149d64", "tagName":"ripe" }, { "probability":0.44179836, "tagId":"bb091037-16e5-418e-a9ea-31c6a2920f17", "tagName":"unripe" } ] }

关键部分是predictions数组,它包含每个标签的预测结果,每项由标签名(tagName)和概率(probability)组成。返回的概率是 0–1 之间的浮点数:0 表示 0% 匹配该标签,1 表示 100% 匹配。图像分类器会为所有已使用的标签返回百分比,每个标签都有一个「图片匹配该标签」的概率。

💁 注意tagId字段在本例中并未被解析,代码只读取tagNameprobability,并以%.2f%%格式把概率换算成百分比打印到串口。

步骤 6:在buttonPressed中调用classifyImage

buttonPressed函数中,把保存到 SD 卡的代码替换为对classifyImage的调用,或者在图片写入后、缓冲区被删除之前调用:

classifyImage(buffer, length);

💁 如果替换了保存到 SD 卡的代码,可以顺带删除setupSDCardsaveToSDCard函数以精简代码。这一点可以对照两个工程的差异:SD 卡版本 code-camera 的 main.cpp 中buttonPressed调用saveToSDCard(buffer, length),而分类版本则调用classifyImage(buffer, length)后再delete (buffer)释放堆内存。

在 code-classify 的 main.cpp 中,buttonPressed的完整流程是:调用camera.startCapture()开始拍摄 → 循环轮询camera.captureReady()直到 FIFO 就绪 → 打印Image captured→ 通过camera.readImageToBuffer(&buffer, length)将 JPEG 读入堆缓冲区 → 打印图片长度 → 调用classifyImage(buffer, length)delete (buffer)释放内存。主循环loop()检测 C 键(WIO_KEY_C)是否被按下(低电平),按下即触发buttonPressed()并延时 2 秒防抖。

readImageToBuffer的底层实现在 camera.h:它通过 SPI FIFO 突发模式逐字节读取 JPEG 数据,依据0xFFD8(SOI 起始标记)与0xFFD9(EOI 结束标记)定位图像边界,最终返回完整的 JPEG 字节缓冲。

五、上传代码并验证运行结果

上传并运行代码。将摄像头对准水果,按下 C 按钮,即可在串口监视器中看到如下输出:

Connecting to WiFi.. Connected! Image captured Image read to buffer with length 8200 ripe: 56.84% unripe: 43.16%

第一行为 WiFi 连接日志,接着是图像采集日志,最后两行就是分类结果:ripe 56.84%、unripe 43.16%(概率之和为 100%)。与此同时,在 Custom Vision 门户的Predictions标签页中可以看到刚拍摄的图片及相同的预测数值。

六、模型准确率提升:用设备实拍图重新训练

你可能发现设备端拍摄图片的预测结果与预期不符,准确率不如从电脑上传的图片。这是因为模型训练数据与实际预测图像存在差异。要让图像分类器获得最佳效果,训练数据应尽量接近预测时使用的图像——例如,用手机拍摄的训练图与 IoT 设备相机的图像在清晰度、色彩、对比度上都有明显差异。

改进方法(详见 课程 README):

  1. 用 IoT 设备分别拍摄多张成熟与未成熟水果的图片并完成分类;
  2. 在 Custom Vision 门户的Predictions标签页用这些图片重新训练模型;
  3. 如果设备拍摄的图片与原始训练图差异很大,可在Training Images标签页删除全部原始图片后重新训练;
  4. 训练出新迭代并发布;
  5. 更新代码中的端点 URL,重新运行应用;
  6. 重复上述步骤直到预测结果满意。

七、进一步探索与作业

完成分类后,设备已持有预测数值,可据此做出响应:将数据发送到 IoT Hub 供其他系统处理,或控制执行器(如未成熟时点亮 LED)。相关作业要求与评分标准见 assignment.md。

若想深入了解相机与设备端的配置,可继续阅读同课文档 wio-terminal-camera.md(ArduCam 接线与采集)与 single-board-computer-classify-image.md(Raspberry Pi / 虚拟设备路径的分类实现),对照不同硬件路径理解 HTTPS 客户端配置与图像上传的通用模式。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 19:11:00

特征选择实战:粒子群优化(PSO)算法原理与Python实现

做机器学习项目做到第三年的时候&#xff0c;我发现自己最花时间的不是调模型&#xff0c;而是清特征。三百多个特征丢进LightGBM&#xff0c;一次交叉验证跑下来够泡三杯咖啡的。更要命的是特征一多&#xff0c;模型在验证集上明明很漂亮&#xff0c;一上线就暴露出各种毛病。…

作者头像 李华
网站建设 2026/9/15 19:10:14

5套可嵌入可交互的大数据可视化HTML方案

简介&#xff1a;本资源是一套面向前端开发者与大数据可视化初学者的实战型HTML模板合集&#xff0c;聚焦医院统计、物流看板、交通分析等5类真实业务场景&#xff0c;解决从零搭建交互式数据看板的技术门槛问题。压缩包共218个文件&#xff0c;含45个JavaScript脚本&#xff0…

作者头像 李华
网站建设 2026/9/15 19:09:18

基于ThinkPHP实现无限坐席在线客服系统的核心架构与实践

简介&#xff1a;基于ThinkPHP内核开发的无限坐席在线客服系统源码&#xff0c;为需要搭建在线客服的企业或开发者提供了完整可运行方案。系统支持不限数量的坐席接入&#xff0c;集成访客对话窗口、坐席工作台与管理后台&#xff0c;安装过程简单&#xff0c;配置好PHP5.6与My…

作者头像 李华