2023年の振り返り

1月

OAK-D ProとFEMTOのポイントクラウドの比較動画を作成

詳細3次元点群処理を読み終える

OpenParallelGripperのプロモーション用動画を作成

ML-Agentsで深層強化学習にトライ。Google論文にあるタスク複数について成功

自作グリッパーをUR3に対応させる実験

2月

Unity ML-Agents実践ゲームプログラミングを読み終える

やや出遅れたがChatGPTを試し、その性能の高さに感動する

ML-Agentsで黄・青球入り容器から青を取り出すタスクを深層強化学習で行う

3月

WhisperとGPT-3.5のAPIとVOICEVOXを組み合わせて音声会話できるエージェントを実装して遊ぶ

4年ほど使用していたCLOVA Friendsがサービス終了で使用できなくなった

4月

DOBOT MG400を入手

5月

OpenParallelGripperを改良し、Dynamixel XL-330を採用と共に薄型・軽量化を行う

Kickstarterで低価格7軸ロボットのLucid ONEをバック

7月

OpenParallelGripperの内部で使用するボードをOpenRB-150に変更し内部がシンプルになった

8月

PyBulletに入門し、UFACTORY Lite 6の動作をシミュレーションした

逆運動学解法のSR-Inverseを自分で実装しPyBulletでシミュレーションに使用し理解を深める

PyBullet上でKUKA LBR iiwaの動作シミュレーションを試す

PyBullet上のシミュレーション動作とUFACTORY Lite 6実機を同時に動かす

9月

雲行きが怪しいと噂されたFranka Emikaのシミュレーションをなんとなく試す

RealSense T265でUFACTORY Lite 6のTCPをシミュレーション上及び実機で操作する

RealSenseのRGB画像とロボットの姿勢から模倣学習でスプーンで緑の豆のみを拾うタスクを学習させる

自前実装のPythonの逆運動学コードが遅いのでChatGPTを使ってC++に移植し、爆速になった

10月

Meta Quest 3を購入

11月

OpenParallelGripperのXL-330版を公開

Bambu Lab X1-Carbon Comboを購入

前から気になっていたOSSの4指ハンドLEAP Handを作成

ソフトグリッパーをOpenParallelGripperに取り付けて実験

第42回ロボティクス勉強会で「RealSense T265とD415とUFACTORY Lite 6で模倣学習の実験」というタイトルで発表

SUNLUのフィラメントドライヤーFila Dryer S2を購入

https://x.com/EL2031watson/status/1729841895973470270?s=20

Dynamixelをリーダー、Lite 6をフォロワーとするリーダーフォロワーシステㇺを自作

12月

Jetson Orin Nanoを購入

自作リーダーフォロワーシステムのグリッパーも連動させて、色々なタスクを試す

PLA-CFを使用してグリッパー爪を印刷

リーダーフォロワーシステムを双腕化

東明テックの食品乾燥機プチマレンギminiが80℃まで出せてフィラメント2つ入れられることを発見しフィラメントドライヤーとして購入

PA6-CFの印刷にチャレンジ

リーダーフォロワーシステムのプログラムを改良及び机を耐荷重200kgのものに差し替えて操作が滑らかになる

総括

  • 相変わらずガジェットにつぎ込んだ。Bambu Lab X1-Carbon Comboは印刷スピード、印刷品質共に大幅に向上し満足。
  • ML-Agentsで深層強化学習に入門、自作タスクも学習させることができたが報酬設計の難しさを感じた。
  • 多関節ロボットの逆運動学の自前実装を行い、理解が深まった。
  • UFACTORY Lite 6をRGB画像を使って動作生成する模倣学習にチャレンジし、ロボット実機を機械学習を使って動かすことができた。Transformerへの理解も深まった。(実装方針に関してはALOHAの論文がかなり参考になった)
  • Gelloというリーダーフォロワーシステムの論文を見て面白いアイデアだと思い、UFACTORY Lite 6版を自作するに至った。T265を使ったリーダーフォロワーシステムと比べて特異姿勢に気を使う必要が減り、グリッパーも操作できるようにして利便性が大幅に向上した。
  • 突如出てきたGPT-3.5, GPT-4の驚異的な性能に日々世話になった。来年どうなるのか全く想像ができない。OpenAIのAPIを使ったロボット動作生成もそのうち試してみたい。

