这篇博客是 OriginBot家庭助理计划 这个系列的第二篇,详细讲一下怎么样通过OriginBot上的摄像头来起到在家庭中可以移动监控的作用。

这个功能其实还没有完全开发完整,但是代码和步骤已经很多了,决定拆成两篇博客来写。


一、架构

先放一张监控功能整体架构图,配合架构图应该能更好地理解后面的内容。





这个里面用来MQTT方案来解决远程数据传输的问题,详细的介绍可以看这篇博客下面的内容都假设你已经有了一个正常运行的MQTT Server了

其余的部分都会在这篇博客中介绍到。

代码仓库:https://github.com/yexia553/originbot_home_assistant


二、怎么把图片数据从OriginBot上发出来

想要利用OriginBot的摄像头来做监控,第一步要解决的就是想办法把摄像头的数据从OriginBot上能通过网络发出来,虽然官方利用地平线的hobot_websoket提供了一个现成的网页展示图像数据的功能,但是有两个缺点:

  1. 默认启用了人体检测功能,功耗太大
  2. 只能本地展示,不能通过网络查看摄像头数据

从前面的架构图可以看出来,我的方案里有两步:启动摄像头和把数据发送到MQTT Server。

1. 启动摄像头

我在这里使用的官方现成的功能,直接运行以下命令即可:

ros2 launch originbot_bringup camera.launch.py

2. 把数据发送到MQTT Server

用方面的命令启动摄像头后,OriginBot上会出现如下的topic:

root@ubuntu:~# ros2 topic list
/bgr8_image
/camera_info
/compressed_image
/image_raw
/parameter_events
/rosout

其中的image_raw这个topic发布的就是我们需要的图片数据,所以我们需要做的就是写一个Node节点,订阅这个topic,然后把订阅到的数据发送给MQTT。

2.1 创建Node

在OriginBot上的/userdata/dev_ws/src/目录下,创建一个originbot_home_assistant目录,然后在这个目录中创建一个node节点,如下:

ros2 pkg create --build-type ament_python camera_listener



在camera_listner/camera_listener/目录中创建listener.py ,内容如下:

import cv2
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import numpy as np
import paho.mqtt.client as mqtt
import base64


class CameraListener(Node):

    def __init__(self):
        super().__init__('camera_listener')
        self.bridge = CvBridge()
        self.subscription = self.create_subscription(
            Image,
            'compressed_image',
            self.listener_callback,
            10)

        # Initialize the MQTT client
        self.client = mqtt.Client()
        self.client.connect("192.168.0.120", 1883, 60) #if your mqtt server is running on a different machine, change localhost to its IP address.

    def listener_callback(self, msg):
        cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding='passthrough')

        # Convert the image to JPEG
        ret, jpeg = cv2.imencode('.jpg', cv_image)

        if not ret:
            raise Exception('Could not encode image!')

        b64_string = base64.b64encode(jpeg.tobytes()).decode('utf-8')

        # Publish the encoded string via MQTT
        self.client.publish("robot/camera/image_raw" ,b64_string)
        print("sent a image to mqtt server")

def main(args=None):
    rclpy.init(args=args)

    camera_listener = CameraListener()

    rclpy.spin(camera_listener)

    # Destroy the node explicitly
    camera_listener.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

这段代码做的事情其实很简单,就是先去订阅image_raw这个topic,然后把订阅到的数据发送远端的MQTT Server,在这个代码里面就是位于192.168.0.120:1883, 这里需要根据实际情况来修改为真实值。

2.2 编译

按照上面的步骤已经创建了一个Node,现在需要编译,由于我们的Node是在OriginBot工作空间下创建的,直接编译会导致所有的代码都编译一次,这里采用指定package编译的方法。

在/userdata/dev_ws目录下执行以下命令:

colcon build --packages-select camera_listener

2.3 运行

运行之前,需要先刷一次环境变量

source /userdata/dev_ws/install/local_setup.bash

接着就可以运行了:

ros2 run camera_listener camera_listener

到这里,没有报错的话,摄像头拍摄到的图片数据应该就可以发到MQTT Server了, 后面就可以通过一些方法从MQTT Server上接收数据然后使用了。

