techno_memo

個人用の技術メモ。python・ROS・AI系のソフトウェア・ツールなどの情報を記載

Visual studio code を用いたgit操作

この記事の目的

Visual studio codeを用いてgit操作をする方法についてまとめる。

1. Visual studio codeを用いたgit操作

git操作の手段としてはコマンド以外にvisual studio code、source treeなどのツールを利用する方法がある。 Windows/Linuxに依存せず利用可能でソースコード編集の機能も充実しているVSCodeでgit操作する方法についてまとめる。

2. 拡張機能

VS Codeは標準でgit連携機能をサポートしているが下記拡張機能をインストールしておくとソースコードの管理・確認の利便性を向上できる。

## 3. VSCodegithubの連携/ソースコードの取得

すでにgithubにあるリポジトリをクローンする場合、VS codeソースコード管理画面からリポジトリの複製画面を選択する。 f:id:sd08419ttic:20210904150819p:plain

初回のみ、どのgithubアカウントと連携するかの情報を入力する必要がある。ブラウザが立ち上がるのでgithubのアカウント情報を入力してVSCodeとの連携を許可する。 f:id:sd08419ttic:20210904151729p:plain

連携が完了すると自分のアカウントにあるリポジトリの一覧が表示される。 f:id:sd08419ttic:20210904152047p:plain

ここで、他アカウントで作成してあるソースコードをcloneしたい場合には、コマンドパレットにキーワードを入れてgithubリポジトリを検索するか 参照するリポジトリのURLを入力する。ブラウザで検索したソースコードをcloneするURLは各ページのCodeボタンを押して取得できる。

f:id:sd08419ttic:20210904152941p:plain

4.変更履歴の確認

取得したソースコードが過去にどのように変更されたかは過去のコミット履歴から確認できる。 f:id:sd08419ttic:20210904154400p:plain

また特定のファイルについて過去のコミットでどのように変更されたかを確認したい場合にはFileHistoryで確認することができる。

f:id:sd08419ttic:20210904154703p:plain

5.ブランチの作成・移動

ブランチの移動は移動対象ブランチを選択し、『Switch to Branch』を選択して行う。 f:id:sd08419ttic:20210904162100p:plain

新規ブランチ作成はCreate Branchを選択して行う。

また、各ブランチの関係(マージされたタイミングなど)はgit graphで可視化することができる。 git graphはコマンドパレットから『Git Graph : View Git Graph』を選択することで表示できる。

f:id:sd08419ttic:20210904163047p:plain

6.変更の反映 (コミット/プッシュ)

ローカルでの変更内容をコミットは、①ファイルのステージ、②コミット、③プッシュの順に行う。

①ファイルのステージ

ローカルにあるファイルを変更して保存し、過去のコミットと差分が発生するとソース管理画面の"変更"画面に差分が発生しているファイルが表示される。 内容を確認し、"変更をステージ"ボタンを押してファイルをステージングする。

f:id:sd08419ttic:20210904164012p:plain

②コミットメッセージの入力

コミットの変更内容に対する説明用メッセージをメッセージ入力画面に入力し、ローカルリポジトリに変更内容をコミットする。

f:id:sd08419ttic:20210904164552p:plain

③変更内容のプッシュ

ソースコード管理画面のCOMMITの横にあるプッシュボタン(↑)を押して、変更内容をリモートリポジトリにプッシュする。

f:id:sd08419ttic:20210904164823p:plain

7.リモートリポジトリの変更内容取得(プル)、コンフリクトの修正

自分が編集中のリモートリポジトリに対して修正された内容を取得する場合にはソースコード管理画面のCOMMITの横にある↓ボタンでプルを行う。

f:id:sd08419ttic:20210904164823p:plain

この際、自分のコミットとリモートリポジトリの変更内容でコンフリクト(競合)が発生する場合がある。その場合は競合した箇所に対してどちらの内容を反映するかを選択してマージする。

f:id:sd08419ttic:20210911134407p:plain

ROS2 navigation stack odometryの出力

この記事の目的

 ROS2のnavigation stack用にpython scriptからodometryを出力する方法をまとめる。  ROS2とnavigagiton2 は下記記事に従って導入済みであることを前提とする。   sd08419ttic.hatenablog.com

1. スクリプトの構成

ROS2のpythonスクリプトの記述の実装方法は下記のチュートリアルに記載されている。

docs.ros.org

ROSと異なり、rclpyのNodeを継承して処理を記述し、一定周期に呼び出す処理をcallback関数で記述するように変更されている。 簡潔に記述するため、ROS2のノード処理(ノード立ち上げ、周期処理、終了)およびトピック通信(publish/subscribe)と制御に関する処理(自己位置の計算など)を別ファイルに記載する。

1. publish/subscribe処理の実装

ROS2でodometryを10msecでpublishし続けるスクリプトは下記のように記述できる。 下記スクリプトはOdomSimulatorNode生成時のinit関数でodometryのpublisher初期化、cmd_velのsubscribe設定、周期処理呼び出しのtimer設定をしている。 その後、ノード実行中はcmd_velのsubscribe成功時にsub_cmdvel_callbackで受信内容の保存、20msec周期でtimer_callbackから位置情報の更新とpublish、TFのbroadcastを行う。位置情報・TFの計算はodom_simulator_func.pyに記述する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pandas as pd
# import for ros function
import rclpy
from rclpy.node import Node
import tf2_py
from geometry_msgs.msg import PoseStamped, Twist
from std_msgs.msg import Header
from nav_msgs.msg import Path, Odometry
from odom_simulator_func import OdomSimlatorFunc
from tf2_ros.transform_broadcaster import TransformBroadcaster