2022年の振り返り

1月

セールになっていたTipronを購入。

長年使っている3DプリンタCR-10Sのマザーボードを取り換え、静音化を実現。

2月

3月

Kickstarterでバックしていた7軸アームロボAmber B1が到着。

RealSense D405を入手。

4月

ロードセルで遊んだ。

5月

ロードセルをxArm先端に取り付け、荷重計測をテストした。

6月

MakerFaireの出展期限を勘違いし、応募に失敗。

7月

xArmの先端に取り付けたロードセルを使って背中マッサージロボットの試作を行った。

LIPSedge DLを入手

羽田空港近くのai_scapeに行ってきた。

CCDIKをUnityで試してみる。

8月

CNC加工を始めて海外業者に発注。以後頻繁に使うようになる。

深度カメラ9種類のポイントクラウドの比較動画を公開。

6軸力覚センサーのFT300を入手。Python用のライブラリを作成してpypiで公開。

pypi.org

github.com

MetaCatを入手

サイボーグハンドを組み立てた

深度カメラ9種のポイントクラウド比較動画(高難易度な物体の点群比較)を公開。

Stable Diffusionをテスト

9月

Orbbecの深度カメラFEMTOを入手

27インチ4Kモニターを入手

OAK-D Proを入手

10月

KickstarterでバックしていたUFACTORY LITE 6を入手。

11月

LITE 6用の平行グリッパー作成(バージョン1)。ストロークは50mmと純正のグリッパーの3倍以上を達成。

DYNAMIXELに入門

REALFORCE を入手

ATOM Liteと拡張モジュールを買い集める

12月

金属3Dプリントに初トライ。

LITE 6用の平行グリッパーをアップデート(バージョン2)

ATOM S3を入手

LITE 6用の平行グリッパーをオープンソースとして公開

github.com

総括

  • 相変わらずガジェットにつぎ込んだ。今年はAmber B1とUFACTORY LITE 6と2つの新しいアームを入手した。

  • Amber B1は待望の7軸アームだったが、エンコーダーが絶対位置を把握できない仕様のため、起動ごとにキャリブレーションが必要になり、かつ正確な位置合わせが難しく使い勝手の良くない状態だった。

  • UFACTORY LITE 6はxArm 6より小型、安価で作りは値段のわりによくできており、SDKもxArmと共通のものが使えてかなり使い勝手がよかった。付属の平行グリッパーのストロークが16mmととても短く、使い勝手が良くなかった。おかげで自分で平行グリッパーを作ろうという意欲がわき、色々な学びがあった。

  • CNC切削の海外サービスを利用し、比較的安価に金属部品を製造できることがわかった。今後も試作に積極的に利用する予定である。

  • YouTubeに初めてそれなりに動画編集をした動画をアップロードして、動画編集の大変さを身をもって感じた。しかし比較的反響を得られたのでやった甲斐はあった。

  • LITE 6用の平行グリッパープロジェクトに今年の最後の時間の多くを投入し、ほぼゼロからロボット用のグリッパーを作成することができた。Modbus-RTUへの理解が深まったり、部品の選定・軽量化・メンテナンス性・組み立てやすさ・取り付けやすさ等様々な設計上の制約を乗り越えてハードウェアを作成する困難さを感じた。

2021年の振り返り

1月

Anycubic Mega Proの運用を開始

第四級アマチュア無線技士試験合格

第一級アマチュア無線技士試験も受けるので、免許の申請は保留。(12月に結局申請)

2月

myCobotのグリッパーを入手

3月

RealSense F455とD415を入手。

第一級陸上特殊無線技士試験に合格し、免許証を入手。

4月

第一級アマチュア無線技士試験不合格。

法規パートが及ばなかった。モールス信号を始めて学び、古いが自分の中では新鮮さを感じた。

KickstarterでBackしていたPetoi Bittleを組み立てた。

5月

UnityとROSでカメラ画像から深層学習でつかむ対象の姿勢を予測してPick and Placeを行うチュートリアルを試す。

KickstarterでbackしていたLooking Glass Portraitを入手

6月

Unitree Go1が気になったが、予算の問題で購入できず。

7月

Maker Faire Tokyo 2021の出展が不可という連絡を受け取る。

来年再挑戦する予定。

