23個我常用的ROS基本指令

忘了從什麼時候開始,大家標題都喜歡加上數字吸睛,像是什麼”10個路上把妹的必勝開場白”5個你從來不知道為什麼被主管討厭的原因”25個你從來沒聽過的英文單字云云,再加上一些聳動的字眼,或者試著吸引別人目光的句子,諸如看到最後一行我也哭了倒數第二句我笑了”100億人看完後蛋蛋都掉了什麼的,真是鬼話連篇。不過也許資訊農場也感覺到這樣驚悚的寫法已經讓人看膩了,所以逐漸沒再下這樣的標題。這就讓我想起安東尼·德·聖艾修伯里(Antoine Marie Jean-Baptiste Roger de Saint-Exupéry)在<<小王子>>第四章寫道:

大人喜歡數字。
你跟他們說你交了個新朋友,他們從不會問些基本的問題。他們從不對你說:『他的聲音如何?他最喜歡什麼遊戲?他收不收集蝴蝶?』

反而他們會問:『他幾歲?有幾個兄弟?體重多少?他父親賺多少錢?』他們只有靠這些數字才能對他有幾分瞭解。

你若跟大人說:『我看見一棟玫瑰色磚塊築成的房子,窗口有天竺葵,屋頂有白鴒。』

他們根本不能對房子產生任何概念。你必須跟他們說:『我看見一棟很昂貴的屋子。』

他們才嘆道:『噢,好漂亮的房子啊!』

同樣地,你原可以對他們說:『小王子很迷人,會笑,而且正在找一隻羊,這是他存在的明證。任何人想要一隻綿羊,就証明他的存在。』

告訴他們有什麼用呢?他們會聳聳肩,把你當做小孩子。可是你若告訴他們:『他來自B-612號小遊星』,他們就會深信不疑,不會問東問西。

他們就是這樣。

-摘自<<小王子>>第四章


尤其在資訊爆炸的二十一世紀,大家刷臉書分享文可是以秒鐘計算,誰也沒耐心駐足看一篇不感興趣的文章太久,越煽情越刺激越譁眾取寵的標題就越能吸引更多點閱率,不知道這種現象好不好。總之,廢話說太多了,言歸正傳,我也同流合汙地下了一個類似的標題,試圖統整幾個常用的指令,讓初學者能趕緊上手,對
ROS的操作能更加瞭解。


指令


功用


source ~//devel/setup.bash

ROS Master node 連結到指定的工作空間,但是也需要確定這個工作空間創建的環境是在所需的ROS版本。


source /opt/ros//setup.bash

在創建工作空間之前需要移動到某個
ROS
版本時,所使用的指令,請確定這行先打了,才繼續創建工作空間。


catkin_init_workspace

創建工作空間時需要使用的指令。它的用法如下:

$ mkdir –p ~//src

$ cd ~//src

$ catkin_init_workspace

echo
$ROS_PACKAGE_PATH

呼叫工作空間和連結到的ROS函式庫路徑。從這邊可以看得出連結到的ROS 版本是不是正確的。


rospack find

在整個ROS架構下搜尋特定包裹,如果找到,會顯示包裹路徑。若無,則顯示找不到的訊息。


catkin_make (–pkg …)

相當於 CMake中的 make指令,用於編譯整個工作空間的所有程式。若要個別程式包編譯的話,可以用引數 –pkg …


roscd

roscd =
ros + cd
在任何位置上輸入這行指令和包裹路徑,會直接跳到該 ROS
路徑上

cd

Linux 的基本指令。跳到某一路徑上


rosls

rosls = ros + ls
列出ROS架構下某一路徑下的所有資料檔

ls

Linux的基本指令。列出某一路徑下的所有資料檔

pwd

顯示使用者目前所在路徑


catkin_create_pkg

創造新包裹。後面的 dependencies 說明這個包裹需要哪些函示庫的支援,基本上會包括 roscpp, rospy, std_msgs 等,讓包裹支援基本字串輸入輸出、C++Python語言。


