trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

ROS2 create_rateとsleepの注意点

環境: humble

注意点

  1. シングルスレッドで使用した場合デッドロックが発生する
  2. 使用間隔があく場合sleep()しても最初の2回は設定通りの待ち時間にならない
  3. 作ったら壊さないとリソースが解放されない

説明

create_rateは内部でcreate_tiemrを実行してTimerを生成、Timerは設定された周期でevent.set()を実行している。 sleepではevent.wait()(とevent.clear())を実行している。

  • 1.について
    シングルスレッドだとsleep()をよびだしたスレッドが生きてるのでTimerのcallbackがブロックされデッドロックが発生する。

  • 2.について
    使用間隔があくとevent.set()済みになるため1回目のsleep()ではevent.wait()が即終了する。 2回目のsleep()での待ち時間はevent.set()発生タイミング次第。 3回目から設定どおりの待ち時間になる。

Timerとsleep開始・終了タイミング

  • 3.について
    生成したtimerは勝手には消えない(Node._timersに登録されてる)のでリソースも解放されない。解放にはNode.destroy_rateをしてやる必要がある。 github.com

確認

デッドロックが発生する書き方

subscribeのcallbackまでにevent.set()が発生するので1回目のsleepは通る

import rclpy
from rclpy.node import Node
from std_msgs.msg import Empty


class RateTest(Node):
    def __init__(self):
        super().__init__("rate_test")
        self.sub = self.create_subscription(Empty, "rate_test", self.cb_test_rate, 10)
        self.r = self.create_rate(1)

    def cb_test_rate(self, _):
        while rclpy.ok():
            self.get_logger().info("while loop rate_test")
            self.r.sleep()


def main(args=None):
    rclpy.init(args=args)
    sleep_test = RateTest()
    rclpy.spin(sleep_test)
    sleep_test.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()

デッドロック発生

デッドロックは発生しないけどsleep時間が安定しない書き方

マルチスレッドにしたのでデッドロックは発生しない。 使用間隔があくので最初の二回は期待したsleep時間にならない

import rclpy
from rclpy.node import Node
from std_msgs.msg import Empty
from rclpy.executors import MultiThreadedExecutor


class RateTest(Node):
    def __init__(self):
        super().__init__("rate_test")
        self.sub = self.create_subscription(Empty, "rate_test", self.cb_test_rate, 10)
        self.r = self.create_rate(1) # 作り置き

    def cb_test_rate(self, _):
        while rclpy.ok():
            self.get_logger().info("while loop rate_test")
            self.r.sleep()


def main(args=None):
    rclpy.init(args=args)
    sleep_test = RateTest()
    executor = MultiThreadedExecutor() # マルチスレッドにした
    executor.add_node(sleep_test)
    executor.spin()
    sleep_test.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()

1回目のsleep時間はほぼゼロ。2回目は約660ms。3回目以降は約1s

期待通りの待ち時間だけどリソースを食いつぶす書き方

使うときに作成しているのでsleep時間は期待通り。 後処理していないのでリソースを食いつぶす。

import rclpy
from rclpy.node import Node
from std_msgs.msg import Empty
from rclpy.executors import MultiThreadedExecutor


class RateTest(Node):
    def __init__(self):
        super().__init__("rate_test")
        self.sub = self.create_subscription(Empty, "rate_test", self.cb_test_rate, 10)

    def cb_test_rate(self, _):
        while rclpy.ok():
            self.get_logger().info("while loop rate_test")
            self.create_rate(1).sleep() # 作りっぱなし(リソースは解放されない)


def main(args=None):
    rclpy.init(args=args)
    sleep_test = RateTest()
    executor = MultiThreadedExecutor()
    executor.add_node(sleep_test)
    executor.spin()
    sleep_test.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()

最初のsleepから待ち時間は約1s

python3のcpu使用率に注目(わかりやすいように10hzにしてる)

後始末する書き方

Node.destroy_rateで作成したRateを廃棄するのでリソースも解放される

import rclpy
from rclpy.node import Node
from std_msgs.msg import Empty
from rclpy.executors import MultiThreadedExecutor


class RateTest(Node):
    def __init__(self):
        super().__init__("rate_test")
        self.sub = self.create_subscription(Empty, "rate_test", self.cb_test_rate, 10)

    def cb_test_rate(self, _):
        while rclpy.ok():
            self.get_logger().info("while loop rate_test")
            r = self.create_rate(10)
            r.sleep()
            self.destroy_rate(r) # 後始末


def main(args=None):
    rclpy.init(args=args)
    sleep_test = RateTest()
    executor = MultiThreadedExecutor()
    executor.add_node(sleep_test)
    executor.spin()
    sleep_test.destroy_node()
    rclpy.shutdown()


if __name__ == "__main__":
    main()

cpu使用率は1-3%で安定

座標変換について

回転やら座標変換やら調べてたらこんがらがってきたので整理

前提条件

  • 座標変換