这里要重点强调一下,我这里部署的MQTT是在同一网络下的另外一台服务器上,但是这个方案完全适用于互联网的情况,比如从阿里云买一个SaaS版的MQTT Server,这个方法同样可以工作,只要替换相应MQTT的地址就可以了。


三、怎么从MQTT Server接收图片数据

我把这一部分内容放在了后端,在https://www.guyuehome.com/44812 里面提到过,我在OriginBot Home Assistant这个项目中,后端用的Django,所以下面的内容跟Django关联性较大,不感兴趣的可以直接使用代码,不必深究,有兴趣的可以看一下这个python的后端框架。

我创建了一个Django Command, 在originbot_home_assistant/backend/monitor/management/commands/目录创建一个runmqtt.py文件,代码如下:

"""
从MQTT server接收数据,并存到数据库中
"""
from django.core.management.base import BaseCommand
import paho.mqtt.client as mqtt
import base64
import cv2
import numpy as np
from subprocess import Popen, PIPE
from monitor.models import ImageModel


command = [
    'ffmpeg',
    '-y',  # Overwrite output files
    '-f',
    'image2pipe',  # Image format (png, jpeg)
    '-r',
    '10',  # Framerate (fps)
    '-i',
    '-',  # Input comes from pipe
    '-vcodec',
    'libx264',
    '-pix_fmt',
    'yuv420p',  # codec and pixel format suitable for most players
    '-preset',
    'ultrafast',
    '-tune',
    'zerolatency',
    '-f',
    'flv',  # FLV is a common container format
    "rtmp://192.168.0.120:1935/live/test",
]
ffmpeg_pipe = Popen(command, stdin=PIPE)


def on_connect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))
    client.subscribe("robot/camera/image_raw")


def on_message(client, userdata, msg):
    jpg_original = base64.b64decode(msg.payload)
    jpg_as_np = np.frombuffer(jpg_original, dtype=np.uint8)
    image_buffer = cv2.cvtColor(cv2.imdecode(jpg_as_np, flags=1), cv2.COLOR_BGR2RGB)

    ret, frame = cv2.imencode('.png', image_buffer)  # or '.jpg'
    # print("####################################")
    print(frame)
    if not ret:
        raise Exception('Failed to encode image')

    ffmpeg_pipe.stdin.write(frame.tobytes())

    def save_image_to_db(image_buffer):
        """
        把图片数据用base64编码后存到数据库中
        """
        encoded_string = base64.b64encode(
            cv2.imencode('.jpg', image_buffer)[1]
        ).decode()
        img_obj = ImageModel(data=encoded_string)
        img_obj.save()

    save_image_to_db(
        image_buffer
    )  # call this function to save image data into database
    print("saved a image successfully.")


class Command(BaseCommand):
    def handle(self, *args, **options):
        client = mqtt.Client()
        client.on_connect = on_connect
        client.on_message = on_message

        client.connect("192.168.0.120", 1883, 60)

        client.loop_forever()

这段代码主要有三个部分,下面一一介绍。

1. ffmpeg

FFmpeg 是一个开源的音视频处理工具集,其中包括了一套可以录制、转换数字音频、视频,并能将其转化为流的工具。它提供了录制、转码以及流化音视频的完整解决方案,也包含了非常多领域使用的库。

主要组成部分如下:

  1. ffmpeg: 这是一个命令行工具用于转码多媒体文件。它可以识别出大量的格式,并且它可以对这些格式进行转换和输出。

  2. ffprobe: 一个简单的多媒体流分析器命令行工具,用户可以用来查看关于输入文件的信息,例如元数据、帧率、样本率等。

  3. ffplay: 是一个简易的多媒体播放器,基于SDL和FFmpeg软件库。

  4. libavcodec, libavformat, libavfilter, libavdevice, libswresample, libswscale 等库:这些库提供了 FFmpeg 的主要功能性,包括编解码操作,格式转换,过滤,设备交互等。

由于 FFmpeg 的高度可定制性和强大的功能,它被广泛应用在各种涉及到视频处理的场合,比如直播推流、视频剪辑、转码服务等等。

