ROS

ROS系列文整理

由於Robot Operating System(ROS)的中文資源還幾乎不存在(2012年),決定先撒下一點麵包屑,之後有寫出新的也會依序整理到這上面。

我覺得機器人在能源不出現問題之前,是必然崛起的一個領域。機器人的存在可以幫助人類變得更好,被解放的生產力能夠聚焦到更為重要的議題上 – 永續發展,公平正義,世界均富等等,而要在機器人產業做出能推動世界進步的成果,我認為ROS是一個很重要的工具。

現在(2015年)已經多了不少中文資源,大家可以參考ROS網站中文版。另外,最近同實驗室的同學黃昭霖也開始寫一些他自己的筆記、有一位朋友最近也開始寫ROS on Jetson的學習筆記、甚至還有朋友買了Turtlebot來玩並share自己的筆記XD 如果你需要問問題,去ROS Answers絕對是不二選擇(因為有很多ROS package的開發者會到上面回答跟自己package相關的問題),或者可以去這個中文社群另一個中文社群逛逛。

對學習ROS有興趣的朋友來說,能有多一些資源參考總是好的。希望之後有心做ROS的人學習愉快: D

———————————————–

ROS tutorials系列 (Beginner Level)

我的tutorial不詳細go through整個tutorial原文,而是就我認為重要之處詳加說明,而且有些東西原文寫得很清楚就不必重複寫了。換句話說,我仍預期你看下面這系列文章時要搭配原文看,才會比較完整。

0. 什麼是ROS?要怎麼使用ROS? (玩ROS前必看!我盡量寫得淺顯,不是工程師應該也看得懂XD)

1. 淺談ROS file system

2. 新增ROS package

3. 建立ROS packages

4. 了解ROS Node

5. 了解ROS Topics

6. 了解ROS Service跟Parameters

7. 使用rqt_console跟roslaunch

8. 使用rosed來編輯檔案

9. 建立自己的msg檔或srv檔

10.撰寫一個publisher跟subscriber(上手ROS Topic)

11.撰寫一個service跟client(上手ROS Service)

12.使用rosbag記錄和播放資料

13.使用roswtf來幫忙debug

14.探索ROS wiki

15.下一步是什麼?

———————————————–

Object Recognition Kitchen 系列

使用 Object Recogniton Kitchen 的 Linemod 演算法辨識物體

ecto 簡介 (1) – cell 與 plasm

ecto 簡介 (2) – tendrils 與 scheduler


ROS觀念文

用 DDS 開發 ROS 2.0

簡介CRAM(Cognitive Robot Abstract Machine)

簡介Knowrob (機器人知識處理的工具)

比較Topic, Service跟Actionlib

ROS Navigation stack 簡介

———————————————–

ROS實作細節文

Caffe & GoogLeNet,如何幫助機器人更好地辨識物體

改launch file中的參數值

launch file中的條件用法

安裝household object database

收到彩色影像,發布灰階影像topic的方法


ROS雜感

PR2開箱文

ROS Kong 2014照片集

ROS in DARPA Robotics Challenge!

———————————————–

補些關鍵字

ROS(Robot operating system), 機器人作業系統, 教學文章, 範例, 說明

Observation

馬祖東引島 – 魚多多小吃店

這次來馬祖玩,有很多好笑的事情,不過這一篇要說的故事沒有很好笑、反而算是有點慚愧。

故事發生在東引島 downtown 的魚多多小吃店,這家店不是很起眼,離我們住的安逸旅店也只有10公尺,看起來只是一間普通的小吃店(因為招牌就只寫小吃店啊),所以我們也只是想進去吃個粗飽而已。

IMG_3839.JPG

進去之後,自然就是先看一下菜單(可惡…忘記拍菜單),果然…魚還滿多的,但因為實在有太多種菜了,我們實在不太知道怎麼點,而且又想多吃一點馬祖當地的海產,所以我們就請老闆幫我們配 3000 的合菜。

於是老闆就開始配了,他一開始說有一道叫作”夢幻魚”的很好吃,但是會比較貴,問我們要不要,這時我感覺在場至少有 5/7 的人都覺得他應該是想推銷我們貴的魚,但想說既然都來了,所以就點了下去。接著他又幫我們配了幾道海鮮跟熱炒類的食物,但是配到約莫有10道之後,他說這樣可能會到4000多,又讓我們有點小小的懷疑他是不是故意要配貴的 (因為一開始就已經說希望幫我們配3000的菜了,而且4000多平均起來一道要400多也好像有點不對勁),這時候大家的語氣就不是很友善 (也不到沒禮貌喇我覺得,但就是有想要把一些菜取消、趕快點到3000出頭,結束點菜活動的感覺)。總之後來經過一番抉擇,還是點了一桌3800的。

IMG_3835