座標系\Sigma_{0}\Sigma_{1}が存在する。 \Sigma_{1}は原点を通るベクトルnを回転軸として\Sigma_{0}をθ回転させたもの。

\Sigma_{1}の点p1を\Sigma_{0}に変換する座標変換行列をR^{0}_{1}と書く(p_{0}=R^{0}_{1} p_{1})。
\Sigma_{0}の点p0を\Sigma_{1}に変換する座標変換行列はR^{1}_{0} (p_{1}=R^{1}_{0} p_{0})。

  • 回転

原点を通るベクトルnを回転軸として点p0を点p1にθ回転させる回転行列をR_{n}(\theta)と書く。

ベクトル[0,0,1] (Z軸)でθ回転させる回転行列は


R_{z}(\theta)=
\begin{pmatrix}
\cos \theta & -\sin \theta & 0 \\
\sin \theta & \cos \theta & 0 \\
0 & 0 & 1
\end{pmatrix}

3次元座標変換

\Sigma_1\Sigma_0をX,Y,Z軸の順に回転させたものと考える。
X軸を回転軸としてΦ回転->Y軸を回転軸としてθ回転->Z軸を回転軸としてψ回転

固定角

\Sigma_0のX,Y,Z軸を使用した場合を固定角と呼ぶ(他にも呼び方あるらしい)。

固定角での回転

固定角の座標変換行列は


R^{0}_{1}=R_{z}(\psi) R_{y}(\theta) R_{x}(\phi) \\
R^{1}_{0}=(R_{z}(\psi) R_{y}(\theta) R_{x}(\phi))^{t}

オイラー

\Sigma_0のX軸、X軸で回転後のY軸、X軸で回転後のY軸で回転後のZ軸を使用した場合をオイラー角とよぶ(他にも呼び方あるらしい)。

オイラー角での回転

オイラー角の座標変換行列は


R^{0}_{1}=R_{x}(\phi) R_{y}(\theta) R_{z}(\psi) \\
R^{1}_{0}=(R_{x}(\phi) R_{y}(\theta) R_{z}(\psi))^{t}

考え方

  • 回転行列の性質

回転行列は直交行列。また、逆方向に回せば元に戻るので
R_{n}(\theta)^{-1}=R_{n}(\theta)^t=R_{n}(-\theta)

  • 回転行列と座標変換行列の関係

p_{0}=R^{0}_{1} p_{1}を考える。
\Sigma_{1}を逆方向に回転させると\Sigma_{0}に一致する。 p1は動かさないので座標系に対して相対的にθ回転する。 よってp_{0}=R_{n}(\theta) p_{1}。つまりR^{0}_{1}=R_{n}(\theta)

同様にR^{1}_{0}=R_{n}(\theta)^t

  • 3次元での回転

X,Y,Z軸を回転軸とし、3回回すことが多い(例:X軸について回転->Y軸について回転->Z軸について回転)。 軸の選び方は3x3x3=27通り。 二回連続で同じ軸で回転(例:X軸->X軸->Y軸)させても意味がないため有効なのは3x2x2=12通り。

  • 固定角での座標変換行列

簡単なのでまずは回転行列を考える。 単純に\Sigma_{0}のX,Y,Z軸で回転させるだけなので

 R_{n}=R_{z}(\psi) R_{y}(\theta) R_{x}(\phi)

R^{0}_{1}=R_{n}(\theta), R^{1}_{0}=R_{n}(\theta)^tなので


R^{0}_{1}=R_{z}(\psi) R_{y}(\theta) R_{x}(\phi) \\
R^{1}_{0}=(R_{z}(\psi) R_{y}(\theta) R_{x}(\phi))^{t}

簡単なのでまずは \Sigma_{0}から\Sigma_{1}への座標変換行列を考える。 X軸で回転後の座標、Y軸で回転後の座標、Z軸で回転後の座標をそれぞれ\Sigma_{x},\Sigma_{y},\Sigma_{z}(=\Sigma_{1}) と書く。
\Sigma_{x}\Sigma_{0}から\Sigma_{0}のX軸で回転させたものなのでR^{x}_{0}=R_{x}(\phi)^{t}
\Sigma_{y}\Sigma_{x}から\Sigma_{x}のY軸で回転させたものなのでR^{y}_{x}=R_{y}(\theta)^{t}
\Sigma_{z}\Sigma_{y}から\Sigma_{y}のZ軸で回転させたものなのでR^{z}_{y}=R_{z}(\psi)^{t}

まとめると


\begin{aligned}
R^{1}_{0}&=R^{z}_{y} R^{y}_{x} R^{x}_{0} \\
&=R_{z}(\psi)^{t} R_{y}(\theta)^{t} R_{x}(\phi)^{t} \\
&=(R_{x}(\phi) R_{y}(\theta) R_{z}(\psi))^{t}
\end{aligned}

よって


R^{0}_{1}=R_{x}(\phi) R_{y}(\theta) R_{z}(\psi) \\
R^{1}_{0}=(R_{x}(\phi) R_{y}(\theta) R_{z}(\psi))^{t}