class OdomSimulatorNode(Node):

    ##################
    # Initialization #
    ##################
    def __init__(self):
        super().__init__('Odom_Simulator')

        #initialize publisher
        self.OdometryPub_ = self.create_publisher(Odometry, '/odom', 10)
        self._tf_Odompublisher = TransformBroadcaster(self)

        #initialize subscriber
        self.CmdvelSub = self.create_subscription(
            Twist,
            '/cmd_vel',
            self.sub_cmdvel_callback,
            10)

        self.CmdvelSub  # prevent unused variable warning

        #timer configuration
        timer_period = 0.02  # seconds
        self.timer = self.create_timer(timer_period, self.timer_callback)
        #self.path_follow_func = Path_follow_func()
        self.cnt = 0

        self.odomSimFunc = OdomSimlatorFunc()

    def timer_callback(self):
        # publish odom data
        #cmdvel = self.pathfollowerFunc.update_cmd_vel()
        #odom = Odometry()
        odom, tf_odom = self.odomSimFunc.update_odom()
        #calc cmdvel
        odom.header.stamp = self.get_clock().now().to_msg()
        self.OdometryPub_.publish(odom)
        tf_odom.header.stamp = self.get_clock().now().to_msg()
        self._tf_Odompublisher.sendTransform(tf_odom)
        self.cnt += 1
        if self.cnt %10 ==0:
            self.get_logger().info('Publishing Count: "%s"' % self.cnt)

    #subscriber call back for Odometry
    def sub_cmdvel_callback(self, msg):
        self.odomSimFunc.set_cmdvel_from_subscriber(msg)
        test = Twist()
        self.get_logger().info('I heard: "%s"' % msg.linear)

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

    odomSimNode = OdomSimulatorNode()
    rclpy.spin(odomSimNode)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    odomSimNode.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

2. 位置情報・TFの計算方法

位置情報・TFの計算処理は下記のように記述できる。cmd_velで受信した目標速度・ヨーレートから座標を更新する。 ROS2ではTFからクォータニオンオイラー角を変換する関数がなくなってしまったのでtf_lib ファイルに変換処理を記述する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math
# import for ros function
from geometry_msgs.msg import Point, Pose, Quaternion, Twist, Vector3
from std_msgs.msg import Header
from nav_msgs.msg import Odometry
from geometry_msgs.msg import TransformStamped
from tf_lib import euler_from_quaternion, quaternion_from_euler


class OdomSimlatorFunc():

    ##################
    # Initialization #
    ##################
    def __init__(self):

        #Save path_plan flag
        self.save_path_as_csv = True

        #Cmd_vel receive flag (auto control)
        self.subscribe_cmd_vel = False

        #Parameter
        self.MAX_SPEED_KMH = 30.0                       # MAX vehicle speed [km/h]
        self.MIN_SPEED_KMH = -20.0                      # MINIMUM vehicle speed [km/h]
        self.MAX_SPEED_MPS = self.MAX_SPEED_KMH / 3.6   # MAX vehicle speed [km/h]
        self.MIN_SPEED_MPS = self.MIN_SPEED_KMH / 3.6   # MAX vehicle speed [km/h]
        self.MAX_YAW_RATE = 1.57                        # MAX YAWRATE [rad/s]

        #initiali position
        initPosx = 0.0
        initPosy = 0.0

        #Initialize odometry header
        self.odom_header = Header()
        self.odom_header.frame_id = "map"

        # Initialize pose info
        self.sim_pose = Pose()
        self.sim_pose.position.x = initPosx
        self.sim_pose.position.y = initPosy
        self.sim_pose.position.z = 0.0
        self.sim_pose.orientation.x = 0.0
        self.sim_pose.orientation.y = 0.0
        self.sim_pose.orientation.z = 0.0

        # initialize twist info
        self.sim_twist = Twist()

        # Initialize odometry info
        self.sim_odom = Odometry()
        self.sim_odom.header = self.odom_header
        self.sim_odom.child_frame_id = "base_link"
        self.sim_odom.pose.pose = self.sim_pose
        self.sim_odom.twist.twist = self.sim_twist

        self.cmdvel_linear_x = 0.0
        self.cmdvel_linear_y = 0.0
        self.cmdvel_angular_z = 0.0

    #############################################
    # Update odometry form User request cmd_vel #
    #############################################
    def update_odom(self):
        #Update Vehicle Pose

        sampletime = 0.01    #calculate by 10msec
        e = euler_from_quaternion(self.sim_pose.orientation.x, self.sim_pose.orientation.y, self.sim_pose.orientation.z, self.sim_pose.orientation.w)
        yaw_euler = e[2]

        #update pose from user request
        self.sim_pose.position.x = self.sim_pose.position.x + self.cmdvel_linear_x*sampletime*math.cos(yaw_euler)
        self.sim_pose.position.y = self.sim_pose.position.y + self.cmdvel_linear_x*sampletime*math.sin(yaw_euler)
        updated_yaw = e[2] + self.cmdvel_angular_z*sampletime

        updated_quaternion = quaternion_from_euler(0, 0, updated_yaw)
        self.sim_pose.orientation.x = updated_quaternion[0]
        self.sim_pose.orientation.y = updated_quaternion[1]
        self.sim_pose.orientation.z = updated_quaternion[2]
        self.sim_pose.orientation.w = updated_quaternion[3]

        #update timestamp
        #self.odom_header.stamp = rospy.Time.now()
        self.sim_odom.header = self.odom_header
        self.sim_odom.pose.pose = self.sim_pose
        self.sim_odom.twist.twist = self.sim_twist

        #update TF
        map_frame = TransformStamped()
        map_frame.header.frame_id = 'map'
        map_frame.child_frame_id = 'odom'
        map_frame.transform.translation.x = self.sim_pose.position.x
        map_frame.transform.translation.y = self.sim_pose.position.y
        map_frame.transform.translation.z = 0.0
        map_frame.transform.rotation.w = self.sim_pose.orientation.w
        map_frame.transform.rotation.x = self.sim_pose.orientation.x
        map_frame.transform.rotation.y = self.sim_pose.orientation.y
        map_frame.transform.rotation.z = self.sim_pose.orientation.z

        return self.sim_odom, map_frame

    #####################
    # cmdvel subscriber #
    #####################
    def set_cmdvel_from_subscriber(self, msg):
        self.cmdvel_linear_x =msg.linear.x
        self.cmdvel_angular_z =msg.angular.z