但是,隨著菜一道一道的上來(上面這些並不是全部喔,有好幾道已經被收走了),我們發現菜都超大盤(原本想說可能是跟我們一般吃的熱炒一樣小盤、所以才覺得平均一盤400很貴)、老闆推薦的夢幻魚也真的超好吃。我們才開始發現,原來…心靈被汙染的是我們啊…我們很直覺地用都市人的眼光來看待其他人,以為老闆是要推銷我們。

後來跟老闆聊了一下,發現原來是夢幻魚那一條就要1000左右,而且可以感覺到他的出發點比較像是想推薦我們好吃的料理,雖然缺點是會比較貴,但也會有比較深刻的印象。他還拿天下雜誌的報導給我們看,想不到魚多多居然這麼猛,只好趕忙多叫老闆幾聲大大。

最後,慚愧的我,還是跟老闆拍了一張照,並且立下承諾說回來要寫一篇推薦文,希望老闆,可以從看到這篇文章去吃的客人身上間接感受到我們的誠意😄

IMG_3838

至於晚上去山猴叔叔的店,本來老闆娘想要關店、不太想理我們,但聽到我們是安逸旅店的客人,立馬叫我們坐下,讓老闆出來跟我們聊的故事,又是另一段佳話了XDD

(噢然後山猴叔叔的兒子居然是SBL球員宋宇軒!! 還好我偶爾還會看緯來體育新聞,因為山猴叔叔先問我有沒有打籃球,我說有之後他就立馬問我知不知道宋宇軒哈哈)

Robotics, ROS

ecto框架簡介(1) – cell 與 plasm

前言

這次想要跟大家介紹 ROS ORK (Object Recognition Kitchen) 這個函式庫實作物體辨識 pipeline 的機制,其背後運用到的一個重要的函式庫叫做 ecto ,接下來會介紹 ecto 的基本觀念和用法。但因為我想把內容講解得比較詳細,所以不會只花一篇文章的篇幅就介紹完這個工具,這一篇會講到最基本的 cell 與 plasm,讓大家先有初步的認識,更進階的用法甚至是實例會在之後的文章介紹。

ecto 是什麼 & 為什麼要使用 ecto

可以把 ecto 想成一個框架,這個框架可以讓你很方便地把程式用 DAG (Directed Acyclic Graph) 的方式來實作,這樣實作的兩大好處在於模組化跟彈性。

DAG的一個範例:

DAG

首先談到模組化,在 ecto 的世界裡,你首先可以用 C++ 或 Python 寫出一個個的 cell ,這個 cell 就是執行一個功能的單位 (所以命名為 cell,細胞的意思),這種設計的方式讓你在撰寫 cell 的時候,比較不會把好多個功能硬寫在一起,增加了程式的可讀性、也讓後續的擴充彈性變強。

模組化所衍生出的好處就是彈性,因為你只要抽換某個模組,就可以改變整個程式的行為。

以 ORK 為例,在撰寫物體辨識的 pipeline 時,假設有三個步驟: (這邊只是為了方便理解舉例,不是真實情況)

  1. 讀取 Kinect 影像
  2. 使用 Linemod 演算法進行 template matching
  3. 將辨識結果輸出

那用 ecto 實作就會寫成三個 cells,然後再建立一個 ecto 的 plasm,plasm 其實就只是 graph,把寫好的三個cell相連接。 所以,如果我想要實作另一個物體辨識的演算法,我只要改寫第二個 cell 就好,當然有個前提是兩個演算法的 input 和 output 要一致,才不會影響到第一個和第三個 cell。

基本中的基本 – Cells & Plasm 的簡單用法

為了讓大家有見樹又見林的感覺,我們先看一下 ecto 大致上要怎麼用。最簡單的用法大概就是建立兩個 cell ,再用一個 plasm 將這兩個 cell 串成 graph 。

現在我們只要先知道要寫一個 cell 需要在裡面定義四個函式:

static void
declare_params(tendrils&)
static void
declare_io(const tendrils&, tendrils&, tendrils&)
void
configure(const tendrils&, const tendrils&, const tendrils&)
int
process(const tendrils&, const tendrils&)

這四個函式顧名思義就是要定義這個 cell 有哪些參數可以設定、輸入跟輸出是什麼、怎麼設定參數以及 cell 運作時的功能,很符合直覺上的需求。

那假設我們已經定義了兩個 cell – MyAwesomeCell1 跟 MyAwesomeCell2,ru, 剩下的就是建立一個 Plasm 來串接這兩個 cell 並執行,他的程式碼會像這樣:

#!/usr/bin/env python
import ecto
import my_awesome_cpp_ecto_module
import my_awesome_python_ecto_module

# create a plasm
plasm = ecto.Plasm()