確認用ソフト

上記の考えをもとに確認用ソフト作成(chatgptと相談しながら作った。便利な世の中になったもんだ…)。 Intrinsicがオイラー、Extrinsicが固定角。 それっぽく動いてるから多分正しいやろ…。 なおラジオボタンで回転順序変更できるようにしようと思ったけど未。<-変更できるようにした<-十二通りの回転できるようにした。それっぽい感じだから多分正しいやろ…。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from mpl_toolkits.mplot3d import Axes3D
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from itertools import product
from typing import Final

"""
座標の回転、座標変換を描画する

絶対座標上の点(1,1,1)の回転座標上の座標
回転座標上の点(1,1,1)の絶対座標上の座標
を描画

オイラー角と固定角の切り替えができるとする

回転過程も見るならsliderは順序通りに動かすこと
"""


INTRINSIC: Final[str] = "Intrinsic"
EXTRINSIC: Final[str] = "Extrinsic"


class Rotate:
    """基準の座標系に対して指定した順に回転していると考える"""

    def __init__(self, rad: list, order: list, method: str) -> None:
        rotation_matrix_map = {
            "X": self.get_R_x,
            "Y": self.get_R_y,
            "Z": self.get_R_z,
        }
        self.R_0 = rotation_matrix_map[order[0]](rad[0])
        self.R_1 = rotation_matrix_map[order[1]](rad[1])
        self.R_2 = rotation_matrix_map[order[2]](rad[2])

        if method == INTRINSIC:  # オイラー角
            self.rotaion_matrix = self.R_0 @ self.R_1 @ self.R_2
        elif method == EXTRINSIC:  # 固定角
            self.rotaion_matrix = self.R_2 @ self.R_1 @ self.R_0
        else:
            raise NotImplementedError()

    def get_R_x(self, x_rad):
        return np.array(
            [
                [1, 0, 0],
                [0, np.cos(x_rad), -np.sin(x_rad)],
                [0, np.sin(x_rad), np.cos(x_rad)],
            ]
        )

    def get_R_y(self, y_rad):
        return np.array(
            [
                [np.cos(y_rad), 0, np.sin(y_rad)],
                [0, 1, 0],
                [-np.sin(y_rad), 0, np.cos(y_rad)],
            ]
        )

    def get_R_z(self, z_rad):
        return np.array(
            [
                [np.cos(z_rad), -np.sin(z_rad), 0],
                [np.sin(z_rad), np.cos(z_rad), 0],
                [0, 0, 1],
            ]
        )

    """
    次の二つは同じ処理になる
        点を回転させる
        回転座標系の点を基準の座標系に変換する
    """

    def rotate(self, point: list[float]) -> list[float]:
        """
        指定したmethodでpointを回転させる
        Parameters:
        point:list- 回転させたい座標[x,y,z]

        Returns:
        list- 回転後の新しい座標
        """
        return (self.rotaion_matrix @ np.array(point)).tolist()

    def rot_to_base(self, point: list[float]) -> list[float]:
        """
        回転座標系の点を基準座標系に変換する
        Parameters:
        point: list - 回転座標系の座標[x,y,z]

        Returns:
        list - 基準座標系の座標に変換したもの[x,y,z]
        """
        return self.rotate(point)

    """
    次の二つは同じ処理になる
        点を逆回転させる
        基準座標系の点を回転座標系に変換する
    """

    def reverse(self, point: list) -> list:
        """
        逆回転させる(rotateの逆)
        """
        return (self.rotaion_matrix.T @ np.array(point)).tolist()

    def base_to_rot(self, point: list) -> list:
        """
        基準座標系の点を回転座標系に変換する
        """
        return self.reverse(point)