xArm用のSuction Cupを入手

ROS 2の本を揃え、勉強をする決意をする。

8月

Kickstarterでbackして入手したxArm 5 をアップデートしてxArm 6にした。

休日会員だったDMM.make AKIBAが8月で終了ということで最後の加工を行った。

9月

OAK-D-LiteをKickstarterでback

ROS 2の入門として「ScamperとRaspberry Piで学ぶROS 2プログラミング入門」を読んだ。

Mini PupperをKickstarterでback

10月

ROS 2(Foxy)でカルタ取りロボットシステムを作る最初のステップとして、RealSense D435の画像、深度からカルタの位置推定・札認識を実装した。

UFACTORY Lite 6をKickstarterでback

11月

百人一首カルタ取りロボの音声認識部分もシステムに接続し、「読み上げ音声を認識→該当の札を取る」を自動でできるようになった。

12月

ZED Miniを入手

クオータニオン学習用のおもちゃを試作したがいまいちだった。

ロボセミで「百人一首カルタ取りロボでROS 2に入門」のタイトルで発表

OAK-D-Liteが届く

総括

  • 相変わらずガジェットにつぎ込んだ。特にKickstarter経由の比率が高かった。
  • 第一級アマチュア無線技士試験は不合格だったが、陸上特殊無線技士試験に合格して技適の例外申請などで役に立ちそうな期待をもった。
  • ROS 2の学習を始め、百人一首カルタ取りロボットシステムを試しに作ってみてROS 2への理解が深まった。

ROS 2 FoxyでRealSense D435の画像をsubscribeする(QoSの設定とPython、C++実装)

RealSenseをROS 2で利用するときに、少しつまずきがあったので、こちらに記録用にまとめておきます。

具体的には以下のrealsense-rosパッケージを使い、realsenseから得られるRGB画像及びDepth画像をImageトピックとしてpublishしたものを、自分でsubscribeするプログラムを 作成しましたが、ROS 2ではQoS(Quality of Service)の設定を適切にしないと画像がうまくsubscribeできないということを知りました。 重要なのはreliability settingをbest_effortに変更することなのですが、これを含めたrealsense画像をsubscribeするシンプルなプログラムをサンプルとして紹介します。

私の場合、普段はPythonを中心に使っていますが、画像などの重ためのデータを扱う際にノード間のデータのやり取りをプロセス内通信にして余計なメモリへのコピーをなくし、パフォーマンス低下を防げるcomponentの利用は現状C++が必須のようなのと、その他のパッケージでもサンプルがC++で記述されているものもありそうなので、念の為PythonC++の両方の実装を行いました。 なおパッケージ名や関数名などはできるだけ両言語で同じようになるようにしましたが、両言語のパッケージをそれぞれ試される場合は重複を避けるためパッケージ名を変更してください。

Componentに興味のある方はこちらを参考にしてください。 docs.ros.org

事前に以下のサイトの手順に従ってrealsense-rosパッケージをビルドしてください。

github.com

また今回つまずきのポイントになったQoSに関する説明はこちらにあります。やり取りすデータに欠損などが発生した際のデータの扱いに関するルールを事前に定義しておける機能と理解しましたが、今回はその中のReliabilityPolicyというものがRELIABLEになっていると、RealSenseの画像データがsubscribeされないという問題が発生したので設定を変更してBEST_EFFORTにしています。

docs.ros2.org

Python実装

パッケージを作成し、image_subscriber.pyを作成する。

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

image_subscriber.py

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2


class RealSenseSubscriber(Node):
    def __init__(self):
        super().__init__("realsense_subscriber_python")
        self.declare_parameter('image_topic_name', '/camera/color/image_raw')
        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(
            Image,
            image_topic_name,
            self.on_image_subscribed,
            video_qos
        )
        self.sub_img
    
    def on_image_subscribed(self, img):
        img_np = CvBridge().imgmsg_to_cv2(img)
        img_np = cv2.cvtColor(img_np, cv2.COLOR_BGR2RGB)
        cv2.imshow("Image", img_np)
        cv2.waitKey(1)


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

    rclpy.shutdown()
        

if __name__ == "__main__":
    main()

setup.pyを編集し、entry_pointsにrealsense_subscriberを追記する。

from setuptools import setup

