用Sipeed M1s Dock实现网络相机
1 项目介绍
这是电子森林2023寒假一起练平台(3)- 基于Sipeed M1s Dock综合应用的项目。我完成的是项目5 - 网络相机。项目实现了用Sipeed M1s Dock实现网络相机,完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄,通过电脑端编程将图片合成为一个视频。
2 设计思路
首先完成M1s Dock摄像头的驱动,接着实现M1s Dock定时拍照功能,然后M1s Dock通过wifi连接到路由器,再通过TCP将照片发送到局域网内服务器,最后电脑端Python程序完成视频的合成。
3 框图和软件流程图
4 简单的硬件介绍
1、硬件参数:
- 主芯片 BL808 RISC-V 480Mhz + NPU BLAI-100
- 板载 USB 转 UART 调试器(可实现一键点击烧录,无需按实体按键)
- 1.69 寸 240x280 电容触摸屏
- 200W 像素摄像头
- 支持 2.4G WIFI / BT / BLE
- 板载 1 个模拟麦克风、1 个 LED、1 个 TF 卡座
- 引出一路 USB-OTG 到 USB Type-C 接口
2、引脚图
5 实现的功能及图片展示
5.1 安装WSL2
WSL是适用于 Linux 的 Windows 子系统,可以安装 Linux 发行版(例如 Ubuntu),并直接在 Windows 上使用 Linux 应用程序、实用程序和 Bash 命令行工具。
具体可以参考微软官方的文档。
https://learn.microsoft.com/zh-cn/windows/wsl/install
5.2 安装Ubuntu22.04.2LTS
打开Microsoft Store,安装Ubuntu22.04.2LTS。
5.3 配置SDK编译环境
1、安装编译所需要的相关软件
sudo apt-get install git make tree
2、获取例程仓库
git clone https://gitee.com/Sipeed/M1s_BL808_example.git
3、获得 SDK 仓库
git clone https://gitee.com/sipeed/M1s_BL808_SDK.git
4、在 SDK 仓库文件夹下,获取编译工具链
mkdir -p M1s_BL808_SDK/toolchaincd M1s_BL808_SDK/toolchain
git clone https://gitee.com/wonderfullook/m1s_toolchain.git
mv m1s_toolchain Linux_x86_64
cd ../../
5、确定 M1s_BL808_SDK 文件夹所在的路径
cd M1s_BL808_SDK
pwd
6、配置编译工具链路径
export BL_SDK_PATH=/home/user/BL808/M1s_BL808_SDK
5.4 修改并编译DEMO
1、修改camera_streaming_through_wifi官方DEMO
打开例程路径 M1s_BL808_example/c906_app/camera_streaming_through_wifi 中的 main.c 文件。
修改main.c文件中m1s_xram_wifi_connect()函数后面的wifi的SSID和密码,修改m1s_xram_wifi_upload_stream()函数后的服务器地址为本机IP地址。
#include <stdbool.h>
#include <stdio.h>
/* FreeRTOS */
#include <FreeRTOS.h>
#include <task.h>
/* bl808 c906 std driver */
#include <bl808_glb.h>
#include <bl_cam.h>
#include <m1s_c906_xram_wifi.h>
void main()
{
vTaskDelay(1);
bl_cam_mipi_mjpeg_init();
m1s_xram_wifi_init();
m1s_xram_wifi_connect("doudou", "88888888");
m1s_xram_wifi_upload_stream("192.168.2.102", 8888);
}
2、编译SDK
cd M1s_BL808_example/c906_app
./build.sh camera_streaming_through_wifi
然后编译出来的固件就会在 M1s_BL808_example/e907_app/build_out 目录下,名称为 firmware.bin.
5.5 烧录固件
1、下载官方图形化烧录工具
前往 https://dev.bouffalolab.com/download 下载名称为 Bouffalo Lab Dev Cube
的文件。
或者点击https://dev.bouffalolab.com/media/upload/download/BouffaloLabDevCube-v1.8.3.zip链接直接下载。
2、烧录
打开Bouffalo Lab Dev Cube,选择BL808,如下图所示:
点击Browse,选择分区表文件partition_cfg_16M_m1sdock.toml,如下图所示:
分区表文件从https://dl.sipeed.com/fileList/MAIX/M1s/M1s_Dock/7_Firmware/partition/partition_cfg_16M_m1sdock.toml下载,附件里也有。
依次选择boot2(官方提供默认boot2文件boot2_isp_debug.bin)、d0fw(刚才编译的SDK文件,)、firmware(官方提供默认firmware文件)固件文件
boot2:位于 BLDevCube\chips\bl808\builtin_imgs\boot2_isp_bl808_v6.5.4下面
d0fw:就是刚才编译的SDK文件,位于M1s_BL808_example/e907_app/build_out目录下,名称为 firmware.bin
firmware:使用官方提供的默认firmware,可以从下面链接下载
https://dl.sipeed.com/fileList/MAIX/M1s/M1s_Dock/7_Firmware/factory/firmware_20230227.bin
用TypeC的数据线连接UART口到电脑,点击Rerresh按钮进行刷新。在Interface中选择Uart,从Port/SN显示的连续的2个端口号中,选择数字比较大的端口号。
同时按住板子上的 BOOT 键和 RST 键, 然后先松开 RST 键,再松开 BOOT 键,使板子进入串口烧录模式。
点击下载 Create & Download按钮进行烧录。
如上图所示,进度条显示100%,下方log显示“All Success”,表示烧录成功。
2、编写Python程序
import socket
import os.path
import numpy as np
import cv2 as cv
img_path = r"D:\\CV_image\\"
viedo_path = r"D:\\CV_viedo\\"
count = 1
name_count = 1
viedo_count = 1
file_dir = 'd:/CV_image/'
def viedoSave():
list = []
for root ,dirs, files in os.walk(file_dir):
for file in files:
list.append(file) # 获取目录下文件名列表
# VideoWriter是cv2库提供的视频保存方法,将合成的视频保存到该路径中
# 'MJPG'意思是支持jpg格式图片
# fps = 5代表视频的帧频为5,如果图片不多,帧频最好设置的小一点
# (800,600)是生成的视频像素800*600,一般要与所使用的图片像素大小一致,否则生成的视频无法播放
#
video = cv.VideoWriter(viedo_path + str(viedo_count) + '.avi',cv.VideoWriter_fourcc(*'MJPG'),1,(800,600))
for i in range(1,len(list)):
#for i in range(1,60):
#读取图片
img = cv.imread(img_path + list[i-1])
# img = cv.imread(img_path + str((viedo_count - 1) * 60 + i))
# resize方法是cv2库提供的更改像素大小的方法
# 将图片转换为800*600像素大小
# img = cv.resize(img,(800,600))
# 写入视频
video.write(img)
if __name__ == '__main__':
# 创建tcp服务端套接字
# 参数同客户端配置一致,这里不再重复
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立即释放,否则的话在30秒-2分钟之内这个端口是不会被释放的,这是TCP的为了保证传输可靠性的机制。
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 给客户端绑定端口号,客户端需要知道服务器的端口号才能进行建立连接。IP地址不用设置,默认就为本机的IP地址。
tcp_server.bind(("", 8888))
# 设置监听
# 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务与多个客户端
# 不需要让客户端进行等待建立连接
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字tcp_client来完成
tcp_server.listen(128)
# 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 1. 专门和客户端通信的套接字: tcp_client
# 2. 客户端的ip地址和端口号: tcp_client_address
tcp_client, tcp_client_address = tcp_server.accept()
# 代码执行到此说明连接建立成功
print("客户端的ip地址和端口号:", tcp_client_address)
timeF = 15 # 视频帧计数间隔频率
while True:
# 接收客户端发送的数据, 这次接收数据的最大字节数是4
recv_data = tcp_client.recv(4)
mjpeg_len = int.from_bytes(recv_data, 'little')
print("recv len: ", mjpeg_len)
tcp_client.send(recv_data)
recv_data_mjpeg = b''
remained_bytes = mjpeg_len
while remained_bytes > 0:
recv_data_mjpeg += tcp_client.recv(remained_bytes)
remained_bytes = mjpeg_len - len(recv_data_mjpeg)
print("recv stream success")
if recv_data_mjpeg[:2] != b'\xff\xd8' \
or recv_data_mjpeg[-2:] != b'\xff\xd9':
continue
mjpeg_data = np.frombuffer(recv_data_mjpeg, 'uint8')
img = cv.imdecode(mjpeg_data, cv.IMREAD_COLOR)
cv.imshow('stream', img)
if (count % timeF == 0): # 每隔timeF帧进行存储操作
cv.imwrite(img_path + str(name_count) + '.jpg', img)
name_count=name_count+1
count = count + 1
if (count % 60 == 0):
viedoSave()
viedo_count = viedo_count + 1
if cv.waitKey(1) == 'q':
exit(0)
# 关闭服务与客户端的套接字, 终止和客户端通信的服务
tcp_client.close()
# 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务 但是正常来说服务器的套接字是不需要关闭的,因为服务器需要一直运行。
# tcp_server.close()
import socket
import numpy as np
import cv2 as cv
6.2 定义变量
img_path = r"D:\\CV_image\\"
viedo_path = r"D:\\CV_viedo\\"
count = 1
name_count = 1
viedo_count = 1
file_dir = 'd:/CV_image/'
6.3 创建tcp服务端套接字
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
6.4 设置端口号复用,让程序退出端口号立即释放,否则的话在30秒-2分钟之内这个端口是不会被释放的,这是TCP的为了保证传输可靠性的机制。
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
6.5 给客户端绑定端口号,客户端需要知道服务器的端口号才能进行建立连接。IP地址不用设置,默认就为本机的IP地址。
tcp_server.bind(("", 8888))
6.6 设置监听,128是最大等待建立连接的个数
tcp_server.listen(128)
6.7 等待客户端建立连接的请求
tcp_client, tcp_client_address = tcp_server.accept()
6.8 打印客户端IP地址和端口号,用于判断建立连接是否成功
print("客户端的ip地址和端口号:", tcp_client_address)
6.9 接收客户端发送的数据
# 接收客户端发送的数据, 这次接收数据的最大字节数是4
recv_data = tcp_client.recv(4)
mjpeg_len = int.from_bytes(recv_data, 'little')
print("recv len: ", mjpeg_len)
tcp_client.send(recv_data)
recv_data_mjpeg = b''
remained_bytes = mjpeg_len
while remained_bytes > 0:
recv_data_mjpeg += tcp_client.recv(remained_bytes)
remained_bytes = mjpeg_len - len(recv_data_mjpeg)
print("recv stream success")
if recv_data_mjpeg[:2] != b'\xff\xd8' \
or recv_data_mjpeg[-2:] != b'\xff\xd9':
continue
mjpeg_data = np.frombuffer(recv_data_mjpeg, 'uint8')
img = cv.imdecode(mjpeg_data, cv.IMREAD_COLOR)
cv.imshow('stream', img)
6.10 保存照片
if (count % timeF == 0): # 每隔timeF帧进行存储操作
cv.imwrite(img_path + str(name_count) + '.jpg', img)
name_count=name_count+1
count = count + 1
6.11 合成视频
if (count % 60 == 0):
viedoSave()
viedo_count = viedo_count + 1
def viedoSave():
list = []
for root ,dirs, files in os.walk(file_dir):
for file in files:
list.append(file) # 获取目录下文件名列表
# VideoWriter是cv2库提供的视频保存方法,将合成的视频保存到该路径中
# 'MJPG'意思是支持jpg格式图片
# fps = 5代表视频的帧频为5,如果图片不多,帧频最好设置的小一点
# (800,600)是生成的视频像素800*600,一般要与所使用的图片像素大小一致,否则生成的视频无法播放
#
video = cv.VideoWriter(viedo_path + str(viedo_count) + '.avi',cv.VideoWriter_fourcc(*'MJPG'),1,(800,600))
for i in range(1,len(list)):
#for i in range(1,60):
#读取图片
img = cv.imread(img_path + list[i-1])
# img = cv.imread(img_path + str((viedo_count - 1) * 60 + i))
# resize方法是cv2库提供的更改像素大小的方法
# 将图片转换为800*600像素大小
# img = cv.resize(img,(800,600))
# 写入视频
video.write(img)
7 完整代码
import socket
import os.path
import numpy as np
import cv2 as cv
img_path = r"D:\\CV_image\\"
viedo_path = r"D:\\CV_viedo\\"
count = 1
name_count = 1
viedo_count = 1
file_dir = 'd:/CV_image/'
def viedoSave():
list = []
for root ,dirs, files in os.walk(file_dir):
for file in files:
list.append(file) # 获取目录下文件名列表
# VideoWriter是cv2库提供的视频保存方法,将合成的视频保存到该路径中
# 'MJPG'意思是支持jpg格式图片
# fps = 5代表视频的帧频为5,如果图片不多,帧频最好设置的小一点
# (800,600)是生成的视频像素800*600,一般要与所使用的图片像素大小一致,否则生成的视频无法播放
#
video = cv.VideoWriter(viedo_path + str(viedo_count) + '.avi',cv.VideoWriter_fourcc(*'MJPG'),1,(800,600))
for i in range(1,len(list)):
#for i in range(1,60):
#读取图片
img = cv.imread(img_path + list[i-1])
# img = cv.imread(img_path + str((viedo_count - 1) * 60 + i))
# resize方法是cv2库提供的更改像素大小的方法
# 将图片转换为800*600像素大小
# img = cv.resize(img,(800,600))
# 写入视频
video.write(img)
if __name__ == '__main__':
# 创建tcp服务端套接字
# 参数同客户端配置一致,这里不再重复
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立即释放,否则的话在30秒-2分钟之内这个端口是不会被释放的,这是TCP的为了保证传输可靠性的机制。
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 给客户端绑定端口号,客户端需要知道服务器的端口号才能进行建立连接。IP地址不用设置,默认就为本机的IP地址。
tcp_server.bind(("", 8888))
# 设置监听
# 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务与多个客户端
# 不需要让客户端进行等待建立连接
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字tcp_client来完成
tcp_server.listen(128)
# 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 1. 专门和客户端通信的套接字: tcp_client
# 2. 客户端的ip地址和端口号: tcp_client_address
tcp_client, tcp_client_address = tcp_server.accept()
# 代码执行到此说明连接建立成功
print("客户端的ip地址和端口号:", tcp_client_address)
timeF = 15 # 视频帧计数间隔频率
while True:
# 接收客户端发送的数据, 这次接收数据的最大字节数是4
recv_data = tcp_client.recv(4)
mjpeg_len = int.from_bytes(recv_data, 'little')
print("recv len: ", mjpeg_len)
tcp_client.send(recv_data)
recv_data_mjpeg = b''
remained_bytes = mjpeg_len
while remained_bytes > 0:
recv_data_mjpeg += tcp_client.recv(remained_bytes)
remained_bytes = mjpeg_len - len(recv_data_mjpeg)
print("recv stream success")
if recv_data_mjpeg[:2] != b'\xff\xd8' \
or recv_data_mjpeg[-2:] != b'\xff\xd9':
continue
mjpeg_data = np.frombuffer(recv_data_mjpeg, 'uint8')
img = cv.imdecode(mjpeg_data, cv.IMREAD_COLOR)
cv.imshow('stream', img)
if (count % timeF == 0): # 每隔timeF帧进行存储操作
cv.imwrite(img_path + str(name_count) + '.jpg', img)
name_count=name_count+1
count = count + 1
if (count % 60 == 0):
viedoSave()
viedo_count = viedo_count + 1
if cv.waitKey(1) == 'q':
exit(0)
# 关闭服务与客户端的套接字, 终止和客户端通信的服务
tcp_client.close()
# 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务 但是正常来说服务器的套接字是不需要关闭的,因为服务器需要一直运行。
# tcp_server.close()
8 实现的功能及图片展示
8.1 拍照功能
8.2 视频合成功能
9 遇到的主要难题及解决方法
9.1 难题1:WSL2的安装始终不成功。
原因1:windows系统版本过低,,对于 x64 系统:版本 1903 或更高版本,内部版本为 18362 或更高版本。
解决方法:可以尝试通过Windows Update 助手更新系统版本,或者重新安装新版本的windows10或者11。笔者尝试更新版本不成功,最终选择了重新安装win11。
原因2:未安装虚拟化功能
以管理员身份打开 PowerShell 并运行:
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
9.2 SDK编译环境的配置
原因1:权限问题
Linux下任何操作都提示没有权限,比如git clone
解决方法:删除SDK和example所有目录和文件,重新git下载。
9.3 编译错误
原因:未安装make
解决方法:安装make
sudo apt-get install make
9.4 Python端程序的编写
解决方法:在官方程序的基础上,查询opencv函数,增加保存图片和合成视频的功能。
10 未来的计划或建议
Sipeed的采用BL808主控的M1S Dock相比较采用K210主控的M1具有更大的RAM,更高的核心频率,同时支持蓝牙和Zigbee。目前BL808的用户比较少,教程与案例比较少,
缺乏生态。未来计划主要依托博流和矽速官方github、wiki相关文档、例程进行学习,遇到问题可以向电子森林活动群、矽速官方QQ群寻求帮助。主要学习lvgl、模型训练、人脸识别等。