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("/usr/lib/python2.7/dist-packages/")

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來做辨識。

 

Observation

《Mr.Bartender》S2E01.人生有機會成本可以算嗎

在追尋夢想的道路上,因為我們自身的不圓滿,不可避免地遇到重重阻礙,幸運的是,它們也不帶情感地點出我們自身的缺點,讓我們更看清楚自己。

我覺得這部影片有許多亮點,值得為它寫一篇文章記錄。

亮點一 – 女主角的側臉好正啊!

1.jpg

亮點二 – 2:20秒開始的音樂完全抓住我.

亮點三 – 轉折+Ending

看到9:42秒的時候,我本來還有點失望,覺得女主角下的一大串結論看不出啟發性,講到”被吃掉了”的時候實在覺得無趣。

不過下一秒謝祖武立馬看不下去出來打臉!而且就斷在這,讓人好想看下一集啊!!

1

亮點四 – 片尾

字跟人的感覺營造出特殊的立體感,而且燈光讚!

1.jpg

 

Robotics

TensorFlow課程筆記-Assignment 1

作業一-Not MNIST,是要練習把目前學到的Logistic Regression套到notMNIST這個dataset來得到一個可以辨識英文字母的model。

先放個圖當作目前學到概念的整理:

1

因為現在手邊只有一台小筆電,OS是32-bit的Win7,只能考慮用docker,但看了一下安裝方法發現Windows要裝docker得是64-bit,好的那今天就收工囉~等可以用到Ubuntu的時候再繼續!

好了總算重新摸到我的Ubuntu,雖然也是可以直接裝個TensorFlow,不過既然有docker可以玩,那就順便學一下好了!

首先要先裝docker,主要就Follow一下這邊的步驟就可以了~前面的prerequisites記得要做,然後我還要額外upgrade kernel的版本(因為我還是12.04的系統QQ),然後就照步驟安裝docker囉!

安裝完之後,記得再設定一下docker group,就不用每次都得在跑docker之前加上sudo了。就可以來跑作業要用到的docker image:

sudo service docker start
docker run -p 8888:8888 -it --rm b.gcr.io/tensorflow-udacity/assignments

嗯嗯好總算是把環境跑起來了,第一次跑的時候會下載比較久,可以去喝杯水,對身體好。跑起來之後就可以看到這個畫面:

1.png

接下來去瀏覽器輸入127.0.0.1:8888就可以看到iPython的notebook了!

一開始有點不知道怎麼在iPython環境下工作~看起來是可以自由增加Cell然後寫一些程式去跑。如果要起頭可以直接點code cell然後按下Run的按鈕,第二個cell執行到一半確實會在docker notebook首頁看到notMNIST_large.tar.gz的檔案(不過這一段code應該要抓train跟testing的兩組檔案,所以網路不夠快的話大約需要等個幾分鐘),這表示確實有在下載dataset。等到把Problem 1之前的code cell都執行完一次,就可以開始玩Problem 1了。

Problem 1只是要把圖片show出來,這邊有提示可以用IPython.display,查了一下發現滿簡單的。但問題是我不知道image的路徑,假設回到首頁想用點擊的方式看,每次只要一進到notMNIST_large/A/就會當掉(應該是因為檔案太多),於是只好在程式裡把檔案路徑先印出來,再用Image函式show出來,實作上只要開一個cell,在裡面寫

#os.listdir('notMNIST_small/A/')
Image(filename='notMNIST_small/A/RnV0dXJhRUYtTGlnaHRDb25kLm90Zg==.png')

就會跑出來了!
1 (1)

接下來進到Problem 2,這邊一樣是要把dataset裡的image畫出來,不過這次的image是經過讀檔跟一些處理,已經變成ndarray格式的影像。這邊儲存影像的邏輯是dataset[0:num_images, :, :],第一個維度表示第幾張影像,先看一下我們擁有的變數 – train_datasets跟test_datasets:

train_datasets[0] == notMNIST_large/A.pickle
...
train_datasets[9] == notMNIST_large/J.pickle