# create some cells
cell1 = my_awesome_cpp_ecto_module.MyAwesomeCell1(param1=whatever1)
cell2 = my_awesome_python_ecto_module.MyAwesomeCell2(param2=whatever2)

# connect those cells in the plasm
plasm.connect(cell1['output'] >> cell2['input'])

# execute the graph
plasm.execute(niter=2)

重點其實只有 Plasm 的初始化、串接 cell 成 graph 與執行這三個部分:

# create a plasm
plasm = ecto.Plasm()

# connect those cells in the plasm
plasm.connect(cell1['output'] >> cell2['input'])

# execute the graph
plasm.execute(niter=2)

Cell 的機制詳解

從上面的例子,大家應該可以明顯的看出,plasm 因為只是要串連寫好的 cell ,所以設定相對單純(目前我們先不討論 scheduling 等複雜的狀況),但 cell 就不太一樣,上面只提到需要寫四個函式,卻沒有實例讓大家了解怎麼實作,接下來就會介紹比較實際的例子,讓大家了解 cell 要怎麼寫。

首先我們看個簡單的例子,這個 class 是一個 Printer ,我們在產生這個 Printer 的 instance 時,可以設定裡面的兩個 data member – prefix_ 和 suffix_ (或可以理解成參數)

struct Printer
{
  Printer(const std::string& prefix, const std::string& suffix)
      :
        prefix_(prefix),
        suffix_(suffix)
  {
  }
  void
  operator()(std::ostream& out, const std::string& message)
  {
    out << prefix_ << message << suffix_;
  }
  std::string prefix_, suffix_;
};

假設要改寫成 ecto 的 cell,首先我們來宣告參數,透過 declare_params 這個函式可以做到,在這個例子裡,只是先單純宣告有兩個 params,提供這兩個參數的說明(這個說明可以用來自動生成文件,不過我們先忽略),以及預設值。

大家可能會疑惑的地方是,params.declare 這個用法怎麼突然就跑出來了。這是因為在 ecto 裡面,cell 之間的溝通是透過 tendrils 這個類別來處理,但這一篇先不提到 tendrils 的細節,所以才會有點混亂,不過如果去看 tendrils 的 API,就會清楚這中間是怎麼一回事。

static void declare_params(tendrils& params)
{
  params.declare<std::string>("prefix", "A string to prefix printing with.", "start>> ");
  params.declare<std::string>("suffix", "A string to append printing with.", " <<stop\n");
}

 

接著我們來定義 IO 的介面,透過 declare_io 來做,因為這個 cell 只需要接收需要印出的 message ,所以只需要宣告一個 input ,不需要宣告 output 。

static void
declare_io(const tendrils& params, tendrils& inputs, tendrils& outputs)
{
  inputs.declare<std::string>("message", "The message to print.");
}

目前我們已經指定了對外的兩個重點 – 有哪些參數以及 IO 介面。接著該考慮內部使用的設定了,所以第一步是將 declare_params 裡面宣告的參數 (此例中是 prefix 跟 suffix) 跟類別裡面的 data member (此例中是 prefix_ 跟 suffix_) 連接。

void
configure(const tendrils& params, const tendrils& inputs, const tendrils& outputs)
{
  params["prefix"] >> prefix_;
  params["suffix"] >> suffix_;
}

最後的重點就是,實作這個 cell 的功能,我們要把實際做的事情寫在 process 這個函式裡面。

int
process(const tendrils& inputs, const tendrils& outputs)
    {
      std::cout << prefix_ << inputs.get<std::string>("message") << suffix_;
      return ecto::OK;
    }

所以如果把四個函式合起來看,就會像這樣:

#include <ecto/ecto.hpp>
#include <ecto/registry.hpp>
#include <iostream>
#include <string>;
using ecto::tendrils;
namespace overview
{
  struct Printer01
  {
    static void
    declare_params(tendrils& params)
    {
      params.declare<std::string>("prefix", "A string to prefix printing with.", "start>> ");
      params.declare<std::string>("suffix", "A string to append printing with.", " <<stop\n");
    }
    static void
    declare_io(const tendrils& params, tendrils& inputs, tendrils& outputs)
    {
      inputs.declare<std::string>("message", "The message to print.");
    }
    void
    configure(const tendrils& params, const tendrils& inputs, const tendrils& outputs)
    {
      params["prefix"] >> prefix_;
      params["suffix"] >> suffix_;
    }
    int
    process(const tendrils& inputs, const tendrils& outputs)
    {
      std::cout << prefix_ << inputs.get<std::string>("message") << suffix_;
      return ecto::OK;
    }
    std::string prefix_, suffix_;
  };
}
ECTO_CELL(ecto_overview, overview::Printer01, "Printer01",
          "A simple stdout printer with prefix and suffix parameters.");