package_name = 'realsense_subscriber'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='root',
    maintainer_email='root@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            "realsense_subscriber = realsense_subscriber.image_subscriber:main"
        ],
    },
)

package.xmlを編集し、rclpyとsensor_msgsを追記する。

<?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>realsense_subscriber</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="root@todo.todo">root</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>

C++実装

パッケージを作成し、image_subscriber.cppを作成する。

$ cd ~/ros2_ws/src
$ ros2 pkg create --build-type ament_cmake realsense_subscriber
$ touch ~/ros2_ws/src/realsense_subscriber/src/image_subscriber.cpp

image_subscriber.cpp

#include <rclcpp/rclcpp.hpp>
#include <rclcpp/qos.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <cv_bridge/cv_bridge.h>
#include <opencv2/opencv.hpp>


using sensor_msgs::msg::Image;

class RealSenseSubscriber: public rclcpp::Node
{
    public:
        RealSenseSubscriber(rclcpp::NodeOptions options = rclcpp::NodeOptions());
        ~RealSenseSubscriber(){}
    
    private:
        void onImageSubscribed(Image::SharedPtr img);
        rclcpp::Subscription<Image>::SharedPtr sub_img;
        std::string image_topic_name;
};

RealSenseSubscriber::RealSenseSubscriber(rclcpp::NodeOptions options) : Node("realsense_subscriber", options)
{
    image_topic_name = this->declare_parameter<std::string>("image_topic_name", "/camera/color/image_raw");
    rclcpp::QoS video_qos(10);
    video_qos.best_effort();
    video_qos.durability_volatile();
    sub_img = this->create_subscription<Image>(image_topic_name, video_qos, std::bind(&RealSenseSubscriber::onImageSubscribed, this, std::placeholders::_1));
}

void RealSenseSubscriber::onImageSubscribed(Image::SharedPtr img)
{
    auto cv_img = cv_bridge::toCvShare(img, img->encoding);
    cv::cvtColor(cv_img->image, cv_img->image, cv::COLOR_RGB2BGR);
    cv::imshow("Image", cv_img->image);
    cv::waitKey(1);
}

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<RealSenseSubscriber>());
    rclcpp::shutdown();
    return 0;
}

CMakeList.txtの記述を追加する。

cmake_minimum_required(VERSION 3.5)
project(realsense_subscriber)

# Default to C99
if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 99)
endif()

# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
endif()

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

set(ROS_LIBS rclcpp;cv_bridge;sensor_msgs)
find_package(ament_cmake REQUIRED)
foreach(lib IN LISTS ROS_LIBS)
  find_package(${lib} REQUIRED)
endforeach()

set(target image_subscriber)
add_executable(${target} src/${target}.cpp)
ament_target_dependencies(${target} rclcpp OpenCV cv_bridge sensor_msgs)
install(TARGETS ${target} DESTINATION lib/${PROJECT_NAME})

ament_package()

package.xmlに依存ライブラリとして、rclcppとlibopencv-devを追加する。

<?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>realsense_subscriber</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="root@todo.todo">root</maintainer>
  <license>TODO: License declaration</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>rclcpp</depend>
  <depend>libopencv-dev</depend>

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

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

PythonC++共通

こちらをコンパイルする。

$ colcon build --symlink-install --packages-select realsense_subscriber
$ source install/setup.bash

コンパイル後にまずrealsense-rosパッケージにあるrs_launch.pyを立ち上げます。

$ ros2 launch realsense2_camera rs_launch.py

その後、別のターミナルで作成したノードを実行します。

$ ros2 run realsense_subscriber image_subscriber

正しく実行できれば、ウィンドウが表示され、RealSenseから取得した画像が表示されます。(筆者環境ではDocker使用のため、表示にXephyrを使用していますが、マシンにROS 2を直接インストールしている場合はWindowが新規に表示されると思います)

f:id:hygradme:20210925234738p:plain

なおsubscribeするトピック名をパラメータで定義しているので、ノード起動時に引数を指定することでDepth画像を表示したり、別のWebカメラの画像などをpublishするノードを立ててそのtopicを指定すれば、その画像を表示することも可能です。 参考にdepth画像をsubscribeする際の引数の設定がこちらになります。

$ ros2 run realsense_subscriber image_subscriber  --ros-args -p image_topic_name:=/camera/depth/image_rect_raw