tf_lib.py (座標変換処理)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import math

def euler_from_quaternion(x,y,z,w):
    """
    Converts quaternion (w in last place) to euler roll, pitch, yaw
    quaternion = [x, y, z, w]
    Bellow should be replaced when porting for ROS 2 Python tf_conversions is done.
    """

    sinr_cosp = 2 * (w * x + y * z)
    cosr_cosp = 1 - 2 * (x * x + y * y)
    roll = np.arctan2(sinr_cosp, cosr_cosp)

    sinp = 2 * (w * y - z * x)
    pitch = np.arcsin(sinp)

    siny_cosp = 2 * (w * z + x * y)
    cosy_cosp = 1 - 2 * (y * y + z * z)
    yaw = np.arctan2(siny_cosp, cosy_cosp)

    return roll, pitch, yaw


def quaternion_from_euler(roll, pitch, yaw):
    """
    Converts euler roll, pitch, yaw to quaternion (w in last place)
    quat = [x, y, z, w]
    Bellow should be replaced when porting for ROS 2 Python tf_conversions is done.
    """
    cy = math.cos(yaw * 0.5)
    sy = math.sin(yaw * 0.5)
    cp = math.cos(pitch * 0.5)
    sp = math.sin(pitch * 0.5)
    cr = math.cos(roll * 0.5)
    sr = math.sin(roll * 0.5)

    q = [0] * 4
    q[0] = cy * cp * cr + sy * sp * sr
    q[1] = cy * cp * sr - sy * sp * cr
    q[2] = sy * cp * sr + cy * sp * cr
    q[3] = sy * cp * cr - cy * sp * sr

    return q

上記処理を実行し、cmd_velを出力するとodomの出力・TFの発行がされていることをrviz2などで確認できる。 cmd_velは下記コマンド等で発行可能

ros2 topic pub /cmd_vel geometry_msgs/Twist '{linear: {x: 0.1}, angular: {z: 0.3}}'

ソースコードは下記にまとめている。

github.com

ROS2導入・初期設定

この記事の目的

 ROS2の導入と初期設定・注意点等についてまとめる

1. ROS2の導入

Ubuntu20.04を利用している場合、下記サイトに従ってROS2 forxyを導入する。

docs.ros.org

上記手順では、localeをen_US.UTF-8 に切り替えているがこのコマンドを入力せず日本語のままでもインストールできる。

setup.bashの読み込みはROS1と同様に.bashrcに下記記載を加えておくと毎回入力しなくてもよい。

source /opt/ros/foxy/setup.bash

ビルドシステムcolconは下記コマンドでインストールする。(ROS2のインストールとは別に実行する必要があるので注意)

sudo apt install python3-colcon-common-extensions

2. チュートリアルの実施

ROS2の基本的な使い方(パッケージ作成、ビルド、subscribe/publishなど)は下記ページのチュートリアルに記載されている。

docs.ros.org

ROS2ではROSに対して下記のような変更点がある。

  • ビルドシステムの変更(colcon)

ROS2ではパッケージのビルドシステムがcatkinからcolconに変更となっている。 catkinではプロジェクト内の全パッケージがdevelにソフトリンクされていたが、colconでは明示的にインストールするパッケージを記述する必要があるなどの違いがある。また、作成するパッケージがpythonかCかで記述する設定が異なるので注意する。

docs.ros.org

  • roscoreの廃止

ROSではroscoreコマンドでmasterノードを立ち上げ、各ノード間通信を司っていたがROS2では廃止された。 ROS2ではmasterノードを介さず、各ノード同士が通信する仕組みに変更されている。

  • Life Cycle Managerの導入

各ノードの状態(初期化前、無効、有効、終了後)などを管理するための仕組みとしてLife Cycle Managerという仕組みが導入された。

design.ros2.org

後述するnavigation stackではこの仕組に従って状態を操作しなければ適切に動作しない機能があるのに注意する。

  • launchファイルの記述方法変更

ROSではxmlファイルで記述していたlaunchファイルがpythonスクリプトで記述するように変更された。 記述方法は下記のチュートリアルに記載されている。

docs.ros.org

3. navigation stackの導入

移動ロボット用の ROS2 packageとしてnavigation 2を導入する。

導入方法・動作確認は下記サイトの手順に従って行う。 (virtual box のubuntuでの動作などではgazeboo起動までに非常に時間がかかるので注意する)

navigation.ros.org

git設定・コマンドまとめ

この記事の目的

 gitの設定・コマンドについてまとめる。(個人用メモ。逐次追記予定)

1. gitのインストールと初期設定

ubuntuでは下記コマンドでインストールする。

sudo apt install git

インストール後にuser名・メールアドレスを設定する。 (ユーザー名・メールアドレス未設定だとエラーが発生するので注意)

git config --global user.email "UserName"
git config --global user.email "EMail@example.com"

Proxyの設定 (社内ネットワーク等の場合のみ)

git config --global http.proxy http://[proxy]:[port]
git config --global https.proxy http://[proxy]:[port]