它們儲存的東西並不是ndarray,而是.pickle檔。到這邊就不得不了解一下pickle是什麼東西,查了一下發現居然是Python裡面做serialization的工具,所以應該要多做一步讀檔的動作:

f = open(train_datasets[0], 'r')
a = pickle.load(f)
#print(a[1])
plt.imshow(a[1,:,:])
plt.show()

不過奇怪的是,執行這段程式碼的結果是…什麼也沒發生,我明明已經把a[1]印出來看過,是一個28×28的二維矩陣沒錯。首先確保Ipython notebook對於matplotlib支援沒有問題,應該先隨便畫個東西出來看看:

Robotics

TensorFlow課程筆記-Softmax實作

今天受到趙大神的啟發決定來上一下TensorFlow的課,遇到第一個程式作業就有些看不懂的東西,決定用一種意識流的方法來寫寫看筆記,幫助自己更加理解!

原始的程式碼挖了個空,要自己寫softmax

<pre>"""Softmax."""

scores = [3.0, 1.0, 0.2]

import numpy as np

def softmax(x):
 """Compute softmax values for each sets of scores in x."""
 pass # TODO: Compute and return softmax(x)

print(softmax(scores))

# Plot softmax curves
import matplotlib.pyplot as plt
x = np.arange(-2.0, 6.0, 0.1)
scores = np.vstack([x, np.ones_like(x), 0.2 * np.ones_like(x)])

plt.plot(x, softmax(scores).T, linewidth=2)
plt.show()

當務之急是先了解函式的特性

1

所以假設我把[3.0, 1.0, 0.2]傳進來,我想得到的應該是[exp(3.0)/(exp(3.0)+exp(1.0)+exp(0.2)), …],所以就先算出分母,然後把最後的陣列湊出來應該就行了吧。

啊不過要怎麼算exponential咧? 查了一下發現numpy裡就有好用的函式可以算整個陣列的exponential值~

寫出來就像這樣:

import numpy as np

def softmax(x):
 """Compute softmax values for each sets of scores in x."""
 sum=0
 for item in x:
 sum += np.exp(item)
 
 return np.exp(x)/sum

不過我很好奇後面那一坨code詳細做了什麼,怎麼有辦法產生出一張有點酷炫的圖:
1

嗯就來看看這一坨東東:

# Plot softmax curves
import matplotlib.pyplot as plt
x = np.arange(-2.0, 6.0, 0.1)
scores = np.vstack([x, np.ones_like(x), 0.2 * np.ones_like(x)])

plt.plot(x, softmax(scores).T, linewidth=2)
plt.show() 

首先我們可以先預測這一段code到底要幹嘛,解讀起來看看是否印證預測,會比較快。如果光看圖的話,感覺就是想要畫出一個X軸,根據X軸的變化改變scores裡面的三個值,然後把三個值經過softmax之後的值畫在Y軸上(所以Y軸的最大值是1)。

好喔好像滿合理的~

所以首先我們要產生X軸的值,從圖上可以明顯看到最小值是-2、最大值是6,恩所以np.arange應該就是用來產生一系列的X軸值,查到的結果也的確是這樣。

接下來這一行有點噁心,一下vstack一下ones_like的,到底是什麼東西?不要急,一個一個來~

ones_like就只是產生一個全部都是1的list而已,不過list的維度會跟input一樣,所以ones_like(x)會產生一個跟x一樣大小,只是裡面的元素全部都是1。

啊vstack不過就是把幾個list疊在一起而已,上個圖就瞭了:
1

接下來就是最不直覺的地方了,我傳進去softmax(scores)的scores不應該是把三個score值放在同一個list裡嗎?啊怎麼反而分散在三個list裡面?這傳進我的softmax裡面應該會爆吧?

原來這個(.T)偷偷做了transpose!不過這還是不足已解釋傳進去的時候為什麼會有那樣的行為,不過這個問題不是很重要,我懶得再花時間了,有需要的話會再遇到的XD