なおDepth画像のピクセル値はmmで計測された深度情報が入っているので、そのまま表示しても0~255の値の範囲しか通常の画像表示では行えないので、25.5mm程度の距離までしか適切に表示できません。ピクセル値をそれぞれ一定の値で割って(1000など)、0~255の範囲に計測範囲の距離が収まるようにするとわかりやすく画像を表示することができるようになります。

参考

  • ScamperとRaspberry Piで学ぶROS2プログラミング入門(オーム社)
  • ロボットプログラミングROS2の実装・実践(科学情報出版株式会社)

2020の振り返り

1月

RTX2080搭載のノートPC(PT515-51-A76Y8)を購入。外付けのNVMeSSD経由でUbuntuを起動する。

2月

xArmにステーキを焼いてもらう芸を仕込む。

ロボットに調理とかさせる展示等をすることを見越し、食品衛生責任者の講習を受け資格取得。

Universal Robots UR3を入手。Maker Faireへの出展きっかけとなる

3月

2d lidarをいくつか買う。

Spot Microに障害物回避歩行機能を実装。

Windows MRでUniversal Robotsを遠隔操作する。

Spot Microの頭にD435をつける。

4月

Jetson Xavier NXのModuleが届く。

5月

Jetson Xavier NX Developer Kitが届く。

6月

Realsense L515が届く。

Spot Microをブラウザで操作できる用にvue.js+Flaskでコントローラーを簡易Webアプリケーションとして実装。

驚異の1万円という低価格の二足歩行ロボットPLEN.Dを購入。

7月

KickstarterでバックしていたxArmのGripperがついに届く。

書籍「予算1万円でつくる二足歩行ロボット」の通りに二足歩行ロボットを作る。表面実装に初トライ。

8月

RealSense D455が届く。

Spot Microの改良版として自作四足歩行ロボットの足をカーボンで作る。

9月

dogotix-MIC-01Bを注文。

10月

MakerFaire出展、またその出展内容についてロボティクス勉強会で発表する。

docs.google.com

ソフトグリッパーに興味を持ち、3Dプリンターやシリコンゴムで制作する。

Pico Flexxを購入して試す。

Nintedo Switchのマリオカート ライブ ホームサーキット のレースにJetRacerを妨害キャラとして投入し吹っ飛ばす。

11月

M8 Lidarを入手する。

16Lineの3D Lidarがあきらめきれず、Velodyne Lidar VLP-16-HiResのジャンク品を修理して使用可能にする。

Velodyne Lidar VLP-16を入手する。

ROSでVLP-16で取った自前スキャンデータを使って3次元のSLAMを試す。

12月

myCobotを入手。

犬型ロボット dogotix-MIC-01Bが2か月以上の期間を経てついに届く。

youtu.be

youtu.be

youtu.be

その他

自作PCのグラフィックボードをGTX1080 からRTX3090に変更。

  • 暖かいが暖房器具としてはもう少しパワーが欲しい。計算資源としてはなかなか。

iPhone XからiPhone 12 Proに機種変更。

  • Velodyne Lidar繋いでiPhoneで点群を見るアプリを作って「Velodyne Lidar付スマホ」できないか少し調べたが、作るの大変そうなのでとりあえずは諦めた。

総括

  • 相変わらず色々ガジェットを買った。
  • 給付金を見越して Jetson Xavier NXを2種類購入したが、サイズはJetson Nanoとほぼ同じ割に性能がかなり高く今後もっと活用していきたいと思った。
  • CNCフライスでアルミとカーボン切削に挑戦。パラメータの調整がなかなか大変なことを知る。来年余裕あればCNCフライス購入したい。
  • MakerFaireに出展。展示の難しさを知ったがかなり面白かった。
  • 3D Lidar面白い。もう少し有効活用していきたい。
  • 購入した犬ロボは思ったより大きくパワーもあった。試すスペースがなかなかなく、扱いの難しさを感じたが大きな四足歩行ロボットの可能性を感じた。

2019の振り返り

1月

Looking Glass 到着

Cluster, VRChat初体験

  • VRChatのすごさに感動するが、すぐにVR酔いしてしまう。

例のグラボ買う

Windows MR購入

  • NorthStarMeetupで安いという情報を得たのでその場でポチる。