github連携設定 (パスワード入力を省力化できる)

qiita.com

git ignoreの指定

gitinitしたフォルダのうち、管理対象としたくないものを指定する。(拡張子・フォルダ名等の指定が可能) なお、gitでは空フォルダの管理ができないので、空フォルダを管理したい場合はダミーファイルを作成する必要がある。

qiita.com

2. gitコマンド

2.1 リポジトリの作成・クローン

ローカルで作成したフォルダからgitリポジトリを作成する場合 レポジトリを作成したいフォルダを作成、移動してgit initコマンドを実行することでgit repositoryが作成される。(.gitフォルダが生成される)

mkdir testFolder
cd testFolder
git init 

git init コマンドは下記オプションを利用できる。

option 説明
bare リポジトリをBareリポジトリとして作成する
shared 同じグループに所属するユーザーからのpushを許容する

BareリポジトリはWorking Folderを持たないので容量削減が可能 (開発者が操作しないファイルサーバー等に適した設定) 既に存在するレポジトリからローカルレポジトリを作成する場合git clone コマンドを利用する。

#github のレポジトリのclone (URLはブラウザで各レポジトリにアクセスし、"Code"ボタンから確認可)
git clone git@github.com:sd08419ttic/ros_tech_memo.git

#ローカルネットワーク上の他PCのレポジトリのclone
git clone "userName"@"192.168.XXX.XXX":"~/gitFolder"

git clone コマンドは下記オプションを利用できる。

option 説明
bare リポジトリをBareリポジトリとして作成する
shared 同じグループに所属するユーザーからのpushを許容する
branch cloneするブランチ名を指定する
depth 取得する履歴の個数を指定する

更新回数が多い巨大プロジェクトの場合、-- depth 1 オプションを指定することでファイルサイズの消費量を少なくすることができる。

2.2 ブランチの操作

下記でブランチの確認・追加・削除・移動を行う

#ブランチの確認
git branch

#ブランチの新規作成
git branch new branchName

#ブランチの削除
git branch -d branchName

#別ブランチへの移動
git checkout branchName

2.3 コミット操作

下記でローカルのレポジトリに加えた変更内容のコミットを行う。

# git add する前の変更点確認
git diff

#変更内容のadd #-aオプションですべてのファイル、 ファイル名の指定個別指定も可能 
git add -a 
git add test.txt

#addの取り消し
git reset HEAD

#git addしたファイルの変更点確認
git diff --cached

#コミット・メッセージの登録
git commit -m "initial commit"

#直前のコミットの削除
git reset --hard HEAD^

2.4 プル操作

下記でリモートブランチでの変更内容をローカルリポジトリにマージする。

#ローカルブランチ名origin、リモートブランチ名masterrの場合
git pull origin master

2.4 プッシュ操作

下記でコミットした内容をリモートブランチにpushする。

#コミットされている内容の確認(diff) (ブランチmasterを変更している場合)
git fetch origin
git diff HEAD..origin/master

#コミット内容のpush
git push origin master

参考資料

Visual Studio Codeの便利な設定・拡張機能

この記事の目的

 Visual Studio Codeの便利な設定・拡張機能についてまとめる

1. 共通設定

下記拡張機能をインストール (インストール後VScodeの再起動が必要)

marketplace.visualstudio.com

  • スペースの可視化

ファイル⇒基本設定⇒設定画面に移動し、"Editor: Render Whitespace"の設定をALLに変更する。 f:id:sd08419ttic:20200409004442p:plain

  • エディタの縮小・拡大 (Ctrl + マウスホイールのズーム)

ファイル⇒基本設定⇒設定画面に移動し、"Ctrl キーを押しながらマウス ホイールを使用してエディターのフォントをズームします。"を有効化 リセットはコマンドパレットの『エディターのフォントのズームをリセット』を選択する。

f:id:sd08419ttic:20200409004906p:plain

  • 対応する括弧の強調表示

コード上の対応する括弧を可視化するための拡張機能

marketplace.visualstudio.com

  • インデントレベルの強調表示

同じインデントレベルごとに色分けして表示するための拡張機能

marketplace.visualstudio.com

  • アイコンの変更

ファイル拡張子がわかりやすいアイコンに変更する拡張機能。インストール後にファイル⇒基本設定⇒設定画面に移動し、Workbench: Icon Themeからvscode-iconsを選択する。

marketplace.visualstudio.com

  • フォルダ・ファイルパスの補完

フォルダ・ファイルパスを自動補間するための拡張機能。下記をインストール後、エディタで"/"と入力してCtrl + Spaceでフォルダ階層を補間することができる。

marketplace.visualstudio.com

1. ソフト開発用の設定/拡張機能

プログラム言語ごとに役立つ拡張機能をまとめる

C言語用設定・拡張機能

C言語開発の拡張機能(InteliSense/デバッグ機能など)

marketplace.visualstudio.com

プロジェクト設定方法は下記などを参考にする。Windows+個人開発であればVS CodeでなくVisual Studio 2019を使った方が面倒な設定をせずにより充実した機能を利用できる。

qiita.com

C言語でDoxgenのフォーマットに従ったコメントをプレビュー表示することができる拡張機能。Doxgenフォーマットに従ったコメントを記述したC/Hファイルを開いて"Doxgen Previewer"を表示。

marketplace.visualstudio.com

Python用設定・拡張機能

Python開発の拡張機能(InteliSense/デバッグ機能など)

marketplace.visualstudio.com

  • Anaconda

AnacondaをVS Codeで読み込むための機能。コマンドパレットでPython Interpreter からAnaconda仮想環境の切り替えが可能になる。

marketplace.visualstudio.com

2.ドキュメント作成用の設定/拡張機能