roscore

雖然可以想像成 ROS 初始化必要的主節點,不過實際上是一群以ROS為基底的系統需要先行啟動的節點和程式。當輸入

roslaunch

指令時,roscore會自動被啟動,但是如果是用
rosrun

單一啟動一個節點時,就要先啟動 roscore,再啟動節點。


rosnode

必須配合以下引數


list

列出所有正在執行中的節點


info

指定特定節點,並列出該節點的所有屬性


kill

結束執行某特定節點


ping

偵測某一節點是否正在執行


rosrun

執行某節點,記得後面加上節點名稱


roslaunch

執行某一 launch 檔,在後面可以再加上其他輸入,詳情請看這篇


rostopic

必須配合下引數,常用的有以下:


list

列出所有現在有實際公布的訊息主題


echo

回傳某一主題的內容,記得後面加上該主題名稱


hz

回傳某一主題更新的頻率,以每秒幾次計算


info

列出某一主題的屬性

rqt

執行圖形介面化的ROS系統分析程式,有點像是ROS界的示波器。詳情請看這裡

更多精彩的文章,請回到目錄瀏覽!

ROS Launch 檔是什麼?

前面已經提過關於 launch 檔的角色,很類似 bash 檔,基本上就是把所有為了執行某個特定功能所需要的指令都寫在一張紙上,交給 ROS 一次執行開來。舉例來說,今天我想執行 SLAM (即時建圖及定位)的功能,但是這個演算法並不簡單,我必須餵好幾個輸入資料進去,所以,在不知道 launch 檔好處的情況下,我就必須在終端機上個別開分頁這樣操作:

  • 把雷射打開
  • 把里程計(odometry)打開
  • 把 tf 程式打開
  • 把 gmapping 節點打開
  • 把馬達驅動器打開
  • 把 teleoperation 節點打開,因為我想直接操作機器人
  • 把圖形介面打開,因為我像直接看結果

這如果是在同一台電腦上操作還好,但是如果今天如果是遠端怎麼辦?尤其是碰到頻寬有限的問題,那麼遠端頻頻斷線,工作起來真的會要人命。所以,睿智且聰明的設計者們便設計了 launch 檔。只要執行它一次,便會call所有需要的launch 檔和節點,並且可以自訂輸入的參數,可以說是相當方便。但是怎麼個方便法?讓我們繼續看下去。

內容一覽

撰寫第一份 Launch 檔

如果你有機會下載一個別人寫好的包裹,並且打開資料夾進裡面看,可能會找到一個 Launch 資料夾,這裡就是存放所有 Launch 檔的地方,這種存放方法我把它視為某種約定俗成的習慣,因為方便管理嘛,哈!
我們將以 Spencer People Tracking 為範例,將這個專案中一部份的 Launch file 拿出來參考。但是在開始看之前,讓我們先了解 launch 的語法。

Launch 檔實際上使用 YAML 格式,說穿了,Launch 檔就是一種腳本語言(Duh!)所以如果你有碰過 XML, XAML, HTML等語言過,便可駕輕就熟。有幾種關鍵語法:

宣告 launch 檔   …

在launch文件一開頭和結尾都必須用這個宣告框出來,像這樣:

引數

引數通常用來作為執行各節點或 launch 檔所需要的輸入參數,換句話說,設定區域變數,通常需要使用者輸入所需的數值,但也可以事先寫好預設的數值。另外一種用法,是用引數作為一個邏輯判斷,決定那些節點要執行,哪些不用。

引數的語法會像這樣:

其中name是參數的名稱。Value 是參數的值。有時候也用 default=”…”來設定預設值。以下舉幾個例子:

<!—下面章節會再提及這個指令的用法–>

註解

舉幾個例子:

 <!—Turn on laser–>

<!—Fire up Rviz–>

<!—Just want to comment out this line–>

 

呼叫節點

呼叫節點會包含以下幾個參數:

<!—記得後面要寫成 /> 要不然執行的時候會出錯!–>