class Painter:
    def __init__(self) -> None:
        # この順番で宣言すること!(順番を間違うとイベントループが競合する)
        self.root = tk.Tk()
        self.fig = plt.figure()

        self.set_plt()
        self.set_tk()
        self.on_menu_change(None)

        self.plot()

    def set_plt(self):
        """pltの設定"""
        self.ax = self.fig.add_subplot(111, projection="3d")

        # slidarの設定(回転座標系の回転を指定する)
        axcolor = "lightgoldenrodyellow"
        ax_0 = plt.axes([0.2, 0.12, 0.65, 0.03], facecolor=axcolor)
        ax_1 = plt.axes([0.2, 0.07, 0.65, 0.03], facecolor=axcolor)
        ax_2 = plt.axes([0.2, 0.02, 0.65, 0.03], facecolor=axcolor)
        self.slider_0 = Slider(ax_0, "0[rad]", -np.pi, np.pi, valinit=0)
        self.slider_1 = Slider(ax_1, "1[rad]", -np.pi, np.pi, valinit=0)
        self.slider_2 = Slider(ax_2, "2[rad]", -np.pi, np.pi, valinit=0)
        self.slider_0.on_changed(self.on_slider_change)
        self.slider_1.on_changed(self.on_slider_change)
        self.slider_2.on_changed(self.on_slider_change)

    def set_tk(self):
        """tkinterの設定"""
        self.root.wm_title("3D plot with controls")

        self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)

        # プルダウンメニューの設定
        axis = ["X", "Y", "Z"]
        order = filter(lambda o: o[0] != o[1] and o[1] != o[2], product(axis, axis, axis))
        menu_options = ["->".join(o) for o in order]
        self.menu_var = tk.StringVar(self.root)
        self.menu_var.set(menu_options[0])
        tk.OptionMenu(self.root, self.menu_var, *menu_options, command=self.on_menu_change).pack(side=tk.RIGHT)

        # ラジオボタンの設定
        self.radio_var = tk.StringVar()
        self.radio_var.set(INTRINSIC)
        for method in [INTRINSIC, EXTRINSIC]:
            tk.Radiobutton(
                self.root, text=method, variable=self.radio_var, value=method, command=self.on_radio_change
            ).pack(side=tk.LEFT)

        # ボタンの設定
        tk.Button(self.root, text="Reset", command=self.on_reset_button_click).pack(side=tk.BOTTOM)

        # テキストラベルの設定
        self.label_base_to_rot = tk.Label(self.root)
        self.label_base_to_rot.pack(side=tk.BOTTOM)
        self.label_rot_to_base = tk.Label(self.root)
        self.label_rot_to_base.pack(side=tk.BOTTOM)

    def start(self):
        """描画開始"""
        self.root.mainloop()

    def plot(self):
        self.ax.cla()

        # 絶対座標系の表示
        self.plot_coordinate([1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0])

        # スライダー・回転方式に対応した回転座標を計算
        rad_0, rad_1, rad_2 = self.slider_0.val, self.slider_1.val, self.slider_2.val  # [rad]
        rot = Rotate([rad_0, rad_1, rad_2], self.menu_var.get().split("->"), self.radio_var.get())
        # 回転座標系を計算
        sigma_x = rot.rot_to_base([1.0, 0, 0])
        sigma_y = rot.rot_to_base([0, 1.0, 0])
        sigma_z = rot.rot_to_base([0, 0, 1.0])
        self.plot_coordinate(sigma_x, sigma_y, sigma_z, 0.5)

        # 基準座標系の点[1,1,1]を回転座標系に変換する
        self.plot_point([1.0, 1.0, 1.0], "k")
        base_to_rot_point = rot.base_to_rot([1.0, 1.0, 1.0])
        disp_txt = f"{base_to_rot_point[0]:.2f},{base_to_rot_point[1]:.2f},{base_to_rot_point[2]:.2f}"
        self.label_base_to_rot.config(text=f"base:1.00,1.00,1.00->rot:{disp_txt}")
        self.ax.text(1.0, 1.0, 1.0 - 0.5, disp_txt, color="k", fontsize=10)

        # 回転座標系の点[1,1,1]を基準座標系に変換する
        rot_to_base_point = rot.rot_to_base([1.0, 1.0, 1.0])
        disp_txt = f"{rot_to_base_point[0]:.2f},{rot_to_base_point[1]:.2f},{rot_to_base_point[2]:.2f}"
        self.plot_point(rot_to_base_point, "y")
        self.label_rot_to_base.config(text=f"rot:1.00,1.00,1.00->base:{disp_txt}")
        self.ax.text(
            rot_to_base_point[0], rot_to_base_point[1], rot_to_base_point[2] - 0.5, disp_txt, color="k", fontsize=10
        )

        # 基準座標系の[1,1,1]から回転座標系に線を引く
        p_xyz = [1, 1, 1]
        p_xy = rot.rot_to_base([base_to_rot_point[0], base_to_rot_point[1], 0])
        p_x = rot.rot_to_base([base_to_rot_point[0], 0, 0])
        p_xz = rot.rot_to_base([base_to_rot_point[0], 0, base_to_rot_point[2]])
        p_y = rot.rot_to_base([0, base_to_rot_point[1], 0])
        p_yz = rot.rot_to_base([0, base_to_rot_point[1], base_to_rot_point[2]])
        p_z = rot.rot_to_base([0, 0, base_to_rot_point[2]])
        p_ = [0, 0, 0]
        for b_e in (
            zip(p_xyz, p_xy),
            zip(p_x, p_xz),
            zip(p_y, p_yz),
            zip(p_xyz, p_yz),
            zip(p_xyz, p_xz),
            zip(p_y, p_xy),
            zip(p_x, p_xy),
            zip(p_z, p_yz),
            zip(p_z, p_xz),
            zip(p_, p_z),
            zip(p_, p_x),
            zip(p_, p_y),
        ):
            p = [[p1, p2] for p1, p2 in b_e]
            self.ax.plot(p[0], p[1], p[2], linestyle="--", color="k")
        # 相対座標系の[1,1,1]から基準座標系に線を引く
        p_xyz = rot.rot_to_base([1, 1, 1])
        p_xy = [rot_to_base_point[0], rot_to_base_point[1], 0]
        p_x = [rot_to_base_point[0], 0, 0]
        p_xz = [rot_to_base_point[0], 0, rot_to_base_point[2]]
        p_y = [0, rot_to_base_point[1], 0]
        p_yz = [0, rot_to_base_point[1], rot_to_base_point[2]]
        p_z = [0, 0, rot_to_base_point[2]]
        p_ = [0, 0, 0]
        for b_e in (
            zip(p_xyz, p_xy),
            zip(p_x, p_xz),
            zip(p_y, p_yz),
            zip(p_xyz, p_yz),
            zip(p_xyz, p_xz),
            zip(p_y, p_xy),
            zip(p_x, p_xy),
            zip(p_z, p_yz),
            zip(p_z, p_xz),
            zip(p_, p_z),
            zip(p_, p_x),
            zip(p_, p_y),
        ):
            p = [[p1, p2] for p1, p2 in b_e]
            self.ax.plot(p[0], p[1], p[2], linestyle="--", color="y")

        self.ax.set_xlabel("X axis")
        self.ax.set_ylabel("Y axis")
        self.ax.set_zlabel("Z axis")

        self.ax.set_xlim(-1.1, 1.1)
        self.ax.set_ylim(-1.1, 1.1)
        self.ax.set_zlim(-1.1, 1.1)
        self.canvas.draw_idle()

    def plot_point(self, point: list, c):
        """点を描画"""
        self.ax.scatter(point[0], point[1], point[2], color=c, s=100)

    def plot_coordinate(self, sigma_x, sigma_y, sigma_z, alpha=1):
        for (x, y, z), c in zip([sigma_x, sigma_y, sigma_z], ["r", "g", "b"]):
            self.ax.quiver(0, 0, 0, x, y, z, color=c, arrow_length_ratio=0.1, alpha=alpha)

    def on_slider_change(self, val):
        """スライダーの変更を反映する(回転座標の回転)"""
        print(self.slider_0.val, self.slider_1.val, self.slider_2.val)
        self.plot()

    def on_menu_change(self, event):
        """プルダウンメニューの変更を反映する(回転の方法)"""
        print(f"selected: {self.menu_var.get()}")
        order = self.menu_var.get().split("->")
        self.slider_0.label.set_text(f"{order[0]}[rad]")
        self.slider_1.label.set_text(f"{order[1]}[rad]")
        self.slider_2.label.set_text(f"{order[2]}[rad]")
        self.on_reset_button_click()

    def on_radio_change(self):
        """ラジオボタンの変更を反映する(未使用)"""
        selected_radio = self.radio_var.get()
        print(f"Selected radio: {selected_radio}")
        self.on_reset_button_click()

    def on_reset_button_click(self):
        """リセットボタンがクリックされたときの処理(回転を0にする)"""
        print("Reset button clicked!")
        self.slider_0.reset()
        self.slider_1.reset()
        self.slider_2.reset()
        self.plot()