VS Code標準機能を使う場合、下記サイトに従いコマンドパレット⇒『Markdownプレビューを横に表示』でプレビューを確認しながらMarkdownを編集できる。

www.atmarkit.co.jp

上記ではなく拡張機能Markdown Enhanced』をインストールすると『KaTex』を用いた数式の追加も行うことができ利便性が上がる。 表示するにはインストール後にコマンドパレット⇒『Markdown Preview Enhanced: Open Preview to the Side』を選択する。

marketplace.visualstudio.com

設定後のイメージ (プレビュー画面でKaTexの数式プレビューが表示可能)

f:id:sd08419ttic:20200409233429p:plain

Katexに関する記述は下記サイトが参考になる。

https://b1tblog.com/2019/10/08/vscode-math/

記述したmarkdownをインストールするためには下記の拡張機能を利用できる。 インストール後にコマンドパレット⇒『Markdown PDF Export:XXX』を選択する。(PDF/HTML/JPEGなどで出力できる。KaTEXも反映される)

marketplace.visualstudio.com

markdownコピーアンドペーストした画像を張り付ける際には下記拡張機能が便利である。 画像をCtrl+Cでコピーした状態でCtrl+Alt+Vで画像を指定したフォルダにコピーし、Markdownファイルのリンク生成ができる。

marketplace.visualstudio.com

画像を保存するフォルダは下記設定から指定できる。下記設定をしておくと、markdownファイルと同じ階層にあるimageフォルダに画像が追加されていく設定になる。 f:id:sd08419ttic:20200409235148p:plain

  • Review環境

VS CodeでReviewファイルを編集するための拡張機能として下記を利用できる。

marketplace.visualstudio.com

Review自体のインストールが必要となるので下記サイトを参考に導入するとよい。

my-web-site.iobb.net

  • 日本語校正

MarkdownおよびReviewの日本語校正拡張機能として下記を利用できる。冗長な言い回しの添削などに活用できる。

marketplace.visualstudio.com

3.設定・拡張機能の別PCへの移行方法

githubを利用できる環境では下記手順で移動できる (未確認)

https://nelog.jp/vscode-sync

利用するのに問題がある場合、下記のように設定ファイルをコピーすることができる。 (バージョンによって異なる可能性があるので要バックアップ)

qiita.com

画像認識② YoloV3を用いたWebカメラ画像の物体検出

この記事の目的

 YoloV3を用いてWebカメラ画像から物体検出をしROS Topicで判定結果を出力する方法をまとめる

1. 移動ロボット用の物体検出アルゴリズム

 移動ロボットの自律走行では、自車周辺の障害物を認識し車両制御(減速や停止、走行経路の計算など)で利用する必要がある。 その際、カメラ画像に障害物(人や車など)が含まれるかを認識する方法がよく用いられる。物体認識のアルゴリズムは日進月歩で進化しているが、本記事では高精度で計算速度に秀でたYoloV3を用いてWebカメラから物体を検出し、結果をROSトピックとして出力する方法について説明する。(物体認識アルゴリズムの歴史については下記記事が詳しい。)

qiita.com

物体認識の性能としては更に新しいM2Detという手法の方が高性能であるようだが、Pytorch (Python3.Xが必要)なのでROS対応が煩雑であるため今回は見送る。

qiita.com

なお、YoloV3にはROS用のパッケージも提供されているが、今回はWebカメラで読み込んだ画像をOpenCVで取り込んでYoloV3にかけるPythonスクリプトとして下記は利用せずに実装する。 (Webカメラ画像に前処理をかけたり、結果を加工する際に便利なため)

github.com

2. YoloV3のインストール/Python2+OpenCV用のインターフェース設定

YoloV3のインストールは公式サイトの手順で実行できる。

pjreddie.com

ソースのダウンロードと合わせて学習済みモデルを下記コマンドでダウンロードしておく。

#yolov3
wget https://pjreddie.com/media/files/yolov3.weights

#tiny-yolov3
wget https://pjreddie.com/media/files/yolov3-tiny.weights

MAKE時にGPUを有効(NVidiaGPUを搭載したPCのみ)およびOpenCVの読み込みを行いたいのでMakefileの下記オプションを有効にする。

GPU=1
CUDNN=1
OPENCV=1

また、darknetのデフォルトでは画像形式はopencvの画像形式(numpyのndarray)を渡して検出結果を取得する手段が提供されていない。 ndarrayを渡して検出結果がpythonで取得できるように関数の一部を修正する。(下記サイトのQAを参考とした)

github.com

darknet/src/image.cの最後尾に関数を追加

image ndarray_to_image(unsigned char* src, long* shape, long* strides)
{
    int h = shape[0];
    int w = shape[1];
    int c = shape[2];
    int step_h = strides[0];
    int step_w = strides[1];
    int step_c = strides[2];
    image im = make_image(w, h, c);
    int i, j, k;
    int index1, index2 = 0;

    for(i = 0; i < h; ++i){
            for(k= 0; k < c; ++k){
                for(j = 0; j < w; ++j){

                    index1 = k*w*h + i*w + j;
                    index2 = step_h*i + step_w*j + step_c*k;
                    //fprintf(stderr, "w=%d h=%d c=%d step_w=%d step_h=%d step_c=%d \n", w, h, c, step_w, step_h, step_c);
                    //fprintf(stderr, "im.data[%d]=%u data[%d]=%f \n", index1, src[index2], index2, src[index2]/255.);
                    im.data[index1] = src[index2]/255.;
                }
            }
        }

    rgbgr_image(im);

    return im;
}

darknet/src/image.hに関数宣言を追加

