基于Sipeed M1s Dock实现的网络相机
本项目基于Sipeed M1s Dock,运行PikaScript环境,应用Python控制开发板,通过摄像头实现照片的拍摄,通过网络传输拍摄的照片,实现为一个网络相机。同时提供桌面应用接收数据,控制拍照和视频录制。
标签
2023寒假在家练
M1s
sipeed
pikascript
pyqt5
MJPEG
HonestQiao
更新2023-03-28
417

项目介绍:

本项目参考M1s Dock官方案例,实现通过python控制M1s Dock进行联网以及抓取摄像头数据进行网络推流。

在数据接收端,使用PyQT5编写接收程序,通过界面可以实时查看摄像头画面,并可以进行拍照和录像。

 

设计思路:

经过了解官方的资料以及演示程序和代码,了解到M1s Dock本身具有网络功能,同时提供了直接抓取摄像头数据并进行mjpeg推流的底层能力。

官方案例中的camera_streaming_through_wifi,演示了如何联网,以及如何进行mjpeg推流。

案例中的pikascript_demo,则演示了基础的pikascript的python环境。

在camera_streaming_through_wifi中,连接WiFi的信息,以及连接推流服务器的信息,都在c代码中写死了,非常的不方便。因此,考虑结合pikascript_demo,通过python调用WiFi连接和摄像头mjpeg推流,让使用变得方便起来。

同时,原有的推流服务端,有一个基础的使用opencv显示的例子。而我手头原有一个使用本地摄像头的PyQT5的应用,将两者结合,使得其能够支持通过socket推流过来的mjpeg数据流,并进行呈现。

要完成以上的工作,需要进行下面的N项工作:

  1. 了解pikascript的基础功能,以及如何进行模块开发
  2. 了解camera_streaming_through_wifi中进行网络连接和摄像头mjpeg推流的集体调用
  3. 在pikascript中添加模块,实现对2中具体功能的调用
  4. 修改原有的PyQT5的应用,接收mjpeg数据推流

工作1,经过了解,pikascript小巧精干,结构模块化,非常易于移植和二次开发。另外,在学习过程中,也得到了pikascript的作者李昂大佬以及梦程大神的手把手指点,表示特别的感谢。

工作2,M1s_BL808 SDK已经做好了封装,只要调用对应的函数,就能够使用对应的功能。

工作3,有了1、2的基础,这一步并不是很复杂,实现起来虽然有一点点难度,都是仔细下来都可以实现。

工作4,opencv很好的提供了对多种数据来源的支持,所以这一步,难度也不是非常大。

 

硬件介绍:

本项目使用的硬件,由硬禾提供。

另外,也自购了一个外壳:

FuXXEcEOiMmXJguJ5ieXMYzgZFtd

硬件信息也不用多说,直接看官方提供的资料即可:

Fnc7rCezbTVzVNX3yK6mVX1yiFDu

FjquQLyrqY-wtE3rKqjsclm_YsTm

 

作为一款结构紧凑的开发板,自身已经能够对绝大部分应用提供了玩好的支持,因此也就基本不需要外部设备的支持了。

 

实现功能:

通过结合pikascript_demo和camera_streaming_through_wifi,本项目最终实现如下的功能:

  1. M1s Dock运行pikascript,通过串口连接REPL环境,进行操作
  2. 在pikascript环境中,使用python进行WiFi连接
  3. 在pikascript环境中,使用python开启摄像头数据采集
  4. 在pikascript环境中,使用python开启数据上传推流
  5. 在桌面环境中运行接收端程序,可以接收M1s Dock的mjpeg推流
  6. 在桌面程序中,可以点击拍照按钮,保存当前画面的照片
  7. 在桌面程序中,可以点击录像按钮,录制当前显示的画面

 

代码说明:

在文件M1s_BL808_example/c906_app/pikascript_demo/pikascript/BL808.pyi中,添加WiFi和摄像头调用的python接口定义:

import PikaStdDevice


class GPIO(PikaStdDevice.GPIO):
    def platformHigh(self): ...

    def platformLow(self): ...

    def platformEnable(self): ...

    def platformDisable(self): ...

    def platformSetMode(self): ...

    def platformRead(self): ...

class Time(PikaStdDevice.Time):
    def sleep_s(self, s: int): ...

    def sleep_ms(self, ms: int): ...

class WiFi(PikaStdDevice.BaseDev):
    def init(self): ...

    def connect(self, ssid: str, passwd: str): ...

    def upload_stream(self, ip: str, port: int): ...

