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

Google Coral USB Accelerator搭配树莓派4运行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_MobileNet1_665f674ebcf62b980f3578c392bcb3f683efc265.png

而我们这次的 Embedded Teachable Machine是采用 Headless 型式的模型并将该模型套用到我们的数据上面。Headless顾名思义就是去掉头,在这边代表模型的去除模型的最后一层,这时我们将图片丢进去一样会获得一组特征,原本在最后一层会将其输出1000个数值,但这时候我们已经将最后一层去除掉了,变成会直接获取到该张图片的特征向量。

02_Headless_MobileNet1_5e08852297fa8f01ea9fa874e3d933834f5df84b.png

每一次按下按钮的时候,会记录该特征向量以及对应的卷标 ( 第几个按钮 ),下一张照片将透过KNN算法来判断是比较接近哪一个类别,因为相似的图片是会获得相似的特征向量,而KNN就是将相似的数据分类清楚。

03_Kernel_Machine1_5a92e7e7d2afe9475a51965b64ad6ed30f453a56.png

圖片引用於維基百科-K-means

理论终于介绍完了接下来来进行实作吧!之前已经有写过简易版的教学了,但是有时候树莓派接线要查线路图对脚位还是稍微有点麻烦,所以我们这次采用T行转接版,直接将树莓派的脚位都写出来了,使用上会更加方便。

Embedded Teachable Machine 实作

材料表

l 树莓派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_Webcam1_f397c4d49e809ff563065ca8639bdc18b59a99c3.png

步骤二、T行转接版安装

05_Cable1_fd05e94a129d94c1bbe835c82115a2c7f92c7c7c.png

步骤三、按钮跟LED接线方法,这是官方的图,但我们这里有使用T行转接板,所以连接到树莓派的地方稍微不同

06_Sample1_fcf6bd7eecb34772c47dd81ec69d471d5465c921.png

实际安装画面

07_Sample_Real1_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_TBoard1_3adbb96f7ea4ca5413899450609faebc029d17f0.jpg

完成图

09_Finish1_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_button1_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()

综合上述问题我决定来改良一下,首先要找到问题点!在哪里取得图像的?:

1160_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():取得当前影像

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_opencv1_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_addText1_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