#ifdef OPENCV
void *open_video_stream(const char *f, int c, int w, int h, int fps);
image get_image_from_stream(void *p);
image load_image_cv(char *filename, int channels);
image ndarray_to_image(unsigned char* src, long* shape, long* strides);
int show_image_cv(image im, const char* name, int ms);
#endif

darkne.pyに追加

from ctypes import *
import math
import random
import sys #追加
import cv2 #追加

#中略

predict_image = lib.network_predict_image
predict_image.argtypes = [c_void_p, IMAGE]
predict_image.restype = POINTER(c_float)

##以下を追加 (ndarryを読み込む処理)

ndarray_image = lib.ndarray_to_image
ndarray_image.argtypes = [POINTER(c_ubyte), POINTER(c_long), POINTER(c_long)]
ndarray_image.restype = IMAGE

##ここまで

#関数 detect_npをopencv用に追加
def detect_np(net, meta, np_img, thresh=.5, hier_thresh=.5, nms=.45):
    im = nparray_to_image(np_img)
    num = c_int(0)
    pnum = pointer(num)
    predict_image(net, im)
    dets = get_network_boxes(net, im.w, im.h, thresh, hier_thresh, None, 0, pnum)
    num = pnum[0]
    if (nms): do_nms_obj(dets, num, meta.classes, nms);
    res = []
    for j in range(num):
        for i in range(meta.classes):
            if dets[j].prob[i] > 0:
                b = dets[j].bbox
                res.append((meta.names[i], dets[j].prob[i], (b.x, b.y, b.w, b.h)))
    res = sorted(res, key=lambda x: -x[1])
    free_image(im)
    free_detections(dets, num)

    return res
#結果の可視化用関数
def check_detected_result_image(np_img,yolo_res):
    result_img = np_img
    for j in range(len(yolo_res)):
        left_top_x = int(yolo_res[j][2][0] - yolo_res[j][2][2] / 2)
        left_top_y = int(yolo_res[j][2][1] - yolo_res[j][2][3] / 2)
        right_top_x = int(yolo_res[j][2][0] + yolo_res[j][2][2] / 2)
        right_top_y = int(yolo_res[j][2][1] + yolo_res[j][2][3] / 2)
        result_img = cv2.rectangle(result_img, (left_top_x, left_top_y), (right_top_x, right_top_y),(255, 255, 255), 3)
        if len(yolo_res) > 0:
            font = cv2.FONT_HERSHEY_PLAIN
            cv2.putText(result_img, yolo_res[j][0],(max(int(yolo_res[j][2][0]) - 20, 0), int(yolo_res[j][2][1])), font, 3,(255, 255, 0), 1, cv2.LINE_AA)
            cv2.putText(result_img, str("{:.2}".format(yolo_res[j][1])),(max(int(yolo_res[j][2][0]) - 20, 0), int(yolo_res[j][2][1] + 40)), font, 3,(255, 0, 255), 1, cv2.LINE_AA)
    return result_img

上記スクリプトによりdetect_npを呼び出してopencvで読み込んだ画像(ndarray形式)に対する物体検出を行いcheck_detected_result_imageで検出結果の位置とラベルを可視化することができる。

3. Webカメラ画像の物体検出/ROS Topicの出力

次に、上記を利用してWebカメラ画像からYoloV3で物体検出を行い障害物の有無をTrue/FalseでROSトピックとして出力する方法を検討する。 誤検出を減らすために、障害物が画面の比率に対してラベルを"person"のみとし、画面比率で10%の大きさで連続3フレーム以上検出で障害物と判定し連続5フレーム以上検出で障害物フラグを落とすようにする。 ライブラリ・モデルのパスはubuntuのフォルダ構成に合わせて修正する。darknetをインストールしたパスをbashrcのLD_LIBRARY_PATHに追加しておくと良い。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import rospy
from std_msgs.msg import Bool
import numpy as np
from darknet import detect_np,load_net,load_meta,check_detected_result_image

###########################
# Webcam_Object_detection #
###########################
class Webcam_Object_detection():

    ##################
    # Initialization #
    ##################
    def __init__(self):

        rospy.init_node('Webcam_Object_detection', anonymous=True)
        r = rospy.Rate(50)  # 50hz

        #initialize webcam
        self.cap = cv2.VideoCapture(0)

        #load yolo_v3 parameters
        self.net = load_net("/home/xxxx/darknet/cfg/yolov3-tiny.cfg", "/home/xxxx/darknet/cfg/yolov3-tiny.weights", 0)
        self.meta = load_meta("/home/xxxx/darknet/cfg/coco.data")

        self.obstacle_label = ["person"]
        self.obstacle_ratio = 0.1
        self.obstacle_frame_th = 3
        self.obstacle_cancel_frame_th = 5
        ret, frame = self.cap.read()
        self.imsize = frame.shape[0]*frame.shape[1]
        self.obstacle_cnt = 0
        self.obstacle_flg = False

        #initialize publisher
        self.object_detect_publisher = rospy.Publisher("/object_detection", Bool, queue_size=50)

    def detect_object(self,event):
        ret, frame = self.cap.read()

        k = cv2.waitKey(1)
        r = detect_np(self.net, self.meta, frame)
        result_im = check_detected_result_image(frame, r)
        cv2.imshow('Image', result_im )
        cv2.waitKey(1)

        #check obstacle
        flg = False
        for indx in range(len(r)):  #Check detected objects
            for indx2 in range(len(self.obstacle_label)):   #check label name
                #Check Object Label
                if r[indx][0] == self.obstacle_label[indx2]:
                    ratio = r[indx][2][2]*r[indx][2][3]/self.imsize
                    if ratio > self.obstacle_ratio: #check object size (ratio per frame size)
                        flg = True
                        break
            if flg == True:
                break
        if flg == True:
            self.obstacle_cnt = min(self.obstacle_cnt + 1,self.obstacle_cancel_frame_th)
        else:
            self.obstacle_cnt = max(self.obstacle_cnt - 1, 0)

        if self.obstacle_cnt > self.obstacle_frame_th:
            self.obstacle_flg = True
        elif self.obstacle_cnt <= 0:
            self.obstacle_flg = False

        self.object_detect_publisher.publish(self.obstacle_flg)