if __name__ == "__main__":
    painter = Painter()
    painter.start()

ROS 2 でcriticのpluginを作る

動機

図みたいな回り込む感じの経路だといい感じに動いてくれない1。パラメータ調整めんどくさいやだやる気出ないいい感じのcritic作ろ。

回り込む経路

イデア

経路のうちロボットから見える点をgoalとしてGoalDistっぽく使えばええんちゃうか。

見える点をnew goalにする

ソースコードなど

my_direct_dist.cpp

#include "my_critics/my_direct_dist.hpp"
#include <vector>
#include <string>
#include "dwb_critics/alignment_util.hpp"
#include "pluginlib/class_list_macros.hpp"
#include "nav_2d_utils/parameters.hpp"
#include "nav_2d_utils/path_ops.hpp"

namespace my_critics
{

    void MyDirectDistCritic::onInit()
    {
        GoalDistCritic::onInit();
        auto node = node_.lock();
        if (!node)
        {
            throw std::runtime_error("Failed to lock node. Node might be expired or not initialized properly.");
        }
        robot_size_ = nav_2d_utils::searchAndGetParam(
            node,
            dwb_plugin_name_ + "." + name_ + ".robot_size", costmap_->getResolution());
        RCLCPP_INFO(rclcpp::get_logger("DirectDist"), "robot_size_:%lf,%s", robot_size_, dwb_plugin_name_.c_str());
    }

    //  poseとp_x,p_yの途中に障害物がなければ真
    bool MyDirectDistCritic::isPathValid(const double p_x, const double p_y, const geometry_msgs::msg::Pose2D &pose, const double skip_dist)
    {
        const double angle_to_point = atan2(p_y - pose.y, p_x - pose.x);
        const double dist_to_point = hypot(p_y - pose.y, p_x - pose.x);
        unsigned int map_x, map_y;
        bool is_valid = false;
        for (double dist = skip_dist; dist < dist_to_point + skip_dist; dist += skip_dist)
        {
            const double w_x = pose.x + std::min(dist, dist_to_point) * cos(angle_to_point);
            const double w_y = pose.y + std::min(dist, dist_to_point) * sin(angle_to_point);
            if (!costmap_->worldToMap(w_x, w_y, map_x, map_y) ||
                nav2_costmap_2d::MAX_NON_OBSTACLE < costmap_->getCost(map_x, map_y))
            {
                return false;
            }
            else
            {
                is_valid = true;
            }
        }
        return is_valid;
    }

