IoT-For-Beginners 制造篇实战:用 IoT 设备拍照并通过 Custom Vision 分类水果质量

IoT-For-Beginners 制造篇实战:用 IoT 设备拍照并通过 Custom Vision 分类水果质量 IoT-For-Beginners 制造篇实战用 IoT 设备拍照并通过 Custom Vision 分类水果质量【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners本课是「IoT-For-Beginners」制造Manufacturing路径的第 2 课紧接第 1 课训练水果图像分类器之后解决「如何让 IoT 设备真正用上已训练的模型」这一核心问题为设备接入摄像头传感器、把上一课训练的 Custom Vision 模型发布为可调用的迭代版本再从 Wio Terminal、Raspberry Pi 或虚拟设备端拍摄水果图片并上传云端完成成熟度分类。读完本课你将掌握摄像头传感器的底层原理CMOS/APS、SPI 数据通路、三种典型 IoT 硬件平台的拍照代码、Custom Vision 迭代发布流程、Python SDK 与 REST API 两种云端调用方式以及针对端侧图像差异的模型再训练方法。课程定位从「训练模型」走向「端侧推理」上一课1-train-fruit-detector训练了一个能够区分成熟ripe与未成熟unripe水果的 Custom Vision 图像分类器。但模型本身并不产生价值——只有当它被真实设备调用、在真实产线上输出判断结果时才算闭环。本课沿着这条链路展开摄像头传感器理解 IoT 摄像头如何工作以及为什么它和手机摄像头存在巨大差异拍照在 Wio Terminal、Raspberry Pi 和虚拟 IoT 设备上分别编写拍照代码发布模型在 Custom Vision 门户中将迭代iteration发布为可被外部调用的预测端点云端分类分别用 Python SDK树莓派/虚拟设备和 REST APIWio Terminal上传图片并解析分类结果模型改进针对端侧图片与训练图片的差异用真实设备照片重新训练迭代。仓库中本课的完整代码位于 4-manufacturing/lessons/2-check-fruit-from-device 下的code-camera纯拍照与code-classify拍照分类两个目录分别对应 Wio Terminal、树莓派、虚拟设备三套硬件。摄像头传感器IoT 设备的「眼睛」是如何工作的摄像头传感器顾名思义就是可以连接到 IoT 设备的摄像头。它们既能拍摄静态照片也能录制流式视频有的直接返回原始图像数据有的则把数据压缩为 JPEG、PNG 等图像文件。相比大家熟悉的手机摄像头面向 IoT 设备的摄像头通常体积更小、分辨率更低但同样可以买到与高端智能手机一较高下的高分辨率型号还支持可更换镜头、多摄像头阵列、红外热成像乃至紫外摄像头等特殊形态。绝大多数摄像头传感器使用图像传感器image sensor其核心是每一个像素对应一个光电二极管photodiode镜头把图像聚焦到图像传感器表面成千上万乃至数百万个光电二极管各自记录落在其上的光强并将其保存为像素数据。几个需要记住的技术要点 镜头会使图像上下颠倒摄像头传感器再把它翻转回正确方向。人眼也是如此——你看到的影像在眼球后部是倒置的由大脑负责校正。 这类图像传感器学名Active-Pixel SensorAPS其中最流行的一种是互补金属氧化物半导体传感器即常说的CMOS 传感器。摄像头传感器是数字传感器以数字数据形式发送图像信息通常借助一个封装了通信协议的库来完成传输。由于图像数据量远大于温度传感器之类的单值读数摄像头一般通过SPI等高速协议连接。✅ 思考题IoT 设备尤其是单片机硬件在图片大小上会面临哪些限制请从存储、内存与网络带宽三个角度展开思考。用 IoT 设备拍摄待分类图片本课提供了三条硬件路径仓库中对应的分步指南为 wio-terminal-camera.md、pi-camera.md 与 virtual-device-camera.md。Wio Terminal ArduCamSPI/I²C 双总线接线与底层驱动硬件选型Wio Terminal 搭配ArduCam Mini 2MP Plus。这是一颗基于 OV2640 图像传感器、约 200 万像素的摄像头通过 SPI 接口传输图像数据并通过 I²C 配置传感器参数。ArduCam 没有 Grove 插座需要直接用杜邦线连接 Wio Terminal 的 GPIO 引脚。接线对应关系如下表建议先贴好 Wio Terminal 自带的 GPIO 引脚贴纸以便对照ArduCAM 引脚Wio Terminal 引脚说明CS24 (SPI_CS)SPI 片选MOSI19 (SPI_MOSI)SPI 主出从入MISO21 (SPI_MISO)SPI 主入从出SCK23 (SPI_SCLK)SPI 时钟GND6 (GND)地线 0VVCC4 (5V)5V 电源SDA3 (I2C1_SDA)I²C 数据线SCL5 (I2C1_SCL)I²C 时钟线 注意ArduCam 工作电压为 5V与 Grove 传感器常见的 3V 不同供电直接来自 USB-C 供电链路。工程配置在 PlatformIO 中新建名为fruit-quality-detector的 Wio Terminal 工程。ArduCam 库无法通过platformio.ini直接安装需要从其 GitHub 仓库ArduCAM/Arduino克隆或下载 zip并把整个ArduCAM文件夹复制到工程的lib目录必须保证代码位于lib/ArduCam不能只复制文件夹内容。随后在platformio.ini末尾追加编译宏让库只为当前硬件裁剪代码build_flags -DARDUCAM_SHIELD_V2 -DOV2640_CAM其中ARDUCAM_SHIELD_V2告诉库摄像头位于 Arduino 扩展板shield上OV2640_CAM则让库只保留 OV2640 型号的驱动代码以尽量减小固件体积。摄像头封装类在src下新建camera.h封装与 ArduCam 的底层通信。核心的init()方法依次完成复位 CPLD写入寄存器0x07、校验 SPI 总线ARDUCHIP_TEST1寄存器写入/读回0x55、将 MCU 切到MCU2LCD_MODE、读取 OV2640 芯片 ID0x26与0x41/0x42验证传感器型号最后设置 JPEG 格式与分辨率。拍照流程则拆成三段startCapture()冲刷并清空 FIFO 后启动捕获captureReady()轮询CAP_DONE_MASK标志位判断拍摄是否完成readImageToBuffer()以 SPI 突发模式从摄像头 FIFO 读取 JPEG 字节流以0xFF 0xD8起始、0xFF 0xD9结束定位 JPEG 头尾存入动态分配的缓冲区。在main.cpp中实例化摄像头并编写初始化函数#include camera.h Camera camera Camera(JPEG, OV2640_640x480); void setupCamera() { pinMode(PIN_SPI_SS, OUTPUT); digitalWrite(PIN_SPI_SS, HIGH); Wire.begin(); SPI.begin(); if (!camera.init()) { Serial.println(Error setting up the camera!); } }setupCamera先把 SPI 片选引脚置高使 Wio Terminal 成为 SPI 主机再启动 I²C 与 SPI 总线最后调用camera.init()完成传感器配置与接线自检。在setup()末尾调用它若串口输出Error setting up the camera!应逐一核对 ArduCam 与 Wio Terminal 各引脚的接线。按钮触发拍照单片机上的代码是持续循环执行的因此用 Wio Terminal 顶部的 C 键最靠近电源开关的那颗作为拍照触发源。将 C 键配置为INPUT_PULLUP模式未按下输出高电平、按下输出低电平然后在loop()中检测WIO_KEY_C拉低时调用buttonPressed()并延时 2 秒防止长按重复触发void loop() { if (digitalRead(WIO_KEY_C) LOW) { buttonPressed(); delay(2000); } delay(200); }在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); delete(buffer); }摄像头硬件的工作方式是异步的先发送开始捕获的指令摄像头在后台完成取景、JPEG 压缩并暂存在自身缓冲区随后captureReady()轮询确认完成最后readImageToBuffer把图像数据拷入本地字节数组。串口输出示例Connecting to WiFi.. Connected! Image captured Image read to buffer with length 9224 Image captured Image read to buffer with length 11272 不同图像的字节数不同同一分辨率下 JPEG 文件大小取决于画面内容本身。可选用 microSD 卡验证图片。Wio Terminal 只支持最大 16GB 的 microSD 卡需格式化为 FAT32/exFAT。在main.cpp中引入SD/Seeed_SD.h与Seeed_FS.h通过SD.begin(SDCARD_SS_PIN, SDCARD_SPI)初始化然后实现saveToSDCard(byte *buffer, uint32_t length)以自增文件序号生成1.jpg、2.jpg等文件名把缓冲区二进制数据写入SD.open(buff, FILE_WRITE)打开的文件。将saveToSDCard(buffer, length)调用放在delete(buffer)之前。之后即可弹出 SD 卡在电脑上查看照片。实际拍摄效果可参考 images/banana-arducam.jpg——注意白平衡通常需要拍几张才会自动校准前几张可能偏色必要时可在setup()里丢弃前几帧。Raspberry Pi PiCameraMIPI-CSI 摄像头与 legacy 模式硬件连接树莓派使用的 Raspberry Pi Camera Module 通过排线连接。排线插入摄像头时蓝面朝外、金属引脚朝向镜头将 Grove Base Hat 取下后让排线穿过 Hat 的摄像头槽位蓝面朝向 A0/A1 等模拟端口一侧再插入树莓派主板上的 CSI 摄像头插座蓝面朝向 USB/以太网口最后装回 Grove Base Hat。 该摄像头采用MIPI-CSICamera Serial Interface协议是移动行业处理器接口联盟定义的一种专用于传输图像的协议。启用 legacy 摄像头模式Raspberry Pi OS Bullseye 起默认的摄像头软件栈发生了变化导致 PiCamera 库默认不可用其继任者 PiCamera2 当时尚未就绪。需要先进入 legacy 模式该操作同时会启用默认被禁用的摄像头插座sudo raspi-config nonint do_legacy 0 sudo reboot重启后重新打开 VS Code可在 Pi 本机或通过 Remote SSH 连接。拍照程序在用户主目录新建fruit-quality-detector文件夹与app.py安装依赖后编写pip3 install picameraimport io import time from picamera import PiCamera 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())要点解析分辨率设为 640×480。虽然 PiCamera 支持最高 3280×2464 的解析度但图像分类器工作在 227×227 的小图上无需采集和上传更大的图片camera.rotation 0控制图像旋转角度。若排线从摄像头底部接入导致画面方向不对可按需改为180如 images/pi-camera-upside-down.png 所示悬挂场景或其他角度time.sleep(2)留给摄像头启动时间BytesIO对象保存 JPEG 二进制数据image.seek(0)把位置指针拨回开头——本课后面会直接把这段内存数据发送给分类器这正是不直接存文件的原因。运行后当前目录会生成image.jpg可在 VS Code 资源管理器中点开检查是否需要调整旋转角。完整代码见 code-camera/pi/fruit-quality-detector/app.py。虚拟 IoT 设备 CounterFit用文件或摄像头模拟拍摄没有实体硬件时可以用虚拟设备完成整条链路。在 CounterFit 中创建名为Picamera的 Camera 传感器并安装一个模拟 PiCamera 部分接口的 shim 包pip install counterfit-shims-picamera随后在app.py中连接 CounterFit 并初始化「摄像头」from counterfit_connection import CounterFitConnection CounterFitConnection.init(127.0.0.1, 5000) import io from counterfit_shims_picamera import PiCamera camera PiCamera() camera.resolution (640, 480) camera.rotation 0CounterFit 的摄像头来源可设为File上传一张图片或WebCam调用电脑摄像头选好后务必点击Set按钮生效。后续的capture与文件保存代码与树莓派版本完全一致。完整的虚拟设备工程见 code-camera/virtual-iot-device/fruit-quality-detector/app.py关于 CounterFit Python 工程的创建方式可回看 1-getting-started 第 1 课的虚拟设备指南。发布你的图像分类器从「训练迭代」到「可调用端点」模型在上一课训练完成后必须先发布才能被外部应用调用。模型迭代Iteration机制训练模型时门户的Performance标签页会记录迭代历史首次训练生成Iteration 1用预测图片改进后再次训练生成Iteration 2。每次训练都会产生一个新迭代用于追踪不同数据集版本之间的差异。执行Quick Test时可通过下拉菜单切换迭代以横向对比效果。发布某一迭代后设备端会继续使用该已发布版本你可以并行地在新迭代上继续迭代训练直到满意后再发布下一个版本。发布迭代的操作步骤在 Custom Vision 门户fruit-quality-detector项目中选择顶部的Performance标签页从Iterations列表中选择最新迭代点击该迭代的Publish按钮在弹出的Publish Model对话框中将Prediction resource设置为上一课创建的fruit-quality-detector-prediction资源名称保持Iteration2点击Publish发布完成后点击Prediction URL按钮对话框会给出调用预测 API 所需的全部信息。我们需要底部If you have an image file部分的 URL形如https://location.api.cognitive.microsoft.com/customvision/v3.0/Prediction/id/classify/iterations/Iteration2/image其中location是创建 Custom Vision 资源时选择的位置id是一串由字母和数字组成的长 ID。同时复制对话框中的Prediction-Key值——这是调用模型的密钥只有携带该密钥的应用才被允许使用模型其余请求都会被拒绝。✅ 思考题每次发布新迭代都会有不同的名称。如果设备要切换到新的迭代版本你会如何修改设备代码从 IoT 设备调用分类器拿到连接信息后就可以从设备侧发起预测请求了。仓库提供了两套实现Python面向树莓派/虚拟设备与 C REST面向 Wio Terminal。Python 方式Custom Vision Prediction SDK适用于树莓派和虚拟设备。安装官方 SDKpip3 install azure-cognitiveservices-vision-customvision在app.py顶部导入认证与预测客户端并把上一节复制的 URL 与密钥填入from msrest.authentication import ApiKeyCredentials from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient prediction_url prediction_url prediction_key prediction keyPrediction URL 对话框给出的 URL 是为直接调用 REST 端点设计的而 Python SDK 需要把 URL 拆解成不同部分parts prediction_url.split(/) endpoint https:// parts[2] project_id parts[6] iteration_name parts[9]即分别提取出https://location.api.cognitive.microsoft.com端点、项目 ID 与已发布迭代名称。接着创建预测客户端并发起分类prediction_credentials ApiKeyCredentials(in_headers{Prediction-key: prediction_key}) predictor CustomVisionPredictionClient(endpoint, prediction_credentials) image.seek(0) 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之前先把图片指针回拨到开头返回的概率是 0~1 之间的浮点数0 表示 0% 匹配该标签、1 表示 100% 匹配。分类器会为所有训练用过的标签各返回一个概率因此输出形如(.venv) ➜ fruit-quality-detector python app.py ripe: 56.84% unripe: 43.16%该结果也会同步出现在 Custom Vision 的Predictions标签页示例见 images/custom-vision-banana-prediction.png。完整代码见 code-classify/pi/fruit-quality-detector/app.py 与 code-classify/virtual-iot-device/fruit-quality-detector/app.py。Wio Terminal 方式HTTPS REST 调用与证书处理Custom Vision 提供 REST API可通过 HTTPS 从 Wio Terminal 直接调用。但 HTTPS 有个特殊之处浏览器会自动获取服务器证书单片机不会。客户端需要手动携带目标服务器的公钥证书来建立安全连接——这类证书只含公钥无需保密可以硬编码进源码甚至公开分享。第一步配置 SSL 客户端。在config.h中加入Microsoft Azure DigiCert Global Root G2根证书完整 PEM 文本较长此处以注释示意完整内容见 code-classify/wio-terminal/fruit-quality-detector/src/config.hconst char *CERTIFICATE -----BEGIN CERTIFICATE-----\r\n MIIF8zCCBNugAwIBAgIQAueRcfuAIek/4tmDg0xQwDANBgkqhkiG9w0BAQwFADBh\r\n // ... 完整证书内容见仓库 config.h ... -----END CERTIFICATE-----\r\n;如需自行核验该证书可在 macOS/Linux 上执行Windows 可用 WSLopenssl s_client -showcerts -verify 5 -connect api.cognitive.microsoft.com:443输出会列出 DigiCert Global Root G2 证书。然后在main.cpp中引入#include WiFiClientSecure.h声明WiFiClientSecure client;并在connectWiFi中调用client.setCACert(CERTIFICATE);把证书挂到安全客户端上。第二步编写分类函数。在platformio.ini的lib_deps中追加 JSON 解析库bblanchon/ArduinoJson 6.17.3在config.h中加入预测端点与密钥常量同样参考仓库 config.hconst char *PREDICTION_URL PREDICTION_URL; const char *PREDICTION_KEY PREDICTION_KEY;随后在main.cpp中实现classifyImage它接收拍照阶段读出的 JPEG 字节缓冲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.asJsonObject(); JsonArray predictions obj[predictions].asJsonArray(); 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); } } httpClient.end(); }这段代码用HTTPClient建立到预测 URL 的连接底层走已配置证书的WiFiClientSecure发送两个请求头Content-Type: application/octet-stream表明上传的是原始二进制数据Prediction-Key传递预测密钥然后 POST 上传 JPEG 字节数组根据 HTTP 状态码判断结果——200 表示OK。响应是 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与 0~1 的概率值用 ArduinoJson 解码后逐条输出到串口。最后在buttonPressed()中调用classifyImage(buffer, length)放在delete(buffer)之前若无需 SD 卡存档可直接替换保存代码并删除setupSDCard/saveToSDCard函数。上传运行后对准水果按下 C 键串口输出Connecting to WiFi.. Connected! Image captured Image read to buffer with length 8200 ripe: 56.84% unripe: 43.16%完整工程见 code-classify/wio-terminal/fruit-quality-detector其中 platformio.ini 列出了全部依赖库与编译宏。改进模型训练数据与端侧图像之间存在鸿沟设备端预测结果未必如预期精确——端侧相机拍摄的照片与训练时用的图片往往差异巨大因为模型是在与预测数据不同的数据上训练的。例如用手机拍摄训练照片时图像清晰度、锐度与色彩都和 IoT 设备摄像头完全不同上图左为 Raspberry Pi 摄像头拍摄的香蕉右为同一根香蕉、同一位置用 iPhone 拍摄。两者画质差异明显iPhone 画面更锐利、色彩更亮、对比度更高。✅ 思考题除了画质还有哪些因素会导致 IoT 设备拍出的照片预测错误请想想 IoT 设备的实际部署环境——哪些因素会影响拍到的画面改进方法很直接用设备实拍的照片重新训练模型。操作流程如下用 IoT 设备分别拍摄多张成熟与未成熟水果图片在 Custom Vision 门户的Predictions标签页用这些实拍图重新训练模型可参考 第 1 课的重新训练说明如果实拍图与原始训练图差异非常大可在Training Images标签页悬停选中原始图片后点击Delete全部删除训练新的迭代并按照上文步骤发布更新代码中的 endpoint URL重新运行程序反复迭代直到预测结果满意为止。挑战分辨率与光照对预测的影响有多大动手验证两个变量修改设备代码中的图像分辨率观察画质与预测结果是否变化改变拍摄环境的光照条件对比预测差异。更进一步如果为农场或工厂打造一款量产检测设备你会如何保证它在各种环境下都能持续输出一致、稳定的预测结果课后作业让设备对分类结果做出响应本课作业见 assignment.md设备已经拿到每个标签的预测值接下来要让它真正行动起来——可以选择把数据发送到 IoT Hub 供其他系统处理或控制 LED 之类的执行器例如水果未成熟时点亮红灯也可以两者结合把数据发到 IoT Hub由无服务器代码判断成熟度并向设备下发指令驱动执行器。评分标准关注的是响应逻辑是否确实依赖预测值优秀还是只是机械地发送原始数据合格抑或完全无法让设备响应预测待改进。延伸阅读与代码索引除了本课分步指南外还可关注以下仓库内容完整拍照代码code-camera/pi、code-camera/virtual-iot-device、code-camera/wio-terminal对应 4-manufacturing/lessons/2-check-fruit-from-device/code-camera完整分类代码code-classify/pi、code-classify/virtual-iot-device、code-classify/wio-terminal对应 4-manufacturing/lessons/2-check-fruit-from-device/code-classify第 1 课训练分类器的完整背景1-train-fruit-detector本路径后续课将继续推进检测结果的执行与触发3-run-fruit-detector-edge、4-trigger-fruit-detector。 本课模型基于门户在线训练依赖现成的训练图片。若真实场景中无法获得与设备摄像头一致的训练数据还可以改用 Custom Vision 训练 API直接用 IoT 设备拍摄的图片在线训练模型从而绕开数据不匹配的问题参见 Azure 文档中的 Custom Vision SDK 图像分类快速入门。【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考