裡面的參數及其公用:

參數 功用
pkg 表示該節點所在的包裹
type 表示這個節點實際的名稱,也就是開發的時候取的名字
name 雖然也是指該節點的名稱,不過你可以再另外幫這個節點取名字,那麼該節點便會把原名給覆蓋掉,以這個名稱表示。你可以在執行時,用 rqt 或者
rosnode list, rosnode info 等指令查看到。
respawn/required 是當該節點由於不明原因停止執行的時候,會自動重新啟動。而 required 比較霸道一點,當該節點停止執行的時候,會讓整個 launch
檔都停止執行、關閉。
ns 指明在哪一個工作區間(workspace)的時候執行該節點,當你必須在多個子類別的實體(instance)中執行同一個節點的時候會很用。

若要設定該節點的印用參數,可以在節點內下以下指令:

基本上跟上述的引數用法差不多,但是當要引用使用者在上面小節給的數值的話,記得這樣打:

                <!—這是文件一開頭時的引數–>….

<!—記得要加入這個做結尾呦–>

其中,$(arg ….) 會自動去前面的<arg>找數值讀進去。

除了以外,還有其他選項,如以下:

參數 功用
用法是 。將原本節點的輸入管道,接"到"新的topics
上面。我都這樣想像,把節點想像成大象,把他那條長長的鼻子(輸入的topics,from=”…”)拉到我要他吃進去的topics
上面去(to=”target topic”)。
讓該節點讀入環境變數
讓該節點讀進參數設定檔
設定該節點所需的參數

這邊只是列舉幾個比較常見參數。當然,還有更多參數選項,可以參考 ROS Wiki 文件

呼叫其他 launch 檔

它的語法其實就是讓 ROS去找目標launch檔的路徑,一個很有用的寫法,是用 $(find
) 這種語法來直接找包裹下的路徑,所以不管這個包裹的路徑被更改,程式照樣能找得到目標。請看下面範例:

以上是一個啟動openni2.launch 這個 launch
檔的語法,包含在裡面的則是其引數。那又要怎麼知道設定那些引數呢?最簡單的方法就是去看看目標 launch 檔一開頭的
標籤,看看有那些設定可以更改。

邏輯判斷式 if & unless

講到這邊,可能你會有一個疑問。那這樣的腳本語言有沒有判斷式,在某個情況下執行特定節點,另外一個特定情況不要執行呢?有的,但是並不像是你看過的任何高階語言那樣:

If (foo=true){

Return yes ;

}

Else

{

Return no ;

}

那怎麼辦?其實只要轉念一想,我們可以拿作為邏輯判斷的方式,但是必須搭配標籤使用,寫法如下:

同樣的,也可以把標籤中的if 換成 unless,整個設定就變得像是"直到收到值為真或1時,執行該節點或launch檔"。

到時候在終端機執行這個launch 檔的時候,如果要關閉或執行某節點或launch檔,請輸入:


$ roslaunch pkg node load_driver:=false


或者


$ roslaunch pkg node load_driver:=true


這樣就能決定是否執行或跳過某部分不執行。還有,要打”:=”,否則launch檔要不就不理你繼續執行,或者是跳出語法錯誤的訊息。

開發一個大型專案的 Launch 寫法

其實我尚未真的開發過一個大型專案,不過我們可以結合前人的智慧結晶以及自己的開發經驗。

最重要的,就是專案由於功能眾多,有許多節點互相連結,所以會被隔成一層層的,所以,一個rule of thumb就是最上層的節點盡量解結的呼叫下一層的
launch
檔,然後下一層的launch檔在呼叫下一層的launch檔。而參數的設定盡量不要越級,該層級的參數設定就直接寫在該層的launch檔內,而不要上面好幾層的launch檔直接介入。這樣的方法在除錯和閱讀上會清晰不少。