echo spot 導入

  • カメラがついていて、アプリから遠隔監視ができるので、3Dプリンタの印刷状態を遠隔監視する用途に多用するように。

2月

NorthStarMeetup参加で刺激を受け3Dプリンターを購入(CR-10S)

3月

HoloLens 2の実寸大モックを作成

  • 来日していたHoloLens 2の発表プレゼンをしていたMSの方からも賞賛の言葉を得る。

Project North Star作成

  • 3Dプリンターを使った初の大掛かりな製作作業。電源供給等でかなり手こずったが、なんとか動かせた。

Davinci Color Mini購入

  • 安くなってたのでカラープリンターに手を出してしまう。しかし狭い部屋に3Dプリンター二台はやりすぎだったことにあとで気づく(CNCフライスにしておけばよかったか)

4月

JetsonNano購入

xRTechで令和ことブルームと平成の手話で対応する文字を表示するアプリを展示

  • 平成最後のxR Tech Tokyo

Project North StarをT265で6DoF化

5月

Jetson Xavierを購入。現在はxArmの制御用に使用。

Oculus Questを購入

6月

Structure Coreを購入

7月

ZEDを購入

Xtion Pro Liveを購入

SR305を購入

8月

例のグラボで自作PCを作成、TensorflowをGPU使用で動かす

M5Stick VとCを購入

自作アイトラッキング装置を作成、xRTechで展示

第二種電気工事士に合格

Mediapipeのハンドトラッキングを試し性能の高さにびっくりする

JetRacerを作成

Coral Dev Boardを購入

9月

JetBotを作成するが壊れる

JetRacer作成記事を投稿

Spot MiniにインスパイアされたSpot Microというオープンソースの四足歩行ロボの作成に着手(色は青白)

10月

回路Cadに入門、作成した基盤を実際に海外に発注

四足歩行ロボ第二世代を作成(色は青黒)

11月

xArmをいじる。自動水やりを試しにやってみる。

ラズパイ4を購入

12月

AI Carのアドベントカレンダーに投稿。初アドベントカレンダー参加。

四足歩行ロボの骨折を機に足回りの改造、trot歩行に成功

総括

  • 相変わらず色々ガジェットを買った。
  • 3DプリンタでNorthStar, HoloLens 2のモック, JetRacer, JetBot, SpotMicroなどを作成し、3Dプリンターに大変お世話になった。
  • 第二種電気工事士試験に合格、また回路cadでサーボモーターの電源供給用の自作基盤を制作し総じて電気系の知識が結構増えた。
  • 去年まで継続して出していたアプリは今年作らなかったが、ハード制作系に初チャレンジ。色々難しさを知る。
  • xArmをいじり初のロボットハンド操作を体験。購入したセンサー類などと組み合わせ、今後様々なタスクにチャレンジしたい。
  • 四足歩行ロボットはやっと前進ができるようになった。今後深度カメラ or LidarとSLAM、深層学習等を組み合わせ自動で動くペットのような動きをさせたい。

JetRacerの自動走行の仕組み

JetRacerとは

Nvidiaが公開しているJetsonNanoを用いた自動走行カープロジェクトがあります。 github.com

前回の記事ではこちらの作り方を説明しました。

yhrscoding.hatenablog.com

作った当時はとりあえずサンプルのJupyter Notebookをただ実行するだけで動かしていただけでした。 今回こちらで自動走行ができる仕組みについて少し調べてみました。

基本的なラジコンの仕組み

まず自動走行する以前に、JetRacerの改造前のラジコンについて考えてみます。

車本体の位置を制御することに関して、ラジコンのコントローラー(プロポ)からステアリング(Steering、進行方向)・スロットル(Throttle、進行速度)の2つの信号が車側の受信機に届き、値に応じて車に取り付けられている2つのモーターの動きを制御しています。

f:id:hygradme:20191208031003j:plain:w300 f:id:hygradme:20191208031014j:plain:w300

ラジコンの自動走行

ラジコンを自動走行させるには基本的には人間に変わり、何らかのセンサーを使い状況を認識し、判断を行いステアリング・スロットルを適切に制御することで自動走行を実現します。