雖然整個類別被寫成很長,但其實只要熟悉 cell 的基本用法,就不會被這一堆程式碼嚇到。最後想跟大加補充說明一下,上面提到的 cell 寫法有個不直覺的地方是,沒有明顯的繼承關係,所以會覺得不符合我們的 cell 應該要繼承一個 base 的 cell class 的直覺。其實這邊是被 ECTO_CELL 這個 MACRO 給處理掉了,所以才會看起來只有宣告幾個函式就寫完一個 cell 的感覺。

總結

這篇文章簡介了 ecto 的 cell 和 plasm,下一篇將會介紹 tendril 跟 scheduler 的機制,幫助大家更加理解 ecto ,並在未來能運用這個框架來建立自己的應用。

延伸閱讀

  1. When to use DAG (Directed Acyclic Graph) in programming?
  2. ecto 官方網頁的 plasm 介紹
  3. ecto 官方網頁的 cell 詳細介紹
Robotics, ROS

TechBridge專欄 – 使用Object Recogniton Kitchen的Linemod演算法辨識物體

前言

這次要介紹的工具是 ROS 裡面專門用來作物體辨識的 Object Recognition Kitchen (以下簡稱為 ORK ),這個工具比較像是一個框架,裡面包含了好幾種演算法(你可以根據自己的需求使用不同的演算法),這篇文章要介紹的只是其中一種 – Linemod 。 Linemod 是一個辨識效果很不錯的 3D 物體辨識演算法(不過一個使用條件是物體須是剛體),所以想藉此機會分享給大家,以後只要你想要,就可以直接拿這個工具來串自己的機器人應用。

安裝 ORK & Linemod

雖然 ORK 的開發者已經寫了一份滿不錯的安裝tutorial,不過我覺得還是值得為他再寫一份自己的整理筆記,可以把過程中遇到的一些問題都整理下來供大家參考。

我目前跑起來的環境是 Ubuntu 14.04+ROS Indigo,首先來裝一些 ORK 需要用到的 package !

export DISTRO=indigo
sudo apt-get install libopenni-dev ros-${DISTRO}-catkin ros-${DISTRO}-ecto* ros-${DISTRO}-opencv-candidate ros-${DISTRO}-moveit-msgs

接下來就要安裝 ORK 啦,然後因為我比較喜歡 build from source,所以我會下面會放上一份複雜版的安裝方法,裡面會有比較多跟 error 奮鬥的過程,如果你比較喜歡直接玩應用,安裝什麼的懶得管,那看簡單版的安裝方式其實就可以了。

I.超簡單版安裝方式

超簡單版顧名思義就是超簡單,完全不要管我們會用到哪些package,只要是 ORK 底下的 package,都裝下去,缺點就是會多浪費一些硬碟空間。只要用一行指令就搞定:

sudo apt-get install ros-indigo-object-recognition-kitchen-*

I.稍微理解自己裝了什麼的安裝方式

sudo apt-get install ros-indigo-object-recognition-core ros-indigo-object-recognition-linemod ros-indigo-object-recognition-msgs ros-indigo-object-recognition-renderer ros-indigo-object-recognition-ros ros-indigo-object-recognition-ros-visualization

I.Build From Source版安裝方式

首先來先裝跟 ROS 銜接的 package,首先要先開啟 terminal,切到 catkin_workspace/src 底下

git clone http://github.com/wg-perception/object_recognition_msgs
git clone http://github.com/wg-perception/object_recognition_ros
git clone http://github.com/wg-perception/object_recognition_ros_visualization

然後因為今天的主角是 linemod,所以需要安裝相關 package

git clone http://github.com/wg-perception/object_recognition_core
git clone http://github.com/wg-perception/linemod
git clone http://github.com/wg-perception/ork_renderer
cd ../ && catkin_make

在 catkin_make 的過程中可能會碰到因 error 而中斷,會看到類似如下的訊息:

error-1

這時候不要怕,繼續給他 catkin_make 下去,你就會發現編譯進度會神奇地有進展:

error-2

不過我有碰到一個問題,再怎麼重新編譯都沒有用:

In file included from /home/rosindigo/catkin_ws/src/ork_renderer/src/renderer3d.cpp:50:0:
/home/rosindigo/catkin_ws/src/ork_renderer/src/renderer3d_impl_osmesa.h:39:23: fatal error: GL/osmesa.h: 沒有此一檔案或目錄
 #include <GL/osmesa.h>
 ^
compilation terminated.
make[2]: *** [ork_renderer/src/CMakeFiles/object_recognition_renderer_3d.dir/renderer3d.cpp.o] Error 1
make[1]: *** [ork_renderer/src/CMakeFiles/object_recognition_renderer_3d.dir/all] Error 2
make: *** [all] Error 2
Invoking "make -j8 -l8" failed

因為是少了 GL/osmesa.h,所以需要額外下一個指令 sudo apt-get install libosmesa6-dev來安裝。