另外一個我之前開發碰到的問題就是,直接將他人的包裹直接加進自己的專案內部。站在版本控制的觀念而言,每個包裹都是一個檔案庫(repository),除了在本地端維護外,更新的版本也會隨時上傳到雲端。問題就發上在,一旦你將別人的檔案庫加進自己的專案,然後推上遠端自己的檔案庫後,這些檔案庫變成你專案的一部分,再也不是他人的檔案庫,因此也無法更新成最新的版本。當我要把我的這個擁腫的包裹下載到另外一台電腦編譯時,又與我之前安裝的他人的同樣的包裹名稱起衝突。在把他人的包裹去掉,安裝自己的包裹後,發現編譯出錯,但是密密麻麻的訊息,已經讓我很難知道錯誤的源頭。因此,後來也就決定把他人的智慧結晶從我的專案中移除,往後有需要使用到他人包裹中的某些功能時,直接用launch檔呼叫即可。

從這個錯誤中我學到的教訓是,不要把別人的檔案庫直接加進自己的檔案庫內,而是各別克隆(git
clone)和編譯,然後自己的檔案庫只負責自己寫的程式和launch檔。這樣做的好處有四:

  1. 可以獨立測試各包裹,方便除錯
  2. 由於每個包裹仍然是個別的檔案庫,因此仍然可以更新成最新版本,也方便維護與同步
  3. 在不同的平台編譯的時候,方便除錯與維護。
  4. 我們不應該很大喇喇地竊取人家的智慧結晶,占為己有卻不說聲謝謝。(啊!這算是我個人觀感,就別算在原因內了吧!)

怎麼在終端機下指令

像上面的錯誤反省有提到的,Launch檔可以在自己的包裹內呼叫其他包裹的launch檔或節點,在實用上更方便。那麼在終端機時,只要用 roslaunch指令即可,語法是:


$ roslaunch :=… :=… :=…


先宣告launch檔所在的包裹名稱,再來是launch檔名稱,後面的引數arg則是前面小節已經提到過的 標籤,其值可以被終端機上的指令覆蓋掉。實際的例子:


$ roslaunch rtabmap_ros rgbd_mapping.launch rviz:=true rtabmapviz:=false


讓我們來細看上面這行指令。

Pkg name rtabmap_ros
Launch file rgbd_mapping.launch
rviz:=true
rtabmapviz:=false

為了加快並簡化launch的指令,其實可以直接把自打到一半,按Tab鍵,會自動補齊,按兩下Tab鍵則會跳出更多選項讓使用者輸入正確的 launch檔,但是注意,有時候電腦不會幫你寫後面的.launch,需要自己寫完或在按Tab補齊。如果你按Tab老半天,電腦都沒有反應,有兩個選項,一個就是把名字自己打完執行看看,要不然就是直接source,讓ROS連結到正在使用的工作空間上,如下
$ souce ~/your_ws/devel/setup.bash
然後再試試看 roslaunch 一次。

實際範例

好了,大概講完了,我們來看launch檔實際範例,這是spencer_people_tracking 中的 tracking_single_rgbd_senosr.launch
在終端機執行時,請打:
$ roslaunch spencer_people_tracking tracking_single_rgbd_sensor.launch
這個 launch file 的原始碼,在這裡。以下是 Launch 檔:


 <?xml version="1.0″ ?>

<launch>
<!– Launch file arguments –>
<arg name="height_above_ground" default="1.6″/>  <!– in meters, assumes a horizontally oriented RGB-D sensor; important for accurate detection –>
<arg name="load_driver" default="true"/>  <!– set to false if you are already running OpenNi from elsewhere –>
<arg name="visualization" default="true"/> <!– start Rviz with predefined configuration? –>
<arg name="dummy_transforms" default="true"/> <!– publish TF transform for sensor to groundplane –>
<arg name="use_openni1″ default="false"/> <!– compatibility for Kinect v1 –>
<arg name="use_upper_body_detector" default="true"/> <!– use depth-template-based upper-body detector? –>
<arg name="use_hog_detector" default="false"/> <!– use RGB groundHOG detector? Requires cudaHOG library–>

