Unity ROS-Robotics-HUB トピック通信の勉強

# やりたいこと
![スクリーンショット 2023-06-11 212828.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282390/d74348c7-00b4-cd3a-3317-e7ca655747f6.png)

以下の動画。Unity環境内に配置された移動ロボット（車体）を、ROS側のコントローラからの指令を受けて動かし、移動ロボットの姿勢情報とカメラ画像をUnity側からROS側に渡す。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Unity Robotics HUB の感じでROS2 と Unityを連携する <a href="https://t.co/PXL8YApr0r">pic.twitter.com/PXL8YApr0r</a></p>&mdash; moriitkys (@moriitkys) <a href="https://twitter.com/moriitkys/status/1667891328506826753?ref_src=twsrc%5Etfw">June 11, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 注意
更新予定です。以下のすべての記述は雑です（自分用なので）。休みにビール飲みながら検証したものですので。
特にコードはひどいです。
あとこれは以下のデdemoなどを一通り終えた人が対象です。基本的なROS-Unity通信設定ができていない場合はGitHubのインストラクションなどに従って設定してください。

https://github.com/Unity-Technologies/Unity-Robotics-Hub/blob/main/tutorials/ros_unity_integration/publisher.md

https://github.com/Unity-Technologies/Unity-Robotics-Hub/blob/main/tutorials/ros_unity_integration/subscriber.md


# 構成

ROS2側 (Ubuntu 22.04 ROS humble)
・Joyスティックのコントローラからcmd_velをPublish。
・imageトピックを読み取って画像を表示。robot_poseトピックを受け取ってechoで表示。
Unity側 (Windows 11)
・移動ロボットがcmd_velコマンドを受け取って動作。
・ImagePublisherで移動ロボットに取り付けられたカメラと移動ロボットの姿勢情報をPublish。

![rosgraph_UnityROS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282390/95cd2578-575e-9892-730a-6ddc9565007c.png)



# Unity側のコード
以下のコードをScriptsフォルダーに作成し、移動ロボットのGameObjectにアタッチ。

```cs:RosPubSub.cs
using UnityEngine;
using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Geometry;

/// <summary>
///
/// </summary>
public class RosPubSub : MonoBehaviour
{
    ROSConnection rosConnection;
    public string topicName = "robot_pose";

    // The game object
    public GameObject cube;
    // Publish the cube's position and rotation every N seconds
    public float publishMessageFrequency = 0.5f;

    private void MoveObject(TwistMsg cmdMsg)
    {
        //cube.transform.position += new Vector3((float)cmdMsg.linear.z, (float)cmdMsg.linear.y, (float)cmdMsg.linear.x);
        var r = new Vector3((float)cmdMsg.angular.y, -(float)cmdMsg.angular.z, (float)cmdMsg.angular.x);
        cube.transform.position += cube.transform.TransformDirection(new Vector3((float)cmdMsg.linear.z, (float)cmdMsg.linear.y, (float)cmdMsg.linear.x)) * Time.deltaTime;
        cube.transform.localEulerAngles += r;

        TwistMsg robotPose = new TwistMsg();
        robotPose.linear.x = cube.transform.position.x;
        robotPose.linear.y = cube.transform.position.y;
        robotPose.linear.z = cube.transform.position.z;
        robotPose.angular.x = cube.transform.localEulerAngles.x;
        robotPose.angular.y = cube.transform.localEulerAngles.y;
        robotPose.angular.z = cube.transform.localEulerAngles.z;

        rosConnection.Publish(topicName, robotPose);
    }

    void Start()
    {
        // start the ROS connection
        rosConnection = ROSConnection.GetOrCreateInstance();
        rosConnection.RegisterPublisher<TwistMsg>(topicName);


        rosConnection.Subscribe<TwistMsg>("cmd_vel", MoveObject);

    }

    private void Update()
    {

    }
}
```

以下のコードを移動ロボットのカメラオブジェクトにアタッチ。

```cs:ImagePublisherRos.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Sensor;

public class ImagePublisherRos : MonoBehaviour
{
    [SerializeField] private string topicName = "image";
    [SerializeField] private string frameId = "camera";

    private float timeElapsed = 0f;
    private float timeStamp = 0f;

    private ROSConnection rosConnection;
    private CompressedImageMsg imgMsg;

    private GameObject mainCamObj;
    private Camera cam;

    private Texture2D tex;
    private int targetWidth = 1104;
    private int targetHeight = 535;


    // Start is called before the first frame update
    void Start()

    {
        this.mainCamObj = GameObject.Find("Camera_01");
        this.cam = mainCamObj.GetComponent<Camera>();

        var render = new RenderTexture(this.targetWidth, this.targetHeight, 24);
        this.cam.targetTexture = render;
        this.cam.Render();
        RenderTexture.active = render;

        var d_width = this.cam.targetTexture.width;
        var d_height = this.cam.targetTexture.height;

        this.tex = new Texture2D(d_width, d_height, TextureFormat.RGB24, false);
        this.tex.ReadPixels(new Rect(0, 0, d_width, d_height), 0, 0);
        this.tex.Apply();

        // setup ROS
        this.rosConnection = ROSConnection.GetOrCreateInstance();
        this.topicName += "/compressed";
        this.rosConnection.RegisterPublisher<CompressedImageMsg>(this.topicName);

        // setup ROS Message
        this.imgMsg = new CompressedImageMsg();
        this.imgMsg.header.frame_id = this.frameId;
        this.imgMsg.format = "jpeg";

    }

    // Update is called once per frame
    void Update()
    {
        this.timeElapsed += Time.deltaTime;

        if (this.timeElapsed > (0.5f))
        {
            // Update ROS Message
            int sec = (int)Math.Truncate(this.timeStamp);

            uint nanosec = (uint)((this.timeStamp - sec) * 1e+9);

            var render = new RenderTexture(this.targetWidth, this.targetHeight, 24);
            this.cam.targetTexture = render;
            this.cam.Render();
            RenderTexture.active = render;
            var d_width = this.cam.targetTexture.width;
            var d_height = this.cam.targetTexture.height;
            this.tex = new Texture2D(d_width, d_height, TextureFormat.RGB24, false);
            this.tex.ReadPixels(new Rect(0, 0, d_width, d_height), 0, 0);
            this.tex.Apply();

            byte[] data = this.tex.EncodeToJPG();

            var compressedImageMsg = new CompressedImageMsg(this.imgMsg.header, this.imgMsg.format, data);

            this.rosConnection.Publish(this.topicName, compressedImageMsg);

            // Update time
            this.timeElapsed = 0;
            this.timeStamp = Time.time;
        }

    }
}


```
ImagePublisherRos.csのカメラ解像度を設定する部分がべた書きなので、これ改善必要あり。

