项目需求
目标:
完成一个基于Sipeed M1s Dock 的网络相机
具体要求:
1.完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄
2.通过电脑端编程将图片合成为一个视频
一.硬件介绍
主芯片 BL808 RISC-V 480Mhz + NPU BLAI-100
板载 USB 转 UART 调试器(可实现一键点击烧录,无需按实体按键)
板载显示屏座子(可选配 1.69 寸 240x280 电容触摸屏)
板载 MIPI 摄像头座子(可选配 200W 像素摄像头)
支持 2.4G WIFI / BT / BLE
板载 1 个模拟麦克风、1 个 LED、1 个 TF 卡座
引出一路 USB-OTG 到 USB Type-C 接口
二.设计思路
下位机:基于Sipeed M1s Dock的板载WIFI和板载的摄像头接口,通过编程实现连接相应的WiFi将摄像头拍摄的文件上传到固定ip的主机。通过tcp协议Sipeed M1s Dock作为客户端,将摄像头的实时拍摄的jpg文件和文件大小发送到主机服务器。
上位机:基于python的socket编写服务器,服务器和客户端基于tcp建立连接后,先会接收到客户端发来的文件大小,解析文件大小数据后,根据文件大小判断一帧jpg图片的结束,这样很好的解决了粘包问题。将接收到的jpg文件的数据写进相应文件以jpg格式并且按照拍摄顺序命名保存到固定的目录下。循环接收和写入。
接收完成之后基于python的opencv,将一系列的jpg文件合成为一个视频。首先设置了一个图像目录,通过 os.listdir 函数获取该目录下所有以 .jpg 结尾的文件,并将其按名称排序。接着设置输出视频文件名和帧率,然后读取第一张图片获取其大小作为视频的分辨率。接下来,使用 cv2.VideoWriter 函数初始化视频写入器,指定了输出文件名、视频编码器、帧率和分辨率。最后,循环遍历所有图像,并将其写入视频中,最终释放视频写入器。
这样就完成了网络相机的实现,流程图如下:
三.实现过程
1.下位机M1s Dock的功能实现:
首先要修改内核代码,编译后重新烧录。因为在官方提供的SDK中,将目的ip固定了。需要手动修改,在下面目录:
M1s_BL808_SDK/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
此处的端口和ip被固定,需要修改为如下代码,才能让c906的函数传参到e907核心。
while (1) {
vTaskDelay(100);
printf("Socket connect..\r\n");
if (0 > (sock = socket(AF_INET, SOCK_STREAM, 0))) {
continue;
}
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(private.port);//修改这里为传入端口
client_addr.sin_addr.s_addr = inet_addr(private.ip);//修改这里为传入ip
memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
if(-1 == connect(sock,
(struct sockaddr *)&client_addr,
sizeof(struct sockaddr)))
{
closesocket(sock);
continue;
}
之后重新编译e907目录下的firmware,用串口把编译后的firmware上传到M1s Dock。这样就可以上传图片流了。
修改这个固件重新烧录后,便可以在主核心C906中编程,主函数代码如下:
void main()
{
vTaskDelay(1);
bl_cam_mipi_mjpeg_init();
m1s_xram_wifi_init();
m1s_xram_wifi_connect("taylor", "asdfghjkl");
m1s_xram_wifi_upload_stream("192.168.243.41", 8888); //ip port
}
项目中使用的摄像头为MIPI摄像头,通过调用bl_cam_mipi_mjpeg_init()函数进行摄像头初始化设置。在该函数中,会配置摄像头的分辨率、帧率和图像数据的压缩格式等参数,以及开启MIPI传输接口,使得摄像头能够向处理器传输数据。接下来,通过调用m1s_xram_wifi_upload_stream()函数,将摄像头采集到的图像数据上传到指定的IP地址和端口号。因此,该代码实现了通过摄像头采集图像,并通过WiFi上传到指定服务器的功能。
跳转看一下m1s_xram_wifi_upload_stream()函数定义:
int m1s_xram_wifi_upload_stream(char *ip, uint32_t port)
{
m1s_xram_wifi_t op = {0};
strncpy(op.upload_stream.ip, ip, sizeof(op.upload_stream.ip));
op.upload_stream.port = port;
return m1s_xram_wifi_operation(&op, XRAM_WIFI_UPLOAD_STREAM);
}
这段代码实现了将本地的流式数据上传到指定的IP地址和端口号的功能。函数m1s_xram_wifi_upload_stream接受两个参数,ip表示目标服务器的IP地址,port表示目标服务器的端口号。函数内部定义了一个结构体变量op,并使用strncpy将ip复制到结构体中的upload_stream.ip成员中,同时将port赋值给upload_stream.port。
最后,调用m1s_xram_wifi_operation函数并传入操作类型XRAM_WIFI_UPLOAD_STREAM和结构体变量op,以触发上传流操作。该函数的返回值为操作结果,可能是成功或失败。
2.上位机python基于tcp网络服务器实现接收图片
使用Python的socket库监听本机8888端口,并等待客户端连接。
import socket
import os
def receive_images():
# 创建服务端socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen()
print("Server listening on 0.0.0.0:8888")
当客户端连接成功后,服务端开始不断接收客户端发送的图片数据,并将其保存在out_put文件夹下。每张图片以imageN.jpg的形式进行命名,N代表保存的图片数量。当客户端连接异常中断或者传输的图片长度超出80000字节时,服务端会关闭连接。因为经过测试,偶尔接收的图片长度会出现一个特别大的数值,这个现象说明,主机已经错误的把其他数据解析为了图片长度,加上这个代码,会自己把不符合的文件不再接收,实现一个纠错。
# 记录接收图片的数量
count = 1
while True:
# 等待客户端连接
client_socket, client_addr = server_socket.accept()
print(f"Accepted connection from {client_addr}")
while True:
# 接收图片长度信息
len_info = client_socket.recv(4)
if not len_info:
break
length = int.from_bytes(len_info, byteorder='little', signed=True)
print(f"Receiving image with length:{length}")
# 发送确认信息
client_socket.send(b's')
# 接收图片数据
data = b''
while len(data) < length:
chunk = client_socket.recv(length - len(data))
if not chunk:
break
data += chunk
if len(data) != length:
print("Image length exceeded limit, terminating connection")
client_socket.close()
break
# 保存图片
output_folder = 'output'
if not os.path.exists(output_folder):
os.makedirs(output_folder)
#判断是否存在该文件,若存在则命名+1,防止文件被覆盖
image_path = os.path.join(output_folder, f'image{count}.jpg')
while os.path.exists(image_path):
count += 1
image_path = os.path.join(output_folder, f'image{count}.jpg')
#二进制方式保存接受数据
with open(image_path, "wb") as f:
f.write(data)
count += 1
client_socket.close()
server_socket.close()
if __name__ == '__main__':
receive_images()
3.基于opencv实现jpg格式转换为视频
代码首先通过指定图像目录获取所有jpg格式的图片文件列表,并按文件名排序。接下来,代码定义了一个输出视频文件名和帧率。然后通过读取第一张图片,获取它的宽高并初始化视频写入器。最后,代码循环遍历所有的图片文件,将每张图片读取并写入视频中,并在最后释放视频写入器。这样,就可以将所有的图片合成一个视频文件。
import cv2
import os
# 设置图像目录
img_dir = 'out_picture/'
# 获取图像列表
images = [img_dir + f for f in os.listdir(img_dir) if f.endswith('.jpg')]
images.sort()
# 设置输出视频文件名
output_file = 'output.avi'
# 设置视频帧率
fps = 24
# 获取第一张图片的大小
frame = cv2.imread(images[0])
height, width, _ = frame.shape
# 初始化视频写入器
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
video_writer = cv2.VideoWriter(output_file, fourcc, fps, (width, height))
# 循环遍历图像并将其写入视频
for image in images:
frame = cv2.imread(image)
video_writer.write(frame)
# 释放视频写入器
video_writer.release()
四.遇到问题
问题1:Linux环境下的编译环境搭建,克隆官方库的代码后,编译一直不成功,在交流群里求助,得到热心群友解答,然后删除了buildout的文件夹,重新编译才编译成功。
问题2:运行连接wifi的函数时,在后台看不到M1s Dock 的连接,尝试了很久,最后是重新克隆代码,解决了这个奇怪的问题。
问题3:解决了wifi连接后,M1s Dock作为客户端,拿到ip,但是连接不上服务器,咨询交流群群友,修改了firmware后,编译才能连接上主机。
问题4:在linux环境编译firmware总是崩掉或者死机,在群里问群友才解决了,原来是线程过多,改小线程就完成了。
五.展望未来
之后打算学习部署神经网络,在M1s Dock上跑图像识别和运用上语音识别关键字,bl8008性能非常好,很适合用来学习神经网络。然后也可以用M1s Dock上跑Linux的操作系统。总之,不会让这块板子吃灰了。
六.致谢
真的衷心感谢电子森林提供了这样一个学习机会。在这个过程中,我学习了很多新的技能和知识。也特别感谢和我一起参与这个项目的群友,感谢他们帮助、分享经验,让学习过程更加愉快、高效。而且这个项目让我交到了一些新朋友。他们来自不同的背景和领域,但是我们都对学习和技术充满热情,这让我感到非常开心。我相信我们会在未来的学习和工作中相互支持、共同成长。
这次经历让我收获了很多,感激不尽。我希望自己能够继续保持学习的热情和乐观的态度,不断提升自己的技能和能力,为自己和周围的人带来更多的价值。