現在の標準のJetRacerではスロットルはあらかじめ指定した固定値を常に維持し、車正面に搭載のカメラから得られた情報を元にステアリングの値を変化させることでコースに沿った走行が実現しています。
ちなみにJetRacerでスロットルを状況に応じて自動で操作するということは可能で、方法はいくつか考えられます(前に参加した大会ではステアリング角に応じた速度の値を決めることでコーナーでは低速、直線では高速にする改良を行い、タイムを大幅に縮めることができました)

スロットル制御については以下の記事でもmasato-kaさんが触れられていますので、興味のある方は参考にしてください。 masato-ka.hatenablog.com

カメラ画像からからステアリング角を決める仕組み

上で述べたように、自動走行には人間の目に変わりコースの形状・車の位置などを認識し、それに応じた制御をする必要があります。
こちらは実際の自動車の自動運転では車両前方につけたカメラで白線認識を行ったり、奥行きまで認識できるセンサー(Lidar、ステレオカメラ、ミリ波レーダー)などを用い、車の空間における位置関係を把握しステアリング・スロットルの判断を行うということが行われていると思われます。白線認識を行ったり、LidarなどのセンサーをJetRacerに搭載することも可能ですが、システムが複雑になったり、重量・価格などがネックになってしまいます。

もっともシンプルに自動走行を実現するには通常のWebカメラのようなカメラ1つ(単眼カメラと呼ばれる)からステアリングを直接推定することです。 そんなこと可能なのかと思われるかもしれませんが、近年人気のディープラーニングを使って、「画像を入力->ステアリング角を出力」を行ったという論文をNvidiaが数年前に発表しました。

End to End Learning for Self-Driving Cars (2016) https://images.nvidia.com/content/tegra/automotive/images/2016/solutions/pdf/end-to-end-dl-using-px.pdf

詳細は省きますが、内容としては「道路を走行中の画像」と「その画像を撮影した時点での車のステアリング角」のペアのデータを大量に収集し、CNNというニューラルネットワークにその関係を学習させ、走行中にその時点での画像から直ちにその時点で取るべきステアリング角を推定し実際にステアリング角を変化させるということを行っています。

f:id:hygradme:20191208042121p:plain:w400
画像とステアリング角を使ってネットワークを学習し推論する

JetRacerの自動走行もこの論文と同様の原理で自動走行しています。 唯一異なる点は論文のように実際の走行時のステアリング角を学習のターゲットとせず、各画像に付与した車が向かうべきターゲットピクセル座標(x,y)を使って正しいステアリング角を擬似的に作り出している点です。

具体的に見ていきましょう。

上記のレポジトリ内の jetracer/notebooks/interactive_regression.ipynb を順に実行することでデータ収集、学習を行うことができますが、実行して画像を収集していく過程で、TASKという変数名で指定した名前から始まるフォルダが、jetracer/notebooks内に作成されています。 そのフォルダ内のapex内に画像ファイルが以下のようなファイル名で保存されていると思います。 99_109_xxxxxxxxxxxx.jpg

このファイル名先頭の2つの数字が画像を保存する際にクリックしたピクセル座標x,yに対応しています。

なおこのファイルの保存の処理はnotebooks/xy_dataset.py内のXYDatasetクラスのsave_entry関数で行われています。

class XYDataset(torch.utils.data.Dataset):
    (中略)
    def save_entry(self, category, image, x, y):
        category_dir = os.path.join(self.directory, category)
        if not os.path.exists(category_dir):
            subprocess.call(['mkdir', '-p', category_dir])
            
        filename = '%d_%d_%s.jpg' % (x, y, str(uuid.uuid1()))
        
        image_path = os.path.join(category_dir, filename)
        cv2.imwrite(image_path, image)
        self.refresh()

filenameを x_y_uuid.jpgという形式にしてフォルダに保存していることがわかります。 jupyter notebook上のカメラ画像の座標をクリックするごとにこの関数が呼ばれ、画像とターゲットのピクセル座標をファイル名内に入れて保持します。

ネットワークをトレーニングする際にこの画像とファイル名を読み込んで学習を行います。 なお実際にネットワークから出力するx,yの値はピクセル座標ではなく、画像サイズを元の値から割って得られる -1 から1の値です。

なおこの処理は上の画像を保存する処理が入っているnotebooks/xy_dataset.py内に記述されており、