if __name__ == "__main__":

    test = Webcam_Object_detection()
    rospy.Timer(rospy.Duration(0.1), test.detect_object)
    rospy.spin()

動作結果は下記のようになる。 f:id:sd08419ttic:20200326225659p:plain

github.com

ROS 移動ロボットの経路追従制御(Pure Pursuit)

この記事の目的

 ROSで移動ロボットの経路追従制御を実装する

1. move_baseにおけるロボットの移動経路追従

前回の投稿でROSシミュレーション上に走行経路を設定したので、今回は経路に追従するように車両の駆動・操舵量を計算するアルゴリズムについて検討する。

wiki.ros.org

ROSのmove_baseではLocal_plannerを用いて算出した経路に対してロボットが経路を追従するための車速と角速度をcmd_velというTopicで発行する。

wiki.ros.org

cmd_velはあくまで車体全体の速度・角速度であり、実際のロボットのアクチュエータの制御量(アクセル/ステアリング操作量)は別途ロボットの適正に応じて計算する必要がある。

今回は最も単純な経路追従アルゴリズムの1つであるPure Pursuitを実装する。

2. Pure Pursuitによる移動ロボットの経路追従

Pure Puruitは移動ロボット用の経路追従制御であり、自動運転のステアリング制御でも用いられる。

tech.tier4.jp

PurePursuitはロボットが経路を追従する際に、前方に目標とする点前方注視点(Lookahed Point)を設定して自車の進行方向がLookahed距離に向かうように車両の操舵量を決める制御である。 前方注視点までの距離をどのように決めるかによってステアリング時の安定性に大きく影響する。

jp.mathworks.com

3. Pure Pursuitの実装

今回は車両角速度までをPurePusuitによって決めるスクリプトを実装する。 経路追従時の車速は固定として、ユーザーがパラメータで与えるものとしている。

処理の大まかな流れとしては、

  • 1.自車位置と経路の各点の距離を計算して自車と最も近い点を選択する

  • 2.自車の近い点からLookAhedDistance遠い点を目標点として設定する (LookAhedDistanceは任意パラメータ)

  • 3.目標点と自車位置の相対的な角度をarctanを用いて求める

  • 4.自車の向きを考慮して操舵の方向を決め、車速と角度変化量を用いて角速度を算出する。

上記を車両が経路終端に到達するまで繰り返す。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pandas as pd
import numpy as np
import math
# import for ros function
import rospy
import tf
from geometry_msgs.msg import PoseStamped, Twist
from std_msgs.msg import Header
from visualization_msgs.msg import Marker
from nav_msgs.msg import Path, Odometry

