嘿!您似乎在 United States,您想使用我们的 English 网站吗?
Switch to English site
Skip to main content

Google Coral USB Accelerator搭配Raspberry Pi4運行Embedded Teachable Machine – 下篇

作者

張嘉鈞

難度

普通

Embedded Teachable Machine 介紹

Embedded Teachable Machine是Google在推出Coral USB Accelerator時所設計搭配的一個小專案,可透過按鈕來執行拍照並且即時進行圖片分類,少量的資料就可以完成訓練。

會這麼強大、快速的主要原因是透過已經訓練好的模型 ( Mobile Net ) 來進行辨識,它可以明確的分類當初訓練的1000種類別圖片 ( http://image-net.org/index ),在神經網路輸出結果前,它會針對輸入的圖片進行特徵擷取獲得一組特徵或稱為語意 ( semantic representation ),神經網路的最後一層再根據這些特徵去分類到底比較符合1000種類別中的哪一個類別,實際上會輸出1000個數值,分數最大的代表該模型認為可能是屬於該類別。

01_Norm_MobileNet_665f674ebcf62b980f3578c392bcb3f683efc265.png

而我們這次的 Embedded Teachable Machine是採用 Headless 型式的模型並將該模型套用到我們的資料上面。Headless顧名思義就是去掉頭,在這邊代表模型的去除模型的最後一層,這時我們將圖片丟進去一樣會獲得一組特徵,原本在最後一層會將其輸出1000個數值,但這時候我們已經將最後一層去除掉了,變成會直接獲取到該張圖片的特徵向量。

02_Headless_MobileNet_5e08852297fa8f01ea9fa874e3d933834f5df84b.png

每一次按下按鈕的時候,會記錄該特徵向量以及對應的標籤 ( 第幾個按鈕 ),下一張照片將透過KNN演算法來判斷是比較接近哪一個類別,因為相似的圖片是會獲得相似的特徵向量,而KNN就是將相似的數據分類清楚。

03_Kernel_Machine_5a92e7e7d2afe9475a51965b64ad6ed30f453a56.png

理論終於介紹完了接下來來進行實作吧!之前已經有寫過簡易版的教學了,但是有時候樹梅派接線要查線路圖對腳位還是稍微有點麻煩,所以我們這次採用T行轉接版,直接將樹莓派的腳位都寫出來了,使用上會更加方便。

Embedded Teachable Machine 實作

材料表

l Raspberry Pi 4及其電源線 X1

l 樹莓派T型GPIO擴展板+40P排線 X1

l Google Coral USB Accelerator X1

l WebCam X1

l 單芯線自行裁切

l 330 Ohm 1/4W電阻 X4

l 不同色LED X4

l 4 pin按鈕 X5

l 10cm公母杜邦線 X10

l 麵包板 X1

安裝步驟

步驟一、樹莓派接上電源、Coral、Webcam

04_Coral_Webcam_f397c4d49e809ff563065ca8639bdc18b59a99c3.png

步驟二、T行轉接版安裝

05_Cable_fd05e94a129d94c1bbe835c82115a2c7f92c7c7c.png

步驟三、按鈕跟LED接線方法,這是官方的圖,但我們這裡有使用T行轉接板,所以連接到樹莓派的地方稍微不同

06_Sample_fcf6bd7eecb34772c47dd81ec69d471d5465c921.png

圖片取自Google Coral原廠網站

實際安裝畫面

07_Sample_Real_f3481d4bb07282e4ee6b5aac9aef0f4b1ddc3e62.jpg

步驟四、連接T行轉接版

樹莓派GPIO腳位及座標

麵包板接線

GPIO4 (J,4)

黃色按鈕(A,5)

GPIO17(J,6)

黃色LED燈電阻(A,7)

GPIO27(J,7)

綠色按鈕(A,12)

GPIO22(J,8)

綠色LED燈電阻(A,14)

GPIO5(J,15)

橘色按鈕(A,19)

GPIO6(J,16)

橘色LED燈電阻(A,21)

GPIO13(J,17)

紅色按鈕(A,26)

GPIO19(J,18)

紅色LED燈電阻(A,28)

GPIO26(J,19)

藍色按鈕(A,35)

08_TBoard_3adbb96f7ea4ca5413899450609faebc029d17f0.jpg

完成圖

09_Finish_74b6f7b8359559a41bb126a9712557b6300c1bac.jpg

準備執行環境

cd /home/pi

git clone https://github.com/google-coral/project-teachable.git

cd project-teachable

sh install_requirements.sh

修改程式碼的GPIO腳位

為了讓線都接在T行轉接版的同一側所以要修改一下GPIO腳位,在teachable.py中的class UI_Raspberry中修改:

# self._buttons = [16 , 6 , 5 , 24, 27]
# self._LEDs = [20, 13, 12, 25, 22]

self._buttons = [26 , 4 , 27 , 5, 13]
self._LEDs = [21, 17, 22, 6, 19]

執行GPIO測試

透過下列程式碼進行GPIO的測試,執行之後按下按鈕進行測試,如果接法與我相同,左至右的按鈕個別是 [ 1, 2 ,3 ,0 ]。

cd ~/project-teachable

python3 teachable.py --testui

10_button_381dd45692fa414c235d9e961d5128f1ef10a6e4.png

執行程式

cd ~/project-teachable

python3 teachable.py

雖然是個良好的體驗,但是還是有些不方便的地方,第一個是拍完照無法儲存下一次開起就重新開始;第二個是用gstreamer來做除了不熟悉之外,用MobaXterm遠端的時候也無法讀取,所以想要改良成OpenCV。

改良一、儲存照片並能重新讀取

我預計要修改的程式是Teachable.py中的TachableMachineKNN,這邊是針對圖片進行KNN分類的原始程式碼,先進行分析一下,一開始要先宣告buffer跟KNN用的engine,在Classify函式中會對該圖進行inference,獲得特徵語意 ( emb ),接著使用 Counter來獲取buffer中最多的類別,for迴圈的部分則是要顯示LED燈的資訊,最後的if是按下四個按鈕就離開程式:

class TeachableMachineKNN(TeachableMachine):

 def __init__(self, model_path, ui, KNN=3):
 TeachableMachine.__init__(self, model_path, ui)
 self._buffer = deque(maxlen = 4)
 self._engine = KNNEmbeddingEngine(model_path, KNN)

 def classify(self, img, svg):
 # Classify current image and determine
 emb = self._engine.DetectWithImage(img)
 self._buffer.append(self._engine.kNNEmbedding(emb))
 classification = Counter(self._buffer).most_common(1)[0][0]
 # Interpret user button presses (if any)
 debounced_buttons = self._ui.getDebouncedButtonState()
 for i, b in enumerate(debounced_buttons):
 if not b: continue
 if i == 0: self._engine.clear() # Hitting button 0 resets
 else : self._engine.addEmbedding(emb, i) # otherwise the button # is the class
 # Hitting exactly all 4 class buttons simultaneously quits the program.
 if sum(filter(lambda x:x, debounced_buttons[1:])) == 4 and not debounced_buttons[0]:
 self.clean_shutdown = True
 return True # return True to shut down pipeline
 return self.visualize(classification, svg)

有了初步的瞭解之後先來整理一下思緒,我的作法很簡單,預計在一開始呼叫TeachableMachine的時候先讀取特定資料夾,並且把所有資料丟進engine中,接著再進行與上述雷同的動作,我先介紹一下新增的三個副函式

  1. check_dir:確認資料夾是否存在?如果不存在就創建一個,如果存在就讀取該資料夾所有類別的照片。
  2. clear_dir:刪除資料夾內容並創建一個空的。
  3. reload_dir:將讀取的資料丟進TeachableMachineEngine先進行訓練。

開始之前,因為我們需要先導入shutil函式庫:

import shutil

為了完成這個功能,第一步是按下按鈕的時候可以儲存圖片,需要先修改classify資料夾,第一個修改的地方是「按下清除按鈕的時候」,除了清除engine的資料外還須清除儲存的所有照片,也就是運行clear_dir()函式,接著修改的是「按下其他按鈕的時候」需要進行儲存的動作,這邊檔案格式是PIL所以直接img.save就可以了:

 def classify(self, img, svg):
 
 # Classify current image and determine
 emb = self._engine.DetectWithImage(img)
 self._buffer.append(self._engine.kNNEmbedding(emb))
 classification = Counter(self._buffer).most_common(1)[0][0]
 # Interpret user button presses (if any)
 debounced_buttons = self._ui.getDebouncedButtonState()
 for i, b in enumerate(debounced_buttons):
 if not b: continue
 if i == 0:
 self._engine.clear() # Hitting button 0 resets
 self.clear_dir() ### Modify by Chun : clear data folder

 else :
 self._engine.addEmbedding(emb, i) # otherwise the button # is the class
 
 ### Modify by Chun : Save Image & Label
 save_path = os.path.join(self.trg_folder[i-1], f'{str(self.img_nums[i-1])}.jpg')
 img.save(save_path)
 self.img_nums[i-1] += 1
 ### End of Modify

有了拍照儲存的動作之後,我們需要在一開始執行的時候就讀取舊有的資料,並且先運行KNN,由於只要運行一次就可以所以我們修改的地方會著重在init當中,data_path是我們預設的資料夾,會先進行check_dir()查看資料夾是否存在、是否有資料,如果有資料的話img_nums就會大於0,接著再進行reload_dir():

 def __init__(self, model_path, ui, KNN=3):
 TeachableMachine.__init__(self, model_path, ui)
 self._buffer = deque(maxlen = 4)
 self._engine = KNNEmbeddingEngine(model_path, KNN)
 
 ### Modify
 self.cls_nums = KNN+1
 self.data_path = 'data'
 self.trg_folder = [] # trg_folder = './data/{Class}'
 self.img_nums = [0, 0, 0, 0] # img_nums = [ x, x, x, x], count each class's images
 self.check_dir()
 
 if sum(self.img_nums) != 0:
 print('\n', 'Reload Data', end=' ... ')
 self.reload_data()
 ### End of Modify

其餘三個副函式的內容如下:

 def check_dir(self):
 
 print('\n', 'Check Dir', end=' ... ')
 for cls in range(1, self.cls_nums+1): # Classes from 1 to 4
 self.trg_folder.append(os.path.join(self.data_path, str(cls)))
 
 # Check Directory is existed or not 
 if os.path.exists(self.trg_folder[cls-1]) is False:
 os.makedirs(self.trg_folder[cls-1])
 self.img_nums[cls-1] = 0
 else:
 self.img_nums[cls-1] = len(os.listdir(self.trg_folder[cls-1]))
 
 def clear_dir(self):
 shutil.rmtree(self.data_path) 
 self.check_dir()
 print('\n\n Clear \n\n')
 
 def reload_data(self):

 t_start = time.time()
 for cls in range(1, self.cls_nums+1): # 1 ~ 4
 if self.img_nums[cls-1] != 0 :
 for idx in range(0, self.img_nums[cls-1]):
 img = Image.open(os.path.join(self.trg_folder[cls-1], f'{idx}.jpg'))
 emb = self._engine.DetectWithImage(img)
 self._buffer.append(self._engine.kNNEmbedding(emb))
 classification = Counter(self._buffer).most_common(1)[0][0]
 self._engine.addEmbedding(emb, cls)
 print('Done({:.3f}s)'.format(time.time()-t_start)) 

最後一步就是將main()中的 TeachableMachineKNN改成你修改好的版本,如果你是像我一樣額外寫一個副函式的話就需要修改,如果只是修改原本的就可以不用更改。

# teachable = TeachableMachineKNN(args.model, ui)
teachable = TeachableMachineKNN_ByChun(args.model, ui)

修改後的結果:

第二篇

改良二、修改成OpenCV

上一篇已經將Embedded Teachable Machine 改良可以儲存資料以及讀取舊有資料了,接下來我想將其改良成OpenCV格式, Gstream雖然效能比較強大但我還在熟悉中,如果使用MobaXterm遠端的時候也會取得不到畫面,再來就是Tkinter上我確定能使用OpenCV但是Gstream還要研究。

INFO:
Modify Original Code to Save and Reload Data, and change PyGi to OpenCV.

Modify Items:
1. Modify TeachableMachineKNN_ByChun
2. Change PyGi to OpenCV in main()
3. Use Thread to Improve Delay of Streaming : ThreadCapture()
4. Modify TeachableMachine.visiual() to get_results()

綜合上述問題我決定來改良一下,首先要找到問題點!在哪裡取得圖像的?:

1159_f5684e2694e72329e07acb12a6caeb1c7725d2d8.png

找到了!在teachable_reload.py中的第386行,應該是類似開一個Thread不斷運行teachable.classify的用法,所以接下就是將其註解掉開始一連串修改之旅吧!

由於它的寫法是類似Thread的寫法所以我也直接開一個Thread來執行,使用Thread也可以讓影像更流暢,取用的流程更直覺,基本上OpenCV Thread的寫法都很雷同,注意的點就是我特別導入了 knn,方便日後直接將影像跟KNN Engine調用,主要的幾個函式:

  1. start():開啟線程,不斷執行current_frame取得最新影像
  2. stop():關閉線程
  3. get_frame():「回傳」當前影像
  4. crop_frame():裁切影像
  5. current_frame():取得當前影像
  6. run_knn():將當前影像丟入KNN引擎並回傳結果
### Modify by Chun
class ThreadCapture():
 
 def __init__(self, knn):
 self.frame = []
 self.status = False
 self.isStop = False
 self.knn = knn
 self.cap = cv2.VideoCapture(0)
 self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
 self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 480)
 
 def start(self):
 threading.Thread(target=self.current_frame, daemon=True, args=()).start()

 def stop(self):
 self.isStop = True
 
 def get_frame(self):
 return self.frame
 
 def crop_frame(self):
 h = self.frame.shape[0]
 w = self.frame.shape[1]
 cut = int((w-h)/2)
 self.frame = self.frame[0:h, cut:w-cut]
 
 def current_frame(self):
 while(not self.isStop):
 self.status, self.frame = self.cap.read()
 self.crop_frame()
 self.cap.release()

 def run_knn(self):
 img_resize = cv2.resize(self.frame, (224, 224))
 img = cv2.cvtColor(img_resize, cv2.COLOR_BGR2RGB)
 img_pil = Image.fromarray(img)
 return self.knn.classify(img_pil)