裝到這邊還會有一個問題,就是雖然編譯都已經過了,但 roscd object_recognition_core 時都會出現無法找到此 package 的 error,由於這會對後續要執行演算法時造成問題,所以還是要來處理一下。

我們先重新理一下思路,理論上,編譯過之後就會被加入到 roscd 可以找到的 path 中,但是 roscd 顯然就表示沒有被加進  ROS_PACKAGE_PATH 中,google 了一下發現到有人也遇過類似的問題,解法也確實就是把我們 git clone 下來的那些 package 加入到 ROS_PACKAGE_PATH 中,這樣的話問題就簡單啦!

先 vim ~/.bashrc一下,然後在最下面補上一行:

export ROS_PACKAGE_PATH="$ROS_PACKAGE_PATH:/home/rosindigo/catkin_ws/src"

補完的結果看起來就像:

bashrc

接上並從 RGB-D Sensor 收資料

接下來我們要先處理感測器這塊,畢竟如果沒有感測器,那就根本不用辨識物體的對吧。原本官方教學上是建議用 roslaunch openni2_launch openni2.launch,但我跑的時候一直碰到錯誤,後來是用 freenect 的 launch 檔才成功,大家可以試試用 roslaunch freenect_launch freenect.launch

跑起來之後應該就可以在 Rviz 看到以下的畫面:

kinect

安裝物體辨識資料庫 & 加入 model 

ORK 是一套以 template matching 方法為主的辨識工具,也就是說,他會把感測器看到的物體跟資料庫中的物體比對,夠相似的就算是辨識成功,所以我們接著要來處理資料庫這一塊。首先要安裝 CouchDB 這個工具 (用 sudo apt-get install couchdb )。

