内容介绍
项目介绍
本项目基于Sipeed M1s Dock,使用M1s_BL808_SDK与VSCode开发,实现了网络相机功能,可进行长时间拍摄,并在PC上获取并保存拍摄的图像,合成多帧图像为视频。
硬件介绍
Sipeed M1s Dock是基于Sipeed M1s模组来设计的一款核心板,引出了MIPI CSI、SPI LCD等FPC接口,免去接线难的烦恼。使用最精简的设计,用于客户对模组进行模组评估,或者爱好者直接上手游玩等用途。主要包含:
- 主芯片 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 接口
软件实现
设计思路
主体软件实现分两个模块:BL808端、上位机端(一般为PC/服务器),二者通过TCP协议进行数据流传输。软件设计思路如下图所示,具体细节在后续章节讲。
BL808
主要参考官方提供的示例:camera_stremaing_through_wifi
。将其中的wifi与IP对应修改为自己的wifi与上位机实际IP。注意上位机与BL808要连接至同一WIFI,确保在同一局域网下。
⚠️ 对固件有一定要求,参见:WIFI 串流摄像头 DEMO;板卡的固件要求为 firmware_20230227.bin
,通过串口烧录即可。
#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();
// Change WIFI and IP into your's
m1s_xram_wifi_connect("ssid", "password");
m1s_xram_wifi_upload_stream("192.168.3.16", 8888);
}
之后,按教程编译并下载即可运行。主要功能依次为MIPI初始化、WIFI初始化、WIFI连接、将摄像头流数据上传至上位机所在IP的8888端口。
上位机
在运行程序之前,需要关闭防火墙/打开8888端口(直接关闭防火墙对于PC/服务器来说并不安全,建议开放8888端口即可)。总体上位机软件基于Python实现,通过socket建立TCP套接字服务,应答网络请求,将BL808上传的数据流根据一定格式解码为图片,并保存本地。从而,合成多帧图像为视频。
pip包依赖主要为numpy、opencv-python;Python版本3.8以上基本均可。main()
函数内,主要工作有如下三点:
- 创建以时间命名的唯一文件夹,存放图片与视频;
- 通过socket库建立TCP套接字服务,获取BL808上传的流数据,并保存图像
getStreamData()
;具体为:根据特殊的格式使用np.frombuffer()
存成uint8
格式,并通过cv2.imdecode()
解码成图片,保存为jpg格式;在接收客户端发送的数据时,接收的最大字节数为4,先获取此次信息的长度,并通过while()
循环直至接收到这么多的数据;通过判断数据结尾2位是否为\xff\xd8
或\xff\xd9
,只要不满足其一则说明接收到了完整的一帧图像,从而进行下一帧图像接收; - 待获取一段时间图像结束后,可通过
makeVideo()
合成视频。虽然有考虑在获取的同时,不断拼接图片成视频,但因为该应用多用于无人值守监控等场景,因此似乎不会有边采集图像边生成视频的需求,而是将采集的图片合成视频作为存档备用,所以将获取图像与合成视频两个功能分开实现,在代码上更易实现,也更符合实际需求。
def main():
fps = 30
dir_name: Path = Path(__file__).parent / ("./images_" +
datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S'))
if not dir_name.exists():
dir_name.mkdir(parents=False, exist_ok=True)
getStreamData(dir_name)
makeVideo(dir_name, fps)
在getStreamData()
内,通过camera_streaming_through_wifi
的示例编写程序,多加了一个保存图像的步骤,为了合成视频时方便顺序遍历,保存的图像依次命名为0、1、2、3……
def getStreamData(img_dir: Path):
# 创建tcp服务端套接字
# 参数同客户端配置一致,这里不再重复
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server.settimeout(10)
# 设置端口号复用,让程序退出端口号立即释放,否则的话在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()
# 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 1. 专门和客户端通信的套接字: tcp_client
# 2. 客户端的ip地址和端口号: tcp_client_address
tcp_client, tcp_client_address = tcp_server.accept()
# 代码执行到此说明连接建立成功
print("客户端的ip地址和端口号:", tcp_client_address)
count = 0
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 = cv2.imdecode(mjpeg_data, cv2.IMREAD_COLOR)
img_path = img_dir / f"{count}.jpg"
cv2.imwrite(str(img_path), img)
count = count + 1
cv2.imshow('stream', img)
if cv2.waitKey(1) == 27:
break
# 关闭服务与客户端的套接字, 终止和客户端通信的服务
tcp_client.close()
最后通过cv2.waitKey(1) == 27
判断用户是否按下Esc,按下则退出循环,结束socket连接,结束获取图像的工作。同时,开始合成多帧图像为视频并存档:
def makeVideo(img_dir: Path, fps: int):
video_path: Path = img_dir / "output.mp4"
# MP4V costs less storage
video = cv2.VideoWriter(str(video_path), cv2.VideoWriter_fourcc(
'M', 'P', '4', 'V'), fps, (800, 600))
for image in img_dir.iterdir():
img = cv2.imread(str(image))
video.write(img)
video.release()
合成视频的帧率亲测30帧是比较合适的,既节省了存储开销,也能获得不错的视频效果,满足大多数场景需求。保存为mp4
格式,编码格式选择广泛使用的MPEG-4(MP4V),通过img_dir.iterdir()
遍历所存放图片的文件夹,将所有图片按顺序合成为视频,使用video.write()
,最后通过video.release()
,完成视频生成。
功能展示
参见视频演示
项目总结
此次项目,本来是看中Sipeed官方能提供一些现成的模型,才尝试人脸识别题目,但是官方并没有提供,自己在github找了很久,大多数卡在了模型转换那里,同时后面模型还要经过博流自己的工具链做转换,比较耗时。因此就换为了这个较简单的题目,通过这个项目,也能看出目前这块开发板的技术支持还有提升空间,但开发板本身性能很强,可以实现长时间连续拍摄。