class Camera(PikaStdDevice.BaseDev):
    def mipi_mjpeg_init(self): ...

 

在文件M1s_BL808_example/c906_app/pikascript_demo/pikascript/pikascript-lib/BL808/BL808_WiFi.c中,添加WiFi接口的具体实现:

#include "BL808_WiFi.h"
#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 BL808_WiFi_connect(PikaObj *self, char* ssid, char* passwd);
// void BL808_WiFi_init(PikaObj *self);
// void BL808_WiFi_upload_stream(PikaObj *self, char* ip, int port);

void BL808_WiFi_init(PikaObj* self) {
    vTaskDelay(m1s_xram_wifi_init());
}

void BL808_WiFi_connect(PikaObj* self, char* ssid, char* passwd) {
    vTaskDelay(m1s_xram_wifi_connect(ssid, passwd));
}

void BL808_WiFi_upload_stream(PikaObj* self, char* ip, int port) {
    vTaskDelay(m1s_xram_wifi_upload_stream(ip, port));
}

在文件M1s_BL808_example/c906_app/pikascript_demo/pikascript/pikascript-lib/BL808/BL808_Camera.c中,添加摄像头调用接口的具体实现:

#include "BL808_Camera.h"
#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 BL808_Camera_mipi_mjpeg_init(PikaObj *self);

void BL808_Camera_mipi_mjpeg_init(PikaObj* self) {
    vTaskDelay(bl_cam_mipi_mjpeg_init());
}

 

以上功能,参考了 M1s_BL808_example/c906_app/camera_streaming_through_wifi/main.c的代码:

#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("OpenBSD", "************");
    m1s_xram_wifi_upload_stream("192.168.1.15", 8888);
}

 

其中:

  • m1s_xram_wifi_init():表示初始化WiFi设备

  • m1s_xram_wifi_connect(ssid, passwd):表示连接到WiFi路由器

  • m1s_xram_wifi_upload_stream(ip, port):表示连接到推流服务器

  • bl_cam_mipi_mjpeg_init():表示初始化摄像头mjpeg数据读取

 

在picascript中,其分别对应:

  • BL808.WiFi.init()
  • BL808.WiFi.connect(ssid, passwd)
  • BL808.WiFi.upload_stream(ip, port)
  • BL808.Camera.mipi_mjpeg_init()

 

最后是桌面pyqt5程序:

from PyQt5 import QtWidgets
from PyQt5.QtGui import QImage, QPixmap, QKeySequence
from PyQt5.QtCore import QThread
import sys, cv2, threading, random, signal
import numpy as np
import socket
import time, datetime

# 0-摄像头 1-socket
CAMERA_SOURCE = 1
CAMERA_LOCAL_INDEX = 0  # 如果使用本地摄像头,则表示其videoN的N
CAMERA_SOCKET_PORT = 8888 # 如果视同socket,设置端口

# 应用定义
app = QtWidgets.QApplication(sys.argv)
window_w, window_h = 640, 480               # 窗口宽度和高度
scale = 0.58                                # 视频信息宽高比

# 界面定义
Form = QtWidgets.QWidget()
Form.setWindowTitle('网络图像流' if CAMERA_SOURCE == 1 else 'USB摄像头')
Form.resize(window_w, window_h)

# 窗口大小改变时自动调整按钮
def windowResize(self):
    global window_w, window_h, scale
    window_w = Form.width()                 # 窗口宽度
    window_h = Form.height()                # 窗口高度
    label.setGeometry(0,0, window_w, int(window_w*scale))  # 调整 QLabel 尺寸
    btn1.setGeometry(10, window_h-40,70,30)  # 调整按钮位置
    btn2.setGeometry(80, window_h-40,70,30) # 调整按钮位置
    btn3.setGeometry(window_w - 80, window_h-40,70,30) # 调整按钮位置

Form.resizeEvent = windowResize             # 设置窗口大小改变时触发

# 关闭应用时的处理
ocv = True                                  # 设置是否处理视频
def closeOpenCV(self):
    global ocv, output
    ocv = False                             # 关闭窗口时,停止处理视频
    print("关闭程序")
    try:
        output.release()                    # 关闭窗口时,释放视频处理资源
    except:
        pass

Form.closeEvent = closeOpenCV               # 窗口关闭时触发

label = QtWidgets.QLabel(Form)
label.setGeometry(0,0, window_w, int(window_w*scale)) # 设置 QLabel 的位置和大小