class XYDataset(torch.utils.data.Dataset):
    (中略)
    def __getitem__(self, idx):
        ann = self.annotations[idx]
        image = cv2.imread(ann['image_path'], cv2.IMREAD_COLOR)
        image = PIL.Image.fromarray(image)
        width = image.width
        height = image.height
        if self.transform is not None:
            image = self.transform(image)
        
        x = 2.0 * (ann['x'] / width - 0.5) # -1 left, +1 right
        y = 2.0 * (ann['y'] / height - 0.5) # -1 top, +1 bottom
        
        if self.random_hflip and float(np.random.random(1)) > 0.5:
            image = torch.from_numpy(image.numpy()[..., ::-1].copy())
            x = -x
            
        return image, ann['category_index'], torch.Tensor([x, y])

の中の

 x = 2.0 * (ann['x'] / width - 0.5) # -1 left, +1 right
 y = 2.0 * (ann['y'] / height - 0.5) # -1 top, +1 bottom

でターゲットのピクセル座標ann['x']、ann['y'] がそれぞれ-1 ~ 1 に変換された x,yになっていることがわかるかと思います。

お気づきかもしれませんが、このターゲットのピクセル座標を予測しても、実際のステアリング角とは異なるので、通常は予測結果をそのままラジコンのステアリング角には使用できません。

JetRacerではこの予測したピクセル座標(-1~1に正規化)のxの値をそのままステアリング角とみなします。ステアリング角も-1~1の範囲に正規化されているため、例えばもっとも左にステアリング角を取るとき(steering = -1)はx=-1となり、もっとも右にステアリング角を取るとき(steering = 1)はx=1となります。

f:id:hygradme:20191208145724p:plain:w400
画像内のピクセル座標とステアリング角の対応付け

このxの値をそのままステアリング角とみなすというのはやや極端かと思いますが、カメラが車体進行方向に対してまっすぐに取り付けられているとするならば、画像の真ん中を境にマークの位置に応じて左右のステアリング値に直接変換するというのはもっとも簡単で手軽な方法の1つだと思います。

実際の画像を見ていただくとわかりますが、真ん中から左側をクリックするときはステアリングは左、右側をクリックするときはステアリングは右になっていそうなことがわかると思います。 f:id:hygradme:20191208152754j:plain:w350 f:id:hygradme:20191208152822j:plain:w350

ピクセル座標の値をステアリング角とみなして、実際にラジコンにステアリング指示を行う処理はjetracer/notebooks/road_following.ipynbの最後のセルに記述されています。

import numpy as np

STEERING_GAIN = 0.75
STEERING_BIAS = 0.00

car.throttle = 0.15

while True:
    image = camera.read()
    image = preprocess(image).half()
    output = model_trt(image).detach().cpu().numpy().flatten()
    x = float(output[0])
    car.steering = x * STEERING_GAIN + STEERING_BIAS

の中の

    x = float(output[0])
    car.steering = x * STEERING_GAIN + STEERING_BIAS

の部分が、学習済みモデルに画像を入れて出力されたピクセル座標のxの値を取り出し、ステアリング角(car.steering)に変換している部分です。 実際の処理ではSTEERING_GAINという値をかけることで、ステアリングの値を多少小さめに指示する操作を行っています。こちらの値は使用するラジコンの最大ステアリング角等を見て調整することでより性能の良い自動走行を実現できると思われます。 またSTEERING_BIASはステアリング値に一定値を常に加える処理ですので、カメラが車体本体から左右にずれていたり、重力の軸周りに回転してしまっている場合あるいはラジコンのステアリング機構自体の左右へのズレを補正するためのもので、そういうズレがなければ通常STEERING_BIAS=0.00で問題ないと思います。

ピクセル座標のyの値は標準のJetRacerでは使用しておりませんが、こちらは画像をクリックしてアノテーションを行うときのことを考えると、奥行き方向に関連する情報を持っている可能性があると思いますので(コース上をクリックするとするならば視界の先をどれだけ見渡せるかで速度を出せるかどうかの目安にできる?)、速度(throttle)に関する制御に使えるのではないかと考えられます。

以上で簡単ですが、JetRacerの自動走行の仕組みについて自分の理解の範囲で解説させていただきました。 勘違いや、JetRacerの自動走行の仕組みについてより詳しい背景等ご存知でしたら是非ご指摘願います。