### End of Modify

取得影像沒問題了,但是最麻煩的地方是self.knn.classify(img_pil),這是我修改後的副函式,先來看一下原本的,光是引入的數值就有兩個,第一個是img,第二個是svg:

def classify(self, img, svg):
 #省略省略
 return self.visualize(classification, svg)

Visualize的部分我沒太深入研究但是可以看的出來svg應該是原圖的意思,就算不了解Gstreamer但可以看到關鍵字add、text,代表在圖上加入文字:

 def visualize(self, classification, svg):
 self._frame_times.append(time.time())
 fps = len(self._frame_times)/float(self._frame_times[-1] - self._frame_times[0] + 0.001)
 # Print/Display results
 self._ui.setOnlyLED(classification)
 classes = ['--', 'One', 'Two', 'Three', 'Four']
 status = 'fps %.1f; #examples: %d; Class % 7s'%(
 fps, self._engine.exampleCount(),
 classes[classification or 0])
 print(status)
 svg.add(svg.text(status, insert=(26, 26), fill='black', font_size='20'))
 svg.add(svg.text(status, insert=(25, 25), fill='white', font_size='20'))

我們希望獲得的應該是status中的Class欄位的內容,也就是 classes[classification or 0],這個是它辨識出來的結果,所以我複製了visualize()命名為get_results() 該函式將返回status跟classes[classification or 0],並且將gstream顯示的程式刪掉:

def get_results(self, classification):
 self._frame_times.append(time.time())
 fps = len(self._frame_times)/float(self._frame_times[-1] - self._frame_times[0] + 0.001)
 # Print/Display results
 self._ui.setOnlyLED(classification)
 classes = ['--', 'One', 'Two', 'Three', 'Four']
 status = 'fps %.1f; #examples: %d; Class % 7s'%(
 fps, self._engine.exampleCount(),
 classes[classification or 0])
 return status, classes[classification or 0]

並且 classify的部分原本有svg這個引入參數,也需要將其刪掉只留下img:

class TeachableMachineKNN_ByChun(TeachableMachine):
 # 省略省略
 def classify(self, img):
 # 省略省略
 return self.get_results(classification) ### Modify by Chun

最後在main()的部分使用OpenCV開啟即時影像,先宣告剛剛寫好的物件ThreadCapture,停留一秒確保thread有擷取到影像,使用while迴圈持續獲取最新的影像,取得影像後就執行run_knn()並顯示結果、圖片,按下按鍵q的時候跳出迴圈,停止線程:

### Modify by Chun 
# print('Start Pipeline.')
# result = gstreamer.run_pipeline(teachable.classify)
stream = ThreadCapture(teachable)
stream.start()
time.sleep(1) # 等待thread擷取到攝影機影像