# 存储文件时使用的文件名
def rename():
    # return str(random.random()*10).replace('.','')
    return datetime.datetime.now().strftime('%Y%m%d_%H%M%S')

photo = False                                # 按下拍照按钮时,设置处于拍照状态

# 按下拍照按钮时的处理
def takePhoto():
    global photo
    photo = True                            # 设定拍照状态为True
    print("马上拍照")

btn1 = QtWidgets.QPushButton(Form)
btn1.setGeometry(10, window_h-40,70,30)      # 设置拍照按钮的位置和大小
btn1.setText('拍照')
btn1.clicked.connect(takePhoto)             # 按下拍照按钮时触发

fourcc = cv2.VideoWriter_fourcc(*'mp4v')    # 设置视频中的存储格式
recorderType = False                        # 按下录像按钮时,设置处于录像状态

# 按下录像按钮时的处理
def recordVideo():
    global recorderType, output
    if recorderType == False:
        # 如果按下按钮时没有在录像,则开始录像
        # 设定存储的视频信息
        output = cv2.VideoWriter(f'videos/{rename()}.mp4', fourcc, 20.0, (window_w, int(window_w*scale)))
        recorderType = True                   # 设置正在录制状态
        btn2.setGeometry(80, window_h-40,200,30)  # 根据显示内容设置大小
        btn2.setText('录像中,点击停止保存')
    else:
        # 如果按下按钮时正在在录像,则停止录像
        output.release()                    # 释放视频存储资源
        recorderType = False                # 设置非录制状态
        btn2.setGeometry(80, window_h-40,70,30)   # 根据显示内容设置大小
        btn2.setText('录像')

btn2 = QtWidgets.QPushButton(Form)
btn2.setGeometry(80, window_h-40,70,30)      # 设置录像按钮的位置和大小
btn2.setText('录像')
btn2.clicked.connect(recordVideo)           # 按下录像按钮时触发

# 按下退出按钮时的处理
def quitApp():
    global video_server
    print("退出程序")
    closeOpenCV(False)
    app = QtWidgets.QApplication.instance()
    app.quit()

btn3 = QtWidgets.QPushButton(Form)
btn3.setGeometry(window_w-80, window_h-40,70,30)      # 设置退出按钮的位置和大小
btn3.setText('退出')
btn3.clicked.connect(quitApp)           # 按下退出按钮时触发

# 本地摄像头处理服务
def video_opencv_server():
    global window_w, window_h, scale, photo, output, recorderType, ocv
    cap = cv2.VideoCapture(CAMERA_LOCAL_INDEX)
    if not cap.isOpened():
        print("Cannot open camera")
        exit()

    while ocv:
        ret, frame = cap.read()             # 读取摄像头视频帧
        if not ret:
            print("Cannot receive frame")
            break
        try:
            frame = cv2.resize(frame, (window_w, int(window_w*scale)))  # 修改帧大小
            if photo == True:
                name = rename()                 # 设置文件名称
                cv2.imwrite(f'photos/{name}.jpg', frame) # 存储图片
                photo = False                   # 拍照完,设置非拍照状态
            if recorderType == True:
                output.write(frame)             # 按下录像按钮时,输出到存储文件
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)  # 设置为 RGB
            height, width, channel = frame.shape
            bytesPerline = channel * width
            img = QImage(frame, width, height, bytesPerline, QImage.Format_RGB888)
            label.setPixmap(QPixmap.fromImage(img))   # 显示
        except:
            pass

if CAMERA_SOURCE == 0:
    # video_server = threading.Thread(target=video_opencv_server)     # 使用线程执行
    video_server = QThread()
    video_server.run = opencv
    video_server.start()

