【2023寒假一起练】基于Sipeed M1s Dock的网络相机
本项目基于Sipeed M1s Dock,使用M1s_BL808_SDK与VSCode开发,实现了网络相机功能,可进行长时间拍摄,并在PC上获取并保存拍摄的图像,合成多帧图像为视频。
标签
RISC-V
寒假一起练
BL808
网络相机
sipeed
葉SiR
更新2023-03-28
北京大学
390

内容介绍

项目介绍

本项目基于Sipeed M1s Dock,使用M1s_BL808_SDK与VSCode开发,实现了网络相机功能,可进行长时间拍摄,并在PC上获取并保存拍摄的图像,合成多帧图像为视频。

👉 Github repo

硬件介绍

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协议进行数据流传输。软件设计思路如下图所示,具体细节在后续章节讲。

FgJF9oZifca3JvztlzhsC9LUwtHd

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()函数内,主要工作有如下三点:

  1. 创建以时间命名的唯一文件夹,存放图片与视频;
  2. 通过socket库建立TCP套接字服务,获取BL808上传的流数据,并保存图像getStreamData();具体为:根据特殊的格式使用np.frombuffer()存成uint8格式,并通过cv2.imdecode()解码成图片,保存为jpg格式;在接收客户端发送的数据时,接收的最大字节数为4,先获取此次信息的长度,并通过while()循环直至接收到这么多的数据;通过判断数据结尾2位是否为\xff\xd8\xff\xd9,只要不满足其一则说明接收到了完整的一帧图像,从而进行下一帧图像接收;
  3. 待获取一段时间图像结束后,可通过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找了很久,大多数卡在了模型转换那里,同时后面模型还要经过博流自己的工具链做转换,比较耗时。因此就换为了这个较简单的题目,通过这个项目,也能看出目前这块开发板的技术支持还有提升空间,但开发板本身性能很强,可以实现长时间连续拍摄。

附件下载

M1s-Dock-Network-Camera.zip
包含BL808与PC上位机两部分代码

团队介绍

北京大学 软件与微电子学院 电子信息专业就读
团队成员
葉SiR
二次元の开发者;👉 GitHub: https://github.com/KafCoppelia

评论

0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号