接下來檢查一下是否有安裝成功 (用 curl -X GET http://localhost:5984 )。

couchdb

安裝成功之後,你就已經擁有一個 local 的資料庫了,現在要做的事情是加入一個 model。對於 ORK 來說,他需要的是物體名稱、物體的 3D 模型等資訊,就讓我們先加一個叫做 coke 的模型。

rosrun object_recognition_core object_add.py -n "coke " -d "A universal can of coke" --commit

執行上面這個指令之後,你可以去 http://localhost:5984/_utils/database.html?object_recognition/_design/objects/_view/by_object_name 看看自己的資料庫裡是否已經新增了這個物體:

object_id

接下來就是要指定這個物體的 3D 模型是什麼,這邊就需要先下載個 package 並編譯。

git clone https://github.com/wg-perception/ork_tutorials

ork_tutorials裡面有一個 coke.stl 檔,他就是一個可樂罐的 3D 模型,足夠讓我們先用來辨識。

rosrun object_recognition_core mesh_add.py 0be612246c9b0a00baaa4adefb0009eb /home/rosindigo/catkin_ws/src/ork_tutorials/data/coke.stl --commit

 

執行 Linemod 演算法 (Training & Detection)

好了!終於要進入正題了,這一塊一樣有 官方tutorial 可以參考,我們先從 Training 開始。

rosrun object_recognition_core training -c `rospack find object_recognition_linemod`/conf/training.ork

這個 training 指令會利用資料庫裡的 3D 模型建立辨識時所需要的 template,如果執行成功,你會看到如下的訊息:

training

如果已經訓練完畢,下一步就是用他來辨識物體啦。

演算法簡介

既然 Linemod 是一個這麼強大的演算法,試著去稍微了解一下演算法也是很合理的,原始的論文在這邊 (2011 ICCV 的 oral 論文),以下的圖片也是出自這篇論文。

首先來看一下這篇論文的辨識結果:

1

這個演算法的核心概念就是整合多種不同的 modalities,把 modality 想成物體的不同特徵可能比較好懂,例如下圖中就有兩種 modalities – gradient 跟 surface normal,而因為這兩種特徵所表達的特性不一樣,所以可以互補,進而達到更好的辨識效果。

modalities.jpg

所以說,Linemod 需要先有已知的物體模型,然後先取得這個物體各種 modlaities 的template,這樣在辨識的時候就可以拿 template 來比對。

不過這概念在電腦視覺領域中並不特別,因為同時使用不同特徵來加強物體辨識的效果是很直覺的,也有很多不同的方法可以做到這件事情,所以這篇論文還提出了一個方法來增進 similarity measurement 的效率 ( similarity measurement 的意思是 measure 儲存的 template 跟現在看到的影像資料有多接近)。

總結

這篇文章很簡略地介紹了 ORK 的安裝、基本的使用方式(使用 Linemod )、還有演算法簡介,有興趣的讀者可以自己動手玩玩看 (如果沒有實體的 RGB-D sensor,你也可以試著用 Gazebo 裡面的 Kinect 來模擬)!

延伸閱讀

  1. 使用 ORK 的 tabletop 演算法
  2. 建立自己的 ORK 模型
  3. ORK 開發者介紹演講
  4. Linemod的OpenCV實作
ROS

Techbridge專欄 – 用DDS開發ROS 2.0?

很快地就過了一個月了,很快地又要來寫TB Weekly專欄啦!

前言

這週想要向大家介紹ROS 2.0的底層實作概念,雖然比較不會有程式實作的討論,但我覺得這一塊的深度滿值得介紹的,因為使用ROS有好幾種層次:

  1. 使用ROS的各種工具來建立自己的應用
  2. 在開發上碰到一些問題,修改現成的package來滿足自己的需求
  3. 開發自己的演算法,發布自己的package給其他人使用
  4. 協助開發與維護ROS的核心

這篇文章要討論的議題已經落在第四個層次,所以對於一般的使用者來說,可能不太具有直接應用的價值,但如果對於ROS的底層實作有更深入的理解(知道他是怎麼開發出來的、有哪些限制、有哪些優點),就可以在利用ROS撰寫自己的應用時,更能開發出效能最佳化的應用。

為什麼要開發ROS 2.0?

事實上,如果ROS 1.0 已經足夠完美,那我們就沒有必要討論ROS 2.0。不過事情當然不是這樣,因為ROS 1.0在開發的時候,是圍繞著一隻機器人來開發的,雖然當初的設計已經讓ROS變得很有彈性,可以被應用在各式各樣的機器人上,但是隨著使用者越來越多,超乎開發者想像的使用情境也越來越多。

也就是說,如果開發者們不積極地開始開發下一代的ROS,遲早會無法滿足越來越複雜而多樣化的需求。對於這些使用情境的具體描述,可以參考這裡

開發ROS系統需要實現的模塊

首先來談論一下建立整個系統上,需要考慮的幾個重點:

  • Discovery功能
  • Publisher-Subscriber功能
  • Service 與 Action功能

Discovery功能的意義是,只要有新的node啟動,就能在整個ROS node的網路中被看見(概念很像是我打開手機的wifi熱點分享,其他裝置就應該要可以發現有這個wifi熱點)。

接下來的Publisher-Subscriber功能、Service功能跟Action功能其實就是ROS使用者熟悉的Topic、Service跟Actionlib啦,本質上這幾種功能在處理的都是node之間的溝通(也就是程式之間的溝通,大家可以想像要讓一隻機器人正常運作,電腦上需要運行的程式一定是很多個,而且需要彼此溝通,所以底層的溝通機制需要有人來實作,不然就是…想開發機器人程式的你得自己實作)。如果你不太確定自己知不知道我在說什麼,可以看看這一篇區分Topic、Service跟Actionlib的文章

DDS的系統層概念

想要實作上面這些功能,DDS並不是唯一的選擇,但是,OSRF的開發者經過嘗試之後,覺得這是最好的開發選項。細節理由可以看延伸閱讀的第3篇文章,這部分已經有中文翻譯了。

api_levels.png

從上面這張圖可以清楚地看出,使用者所需要接觸到的只有最上面的兩層。使用者自己寫的code就屬於Userland Code,而使用者自己寫的code中呼叫到的ROS API (例如ros::init())就屬於ROS client library API,而DDS的API則是在更底層被ROS client library API所使用。

有趣的地方是,為了保持彈性,OSRF的開發者們希望使用者可以自己選擇底層使用的是哪一個版本的DDS (DDS像是一種標準,所以可以有不同公司提供自己的實作版本)。

一點細節的延伸

上面討論的都是概念的理解,對於技術有興趣的你想必沒辦法接受,所以就讓我們來看一點技術細節吧!

我們還是一樣先站在開發者的角度,最基本我們需要提供的工具就是Node初始化的函式對吧,這個函式的長相就像:


Node::Node(std::string name): running_(true)
{
 /*----------------------親切的中文註解來囉!!!----------------------*/
 nodes_.push_back(this);
 subscription_iterator_ = subscriptions_.end();
 name_ = name;
 //取得了DDS的DomainParticipantFactory的instance,很像是一個node產生器的感覺
 dpf_ = DDS::DomainParticipantFactory::get_instance();
 checkHandle(dpf_.in(), "DDS::DomainParticipantFactory::get_instance");
 DDS::DomainId_t domain = DDS::DOMAIN_ID_DEFAULT;

 //實際產生一個participant,應該就是一個node
 participant_ = create_participant( domain, PARTICIPANT_QOS_DEFAULT, NULL,DDS::STATUS_MASK_NONE);
 checkHandle(participant_.in(), "DDS::DomainParticipantFactory::create_participant");
 /*----------------------看到這裡就好囉!!!----------------------*/

 // Create the default QoS for Topics
 DDS::ReturnCode_t status = participant_get_default_topic_qos(default_topic_qos_);
 checkStatus(status, "DDS::DomainParticipant::get_default_topic_qos");
 default_topic_qos_.reliability.kind = DDS::BEST_EFFORT_RELIABILITY_QOS;

 // Create the default QoS for Publishers
 status = participant_get_default_publisher_qos(default_publisher_qos_);
 checkStatus(status, "DDS::DomainParticipant::get_default_publisher_qos");
 default_publisher_qos_.partition.name.length(1);
 default_publisher_qos_.partition.name[0] = "ros_partition";

 // Create the default QoS for Subscribers
 status = participant_get_default_subscriber_qos(default_subscriber_qos_);
 checkStatus(status, "DDS::DomainParticipant::get_default_publisher_qos");
 default_subscriber_qos_.partition.name.length(1);
 default_subscriber_qos_.partition.name[0] = "ros_partition";

 // Create a waitset for spin
 waitset_ = new DDS::WaitSet();

 // Create a parameter server for this node
 create_parameter_server(name);
}

大家先不要嚇到,一下有太多細節本來就不可能看懂,大家只需要看我用註解標記起來的區域,體驗一下什麼叫做ROS client library API呼叫DDS API的感覺就好。

那對於一個使用者來說,假設我今天要啟動一隻機器人,那就需要呼叫建立node的函式,看起來就像:


TurtleApp(int& argc, char** argv): QApplication(argc, argv)
{
rclcpp::init(argc, argv);
nh_ = rclcpp::create_node("turtlesim");
}

你一定覺得奇怪,rclcpp::create_node()跟上面我講的Node::Node()建構子根本接不起來啊? 所以這邊就要補上一點點程式碼,想必你就懂了:


void rclcpp::init(int argc, char** argv)
{
if (globally_initialized)
{
throw AlreadyInitializedError();
}
/* Register a signal handler so DDS doesn't just sit there... */
if (signal(SIGINT, Node::static_signal_handler) == SIG_ERR)
{
fputs("An error occurred while setting a signal handler.\n", stderr);
}
globally_initialized = true;
}

Node::Ptr rclcpp::create_node(const std::string &name)
{
return Node::Ptr(new Node(name));
}

OK!簡介就到這邊啦,如果對於實作細節有興趣深入的讀者,不妨去看看ROS 2.0的github repo,詳細的程式碼全部都是開源的,所以可以從中學習開發的細節。

延伸閱讀:

  1. 為什麼要開發ROS 2.0?
  2. 使用ZeroMQ跟相關的函式庫來開發ROS
  3. 使用DDS來開發ROS (仍在趕工中,歡迎開issue催促翻譯者QQ)
  4. ROS 2.0 wiki
  5. ROS DDS Prototype (Github Repo)
Practical

數位濾波器的學習筆記

因為濾波器的應用實在太廣了,趁著有空之餘,來學習一些基礎知識,順便將自己的筆記分享出來。

首先談一下濾波器的基本觀念。如果大家仔細觀察,就會發現,在我們的世界裡,訊號是無所不在的,你現在看到的這段文章就是透過光的訊號傳遞、你說的話是聲音訊號、你上網是透過網路訊號,這些訊號是濾波器想要處理的對象,所以你可以想像濾波器是一個黑盒子,吃進訊號、輸出訊號。而濾波器這個黑盒子一定有做一些事情,讓輸出的訊號比輸入的訊號”好”,不然就不需要這個濾波器了,所以接下來要談談到底會怎麼個變好法。

數位濾波器分成兩種類型 – IIR (Infinite Impulse Response) 跟 FIR (Finite Impulse Response)。

首先來看一下相對簡單的FIR,推薦一個youtube上的影片,裡面有一張圖片滿清晰地說明了FIR的基本觀念:

fir

左上方的訊號就是原始的訊號,右邊是我們希望可以取出的訊號,如果說左邊訊號中所顯示的一些凹陷是由雜訊造成的,那右邊的訊號相對起來就是比較好的,因為不受雜訊干擾。從理論上比較簡單的想法是先做傅立葉轉換得到頻率域的成分,接著套一個low-pass filter就可以把主要的sine成分取出來,最後再做反轉換得到右邊的訊號。而FIR在做的事就可以達到跟以上過程一樣的效果。

FIR的作法在上圖的左下角呈現,在這個例子中,一次處理訊號中的4個點,每個都乘上一個係數f,最後再加總起來,得到一個新的訊號點輸出。所以可以想像成我吃進4的原始訊號的點,只對應到右邊輸出訊號的一個點而已。

看到這邊,你應該就可以發現FIR設計的兩個重點:

  1. 我要使用幾個輸入訊號點來產生一個輸出訊號點?
  2. 我的係數f要怎麼設計?

這個部分就看你的應用中想要用濾波器來做什麼事情,可以自行調整這兩個變數來測試濾出來的訊號是否符合你的要求。

以上是layman’s term的講法,幫助理解最簡單的概念,如果你想繼續深入,可以來看看理論的說明(來自陽明大學盧家鋒老師的教學影片,講得很清楚)。

接下來看一下IIR,推薦這部影片,裡面畫了一張圖清晰地把IIR的概念呈現出來:

cmpr.jpg

看了上圖應該可以比較 清楚的比較出,FIR用來產出輸出訊號的”原料”只有輸入訊號;而IIR用來產生輸出訊號的原料除了輸入訊號之外、還包含了更早之前的輸出訊號。

最後附上幾張用比較數學的方式來呈現FIR跟IIR概念的圖結束這回合: (來自盧家鋒老師的投影片)

io

上面這張表示我們只需要設計a和b這兩組係數,就可以設計出一個濾波器,而這個濾波器在時間域對訊號的作用方式由第二個式子表示。

firr

FIR就只用到b這組係數,由時間域的式子可以看出,產生一個訊號的輸出點,只會用到K個輸入訊號點,所以這也是為什麼被稱作”有限”的原因。

iir

IIR會用到a的係數,所以輸出的訊號點y(n)還會包含到前面的輸出訊號點y(n-l),會持續用到整段訊號,這種不斷使用到前面訊號的特性,有一種不斷輪迴的感覺,所以被稱作”無限”。

Robotics, ROS

Caffe & GoogLeNet,及其在機器人上的應用

這週的TB Weekly技術週刊會開始一個全新的嘗試,我們會開始產生正體中文的技術專欄(編輯群輪流囉XD),希望可以藉這個project分享更多優質的技術內容給大家,目前暫時先把內容寫在這邊,晚點再port到統一的專欄發表平台上。

首先介紹一下這次要談的內容,是有關使用深度學習的模型來做物體辨識的應用。深度學習在這幾年來變得很火紅,相關的框架也相當多,這次之所以想談caffe,是因為已經有現成的方法可以將它應用到機器人上面。(有位台灣的開發者弄了一個叫做ros_caffe的package來串接ROS(機器人作業系統)跟Caffe,可以將Caffe辨識的結果丟到一個ROS的topic,其他的node就可以自己取用。這使得機器人辨識物體的能力得以大幅增加)

基本的安裝方法可以參考這個連結,假設已經裝成功,那至少就已經有基本的環境可以用(有一個caffe的資料夾被放在你安裝的路徑),接下來需要下載GoogLeNet的model,只要用caffe/scripts資料夾裡的程式幫忙就行了:

$./scripts/download_model_binary.py ./models/bvlc_googlenet

假設已經下載好model,接下來就可以用一個小程式來跑跑看GoogLeNet了:


import numpy as np
import matplotlib.pyplot as plt

# Make sure that caffe is on the python path:
caffe_root = '../' # this file is expected to be in {caffe_root}/examples
import sys
sys.path.insert(0, caffe_root + 'python')
sys.path.append(&amp;quot;/usr/lib/python2.7/dist-packages/&amp;quot;)

import caffe

# Set the right path to your model definition file, pretrained model weights,
# and the image you would like to classify.
MODEL_FILE = '../models/bvlc_googlenet/deploy.prototxt'
PRETRAINED = '../models/bvlc_googlenet/bvlc_googlenet.caffemodel'
IMAGE_FILE = 'images/cat.jpg'

caffe.set_mode_cpu()
net = caffe.Classifier(MODEL_FILE, PRETRAINED,
 mean=np.load(caffe_root + 'python/caffe/imagenet/ilsvrc_2012_mean.npy').mean(1).mean(1),
 channel_swap=(2,1,0),
 raw_scale=255,
 image_dims=(256, 256))

input_image = caffe.io.load_image(IMAGE_FILE)
plt.imshow(input_image)
plt.show()

prediction = net.predict([input_image])
plt.plot(prediction[0])
plt.show()
print 'predicted class:', prediction[0].argmax()

接下來只要執行(因為程式放在examples資料夾底下):

$python ./examples/googlenet_example.py

就可以看到一隻貓的影像,關掉影像之後就會看到貓的類別被輸出在terminal。

到目前為止算是驗證了可以跑起GoogLeNet。接下來,如果想往下跟ros_caffe的串接可以參考外國鄉民的文章,裡面有完整而詳細的步驟。如果你已經安裝過caffe,可以參考這個issue。另外,需要注意的是,外國鄉民跑的只有global的結果,也就是一張影像中只有一個最顯著的物體會被辨識,如果要辨識一張影像中的各個物體,可能就要自己在中間串接一個負責做segmentation的node,再把各個切出來的區塊餵給ros_caffe來做辨識。