#######################################
# Simple Path follower (Pure Pursuit) #
#######################################
class Simple_path_follower():

    ##################
    # Initialization #
    ##################
    def __init__(self):

        rospy.init_node('Simple_Path_Follower', anonymous=True)
        self.r = rospy.Rate(50)  # 50hz

        self.target_speed = 1.0             #target speed [km/h]
        self.target_LookahedDist = 0.5      #Lookahed distance for Pure Pursuit[m]

        #first flg (for subscribe global path topic)
        self.path_first_flg = False
        self.odom_first_flg = False
        self.position_search_flg = False
        self.last_indx = 0

        #initialize publisher
        self.cmdvel_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=50)
        self.lookahed_pub = rospy.Publisher("/lookahed_marker", Marker, queue_size=50)

        #initialize subscriber
        self.path_sub = rospy.Subscriber("/path", Path, self.cb_get_path_topic_subscriber)
        self.odom_sub = rospy.Subscriber("/odom", Odometry, self.cb_get_odometry_subscriber)



    def publish_lookahed_marker(self,x,y,yaw_euler):

        marker_data = Marker()
        marker_data.header.frame_id = "map"
        marker_data.header.stamp = rospy.Time.now()

        marker_data.ns = "my_name_space"
        marker_data.id = 0

        marker_data.action = Marker.ADD

        marker_data.pose.position.x = x
        marker_data.pose.position.y = y
        marker_data.pose.position.z = 0.0

        temp_quaternion = tf.transformations.quaternion_from_euler(0,0,yaw_euler)

        marker_data.pose.orientation.x = temp_quaternion[0]
        marker_data.pose.orientation.y = temp_quaternion[1]
        marker_data.pose.orientation.z = temp_quaternion[2]
        marker_data.pose.orientation.w = temp_quaternion[3]

        marker_data.color.r = 1.0
        marker_data.color.g = 0.0
        marker_data.color.b = 0.0
        marker_data.color.a = 1.0

        marker_data.scale.x = 0.1
        marker_data.scale.y = 0.1
        marker_data.scale.z = 0.1

        marker_data.lifetime = rospy.Duration()
        marker_data.type = 0

        self.lookahed_pub.publish(marker_data)


    ###################
    # Update cmd_vel  #
    ###################
    def update_cmd_vel(self):
        if self.path_first_flg == True and self.odom_first_flg == True:

            dist_from_current_pos_np = np.sqrt(np.power((self.path_x_np-self.current_x),2) + np.power((self.path_y_np-self.current_y),2))
            min_indx = dist_from_current_pos_np.argmin()
            nearest_x = self.path_x_np[min_indx]
            nearest_y = self.path_y_np[min_indx]
            # Get nearest Path point at first time
            if self.position_search_flg == False:
                self.pass_flg_np[0:min_indx] = 1    #Set pass flg
                self.position_search_flg = True
            else:
                # Check pass flg from vehicle position
                for indx in range (self.last_indx,self.path_x_np.shape[0]):
                    if dist_from_current_pos_np[indx] < 1.0:
                        self.pass_flg_np[indx] = 1
                    else:
                        break
            self.last_indx = min_indx

            #check goal
            if self.pass_flg_np[self.path_x_np.shape[0]-1] == 1:
                cmd_vel = Twist()
                self.cmdvel_pub.publish(cmd_vel)
                print("goal!!")
                return
            #calculate target point
            dist_sp_from_nearest = 0.0
            target_lookahed_x = nearest_x
            target_lookahed_y = nearest_y
            for indx in range (self.last_indx,self.path_x_np.shape[0]):
                dist_sp_from_nearest = self.path_st_np[indx] - self.path_st_np[self.last_indx]
                if (dist_sp_from_nearest) > self.target_LookahedDist:
                    target_lookahed_x = self.path_x_np[indx]
                    target_lookahed_y = self.path_y_np[indx]
                    break

            #calculate target yaw rate
            target_yaw = math.atan2(target_lookahed_y-self.current_y,target_lookahed_x-self.current_x)

            #check vehicle orientation
            # if target_yaw - self.target_yaw_last < -math.pi:
            #     target_yaw = 2*math.pi + target_yaw
            # elif target_yaw - self.target_yaw_last > math.pi:
            #     target_yaw = 2*math.pi - target_yaw

            yaw_diff = target_yaw - self.current_yaw_euler

            if yaw_diff > math.pi:
                yaw_diff = yaw_diff % math.pi
            elif yaw_diff < -math.pi:
                yaw_diff = yaw_diff%(-math.pi)


            sample_sec = dist_sp_from_nearest/(self.target_speed/3.6)
            if sample_sec != 0.0:
                yaw_rate = math.fabs(yaw_diff)/sample_sec
            else:
                yaw_rate = 0.0

            # check vehicle orientation and target yaw
            if math.fabs(target_yaw - self.current_yaw_euler) < math.pi:
                if (target_yaw) < (self.current_yaw_euler):
                    yaw_rate = yaw_rate * (-1.0)
            elif math.fabs(target_yaw - self.current_yaw_euler) > math.pi:
                if (target_yaw) > (self.current_yaw_euler):
                    yaw_rate = yaw_rate * (-1.0)

            print(yaw_diff*180/math.pi,target_yaw*180/math.pi,self.current_yaw_euler*180/math.pi)

            #Set Cmdvel
            cmd_vel = Twist()
            cmd_vel.linear.x = self.target_speed/3.6    #[m/s]
            cmd_vel.linear.y = 0.0
            cmd_vel.linear.z = 0.0
            cmd_vel.angular.x = 0.0
            cmd_vel.angular.y = 0.0
            cmd_vel.angular.z = yaw_rate
            self.cmdvel_pub.publish(cmd_vel)

            #publish maker
            self.publish_lookahed_marker(target_lookahed_x,target_lookahed_y,target_yaw)
            #print("cmd_vel_update")
            self.r.sleep()
            return


    ####################################
    # Callback for receiving Odometry  #
    ####################################
    def cb_get_odometry_subscriber(self,msg):
        self.current_x = msg.pose.pose.position.x
        self.current_y = msg.pose.pose.position.y
        e = tf.transformations.euler_from_quaternion((msg.pose.pose.orientation.x, msg.pose.pose.orientation.y, msg.pose.pose.orientation.z, msg.pose.pose.orientation.w))
        yaw_euler = e[2]
        self.current_yaw_euler = yaw_euler
        self.odom_first_flg = True
        #print("odom_sub")

    ######################################
    # Callback for receiving path topic  #
    ######################################
    def cb_get_path_topic_subscriber(self,msg):
        if self.path_first_flg != True:
            self.path_x_np = np.zeros([len(msg.poses)])
            self.path_y_np = np.zeros([len(msg.poses)])
            self.path_st_np = np.zeros([len(msg.poses)])
            self.pass_flg_np = np.zeros([len(msg.poses)])
            last_x = 0.0
            last_y = 0.0
            last_st = 0.0
            for indx in range(len(msg.poses)):
                self.path_x_np[indx] = msg.poses[indx].pose.position.x
                self.path_y_np[indx] = msg.poses[indx].pose.position.y
                self.path_st_np[indx] = last_st + math.sqrt((self.path_x_np[indx]-last_x)**2 + (self.path_y_np[indx]-last_y)**2)
                last_x = self.path_x_np[indx]
                last_y = self.path_y_np[indx]
                last_st = self.path_st_np[indx]
            self.path_first_flg = True
            #print("path(first)")

if __name__ == '__main__':
    print('Path following is Started...')
    test = Simple_path_follower()
    try:
        while not rospy.is_shutdown():
            test.update_cmd_vel()
    except KeyboardInterrupt:
        print("finished!")

上記スクリプトでは、目標点を可視化するためmakerを用いて前方注視点を可視化している また、以前に作成した車両シミュレーション処理をcmd_velを受信して車速・角速度を変更できるように下記のように書き換える。


#中略

    #####################
    # cmdvel subscriber #
    #####################
    def cb_get_cmdvel_subscriber(self, msg):
        self.cmdvel_linear_x =msg.linear.x
        self.cmdvel_linear_y =msg.linear.x
        self.cmdvel_angular_z =msg.angular.z
        pass

下記のように経路追従を行うことができる。

f:id:sd08419ttic:20200322151817p:plain

github.com