前言
- 本系列教程旨在使用
UE5
配置一个具备激光雷达
+深度摄像机
的仿真小车,并使用通过跨平台的方式进行ROS2
和UE5
仿真的通讯,达到小车自主导航的目的。 - 本教程使用的环境:
- ubuntu 22.04 ros2 humble
- windows11 UE5.4.3
- python8
- 本系列教程将涉及以下内容:
- UE仿真环境和简易小车的搭建
- UE仿真雷达数据和RGBD深度相机数据的获取
- 使用ROS2和UE5进行通讯
- 使用ROS2导航Nav2及其相关模块对小车进行自主通讯
- UE5部分C++编写和ROS2C++代码编写
- 部分插件使用python进行编写
- UE5系列教程:UE5-C++入门教程(一):使用代码创建一个指定目标的移动小球-CSDN博客
- 本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客
UE介绍
-
UE5(Unreal Engine5)
是由Epic Games开发的一款功能强大、跨平台的最新版的游戏开发引擎,以其高质量的实时渲染效果、多编程语言支持、丰富的市场资源和活跃的社区而闻名,广泛应用于游戏、影视、建筑等多个领域,不断推动着数字内容产业的发展。 -
更多相关内容可以移步自我的教程UE5-C++入门教程(一):使用代码创建一个指定目标的移动小球-CSDN博客
UnrealCV
插件
-
UnrealCV是一个开源项目,旨在将Unreal Engine(UE)与计算机视觉(CV)领域结合起来。它允许开发者利用UE的高质量渲染能力来生成用于机器学习和计算机视觉研究的数据集和场景。UnrealCV提供了一个Python接口,允许用户通过Python脚本控制UE引擎,从而实现自动化场景渲染、数据采集和交互。
-
更多内容->UnrealCV官网
安装
-
这里来介绍以下如何为
UE5
配置UnrealCV
插件插件 -
首先我们先登录
UnrealCV
的github仓库,由于官方提供的二进制release目前只支持到UE4.16版本,所以这里我们选择源码安装 -
注意选择5.2分支,目前
UnrealCV
已经支持到UE5.4版本
-
git clone
后我们会得到一个unrealcv
的文件夹,文件夹内容大致如下 -
找到你的项目文件夹(该文件夹下有一个以
.uproject
结尾的项目文件),创建一个名为Plugins
的文件夹 -
把上述你下载的
unrealcv
文件夹整个移动到Plugins
文件交下,然后重新打开这个项目,项目会弹出UnrealCV插件
是否进行编译的提示,选择是,然后就会进行编译 -
安装好后,在
UE5
界面左上角的编辑
,在下拉菜单找到插件
,搜索UnrealCV
-
返回UE主界面,找到
绿色箭头
开始运行此关卡(请务必记得这一步!!!) -
在左下方的
控制台
输入vget /unrealcv/status
确认插件配置情况 -
在左侧
输出日志
得到如下输出则表示插件配置成功
报错指南
-
控制台
输入vget /unrealcv/status
后UE
直接闪退 -
检查是否进行
绿色箭头
开始运行此关卡此步骤(八成问题) -
检查
UE
下载是否完全,检查UE
版本是否为5.4.3
,检查unrealcv
下载的分支是否为5.2
使用python对Unreal客户端与服务器进行连接通讯
-
UnrealCV 实现了在游戏和计算机视觉算法之间的进程间通信 (IPC)。这种通信可以用下图来总结。由
UE
创建的游戏将通过加载UnrealCV服务器
作为其模块来扩展。当游戏启动时,UnrealCV
将启动一个TCP 服务器
并等待命令。任何程序都可以使用UnrealCV客户端
代码发送纯文本 UnrealCV 命令来控制场景或检索信息。 -
下面我们来简单的写一段代码进行通讯测试,默认的
UnrealCV服务器
运行在本地的9000端口,我们通过简单的配置完成下述代码
from unrealcv import Client
client = Client(('localhost', 9000)) # 连接到 UnrealCV 服务器
if client.connect(): print('UnrealCV connected successfully')
else: print('UnrealCV is not connected') exit() response = client.request('vget /unrealcv/status')
print(response)
# 断开连接
client.disconnect()
- 上述运行结果将会输出和我们在UE控制台同样的内容输出,即视为通讯连接成功,此过程中确保UE允许了UnrealCV插件且UE处于运行此关卡中
使用UnrealCV
进深度和分割图像的获取
-
这里我们介绍
UnrealCV
的一些基础用法,首先我们使用UE
简单在摄像机视角前搭建一些简单物体 -
为相机添加初始位置,这里我们假定设置相机的位置为0,0,0(运行此关卡默认的摄像头视角就是0,0,0),注意这里必须设置,否则后面拍摄的深度和分割图像可能无法正确拍摄。
camera_settings = { 'location': {'x': 0, 'y': 0, 'z': 0}, # 相机位置 'rotation': {'pitch': 0, 'yaw': 0, 'roll': 0} # 相机旋转
}
# 使用 vset 命令设置相机的位置
client.request('vset /camera/0/location {x} {y} {z}'.format(**camera_settings['location'])) # 使用 vset 命令设置相机的旋转
client.request('vset /camera/0/rotation {pitch} {yaw} {roll}'.format(**camera_settings['rotation']))
- 然后我们在上述连接的基础上添加如下代码,下述的代码将保存
- lit:摄像机原始图像
- object_mask:物体分割图像
- depth:深度图像(注意这里深度图像保存为
npy格式
)
client.request('vget /camera/0/lit C:/Users/lzh/Desktop/UE5_ROS2_project/camera/lit.png')
client.request('vget /camera/0/object_mask C:/Users/lzh/Desktop/UE5_ROS2_project/camera/object_mask.png')
client.request('vget /camera/0/depth C:/Users/lzh/Desktop/UE5_ROS2_project/camera/depth.npy')
- 我们简单对图像进行展示,便得到以下图像
# 加载图像
lit_img = plt.imread(r'C:/Users/lzh/Desktop/UE5_ROS2_project/camera/lit.png')
object_mask_img = plt.imread(r'C:/Users/lzh/Desktop/UE5_ROS2_project/camera/object_mask.png')
depth_img = np.load(r'C:\Users\lzh\Desktop\UE5_ROS2_project\camera\depth.npy') # 创建图形和子图
fig, axes = plt.subplots(1, 3, figsize=(15, 5)) # 显示 lit 图像
axes[0].imshow(lit_img)
axes[0].set_title('Lit Image')
axes[0].axis('off') # 显示 object_mask 图像
axes[1].imshow(object_mask_img, cmap='gray')
axes[1].set_title('Object Mask')
axes[1].axis('off') # 显示 depth 图像
axes[2].imshow(depth_img, cmap='gray', vmin=0, vmax=1300)
axes[2].set_title('Depth Image')
axes[2].axis('off') # 显示图形
plt.tight_layout()
plt.show()
实时进行图像读取与显示
- 上面我们进行了单张文件的保存,下面我们来介绍如何进行实时读取,这里我们介绍核心函数,通过下述三个函数,我们就能实时返回读取到的图像数据,为此我们需要进行解码
data=client.request('vget /camera/0/lit png')
data=client.request('vget /camera/0/object_mask png')
data=client.request('vget /camera/0/depth npy')
- 对于普通图像和分割图像,我们只需要
lit_img = cv2.imdecode(np.frombuffer(self.data, np.uint8), cv2.IMREAD_COLOR)
object_mask_img = cv2.imdecode(np.frombuffer(self.data, np.uint8), cv2.IMREAD_COLOR)
- 对于深度图像,如果我们希望使用
opencv-python
进行展示,我们需要进行转换
import io
def read_npy(self,res): return np.load(io.BytesIO(res))
depth_img = self.read_npy(self.data)
gray_depth = cv2.convertScaleAbs(depth_img, alpha=(255.0 / 1300.0))
# 使用cv2.applyColorMap将灰度图转换为伪彩色图
colored_depth = cv2.applyColorMap(gray_depth, cv2.COLORMAP_JET)
- 通过上述核心代码,我们只需要简单写一个循环,进行展示即可
- 在不额外添加移动物体以及移动摄像机的前提下,我们移动观测者小球,便能得到以下实时画面
封装
- 为了后续教程我们方便使用,我们封装以下整个流程
from unrealcv import Client
import cv2 # OpenCV
import numpy as np
import io
import time
class UE5CameraCenter: def __init__(self): self._client = Client(('localhost', 9000)) self._connection_check() self._camera_init() self._lit_image=LitImage() self._object_mask=ObjectMaskImage() self._depth_mask=DepthImage() def __del__(self): self._client.disconnect() def _connection_check(self): '''检查是否连接''' if self._client.connect(): print('UnrealCV connected successfully') else: print('UnrealCV is not connected') exit() def set_camera_pose(self,x,y,z,pitch,yaw,roll): '''设置摄像头位置''' camera_settings = { 'location': {'x': x, 'y': y, 'z': z}, # 相机位置 'rotation': {'pitch': pitch, 'yaw': yaw, 'roll': roll} # 相机旋转 } # 设置相机的位置 self._client.request('vset /camera/0/location {x} {y} {z}'.format(**camera_settings['location'])) # 设置相机的旋转 self._client.request('vset /camera/0/rotation {pitch} {yaw} {roll}'.format(**camera_settings['rotation'])) def _camera_init(self): '''摄像头初始化''' self.set_camera_pose(0,0,0,0,0,0) def get_camera_data(self, camera_type): valid_types = {'lit', 'object_mask', 'depth'} # 检查 camera_type 是否在有效类型中 if camera_type not in valid_types: raise ValueError(f"Invalid camera type. Expected one of {valid_types}, but got '{camera_type}'.") # 根据camera_type获取相应的数据 if camera_type == 'lit': return self._lit_image.get_image(self._client) elif camera_type == 'object_mask': return self._object_mask.get_image(self._client) elif camera_type == 'depth': return self._depth_mask.get_image(self._client) def test(self): while True : self._lit_image.get_image(self._client) self._object_mask.get_image(self._client) self._depth_mask.get_image(self._client) self._lit_image.display() self._object_mask.display() self._depth_mask.display() class Image: def __init__(self): self.data_np = None def get_image(self,client): pass def display(self): pass class LitImage(Image): def __init__(self): self.data_np = None def get_image(self,client): data = client.request('vget /camera/0/lit png') self.data_np=cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) def display(self): cv2.imshow("lit_img", self.data_np) key = cv2.waitKey(1)
class ObjectMaskImage(Image): def __init__(self): self.data_np = None def get_image(self,client): data=client.request('vget /camera/0/object_mask png') self.data_np=cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) def display(self): cv2.imshow("object_mask_data", self.data_np) key = cv2.waitKey(1)
class DepthImage(Image): def __init__(self): self.data_np = None def get_image(self,client): data=client.request('vget /camera/0/depth npy') self.data_np = self.read_npy(data) def display(self): gray_depth = cv2.convertScaleAbs(self.data_np, alpha=(255.0 / 1300.0)) # 使用cv2.applyColorMap将灰度图转换为伪彩色图 colored_depth = cv2.applyColorMap(gray_depth, cv2.COLORMAP_JET) cv2.imshow("depth_img", colored_depth) def read_npy(self,res): return np.load(io.BytesIO(res)) def main(): ue5_cam_center=UE5CameraCenter() ue5_cam_center.test()
if __name__ =='__main__': main()
- 并预留三个接口
- 设置摄像机位置
def set_camera_pose(self,x,y,z,pitch,yaw,roll):
- 获取摄像机图像
def get_camera_data(self, camera_type):
- 测试(显示摄像机画面)
def test(self):
- 设置摄像机位置
UnrealCV
指令大全
通用命令
vget /unrealcv/status
:获取 UnrealCV 插件的状态。vget /unrealcv/help
:列出所有可用的命令及其帮助信息。vget /unrealcv/version
:获取 UnrealCV 的版本信息。vget /scene/name
:获取当前场景的名称。vget /level/name
:获取当前级别的名称。
对象命令
vget /objects
:获取场景中所有对象的名称。vset /objects/spawn_cube
:在场景中生成一个用于调试的立方体。vget /object/[str]/location
:获取指定对象的坐标位置。vset /object/[str]/location [float] [float] [float]
:设置指定对象的坐标位置。vget /object/[str]/rotation
:获取指定对象的旋转。vset /object/[str]/rotation [float] [float] [float]
:设置指定对象的旋转。vset /object/[str]/color [uint] [uint] [uint]
:设置指定对象的颜色。vset /object/[str]/destroy
:销毁指定对象。
相机命令
vget /cameras
:列出场景中所有传感器(相机)。vset /camera/[uint]/location [float] [float] [float]
:设置指定相机的位置。vset /camera/[uint]/rotation [float] [float] [float]
:设置指定相机的旋转。vget /camera/[uint]/lit [str]
:从指定相机获取光照图像。vget /camera/[uint]/depth [str]
:从指定相机获取深度图像。vget /camera/[uint]/normal [str]
:从指定相机获取表面法线图像。vget /camera/[uint]/object_mask [str]
:从指定相机获取对象掩码图像。vset /camera/[uint]/fov [float]
:设置指定相机的视场角。
视图模式命令
vset /viewmode [str]
:设置视图模式,如lit
(光照)、normal
(法线)、depth
(深度)、object_mask
(对象掩码)等。vget /viewmode
:获取当前的视图模式。
其他命令
vrun [str]
:运行 Unreal 引擎内置的命令。vexec [str]
:运行 Unreal 引擎蓝图函数。vbp [str]
:运行 Unreal 引擎蓝图函数。
小结
- 本节介绍了如何使用
UnrealCV
插件在UE
中获取原始,深度,分割图像,并实时进行读取 - 下一节我们将进行跨平台实现图像数据的传输
- 感谢大家对本教程的支持,如有错误,欢迎指出~