    bool MyDirectDistCritic::prepare(
        const geometry_msgs::msg::Pose2D &pose, const nav_2d_msgs::msg::Twist2D &vel,
        const geometry_msgs::msg::Pose2D &goal,
        const nav_2d_msgs::msg::Path2D &global_plan)
    {
        double costmap_resolution = costmap_->getResolution();
        nav_2d_msgs::msg::Path2D adjusted_global_plan = nav_2d_utils::adjustPlanResolution(
            global_plan,
            costmap_resolution);
        int start_idx = -1;
        int end_idx = -1;
        // 有効な範囲のパスを出しておく
        for (unsigned int i = 0; i < adjusted_global_plan.poses.size(); ++i)
        {
            double p_x = adjusted_global_plan.poses[i].x;
            double p_y = adjusted_global_plan.poses[i].y;
            unsigned int map_x, map_y;
            if (!costmap_->worldToMap(p_x, p_y, map_x, map_y))
            {
                break;
            }
            if (start_idx == -1)
            {
                start_idx = static_cast<int>(i);
            }
            end_idx = static_cast<int>(i);
        }
        // 有効ではないので処理しない
        if (start_idx == -1 || end_idx == -1)
        {
            RCLCPP_ERROR(rclcpp::get_logger("MyDirectDist"), "non valid index.");
            return GoalDistCritic::prepare(pose, vel, goal, global_plan);
        }

        // パスのうち、直進で到達できる最も遠い点を求める
        //   処理を軽くするため適当にskipしながら処理
        //   endからやれば計算量減りそうだけど、skipしてるので通れない隙間を貫通する可能性があるのでstartからやる
        const double skip_dist = std::max(robot_size_ / 2.0, costmap_resolution);
        const unsigned int skip_idx = static_cast<unsigned int>(std::max(ceil(skip_dist / costmap_resolution) - 1, 1.0));
        int target_point_idx = -1;
        for (unsigned int i = static_cast<unsigned int>(start_idx); i < static_cast<unsigned int>(end_idx) + skip_idx; i += skip_idx)
        {
            unsigned int path_idx = std::min(i, static_cast<unsigned int>(end_idx));
            const double p_x = adjusted_global_plan.poses[path_idx].x;
            const double p_y = adjusted_global_plan.poses[path_idx].y;
            if (isPathValid(p_x, p_y, pose, skip_dist))
            {
                target_point_idx = static_cast<int>(path_idx);
            }
            else
            {
                break;
            }
        }
        if (-1 < target_point_idx)
        {
            nav_2d_msgs::msg::Path2D target_poses;
            target_poses.poses.push_back(adjusted_global_plan.poses[target_point_idx]);
            return GoalDistCritic::prepare(pose, vel, goal, target_poses);
        }
        else
        {
            RCLCPP_ERROR(rclcpp::get_logger("MyDirectDist"), "can't find target.");
            return GoalDistCritic::prepare(pose, vel, goal, adjusted_global_plan);
        }
    }
}

PLUGINLIB_EXPORT_CLASS(my_critics::MyDirectDistCritic, dwb_core::TrajectoryCritic)

my_direct_dist.hpp

#ifndef MY_CRITICS__MY_DIRECT_DIST_HPP_
#define MY_CRITICS__MY_DIRECT_DIST_HPP_

#include "my_critics/visibility_control.h"
#include <vector>
#include <string>
#include "dwb_critics/goal_dist.hpp"

namespace my_critics
{
  class MyDirectDistCritic : public dwb_critics::GoalDistCritic
  {
  public:
    MyDirectDistCritic()
        : robot_size_(0.0){};
    void onInit() override;
    bool prepare(
        const geometry_msgs::msg::Pose2D &pose, const nav_2d_msgs::msg::Twist2D &vel,
        const geometry_msgs::msg::Pose2D &goal, const nav_2d_msgs::msg::Path2D &global_plan) override;

  protected:
    bool isPathValid(const double p_x, const double p_y, const geometry_msgs::msg::Pose2D &pose, const double skip_dist);
    double robot_size_;
  };

} // namespace my_critics

#endif // MY_CRITICS__MY_DIRECT_DIST_HPP_

CMakeList.txt