<!– Run OpenNi driver if desired –>
<group ns="spencer/sensors" if="$(arg load_driver)">
<arg name="driver_launchfile" value="$(find openni2_launch)/launch/openni2.launch" unless="$(arg use_openni1)"/>  <!– Asus Xtion Pro / Primesense–>
<arg name="driver_launchfile" value="$(find openni_launch)/launch/openni.launch"   if="$(arg use_openni1)"/>      <!– MS Kinect v1 –>

<include file="$(arg driver_launchfile)">
<arg name="camera" value="rgbd_front_top"/>
<arg name="device_id" value="#1″/>
<arg name="depth_registration" value="true"/>
</include>
</group>

<!– Set ground plane distance –>
<rosparam param="/spencer/perception_internal/people_detection/ground_plane/distance" subst_value="true">$(arg height_above_ground)</rosparam>

<!– Set up dummy transforms into an imaginary robot and odom frame –>
<group if="$(arg dummy_transforms)">
<node name="tf_base_footprint" pkg="tf" type="static_transform_publisher" args="0 0 $(arg height_above_ground) 0 0 0 base_footprint rgbd_front_top_link 10″/>
<node name="tf_odom"           pkg="tf" type="static_transform_publisher" args="0 0 0 0 0 0 odom base_footprint 10″/>
</group>

<!– Detectors –>
<group>
<!– Remap input topics of detectors from “depth" to “depth_registered" in case we are using the OpenNi 1 driver (for MS Kinect).
Necessary because OpenNi 1 does not publish both the registered and unregistered depth images (as OpenNi 2 does), and we want to keep depth registration enabled. –>
<remap from="/spencer/sensors/rgbd_front_top/depth/image_rect" to="/spencer/sensors/rgbd_front_top/depth_registered/image_rect"  if="$(arg use_openni1)"/>
<remap from="/spencer/sensors/rgbd_front_top/depth/camera_info" to="/spencer/sensors/rgbd_front_top/depth_registered/camera_info" if="$(arg use_openni1)"/>

<include file="$(find spencer_people_tracking_launch)/launch/detectors/front_rgbd_detectors.launch">
<arg name="upper_body" default="$(arg use_upper_body_detector)"/>
<arg name="hog" default="$(arg use_hog_detector)"/>
</include>
</group>

<!– People tracking –>
<include file="$(find spencer_people_tracking_launch)/launch/tracking/freiburg_people_tracking.launch">
<arg name="rgbd" default="true"/>
<arg name="laser_low_confidence_detections" default="false"/>
</include>

<!– As there is not yet any high-recall/low-confidence detector for RGB-D, and we are not using laser, tracks may get deleted too quickly in case of missed detections.
To deal with this, for the moment, we increase the maximum number of occluded frames to be  a bit more tolerant towards missed detections.
This works fine in uncrowded environments which are not very dynamic. –>
<rosparam param="/spencer/perception_internal/people_tracking/srl_nearest_neighbor_tracker/max_occlusions_before_deletion">50</rosparam>
<rosparam param="/spencer/perception_internal/people_tracking/srl_nearest_neighbor_tracker/max_occlusions_before_deletion_of_mature_track">200</rosparam>

<!– Group tracking –>
<include file="$(find spencer_people_tracking_launch)/launch/tracking/group_tracking.launch"/>

<!– RViz visualization –>
<node name="tracking_visualization_rviz" pkg="rviz" type="rviz" args="-d $(find spencer_people_tracking_launch)/rviz/tracking-single-rgbd-sensor.rviz" if="$(arg visualization)"/>

</launch>


更多精采文章,請回目錄瀏覽!

在 ROS 內撰寫 PCL 節點的初體驗心得

昨天和今天為了區區寫一個 Downsample 點雲圖(Point Cloud)的節點,就耗了好幾個小時,不過也觀察到一個 pcl_ros 教學文件一個並未為刻意提及的問題。