使用ffmpeg之前需要先安装,sudo apt-get install ffmpeg

runmqtt.py中的command列表和ffmpeg_pipe = Popen(command, stdin=PIPE)作用就是是利用ffmpeg来将从MQTT Server接收到图片数据生成流媒体。

2. 把数据存到数据库中

save_image_to_db这个函数的作用就是把接收到数据存到数据库中,这一部分是可以选的,或者可以定制一下,比如符合一定条件时候才存到数据库中,因为如果选择每一帧图片都落盘的话,对磁盘的消耗估计很大,也会影响后端的性能。

3. 运行MQTT Client

最后的Command里面的handle函数中,其实就是运行一个MQTT Client的套路代码,可以不必深究,使用即可。

代码解释完了,下面该运行了。

在originbot_home_assistant/backend/ 目录下执行以下命令:

python manage.py runmqtt

这个时候应该就会出现日志了。



在originbot_home_assistant/backend/monitor/management/commands/目录中,还有一个代码文件叫做get_image.py,它的作用是从数据库根据时间戳获取最新的一张图片数据并生成一个jpg文件保存在本地。

""""
从数据中获取最近的一张图片然后生产一个图片文件保存到本地
"""
from django.core.management.base import BaseCommand
import cv2
import numpy as np
from datetime import datetime
import base64

from monitor.models import ImageModel  # Replace 'myapp' with your actual app name


class Command(BaseCommand):
    help = 'Retrieve the latest image from database and save it to local system'

    def handle(self, *args, **kwargs):
        try:
            # Fetch the most recent image data from database
            img_obj = ImageModel.objects.latest('timestamp')

            decoded_img = base64.b64decode(img_obj.data)

            # Create a numpy array and reshape it into an OpenCV image matrix
            npimg = np.fromstring(decoded_img, dtype=np.uint8)
            img = cv2.imdecode(npimg, 1)

            # Save the file locally with timestamp in filename to differentiate multiple files
            now = datetime.now()
            time_string = str(now.strftime("%Y_%m_%d-%H_%M_%S"))
            filename = "image_" + time_string + ".jpg"

            cv_state = cv2.imwrite(filename, img)
            if cv_state:
                self.stdout.write(
                    self.style.SUCCESS(f'Successfully saved image as {filename}')
                )
            else:
                self.stdout.write(self.style.ERROR('Failed to write image.'))

        except ImageModel.DoesNotExist:
            self.stdout.write(self.style.WARNING('No images found in the database.'))

四、利用Nginx作为流媒体服务器

上面提到了在后端利用ffmpeg将图片数据生成流媒体,有了流媒体内容之后,还需要一个流媒体服务器,这样才能对外提供视频服务,很多直播平台其实也是这么做的,只是每一个直播平台采用的流媒体服务器可能不一样。


Nginx 是一个高效、灵活且可扩展的开源 Web 服务器和反向代理服务器。它被设计用于处理大量并发连接,因此在许多高流量环境中都得到了广泛应用,如果读者是互联网行业工程师,我猜大部分人都听过这个名字。


RTMP (Real-Time Messaging Protocol) 是一种用于实时数据传输(如音频、视频或其他类型的数据)的协议,最初由 Macromedia 开发,现在属于 Adobe。RTMP 主要被用于实时流媒体系统。


Nginx 默认并不支持 RTMP 协议,但可以通过添加第三方模块来获得这个功能。其中最知名的就是 nginx-rtmp-module,这是一个为 Nginx 写的开源模块,提供了对 RTMP 和 HLS(HTTP Live Streaming)等流媒体协议的支持。


nginx-rtmp-module 提供了以下功能:

  1. 接收 RTMP 流
  2. 发布 RTMP 流
  3. 转播/拉取 RTMP 流
  4. 将 RTMP 流转换为 HLS 或 MPEG-DASH

以下是安装Nginx和RTMQ模块的步骤和命令:

# 1. 安装编译工具及开发库
sudo apt-get install build-essential libpcre3 libpcre3-dev libssl-dev
# 2. 下载 Nginx 源码并解压
wget http://nginx.org/download/nginx-1.18.0.tar.gz
tar -zxvf nginx-1.18.0.tar.gz
# 3. 下载 RTMP 模块源码并解压
git clone https://github.com/arut/nginx-rtmp-module.git
# 4. 配置 Nginx,并添加 RTMP 模块
cd nginx-1.18.0
./configure --with-http_ssl_module --add-module=../nginx-rtmp-module
# 5. 编译安装
make 
sudo make install
# 6. 启动
sudo systemctl start nginx



Nginx安装之后,要创建一个配置文件,如果是在ubuntu上按照上述命令安装的话,配置文件应该位于/use/local/nginx/conf/nginx.conf, 编辑并替换为以下内容:

worker_processes auto;
events {}
http {
    server {
        listen      8080;

        location /live {
            # Serve HLS fragments
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root tmp/hls;
            add_header Cache-Control no-cache;
        }
    }
}
rtmp {
    server {
        listen 1935;
        chunk_size 4000;

        application live {
            live on; # Enable live streaming

            # Turn on HLS
            hls on;
            hls_path /tmp/hls;
            hls_fragment 3;
            hls_playlist_length 60;

            record off;        
        }
    }
}

配置中http部分是后面要对外提供视频播放服务的,端口是8080,可以根据需要修改。

rtmq部分就是流媒体相关的,我这里提供了一个最基础的可用配置,也可以根据需要自定义。

修改Nginx配置之后需要重启才能生效,sudo systemctl restart nginx

五、UI播放视频

前面的博客中有提到,这个项目的前端是用VUE3开发的,由于前端会涉及很多VUE3相关的细节,我不会在这里详细说,只说一些比较重要的步骤和代码,整体代码可以看github上的代码仓库。


我使用了video.js来处理HLS流媒体,需要先安装:

npm install --save video.js@7
npm install --save @videojs/http-streaming



然后在originbot_home_assistant/frontend/src/components/目录下创建VideoPlayer.vue, 代码如下:

<template>
    <div class="VideoPlayer">
        <video ref="videoPlayer" class="video-js vjs-default-skin" controls></video>
    </div>
</template>

<script>
import { onMounted, onBeforeUnmount, ref } from 'vue';
import videojs from 'video.js'
import 'video.js/dist/video-js.css'

export default {
    name: 'VideoPlayer',
    props: ['url'],
    setup(props) {
        const videoPlayer = ref(null);
        let player;

        onMounted(() => {
            player = videojs(videoPlayer.value, {}, () => {
                console.log('Player ready')
                player.src({
                    src: props.url,
                    type: 'application/x-mpegURL', // Or other relevant type depending on the stream
                })
                player.play();
            });
        });

        onBeforeUnmount(() => {
            if (player) {
                player.dispose();
            }
        });

        return {
            videoPlayer
        };
    },
}
</script>

<style scoped>
.video-js {
    width: 640px;
    height: 360px;
}
</style>

要特别注意props: ['url'], 这在使用这个component的时候是需要传递进来的。


接着在originbot_home_assistant/frontend/src/views/monitor/目录下创建Monitor.vue文件,代码如下:

<template>
    <div>
        <VideoPlayer :url="'http://192.168.0.120:8001/hls/test.m3u8'" />
    </div>
</template>

<script>
import VideoPlayer from '../../components/VideoPlayer.vue';

export default {
    components: {
        VideoPlayer
    },
};
</script>

可以看到我在<VideoPlayer :url="'http://192.168.0.120:8001/hls/test.m3u8'" />中传入了url这个属性,大家在尝试的时候要注意把地址和端口换成自己的实际值。


到这里,在originbot_home_assistant/frontend目录下执行:

npm install
npm run dev

就可以在浏览器看到OriginBot摄像头传过来的图像数据啦。


效果如下:



一开始提到过,这个功能还没有开发完成,主要有以下几个问题待解决:

  1. 视频播放窗口要调整一下来适配浏览器
  2. 播放视频的有卡顿,而且图片像是被压缩过,有一点失真
  3. 变量处理,现在代码中设计的IP地址的都是硬编码的,其他人想要测试的时候修改起来比较麻烦,要抽出来单独处理



等完全开发完了再写一篇博客介绍一下。