
你觉得这篇文章怎么样? 帮助我们为您提供更好的内容。

Thank you! Your feedback has been received.

There was a problem submitting your feedback, please try again later.

你觉得这篇文章怎么样?
作者 |
張嘉鈞 |
難度 |
普通 |
Embedded Teachable Machine 介紹
Embedded Teachable Machine是Google在推出Coral USB Accelerator時所設計搭配的一個小專案,可透過按鈕來執行拍照並且即時進行圖片分類,少量的資料就可以完成訓練。
會這麼強大、快速的主要原因是透過已經訓練好的模型 ( Mobile Net ) 來進行辨識,它可以明確的分類當初訓練的1000種類別圖片 ( http://image-net.org/index ),在神經網路輸出結果前,它會針對輸入的圖片進行特徵擷取獲得一組特徵或稱為語意 ( semantic representation ),神經網路的最後一層再根據這些特徵去分類到底比較符合1000種類別中的哪一個類別,實際上會輸出1000個數值,分數最大的代表該模型認為可能是屬於該類別。
而我們這次的 Embedded Teachable Machine是採用 Headless 型式的模型並將該模型套用到我們的資料上面。Headless顧名思義就是去掉頭,在這邊代表模型的去除模型的最後一層,這時我們將圖片丟進去一樣會獲得一組特徵,原本在最後一層會將其輸出1000個數值,但這時候我們已經將最後一層去除掉了,變成會直接獲取到該張圖片的特徵向量。
每一次按下按鈕的時候,會記錄該特徵向量以及對應的標籤 ( 第幾個按鈕 ),下一張照片將透過KNN演算法來判斷是比較接近哪一個類別,因為相似的圖片是會獲得相似的特徵向量,而KNN就是將相似的數據分類清楚。
理論終於介紹完了接下來來進行實作吧!之前已經有寫過簡易版的教學了,但是有時候樹梅派接線要查線路圖對腳位還是稍微有點麻煩,所以我們這次採用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
步驟二、T行轉接版安裝
步驟三、按鈕跟LED接線方法,這是官方的圖,但我們這裡有使用T行轉接板,所以連接到樹莓派的地方稍微不同
圖片取自Google Coral原廠網站
實際安裝畫面
步驟四、連接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) |
完成圖
準備執行環境
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
執行程式
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中,接著再進行與上述雷同的動作,我先介紹一下新增的三個副函式
- check_dir:確認資料夾是否存在?如果不存在就創建一個,如果存在就讀取該資料夾所有類別的照片。
- clear_dir:刪除資料夾內容並創建一個空的。
- 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()
綜合上述問題我決定來改良一下,首先要找到問題點!在哪裡取得圖像的?:
找到了!在teachable_reload.py中的第386行,應該是類似開一個Thread不斷運行teachable.classify的用法,所以接下就是將其註解掉開始一連串修改之旅吧!
由於它的寫法是類似Thread的寫法所以我也直接開一個Thread來執行,使用Thread也可以讓影像更流暢,取用的流程更直覺,基本上OpenCV Thread的寫法都很雷同,注意的點就是我特別導入了 knn,方便日後直接將影像跟KNN Engine調用,主要的幾個函式:
- start():開啟線程,不斷執行current_frame取得最新影像
- stop():關閉線程
- get_frame():「回傳」當前影像
- crop_frame():裁切影像
- current_frame():取得當前影像
- 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離開程式:
放上文字的方法非常簡單,只需要增加下列程式在print(info)下方就可以了:
cv2.putText(frame, info, (10,40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 1, cv2.LINE_AA)
結語
這樣子就完成第二個專案了!這次帶大家認識樹梅派加上Coral加速,光是第一個範例已經能大幅增加速度,接著透過第二個範例熟悉Coral的用法,也順便介紹了類似K-NN的方法 (Embedded),上下兩篇整個技術量充足阿!
相關文章
用 Google Coral USB Accelerator 搭配 Raspberry Pi 實作 Teachable Machine