# mjpeg数据流处理服务
def mjpeg_socket_server():
    global window_w, window_h, scale, photo, output, recorderType, ocv

    # 创建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(("", CAMERA_SOCKET_PORT))

    # 设置监听
    # 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务与多个客户端
    # 不需要让客户端进行等待建立连接
    # listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字tcp_client来完成
    tcp_server.listen(128)

    print("等待客户端连接...")

    # 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
    # 1. 专门和客户端通信的套接字: tcp_client
    # 2. 客户端的ip地址和端口号: tcp_client_address
    tcp_client, tcp_client_address = tcp_server.accept()

    # 代码执行到此说明连接建立成功
    print("客户端的ip地址和端口号:", tcp_client_address)

    count = 0
    while ocv:
        if True:
            # 接收客户端发送的数据, 这次接收数据的最大字节数是4
            recv_data = tcp_client.recv(4)
            mjpeg_len = int.from_bytes(recv_data, 'little')
            count+=1
            if count % 1000 == 1:
                print("\trecv 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)
            if count % 1000 == 1:
                print("\trecv 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)
            # cv2.imshow('stream', img)

            if count==1:
                sp = img.shape
                sz1 = sp[0] #height(rows) of image
                sz2 = sp[1] #width(colums) of image
                sz3 = sp[2] #the pixels value is made up of three primary colors
                print('网络图像: width=%d \theight=%d \tnumber=%d' % (sz1, sz2, sz3))
                scale = sz1/sz2

            frame = cv2.resize(img, (window_w, int(window_w*scale)))  # 改变帧大小
            if photo == True:
                name = rename()                 # 设置文件名称
                name_save = f'photos/{name}.jpg'
                print("照片存储:%s" % name_save)
                cv2.imwrite(name_save, frame) # 存储图片
                photo = False                   # 拍照完,设置非拍照状态
            if recorderType == True:
                output.write(frame)             # 按下录像按钮时,输出到存储文件
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)  # 设置为 RGB
            height, width, channel = frame.shape
            bytesPerline = channel * width
            img = QImage(frame, width, height, bytesPerline, QImage.Format_RGB888)
            label.setPixmap(QPixmap.fromImage(img))   # 显示
        # except:
            # pass

    # 关闭服务与客户端的套接字, 终止和客户端通信的服务
    tcp_client.close()

    # 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务 但是正常来说服务器的套接字是不需要关闭的,因为服务器需要一直运行。
    # tcp_server.close()

if CAMERA_SOURCE == 1:
    # mjpeg_server = threading.Thread(target=mjpeg_socket_server)     # 使用线程执行
    video_server = QThread()
    video_server.run = mjpeg_socket_server
    video_server.start()

Form.show()
sys.exit(app.exec_())

该程序中,有较为详细的注释,基本逻辑为:

  1. 使用pyqt5,显示桌面应用界面
  2. 启动socket接收数据流的线程
  3. 新的线程中,接收到mjpeg推流数据,界面,并使用opencv显示到界面上

 

操作步骤:

  1. 参考M1s Dock官方说明,下载好源码和SDK,配置好开发编译环境
  2. 按照如上的代码说明,修改 M1s_BL808_example/c906_app/pikascript_demo 下对应的源码
  3. 使用pikaCompiler进行预编译
    1. 下载pikapython源码:https://gitee.com/Lyon1998/pikapython
    2. 在pikapython/tools/pikaCompiler目录中,运行cargo build进行编译;运行cargo,需要事先准备好rust开发环境
  4. 将生成的pikapython/tools/pikaCompiler/target/debug/rust-msc文件,拷贝到M1s_BL808_example/c906_app/pikascript_demo/pikascript/目录下
  5. 运行M1s_BL808_example/c906_app/pikascript_demo/pikascript/rust-msc
  6. 在M1s_BL808_example/c906_app/目录下,运行./build.sh pikascript_demo,编译完成后,将M1s_BL808_example/c906_app/build_out/d0fw.bin文件拷贝出来备用
  7. 参考官方文档,刷 firmware_20230227.bin 固件
  8. 参考官方文档,U盘刷d0fw.bin固件,然后重新连接,确保串口可用
  9. 打开桌面接收程序
  10. 使用串口连接到pikascript,并设置网络和开启推流:
    1. Fmm-_p5I2pI_RNFZT2eAxt_tKn1E
  11. 连接成功后,桌面接收程序命令行将会提示:
    1. Fq17PyP-X6UB1UE2ry-WDWGSrz22
  12. 桌面程序开始显示摄像头画面:
    1. FrSVeIAHPvA4kvvmH-3YFFD7XG-V
  13. 点击拍照,或者点击录像后:
    1. 命令行输出对应信息
      1. Fr3Co2tY37BWZUNwa_8BTVNGnrZW
    2. 存储目录里面对应的文件存储:
      1. FjIzInwKXDElZryOIZNHve1NiKbC
附件下载
代码提交.zip
M1s Dock端代码和桌面应用代码
团队介绍
一个狂热的开源爱好者和传播者,同时也是一名极客爱好者,长期关注嵌入式发展和少儿创客教育,既擅长互联网系统架构设计与研发,又拥有丰富的嵌入式研发经验。为人精力充沛,古道热肠,圈内人称乔大妈、乔帮主。
团队成员
HonestQiao
狂热的开源爱好者和传播者
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号