AI与机器学习的结合给机器视觉行业带来了新的发展,在机器学习模型、算法加持之下,使得解决更具挑战性的问题成为可能。那你有想过使用视觉AI传感器自行构建机器学习模型吗?
今天的分享来自柴火认证会员温燕铭,手把手带你用工业级SenseCAP A1101视觉AI传感器体验机器学习。
作者介绍
温燕铭
会员编号:CHSZ2020001034
90后,女,香港中文大学法学硕士,硬件产品经理,发明爱好者,创业者,十多年科技实践和创客经验。曾就职于迪拜的科技创新加速器、深圳创客教育机构、开源硬件企业,在深圳创办了公司专门从事专利和产品研发,拥有二十余项国家专利授权,国家法律职业资格证,教师资格证,多旋翼无人航空系统机长证。曾获深圳市无人机组装大赛冠军,深圳逐梦杯大学生创新创业大赛二等奖。
本案例将通过一个目标分类的模型训练过程,让任何对机器学习没有概念的用户都可以轻易体验机器学习模型训练中数据采集、数据标记、模型训练、设备部署的完整模型训练流程。
机器学习属于人工智能的子集,人工智能是基于数据处理来做出决策和预测。在机器学习中,通过给计算机输入经过人为标记过的数据集,算法会不断进行训练,从数据集中发现模式和相关性,然后根据数据分析结果做出最佳决策和预测。训练模型是机器学习中重要的一环,一个基本的模型训练和部署流程包括数据采集、数据标记、模型训练和设备部署。
这个项目分享将展示如何使用SenseCAP A1101视觉AI传感器,拍摄收集图像数据,从Roboflow进行数据标注并生成数据集,通过谷歌Colab和TensorFlow Lite训练出一个“剪刀、石头、布”手势识别模型,最后部署AI模型到设备并验证结果。如果用户有数据展示和分析需要,还可以将数据通过LoRaWAN无线传输到网关,并通过WiFi上传到SenseCAP云平台。
一、准备
1.1 硬件准备
-
电脑
-
USB Type-C线
1.2 硬件介绍
SenseCAP A1101
SenseCAP A1101 LoRaWAN视觉AI传感器是矽递科技新推出的一款LoRaWAN人工智能视觉传感器,带有一个30万像素82°广角摄像头,支持1-5米内的图像识别,可以通过USB连接电脑实时显示图像识别结果。
内置Wio-E5 LoRa模组,支持将图像识别结果通过低功耗、远距离传输方式上传到SenseCAP Cloud或第三方云平台。支持TensorFlow Lite和PyTorch,开发者可以通过TensorFlow Lite训练模型,小白用户也可以无需编写一行代码,用官方提供的文档体验整个模型训练流程。内置了人体目标识别和人数检测两个官方模型,方便用户测试。
和SenseCAP系列出的所有的工业传感器节点一样,产品自带IP66等级防水外壳,适用于部署到户外环境,19Ah大容量锂电池可以让设备长时间免维护。
SenseCAP M2 LoRaWAN网关
LoRa是一种低功耗、长距离的无线传输技术,不同于高功耗、室内使用的WiFi,LoRa更适用于低带宽、数据量小、长期免维护场景下的户外部署,可以做到长达数公里的网络覆盖。
SenseCAP M2是一个高性能的可提供LoRa信号覆盖的LoRaWAN网关,支持连接到Helium LongFi网络,能够为远程LoRaWAN设备提供数公里的无线网络覆盖范围和数据传输能力,并通过Helium获得别的用户利用网关进行数据传输产生的数据收益。
SenseCAP M2目前有868mHz和915mHz两种频段版本可选,因为SenseCAP A1101支持860-930mHz频段,所以可以选择任意一个版本进行测试。SenseCAP暂时还没推出国内470mHz频段版本的A1101。
1.3 软件准备
1.3.1 电脑环境安装
图像采集需要通过USB连接设备到电脑,通过运行脚本程序来实现,在此之前,我们需要给电脑安装好环境。Windows、Linux和 Intel Mac电脑系统的软件设置相同,而M1/M2 Mac则有所不同。
针对Windows、Linux、Mac:
确保计算机上已经安装了Python。如果没有,请访问Python官方页面(https://www.python.org/downloads/)下载并安装最新版本的Python,这里用到的是3.11.1版本。
运行以下代码安装libusb1依赖库。
pip3install libusb1
针对M1/ M2芯片的Mac:
-
首先通过以下代码安装Homebrew软件:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
通过以下代码安装conda:
brew install conda
-
通过以下代码下载 libusb
wgethttps://conda.anaconda.org/conda-forge/osx-arm64/libusb-1.0.26-h1c322ee_100.tar.bz2
-
通过以下代码安装 libusb
condainstall libusb-1.0.26-h1c322ee_100.tar.bz2
1.3.2 下载SenseCAP Mate 应用程序
SenseCAP Mate是矽递推出的针对SenseCAP LoRa系列产品的设备添加、配置、管理和数据展示的手机端App,在本案例中用于连接SenseCAP A1101,配置设备,和进行数据显示。根据手机操作系统通过以下链接下载安装。
-
Google Play
-
安卓
https://play.google.com/store/apps/details?id=cc.seeed.sensecapmate&hl=en&gl=US
-
APK下载
https://install.appcenter.ms/orgs/seeed/apps/sensecap-mate/distribution_groups/public
-
iOS
https://apps.apple.com/gb/app/sensecap-mate/id1619944834
1.3.3 下载SenseCAP A1101固件
设备原始固件
A1101_default_firmware.uf2
https://github.com/Seeed-Studio/Seeed_Arduino_GroveAI/raw/master/tools/A1101_default_firmware.uf2
图像采集固件
capture_images_A1101_firmware.uf2
https://github.com/Seeed-Studio/Seeed_Arduino_GroveAI/raw/master/tools/capture_images_A1101_firmware.uf2
二、模型训练过程
2.1 第一步:图像采集
需要让计算机拥有学习的能力并学会识别和判断,我们首先要给计算机输入学习的原料,告诉计算机需要识别的内容,这个原料也就是我们所说的数据集。用于模型训练的数据集可以来自于设备采集,也可以直接利用网上现有公开数据集,但是为了识别准确,和体验完整的模型训练流程,这里我们会用SenseCAP A1101直接进行采集。
2.1.1拷录图像采集固件
通过USB Type-C连接线将SenseCAP A1101连接到PC,双击设备上的配置按钮进入大容量存储模式,此时电脑会弹出一个命名为“VISIONAI”的驱动器。
将刚才下载好的SenseCAP A1101图像采集固件“capture_images_A1101_firmware.uf2”文件拖放到电脑弹出的“VISIONAI”驱动器,一旦uf2 完成复制到驱动器中,驱动器就会自动消失说明已经成功上传到设备中。
2.1.2在电脑运行图像采集脚本
图像采集脚本代码请见文末附件,也可通过以下链接获得:
https://github.com/Seeed-Studio/Seeed_Arduino_GroveAI/blob/master/tools/capture_images_script.py
直接下载.py文件或将链接中的代码复制到一个新建记事本,命名为“capture_images_script.py”,保存到所在目录,通过以下命令行运行脚本。
python3 capture_images_script.py
该脚本默认每300ms采集一张图片,如果需要修改采集时间间隔,作为例子,改为1000ms采集一次,可以参照使用以下代码:
python3 capture_images_script.py --interval 1000
脚本运行后,就可以用SenseCAP A1101摄像头对准要采集的目标进行图像采集了。此时电脑会自动新建一个名为“save_img”的文件夹,采集到的照片会实时存储在该文件夹内,同时也可以通过脚本运行获得实时摄像头数据。
本案例是想让设备分辨“剪刀石头布”的手势,所以我分别就剪刀、石头、布的手势各自作了一定数量的图像采集。比较理想的情况下是光照情况好的单一背景下进行图像采集,办公室的背景比较繁杂,灯光效果也不好(灯管炫光),这些对于数据集的质量都会有一定影响,但是这里为了示范,也同时想验证不同采集质量下模型的识别率,就简单的在办公室环境进行了采集。
2.1.3退出图像采集
拍摄完足够的图像后,单击终端窗口并按下以下组合键可以停止拍摄:
-
Windows: Ctrl + Break
-
Linux: Ctrl + Shift + \
-
Mac: CMD + Shift + \
2.1.4重新拷录原始设备固件
因为图像采集固件是单独的固件,和SenseCAP A1101设备出厂固件不一致,在完成数据集的图像采集后,需要将SenseCAP A1101内部的固件更改回原始状态,以便再次加载对象检测模型进行检测。
参照上文拷录图像采集固件的步骤,再次通过USB Type-C连接SenseCAP A1101设备,双击配置按钮进入引导模式将已经下载的名为“A1101_default_firmware.uf2”的设备原始固件重新拷录到SenseCAP A1101中。
2.2 第二步:图像标记
Roboflow(https://app.roboflow.com/)是一款在线的标注工具。可以方便上传图像、对所有图像进行标注,生成数据集,将数据集导出为.zip文件或代码,在使用前,我们需要注册一个账号并新建一个项目。
2.2.1创建项目
需要设置项目名称,开源许可方式,项目类型和项目介绍,项目类型我们这里选择“Object Detection”目标检测。
2.2.2 上传图片
我们可以方便地在“Upload”栏目里将设备采集到的图像数据用直接拖拽或文件夹选取的方式上传到Roboflow。
上传完成后,可以根据需要,调整“Train”、“Valid”、“Test”数据的比例,分别用于训练、验证、和测试。
2.2.3 进行标记
在“Annotate”栏目对每张图片中的需要识别的地方进行框选,然后打上设定好的数据标签,这里我设置的是“Paper”、“Scissors”、“Rock”分别对应“剪刀”、“石头”、“布”。
因为图片数量较多,标注起来需要点耐心,越多的数据输入能保证更高的准确度,我这里为了简单演示,只采集和标注了300张左右,对于模型训练来说,这算是一个非常非常小的数据集。
在Preprocessing一栏,建议将图像大小更改为192x192像素,因为我们将使用这个大小进行训练,并且训练会更快。否则,在训练过程中必须将所有图像转换为192x192,这会消耗更多的CPU资源,使训练过程变慢。
2.2.4 模型导出
完成数据集设置后,就可以在“Generate”栏目进行数据集生成了,可以根据不同的图片和设置生成不同的数据集版本,生成后可以导出为代码,也可以直接导出为.zip文件。在这里,我们需要导出为代码,选择“YOLO v5 Pytorch”格式,“show download code”,最后点击“Continue”。
在导出代码的页面,我们需要将整段代码复制下来,在下一步训练模型的时候将会用到。你会得到一个被遮盖的“api_key”,请不要和别人分享。
2.3 第三步:模型训练
训练可以在本地计算机进行,也可以在云上进行,提供模型训练的云服务平台有很多,我们这里选择Google Colab和矽递官方提供的训练代码,不需要自己编写代码,导入数据集后,跟着代码运行就可以完成一站式整个训练流程。
Google Colab项目链接:
https://colab.research.google.com/gist/lakshanthad/b47a1d1a9b4fac43449948524de7d374/yolov5-training-for-sensecap-a1101.ipynb
在这个链接的代码里,Google Colab主要完成以下几步:
•建立培训环境
•导入数据集
•采用YOLOv5算法进行模型训练
•下载训练好的模型
点击每一段的代码,我们会直接看到运行的过程和结果。
在Step4的代码框里,我们将刚才在Roboflow导出的模型代码全部复制进去替换掉原有内容,然后点击左侧运行按钮。
在最后,我们将可以得到一个名为“model-1.uf2”的模型文件,这时,我们就可以将训练好的模型部署到SenseCAP A1101设备中了。
2.4 第四步:设备部署
设备部署包含两个步骤,将模型部署到设备上,再将设备部署在实际使用场景里,这里我们缺乏实际可部署的场景,只是作为模型验证使用的话,我们就简单介绍如何将模型部署到设备。
拷录模型的过程和拷录固件流程是一样的,通过USB Type-C将SenseCAP A1101连接到电脑,通过USB Type-C连接线将SenseCAP A1101连接到PC,双击设备上的配置按钮进入大容量存储模式,将模型文件直接拖到弹出的“VISIONAI”的驱动器,驱动器闪退就代表模型拷录成功了。
SenseCAP A1101一共支持添加4个模型,如果用户有多个模型需要拷录,只需要重复上述步骤即可。
三、验证模型
一切准备就绪,我们可以通过电脑和手机App来测试我们的测试我们的模型输出的结果了!
3.1 App设备连接
首先打开下载好的SenseCAP Mate App。如果没有SenseCAP的账号,需要先注册一个。在Config配置页,选“Vision AI Sensor”,长按设备配置按键3秒,蓝灯闪烁进入蓝牙模式,在“Select Device”页面选择搜索到的对应的设备SN进行连接。
3.2设备配置
连接设备后,在Settings设置页面开始进行以下配置:
-
“Platform”:指的是数据传输到哪个云平台,SenseCAP M2是提供Helium LoRa网络的网关,同时也可以连接到SenseCAP Cloud云平台进行数据展示,在这里我们选择“SenseCAP for Helium”;
-
“Algorithm”:选择算法类型“Object Detection”目标检测;
-
“AI Model”:选择“user defined”用户自定义的model;
-
“Credibility”,指的是选择的置信度,选择范围从1-100%,意思是模型判断为一个目标的可信度达到多少的时候,设备才会认为特定目标被检测到,我们可以自行选择,这里设置50%。
-
“Frequency Plan”,频段计划,因为SenseCAP M2网关目前只有US915和EU868两种国外频段作为选择,我们这里任意选择一个,频段和网关使用的频段对应即可。
-
“Uplink Interval”,设置每隔多久上传一次数据到云平台,根据LoRa的低功耗特性,一般设置5分钟以上。
-
“Packet Policy”,选择默认“2C+1N”模式即可。
点击“Send”发送完成配置,然后回到“General”页,点击下方“Detect”按钮开始检测。
3.3
网页端显示
将SenseCAP A1101通过USB Type-C线连接到电脑,此时电脑会弹出一个弹窗。点击弹窗,即可进入到实时检测的预览界面。如果有些电脑没有自动弹出,也可以访问网址:https://files.seeedstudio.com/grove_ai_vision/index.html
打开网页后选择连接的设备“SenseCAP A1101”并点击“Connect”按钮,这时就可以在屏幕上看到实时检测的界面和目标检测结果了!
可以观察到,在图像识别的界面里,红色框框中就是设备识别到的被检测目标,第一个数字1,2,3分别代表了不用的数据标签,第二个数字代表百分比的置信度。
实时检测预览中不会对应显示我们数据集中定义的数据标签,只会显示数值作为区分,从结果看到,1对应“石头”,2对应“布”,3对应“剪刀”。一些特殊的和“剪刀”和“布”有着共同特征的不在数据集内的手势,设备根据其特征,也做出了一个判断并给出一个置信度,但相比于真正的“布”置信度为91%,给的置信度较低,只有77%。
3.4上传数据到云平台
除了可以在本地直接连接电脑查看实时检测数据外,这些数据还可以以我们在App配置的时间间隔上传到SenseCAP云平台(https://sensecap.seeed.cc/)或第三方云平台,在SenseCAP云平台上,我们可以通过手机端或电脑端查看周期内的数据和进行图表化显示。SenseCAP的云平台和SenseCAP Mate App用的是同一个账号系统。
因为本案例重点在于讲述模型训练过程,就此略去云平台数据展示的部分,有兴趣的用户可以到SenseCAP云平台尝试添加设备和查看数据。
四、总结
总的来说,这次的模型训练得到的模型基本能够判断出想要识别的三个主要目标,但未能实现非常高的精确度,会对一些相似的目标产生误判,总结原因,有以下几个方面:
-
数据集数量相对较小,总共才采集和标记300多张图片,不足以让算法深入学习并给出可靠模型和高精度的结果;
-
数据采集的环境不理想,强光反射、繁杂背景等因素影响,对数据集的质量产生影响;
-
识别的两个目标共同特征多,区分度不够明显,如“布”实际包含了“剪刀”的全部特征;
-
仅通过通用算法进行了一次简单的训练,未有通过后续的判断结果继续进行多次训练优化模型以得到理想效果
附录
图像采集脚本代码:
1import os
2import usb1
3from PIL import Image
4from io import BytesIO
5import argparse
6import time
7import cv2
8import numpy as np
9from threading import Thread
10
11WEBUSB_JPEG_MAGIC = 0x2B2D2B2D
12WEBUSB_TEXT_MAGIC = 0x0F100E12
13
14VendorId = 0x2886 # seeed studio
15ProductId = [0x8060, 0x8061]
16
17class Receive_Mess():
18 def __init__(self, arg, device_id):
19 self.showimg = not arg.unshow
20 self.saveimg = not arg.unsave
21 self.interval = arg.interval
22 self.img_number = 0
23 self.ProductId = []
24 os.makedirs("./save_img", exist_ok=True)
25 self.expect_size = 0
26 self.buff = bytearray()
27 self.device_id = device_id
28 self.context = usb1.USBContext()
29 self.get_rlease_device(device_id, False)
30 self.disconnect()
31 self.pre_time = time.time() * 1000
32 time.time_ns()
33
34 def start(self):
35 while True:
36 if not self.connect():
37 continue
38 self.read_data()
39 del self.handle
40 self.disconnect()
41
42 def read_data(self):
43 # Device not present, or user is not allowed to access device.
44 with self.handle.claimInterface(2):
45 # Do stuff with endpoints on claimed interface.
46 self.handle.setInterfaceAltSetting(2, 0)
47 self.handle.controlRead(0x01 << 5, request=0x22, value=0x01, index=2, length=2048, timeout=1000)
48 # Build a list of transfer objects and submit them to prime the pump.
49 transfer_list = []
50 for _ in range(1):
51 transfer = self.handle.getTransfer()
52 transfer.setBulk(usb1.ENDPOINT_IN | 2, 2048, callback=self.processReceivedData, timeout=1000)
53 transfer.submit()
54 transfer_list.append(transfer)
55 # Loop as long as there is at least one submitted transfer.
56 while any(x.isSubmitted() for x in transfer_list):
57 # reading data
58 self.context.handleEvents()
59
60 def pare_data(self, data: bytearray):
61 if len(data) == 8 and int.from_bytes(bytes(data[:4]), 'big') == WEBUSB_JPEG_MAGIC:
62 self.expect_size = int.from_bytes(bytes(data[4:]), 'big')
63 self.buff = bytearray()
64 elif len(data) == 8 and int.from_bytes(bytes(data[:4]), 'big') == WEBUSB_TEXT_MAGIC:
65 self.expect_size = int.from_bytes(bytes(data[4:]), 'big')
66 self.buff = bytearray()
67 else:
68 self.buff = self.buff + data
69
70 if self.expect_size == len(self.buff):
71 try:
72 Image.open(BytesIO(self.buff))
73 except:
74 self.buff = bytearray()
75 return
76 if self.saveimg and ((time.time() * 1000 - self.pre_time) > self.interval):
77 with open(f'./save_img/{time.time()}.jpg', 'wb') as f:
78 f.write(bytes(self.buff))
79 self.img_number += 1
80 print(f'\rNumber of saved pictures on device {self.device_id}:{self.img_number}', end='')
81 self.pre_time = time.time() * 1000
82
83 if self.showimg:
84 self.show_byte()
85 self.buff = bytearray()
86
87 def show_byte(self):
88 try:
89 img = Image.open(BytesIO(self.buff))
90 img = np.array(img)
91 cv2.imshow('img', cv2.cvtColor(img,cv2.COLOR_RGB2BGR))
92 cv2.waitKey(1)
93 except:
94 return
95
96 def processReceivedData(self, transfer):
97 if transfer.getStatus() != usb1.TRANSFER_COMPLETED:
98 # transfer.close()
99 return
100
101 data = transfer.getBuffer()[:transfer.getActualLength()]
102 # Process data...
103 self.pare_data(data)
104 # Resubmit transfer once data is processed.
105 transfer.submit()
106
107 def connect(self):
108 '''Get open devices'''
109 self.handle = self.get_rlease_device(self.device_id, get=True)
110 if self.handle is None:
111 print('\rPlease plug in the device!')
112 return False
113 with self.handle.claimInterface(2):
114 self.handle.setInterfaceAltSetting(2, 0)
115 self.handle.controlRead(0x01 << 5, request=0x22, value=0x01, index=2, length=2048, timeout=1000)
116 print('device is connected')
117 return True
118
119 def disconnect(self):
120 try:
121 print('Resetting device...')
122 with usb1.USBContext() as context:
123 handle = context.getByVendorIDAndProductID(VendorId, self.ProductId[self.device_id],
124 skip_on_error=False).open()
125 handle.controlRead(0x01 << 5, request=0x22, value=0x00, index=2, length=2048, timeout=1000)
126 handle.close()
127 print('Device has been reset!')
128 return True
129 except:
130 return False
131
132 def get_rlease_device(self, did, get=True):
133 '''Turn the device on or off'''
134 tmp = 0
135 print('*' * 50)
136 print('looking for device!')
137 for device in self.context.getDeviceIterator(skip_on_error=True):
138 product_id = device.getProductID()
139 vendor_id = device.getVendorID()
140 device_addr = device.getDeviceAddress()
141 bus = '->'.join(str(x) for x in ['Bus %03i' % (device.getBusNumber(),)] + device.getPortNumberList())
142 if vendor_id == VendorId and product_id in ProductId and tmp == did:
143 self.ProductId.append(product_id)
144 print('\r' + f'\033[4;31mID {vendor_id:04x}:{product_id:04x} {bus} Device {device_addr} \033[0m',
145 end='')
146 if get:
147 return device.open()
148 else:
149 device.close()
150 print(
151 '\r' + f'\033[4;31mID {vendor_id:04x}:{product_id:04x} {bus} Device {device_addr} CLOSED\033[0m',
152 flush=True)
153 elif vendor_id == VendorId and product_id in ProductId:
154 self.ProductId.append(product_id)
155 print(f'\033[0;31mID {vendor_id:04x}:{product_id:04x} {bus} Device {device_addr}\033[0m')
156 tmp = tmp + 1
157 else:
158 print(
159 f'ID {vendor_id:04x}:{product_id:04x} {bus} Device {device_addr}')
160
161
162def implement(arg, device):
163 rr = Receive_Mess(arg, device)
164 time.sleep(1)
165 rr.start()
166
167
168if __name__ == '__main__':
169 opt = argparse.ArgumentParser()
170 opt.add_argument('--unsave', action='store_true', help='whether save pictures')
171 opt.add_argument('--unshow', action='store_true', help='whether show pictures')
172 opt.add_argument('--device-num', type=int, default=1, help='Number of devices that need to be connected')
173 opt.add_argument('--interval', type=int, default=300, help='ms,Minimum time interval for saving pictures')
174 arg = opt.parse_args()
175 if arg.device_num == 1:
176 implement(arg, 0)
177 elif arg.device_num <= 0:
178 raise 'The number of devices must be at least one!'
179 else:
180 pro_ls = []
181 for i in range(arg.device_num):
182 pro_ls.append(Thread(target=implement, args=(arg, i,)))
183 for i in pro_ls:
184 i.start()
上下滑动查看更多