cmake_minimum_required(VERSION 3.8)
project(my_critics)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_ros REQUIRED)
find_package(nav_2d_utils REQUIRED)
find_package(nav2_util REQUIRED)
find_package(nav2_costmap_2d REQUIRED)
find_package(nav_2d_msgs REQUIRED)
find_package(dwb_core REQUIRED)
find_package(dwb_critics REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(pluginlib REQUIRED)
find_package(rclcpp REQUIRED)

add_library(${PROJECT_NAME} SHARED src/my_direct_dist.cpp)
target_compile_features(${PROJECT_NAME} PUBLIC c_std_99 cxx_std_17)  # Require C99 and C++17
target_include_directories(${PROJECT_NAME} PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
ament_target_dependencies(
  ${PROJECT_NAME}
  "nav_2d_utils"
  "nav2_util"
  "nav2_costmap_2d"
  "nav_2d_msgs"
  "dwb_core"
  "dwb_critics"
  "geometry_msgs"
  "pluginlib"
  "rclcpp"
)

# Causes the visibility macros to use dllexport rather than dllimport,
# which is appropriate when building the dll but not consuming it.
target_compile_definitions(${PROJECT_NAME} PRIVATE "MY_CRITICS_BUILDING_LIBRARY")

install(
  DIRECTORY include/
  DESTINATION include
)
install(
  TARGETS ${PROJECT_NAME}
  EXPORT export_${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_export_include_directories(
  include
)
ament_export_libraries(
  ${PROJECT_NAME}
)
ament_export_targets(
  export_${PROJECT_NAME}
)

pluginlib_export_plugin_description_file(dwb_core critics.xml)
ament_package()

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>my_critics</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="trsing@trsing.com">trsing</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake_ros</buildtool_depend>

  <depend>nav_2d_utils</depend>
  <depend>nav2_util</depend>
  <depend>nav2_costmap_2d</depend>
  <depend>nav_2d_msgs</depend>
  <depend>dwb_core</depend>
  <depend>dwb_critics</depend>
  <depend>geometry_msgs</depend>
  <depend>pluginlib</depend>
  <depend>rclcpp</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

critics.xml

<library path="my_critics">
    <class type="my_critics::MyDirectDistCritic" base_class_type="dwb_core::TrajectoryCritic">
        <description>Scores trajectories based on how far along visible point of the global path they end up.</description>
    </class>
</library>

使い方

namespaceの追加以外は通常のcriticと同じ。

controller_server:
  ros__parameters:
    controller_plugins: ["FollowPath"]
    # DWB parameters
    FollowPath:
      default_critic_namespaces: ["dwb_critics","my_critics"]
      critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist", "MyDirectDist"]
      MyDirectDist.scale: 20.0
      MyDirectDist.robot_size: 0.2

robot_sizeは候補とする経路の点やロボット-点間の障害物有無確認の細かさに関係。大きくしすぎると途中にある障害物を見つけれなかったりするので注意。

注意点

gazeboでちょろっと動かしただけなのでまともに動くかどうかは知らぬ。

その他

CMakeList.txt何もわからん…。


  1. GoalDistCriticはgoalからの距離でコストを張る。障害物等は関係ないので回り込む(goalから離れる)経路と相性が悪い。
    GoalDistCritic

Conflict-based searchメモ

はじめに

お仕事で群制御せななあという感じになってきたので調べてるとMAPFという分野(?)がそれっぽく、 この分野ではCBSを知ってて当然みたいな感触。 のわりに日本語での解説を見つけられなかったので元の論文(多分)を読んだ。

元論文

https://www.sciencedirect.com/science/article/pii/S0004370214001386

CBSだけじゃなくてMAPFの難しさや先行事例についてもざっと説明してくれてるでとてもありがたかった。

今回のメモには書かないけどCBSで最適解が得られることの証明やCBSの欠点とその緩和方法(MA-CBS)についても書いてる。ありがたや。

前提知識

ヒューリスティック探索の基礎(?)。次の資料によくまとまってる。日本語。うれしい。

https://jinnaiyuu.github.io/pdf/textbook.pdf

MAPFについて

multi-agent pathfinding。 複数のエージェントが衝突せずにそれぞれのゴールに到達するまでのパスを求める。 CBSは最適MAPFアルゴリズム

問題定義

  • 入力

    • グラフ: G(V, E)
    • k個のエージェント: a_1,a_2,\dots,a_k
    • 各エージェントのスタート位置とゴール位置: start_i, goal_i
  • 出力
    エージェントが衝突せずにゴールに到着するパス(スタート位置からゴールに到着するまでの一連のアクション)

  • 制約

    • 衝突禁止。同時刻に一つの頂点に2つ以上のエージェントが存在することはできない。
    • すれ違い禁止。連続した時間ステップで2つ以上のエージェントがエッジを横切ることはできない。
  • コスト関数 今回使用するコスト関数はSum-of-costs。 各エージェントがゴールに到着して停止1するまでにかかった時間の総和。

MAPFの難しさ

探索空間が指数関数的に増加する。 1ステップでO(分岐係数^{エージェント数})。 格子型のマップ2でエージェント数が20なら5^{20}=95,367,431,640,625。1ステップでこれ。やってられっか。

CBSアルゴリズム

二つのレベル(High,Low)の処理で構成される。
HighレベルはCT(constraint tree)を探索する。衝突のないnodeが見つかったらそれをgoalとして終了。
Lowレベルは対象エージェントa_iに対するパスを生成する。

constraint tree

二分木3。各ノードは次の情報から構成される。

  • 制約集合(N.constraints):各制約は(a_i, v, t) (時刻tでエージェントa_iに頂点vにいることを禁止)または(a_i, v_1, v_2, t) (時刻tでエージェントa_iv_1からv_2への移動を禁止。すれ違いを防ぐ)。 親ノードのconstraintsに制約を一つ加えたものを持つ。4
  • 解(N.solution):それぞれのエージェントに対するパス。 パスは制約(N.constraints)を満たす。5
  • 総コスト(N.cost):解(N.solution)のコスト。それぞれのパスのコストの総和。ノードのf-value(論文の1.や3.3.1.参照)。

Highレベル

CTに対し、最良優先探索を行う。評価基準はコスト(N.cost)。コストが同じ場合は衝突が少ない方を優先する。 解(N.solution)を検証し、衝突がある場合は衝突を防ぐ制約を追加した子ノードを生成し処理を続ける。 衝突がない場合はノードNをgoalとし処理を終了する。

Lowレベル

対象エージェントa_iと制約集合を受け取り、エージェントa_iに対して制約を満たす最適パスを生成する。 エージェントa_i以外は存在しないものとして探索する。 論文ではA*を使用しているがsingle-agent pathfindingアルゴリズムを使用できる。

疑似コード

python風。

root = Node()
root.constraints = 空集合 # 制約なし
root.solution = 各エージェントのパス # 制約なしでlow level処理したもの
root.cost = root.solutionのコスト
open.put(root)
while not open.empty():
    node = openリスト中の一番コスト低いやつ
    conflicts = node.solutionを検証 # 衝突(a_i,a_j,v,t)のリストを得られるとする
    if 衝突無し: # 衝突がないのでgoal
        return node.solution
    conflict = conflictsの最初の衝突
    for agent in conflict: # a_i, a_jに制約を加えた子ノードを作る
        child = Node()
        child.constraints = node.constraintsに(agent, v, t)を加えたもの
        child.solution = node.solution
        child.solution[agent] = agentのパスを更新 # agentについて制約child.constraintsでlow level処理したもの
        child.cost = child.solutionのコスト
        if 解あり:
            open.put(child)

その他

お仕事の要件に近いのMAPD(Multi-Agent Pickup and Delivery)だわ。


  1. ゴールに居続ける。他の頂点に移動しない。
  2. 上下左右待機で分岐係数5
  3. 簡易化のため。二分木じゃなくてもいい。
  4. 親をたどれば制約を追えるので追加した制約のみ保持しておけばよい。
  5. 追加した制約の対象となるエージェントのパスのみ更新すればよい。対象でないエージェントのパスは変わらないため。

SSLを無視する設定

経緯

情シスからセキュリティ強化のお知らせ。git cloneやらpipやらで証明書のエラーが出るようになったので問い合わせると証明書無視する方向で何とかしろとのお返事。ので証明書無視する設定とか。

証明書を無視する設定

git

GIT_SSL_NO_VERIFY=1 git clone https://github.https://github.com/hoge/hoge.git

qiita.com

GIT_SSL_NO_VERIFY は、SSL証明書の検証を行わないようにGitへ指示します。 これは、GitリポジトリHTTPS経由で利用するために自己署名証明書を使っている場合や、Gitサーバーのセットアップ中で正式な証明書のインストールが完了していない場合などに必要になります。

git-scm.com

pip

pip --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --trusted-host pypi.org install <ライブラリ名...>

tex2e.github.io

Mark this host or host:port pair as trusted, even though it does not have valid or any HTTPS.

pip.pypa.io

vs codeのupdate

/etc/wgetrccheck_certificate = offを追加

stackoverflow.com

If this is set to off, the server certificate is not checked against the specified client authorities. The default is “on”. The same as ‘--check-certificate’.

www.gnu.org

vs codeの設定ではなくwgetの設定なので注意。

curl

curl --insecure https://192.168.1.2

linuxfan.info

By default, every secure connection curl makes is verified to be secure before the transfer takes place. This option makes curl skip the verification step and proceed without checking.

curl.se

感想

セキュリティ強化でSSLインスペクションを採用したっぽい。その結果証明書無視することになったけどこれセキュリティ強化になるのかな?

その他

開発環境損を大事にする転職先募集先です。

Jetson Orinでpyrealsense2を使う

これ

github.com

補足

cmakeのバージョンに注意。上記コメントでは3.13。どのバージョンからかPYTHON_EXECUTABLEの取り扱いが変わってるっぽい(PYTHON_EXECUTABLE->Python_EXECUTABLE?よくわからない…)

追記

cmake 3.25.2でのビルド結果

librealsense 2.54.1
問題なくビルドできた(make -j4まで完了)

librealsense 2.38.1
cmakeでエラー。Could NOT find Python

librealsense 2.38.1でオプションを-DPYTHON_EXECUTABLE->-DPython_EXECUTABLE
問題なくビルドできた(make -j4まで完了)