由於我是使用 ROS Hydro 開發的,也就是內建的 PCL升為 1.7版,所以會碰到有些程式碼用法已經不被接收的情況。幸好,教學文件中也提供了附件當參考,結果後來發現,只有這個附件中的程式碼可以順利被執行,而上述教學文件中的 Downsample 程式碼反而會出錯,可能會顯示的錯誤訊息跟找不到輸入的 cloud_msgs 有關。因此,我們就試著從附件中找出基本骨骼架構,之後可加以修改,作更多點雲圖的處理。

先看教學文件一開頭的程式碼範例:


#include <ros/ros.h>

// PCL specific includes

#include <sensor_msgs/PointCloud2.h>

#include <pcl_conversions/pcl_conversions.h>

#include <pcl/point_cloud.h>

#include <pcl/point_types.h>

ros::Publisher pub;

void

cloud_cb (const
sensor_msgs::PointCloud2ConstPtr& input)

{

// Create a container for the data.
sensor_msgs::PointCloud2 output;

// Do data processing here…

output = *input;

// Publish the data.

pub.publish (output);

}

int

main (int argc,
char** argv)

{

// Initialize ROS

ros::init (argc, argv, “my_pcl_tutorial");

ros::NodeHandle nh;

// Create a ROS subscriber for the input point cloud

ros::Subscriber sub = nh.subscribe (“input", 1, cloud_cb);

// Create a ROS publisher for the output point cloud

pub = nh.advertise (“output", 1);

// Spin

ros::spin ();

}


程式碼大致上分成四個部份,請看主程式:

  1. 宣告這個節點,包括輸入、輸出、和節點的名稱
  2. 宣告這個節點的輸入,還有輸入要到哪個函式去執行
  3. 宣告這個節點的輸出,還有這個輸出口的名稱
  4. 讓節點持續執行

如果以上程式碼編譯、執行過後沒問題,那我們就進展到處理點雲圖的部份,都是在 void could_cb 這邊執行,然後在主程式內的第二步中,在宣告輸入項時,即刻呼叫實作函式去處理點雲圖。我們在把附件拿出來參考。


#include <ros/ros.h>
// PCL specific includes

#include
<pcl_conversions/pcl_conversions.h>

#include <pcl/point_cloud.h>

#include <pcl/point_types.h>
#include
  <pcl/filters/voxel_grid.h>


ros::Publisher pub;

void

cloud_cb (const pcl::PCLPointCloud2ConstPtr& cloud)

{
pcl::PCLPointCloud2 cloud_filtered;

// Perform the actual filtering

pcl::VoxelGrid sor;

sor.setInputCloud (cloud);

sor.setLeafSize (0.1, 0.1, 0.1);

sor.filter (cloud_filtered);

// Publish the data

pub.publish (cloud_filtered);

}
int

main (int argc, char** argv)

{

 // Initialize ROS

ros::init (argc, argv, “my_pcl_tutorial");

 ros::NodeHandle nh;


// Create a ROS subscriber for the input point cloud

ros::Subscriber sub = nh.subscribe (“input", 1, cloud_cb);


// Create a ROS publisher for the output point cloud

pub = nh.advertise (“output", 1);


// Spin

ros::spin ();


直接跳到 void cloud_cb,輸入的引數從

const sensor_msgs::PointCloud2ConstPtr& input

改成

const pcl::PCLPointCloud2ConstPtr& cloud

剩下的程式碼,只是 PCL 教學中的實作部份,已經簡潔到不必從 ROS 格式替換到 PCL,做完處理後再替換回來。在處理 PointCloud 2 格式上來說,相當方便。

  // Perform the actual filtering
  pcl::VoxelGrid<pcl::PCLPointCloud2> sor;
  sor.setInputCloud (cloud);
  sor.setLeafSize (0.1, 0.1, 0.1);
  sor.filter (cloud_filtered);

  // Publish the data
  pub.publish (cloud_filtered);

 

 

更多精采文章,請回目錄瀏覽!