位置姿勢を扱うところはごちゃごちゃしてるので注意。

以下のようにして複数カメラがある場合でも特定のカメラを区別して取得できるように名前を使う。
this.mainCamObj = GameObject.Find("Camera_01");


移動ロボットに取り付けられたカメラの設定は画像のように変更すること。
![スクリーンショット 2023-06-11 220054.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282390/4ba53a7a-b099-53e9-d68e-fd13c48c4d68.png)

# ROS2側

$ cd ~/ros2_ws/src
$ ros2 pkg create --build-type ament_python image_subscriber
$ touch ~/ros2_ws/src/image_subscriber/image_subscriber.py

```py:image_subscriber.py
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
import cv2
import numpy as np

class ImageSubscriber(Node):
    def __init__(self):
        super().__init__("image_subscriber")
        #self.declare_parameter('image_topic_name', '/camera/color/image_raw')
        self.declare_parameter('image_topic_name', '/image/compressed')
        image_topic_name = self.get_parameter('image_topic_name').get_parameter_value().string_value

        video_qos = rclpy.qos.QoSProfile(depth=10)
        video_qos.reliability = rclpy.qos.QoSReliabilityPolicy.BEST_EFFORT

        self.sub_img = self.create_subscription(
            CompressedImage,
            image_topic_name,
            self.on_image_subscribed,
            video_qos
        )
        self.cnt = 0
        print("init image subscriber")
    
    def on_image_subscribed(self, img):
        print("subscribed" + str(self.cnt))
        self.cnt += 1
        np_arr = np.fromstring(img.data.tobytes(), np.uint8)
        input_image = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
        output_image = self.process_image(input_image)
        cv2.imshow("Image", output_image)
        cv2.waitKey(1)
        
    def process_image(self, frame):
        frame = cv2.resize(frame, (640, 480))
        return frame


def main(args=None):
    try:
        rclpy.init(args=args)
        rclpy.spin(ImageSubscriber())
    
    except KeyboardInterrupt:
        pass

    rclpy.shutdown()
        

if __name__ == "__main__":
    main()
```

package.xml

```
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>image_subscriber</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="robotics@todo.todo">robotics</maintainer>
  <license>TODO: License declaration</license>

  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
    <exec_depend>rclpy</exec_depend>
    <exec_depend>sensor_msgs</exec_depend>
  </export>
</package>

```

setup.py
entry_pointsに以下を追加。
"image_subscriber = image_subscriber.image_subscriber:main"

そして
colcon_build


# 実行

Joy Stick　コントローラの実行には以下を参考にする。

https://qiita.com/moriitkys/items/c90e9d292b897725458c

```
ros2 run joy joy_node
#別端末で
ros2 run joy_translate joy_translate_node
```


ROS-TCP-Endpointの実行

```
ros2 run ros_tcp_endpoint default_server_endpoint --ros-args -p ROS_IP:=xxx.xxx.xxx.xxx
```

移動ロボットの画像読み取り

```
ros2 run image_subscriber image_subscriber
```

ロボット姿勢
```
ros2 topic echo robot_pose
```

# 参考
ROS側　いろいろ組み合わせる
ROS 2 FoxyでRealSense D435の画像をsubscribeする(QoSの設定とPython、C++実装)
https://yhrscoding.hatenablog.com/entry/2021/09/26/104445
https://qiita.com/srs/items/3c3c22f1cbd41f90c5aa

なんか肝心なところが無い気がする
https://github.com/Field-Robotics-Japan/UnitySensors

Unity側　string変換などの変なことせずに素直にPublish
https://forum.unity.com/threads/publish-game-camera-stream-via-ros.1124503/
いろいろいらんことしてる
https://github.com/michaeljenkin/unityros/issues/3
肝心のところがない気がする
https://github.com/Field-Robotics-Japan/UnitySensorsROSArchive

Unity　カメラ切り替え
https://mogi0506.com/unity-camera-change/

Unity カメラズーム
https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q13112544023

Unity　オブジェクトの移動の基本
https://teratail.com/questions/206548


使ったモデル
https://www.turbosquid.com/ja/3d-models/3d-model-stylized-cartoon-car-free-free-1778654

https://assetstore.unity.com/packages/3d/environments/urban/city-package-107224