while(True):
 
 status, frame = stream.get_frame()

 if status:
 info, res = stream.run_knn()
 print(info)
 cv2.imshow('Test', frame)

 if cv2.waitKey(1)==ord('q'):
 break

ui.wiggleLEDs(4)
stream.stop()
cv2.destroyAllWindows()
### End of Modify

執行結果如下,可以注意到畫面跟原本的不太一樣,文字也沒有顯示所以下一步要來顯示文字,可以按下按鍵q離開程式:

12_opencv_6e0a0d6fe8e17a72ffd496debcd2f936fd3901c1.png

放上文字的方法非常簡單,只需要增加下列程式在print(info)下方就可以了:

cv2.putText(frame, info, (10,40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 1, cv2.LINE_AA)

13_cv_addText_15c191f727fa889e67d779a37717e72757dabe3f.png

結語

這樣子就完成第二個專案了!這次帶大家認識樹梅派加上Coral加速,光是第一個範例已經能大幅增加速度,接著透過第二個範例熟悉Coral的用法,也順便介紹了類似K-NN的方法 (Embedded),上下兩篇整個技術量充足阿!

相關文章

用 Google Coral USB Accelerator 搭配 Raspberry Pi 實作 Teachable Machine

Embedded Teachable Machine

KNN

CAVEDU Education is devoted into robotics education and maker movement since 2008, and is intensively active in teaching fundamental knowledge and skills. We had published many books for readers in all ages, topics including Deep Learning, edge computing, App Inventor, IoT and robotics. Please check CAVEDU's website for more information: http://www.cavedu.com, http://www.appinventor.tw
DesignSpark Electrical Logolinkedin