介绍

玩了几个月OriginBot,想用它实际做点什么。

OriginBot现在有一个摄像头,有一个雷达,硬件上可以支持自动导航和图像数据传输,所以就打算用它做一个家庭监控工具,最终期望的功能是可以根据指令或者定时在家里巡视一圈或者去指定地点拍个照片/视频上传到云端,可以让我随便在什么地方都能看到家里的情况,其实就有一点像扫地机器人的摄像头功能。

目前实现了可以通过摄像头拍摄环境照片并把照片上传到云端,云端有一个webserver,可以让我通过手机随时随地看到摄像头的拍摄到的照片,但是目前还不能自动导航,以后慢慢做
整体的流程大致是这样的:

  1. 启动OriginBot并启动摄像头
  2. 启动一个图像订阅Node,在这个Node里面写一个订阅器
  3. 把第二部中获取到的摄像头数据通过API上传到web server

摄像头数据订阅节点

1. 启动摄像头节点

OriginBot自己已经实现了一个摄像头数据发布的功能,可以参考相机驱动与可视化 ,但是我们这里使用的是ros2 launch originbot_bringup camera.launch.py 来启动摄像头,而不是地平线的ros2 launch websocket hobot_websocket.launch.py 来启动摄像头,这一点要尤为注意,因为两个命令背后的topic name是不一样的。

另外不使用地平线的命令主要原因是地平线的那个命令背后还有类似人体识别等AI功能,长时间运行会导致续航大大降低。

2. 编写摄像头数据订阅节点

现在摄像头数据已经发布出来了,按照上面的流程还需要一个节点来订阅数据,然后把数据发布到web端。

首先先让我们创建一个功能包,

ros2 pkg create --build-type ament_python originbot_home_monitor

功能包创建之后,originbot_home_monitor目录下的结构如下:

.originbot_home_monitor
├── originbot_home_monitor
│   ├── api_connection.py  # 这个是我们后面创建,用于跟web server通信的
│   ├── cam_sub.py  # 这个是我们后面创建,用于订阅摄像头数据
│   └── __init__.py
├── package.xml
├── resource
│   └── originbot_home_monitor
├── setup.cfg
├── setup.py
└── test
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py

下面是订阅节点的代码,也就是上面的cam_sub.py里面的内容。

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
import cv2
import time
from datetime import datetime
import os
from .api_connection import APIConnection  # 这个包在另外一个文件中,下面解释


class ImageSubscriber(Node):
    def __init__(self, output_dir='output'):
        super().__init__('image_subscriber')
        self.subscription = self.create_subscription(
            Image,
            'image_raw',  # 这个是camera.launch.py中的topic name
            self.callback,
            10)
        self.output_dir = output_dir
        self.conn = APIConnection()

    def callback(self, data):
        # 将ROS2 Image转换为OpenCV图像,假设已安装了cv_bridge
        import cv_bridge
        bridge = cv_bridge.CvBridge()
        img = bridge.imgmsg_to_cv2(data)
        timestamp = str(datetime.now())  # 根据时间戳创建新的输出文件
        output_file_path = '/absolute/path/to/your/output/' + timestamp + '.jpg'
        cv2.imwrite(output_file_path, img)
        self.conn.post_data(output_file_path)
        os.remove(output_file_path)  # 上传成功之后要删除本地文件,否则会占用大量磁盘空间
        time.sleep(60)  # 每分钟获取一次照片并上传,可以自定义

def main(args=None):
    rclpy.init(args=args)
    image_subscriber = ImageSubscriber()
    rclpy.spin(image_subscriber)
    image_subscriber.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

api_connection.py 的作用是跟web server通信,其内容如下:

"""
API CONNECTION FOR IMPORTING WRAPPER
"""
import [logging](https://docs.python.org/3/library/logging.html)
import requests


logging.basicConfig(
    format="%(asctime)s %(levelname)-8s %(message)s",
    level=logging.INFO,
    datefmt="%Y-%m-%d %H:%M:%S",
)


class APIConnection:
    """
    Api Connection
    """
    def __init__(self):
        # 下面需要填写web的ip和端口
        self.api_url = "http://<your ip>:<your port>/videos/upload/"
        self.auth = ('<your user name>', '<your password>')

    def post_data(self, path):
        """
        """
        try:
            with open(path, 'rb') as f:
                files = {'file': f}
                response = requests.post(self.api_url, files=files, auth=self.auth)
                if response.status_code in [200, 201]:
                    logging.info("上传成功")
                    return True
                return False
        except Exception as err:
            logging.error(err)
            return False

3. 为什么上传照片而不是视频

其实我一开始是想上传视频的,但是后面发现视频占用的磁盘空间有点大,就放弃了,但是如果想要上传视频,整个流程都是可以套用的,web服务也能直接使用,不用做什么变化。

以下是一个上传视频的Node代码,供参考:

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
import cv2
import os


class VideoSubscriber(Node):
    def __init__(self, max_file_size=50 * 1024 * 1024, max_file_duration=60):
        super().__init__('video_subscriber')
        self.subscription = self.create_subscription(
            Image,
            'image_topic',   # 这里要换成自己topic name
            self.callback,
            10)
        self.frames = []
        self.fps = 30
        self.width = 640  # 这里要换成实际的size,否则不能正常保存视频文件
        self.height = 480
        self.max_file_size = max_file_size  # 最大文件大小为50MB
        self.max_file_duration = max_file_duration  # 最大文件时长为60秒
        self.output_dir = 'output'
        self.output_file_prefix = 'output'
        self.output_file_suffix = '.mp4'
        self.current_output_file = None
        self.current_output_file_size = 0
        self.current_output_file_duration = 0
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        self.video_writer = cv2.VideoWriter()
        self.create_output_file()

    def create_output_file(self):
        # 创建新的输出文件
        file_number = 1
        while True:
            output_file_path = os.path.join(
                self.output_dir,
                f'{self.output_file_prefix}_{file_number:03d}{self.output_file_suffix}'
            )
            if not os.path.isfile(output_file_path):
                break
            file_number += 1
        self.current_output_file = output_file_path
        self.video_writer.open(
            self.current_output_file,
            fourcc=cv2.VideoWriter_fourcc(*'mp4v'),
            fps=self.fps,
            frameSize=(self.width, self.height)
        )
        self.current_output_file_size = 0
        self.current_output_file_duration = 0

    def callback(self, data):
        # 将ROS2 Image转换为OpenCV图像,假设已安装了cv_bridge
        import cv_bridge
        bridge = cv_bridge.CvBridge()
        img = bridge.imgmsg_to_cv2(data)
        # 将图像添加到帧列表中
        self.frames.append(img)

    def save_frames(self, frames):
        # 将所有帧保存到当前输出文件中
        for frame in frames:
            self.video_writer.write(frame)
            self.current_output_file_size += len(frame.tostring())
            self.current_output_file_duration += 1
            # 如果当前文件已经超过最大值,则创建一个新的输出文件
            if self.current_output_file_size > self.max_file_size or self.current_output_file_duration >= self.fps * self.max_file_duration:
                self.video_writer.release()
                self.create_output_file()

    def spin(self, rate):
        # 循环处理帧,每次上传rate帧
        while rclpy.ok():
            if len(self.frames) >= rate:
                frames_to_save = self.frames[:rate]
                self.frames = self.frames[rate:]
                self.save_frames(frames_to_save)
            rclpy.spin_once(self)


def main(args=None):
    rclpy.init(args=args)
    video_subscriber = VideoSubscriber()
    rate = 30  # 每秒处理30帧
    video_subscriber.spin(rate)
    video_subscriber.destroy_node()
    rclpy.shutdown()

web 服务介绍

服务端的代码放在gitee上了: https://gitee.com/yexia553/home-monitor

这里说一些注意事项和启动步骤。

1. 运行步骤

可以参参考repo里面的README文件

  1. 安装依赖
    pip install -r requirements.txt
  2. 执行迁移
    python manage.py migrate
  3. 收集静态文件
    python manage.py collectstatic
  4. 创建管理员账号密码
     python manage.py createsuperuser 
     # 然后根据提示输入账号密码,如果对公网开放的话账号密码一定要足够复杂,避免被破解
    
  5. 修改DEBUG=False(仅线上环境需要)
    把home_monitor/settings.py里面的DEBUG = True改成DEBUG = False
    这个在线上环境中非常重要,如果不改的话站点极有可能被黑
  6. 运行项目
    python manage.py runserver 0.0.0.0:8000
    上面命令中的0.0.0.0表示允许外部IP访问,8000是项目运行的端口,端口可以自定义.
  7. 登录
    项目成功运行之后可以通过本机浏览器打开http://0.0.0.0:8000/admin,使用第四步创建的账号密码登录,登录成功之后访问http://0.0.0.0:8000/就可以看到摄像头拍摄的照片啦。

下面是一个本地运行的效果展示图:

2. 说明

这个web server是基于Django开发的,有兴趣的同学可以自己去了解一下,是我个人很喜欢一个web框架。