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

NVIDIA Jetson Nano应用- Google Colab云端训练客制化 YOLOv4物件辨识-上篇

作者

张嘉钧

难度

普通

材料表

Webcam X1

NVIDIA Jetson Nano X1

上篇分为两个大项目:

1.YOLOv4训练其他数据集的基本概念

2.如何在Colab上使用YOLOv4

关于YOLOv4这边就不多作介绍了,基本上YOLOv4是现今集大成之作,整合了各种技术,想要了解详细的技术可以在网络上找到很多相关的资源,我这边蛮推荐初学者可以去看吴恩达老师在coursera上面的AI课程,其中也有教到YOLO的运作原理,虽然没有讲到论文这么深入的技术但是却贯穿了YOLO的精随。

Transfer Learning的基本概念

这次实作的部分是关于YOLOv4用于客制化的数据集,还是有几个基本的概念需要提到,像是用已经训练好的权重再去训练其他的数据集,这个动作就叫做「Transfer Learning」,中文可能会称作「迁移式学习」、「转移学习」;主要的迁移式学习有分成两种「Feature Extraction」、「Fine Tuning」。

「Feature Extraction」中文为「特征撷取」,会保留预训练模型CNN的部分也就是保留模型良好的特征撷取的能力,只会重新训练Fully Connected的部分 (简称FC),我自己的理解会把FC当成是排列组合,原本只能辨识猫跟狗的模型,由于已经会撷取猫跟狗的特征,这时候导入马的图片,它也能撷取出特征,只是不懂得将撷取出来的特征归类成新的类别,所以这时候我们只训练FC (排列组合的部分),让他可以重新学习如何将各种特征进行分类;我们用下图来模拟情境,灰白格子代表CNN的权重。

1.透过预训练模型的CNN的部分可以撷取出特定特征

image00110_e00b77ae5620bee4472e1d504f930aaa00e11324.png

2.输入一样是动物类型的图片,就算不是原本训练的数据它一样能撷取出特定的特征

image0028_2570efa373151593cb8d61bb3b9e37a6516f236c.png

3.所以我们只需要冻结 ( freeze ) CNN的部分,重新训练FC即可:

image00310_795cf0aa47f6506b270066ca36a750755245eb8b.png

「Fine Tuning」中文为「微调」,我们直接用训练好的权重进行重新训练,与从头训练不同的地方在于从头训练是一组随机的数值,而预训练模型不是,它将能优化原本就已经训练过的权重,不过这种方式最好还是基于数据集类型雷同的情况下;我们一样用图片来仿真情境。

1.原先预训练模型训练出来的CNN权重将能优秀的辨识出狗的特征

image00411_8a2e68264c06b6d8eea627da9ab3a47a91cf2b7c.png

2.假设我们已经知道下图的权重用来辨识新数据-马更为优秀

image0059_9bdc164b5e72df7c6767c11ec3a4ff92eadae7cc.png

3.从概念上来说,微调会比从头训练更快达到目标

image00615_186538ad526182581810a8f41f2294dd316f0021.png

最后我整理了一个表格,让大家参考一下什么样的情况适合用哪一个技术,以上如果有叙述错误或不清的地方,欢迎在下方留言区告诉我:

image00711_0c40f0a55c709f15479dabbc894d1103e75f5136.png

 

Colab介绍与使用

Transfer Learning概念的部分已经讲完了,接下来就要开始进入实作的部分了!这次我们使用的工具是Colaboratory (简称Colab),如果有看我们早期的文章可以注意到我们很喜欢运用Colab这个平台,这是由Google推出的在线Python程序执行平台,免费版本的Colab提供了8小时的免费GPU可以使用,所以手边没有强大GPU的同学们就可以善用这个平台的资源。

image0087_8c03bc93f7112d0219eaa74e6a0579e6eb417b4f.png

Colab的详细介绍

https://colab.research.google.com/notebooks/intro.ipynb#scrollTo=5fCEDCU_qrC0

首先,我们要先在自己的Google云端硬盘中心增Colab的档案,我们需要在云端硬盘的空白处点击右键→更多→链接更多应用程序

image0098_1e853abf145f9d217a5ed1cac4bb023cc67cc29e.png

在上方搜寻列输入「Colab」就可以进行安装

image0104_e40441f108ec8aef7806d53549a16f1f6a40a5ad.png

安装完再回到云端硬盘点击右键→更多→Google Colaboratory,接着就会看到跟下图一样的画面,这样就成功在你的云端硬盘中开启了Colab

image0115_7c42c02fd552a328780758eece1beb219d64f897.png

接下来有几个重要的部分,第一个要先启动你的GPU,编辑→笔记本设定→硬件加速器→GPU→储存,这样就完成GPU设定了。

image0125_587cf90b74497072501a2eb79d0be6005688b11d.png

我们可以使用各种AI框架的程序来检查,这边我使用PyTorch来检查GPU的状况,第一个print是确认GPU能否运作,第二个print是显示显示适配器的名称,我们将程序复制到区块里面,并且透过Shift+Enter执行程序。

import torch
print(torch.cuda.is_available())

print(torch.cuda.get_device_name())

image0138_6f6c54950ca474b0fbf8e68d96b86fb81098eb1a.png

 

YOLOv4在Colab上如何运作

到目前我们的事前准备已经完成一半了,接下来我们要测试一下在Colab上能否运作YOLOv4,Colab如果没有绑定云端硬盘的话它会自动分配一些空间给你暂存使用,所以这边我们直接使用暂存的空间来测试YOLOv4就可以了,后续会再教如何挂接到云端硬盘,首先一样要先将darknet的Github给Clone下来。

!git clone https://github.com/AlexeyAB/darknet.git

接着需要移动进去darknet的文件夹,在Colab这种交互式Python环境,可以透过%、! 来仿真终端机的指令,特别是cd只能透过%不能透过 !。

%cd ./darknet

在建构darknet之前需要先修改Makefile才行,这边使用Linux的指令sed,-i代表会直接替换档案内容,替换的模式选择s (search),第一个//包住的内容是要搜寻的内容,第二个//中的则是要替换掉的内容。

!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!sed -i 's/CUDNN_HALF=0/CUDNN_HALF=1/' Makefile
!sed -i 's/LIBSO=0/LIBSO=1/' Makefile

最后就可以开始建构了,大概需要两分至三分钟的时间。

!make

我们现在已经可以使用darknet的函式库了,进行推论前还需要下载训练好的权重,我们使用wget直接从网络上抓取,这些档案连结都可以在darknet的github中找到。

# 下载 yolov4-tiny
!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.weights
# 下载 yolov4
!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights

接着可以透过下列指令进行测试,coco.data 存放数据集的信息 像是图片大小、类别等等;yolov4.cfg 则是存放yolov4神经网络模型的信息;yolov4.weights 为刚刚下载的训练好的权重;data/dog.jpg 为输入的数据;-thresh 阀值 越大需要的信心指数越高。

!./darknet detector test ./cfg/coco.data ./cfg/yolov4.cfg ./yolov4.weights data/dog.jpg -i 0 -thresh 0.25

观察输出结果可以看到我们这张dog.jpg中有bicycle、dog、truck、pottedplant以及他们对应的信心指数,还有一个warning表达的是它没有屏幕可以显示,这个无伤大雅,我们可以通过直接在档案总管找到/darknet/predictions.jpg这张图片并点击两下开启查看:

image0143_589ca6176a5a9261f48725316ae62e37060bbb90.png

除此之外也可以透过下列的程序来将结果显示出来,因为matplotlib跟Jupyter有较高的兼容性,而Colab使用的是Jupyter Notebook的环境,我们可以透过 %matplotlib inline 这段程序让matplot的图表显示在Colab当中。

import cv2
import matplotlib.pyplot as plt

# 让 matplot 图表显示在Jupyter Notebook里面
%matplotlib inline

# 透过OpenCV读取图片
path = 'predictions.jpg'
img = cv2.imread(path)
  
# 在 Jupyter Notebook 上需要转换成 Matplot 显示才行
fig = plt.gcf()
fig.set_size_inches(18, 10)
plt.axis('off')
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()

接着我在网络上找到这段程序代码可以在Colab上运行实时影像辨识,第一段程序码表示的是透过Python建构一个Inference的副函式叫做 darknet_helper,通过这个darknet_helper可以获取到辨识结果与输出结果的宽高比例。

# import darknet functions to perform object detections
from darknet import *

# load in our YOLOv4 architecture network
network, class_names, class_colors = load_network("cfg/yolov4.cfg", "cfg/coco.data", "yolov4.weights")
width = network_width(network)
height = network_height(network)

# darknet helper function to run detection on image
def darknet_helper(img, width, height):
  darknet_image = make_image(width, height, 3)
  img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  img_resized = cv2.resize(img_rgb, (width, height),
                              interpolation=cv2.INTER_LINEAR)

  # get image ratios to convert bounding boxes to proper size
  img_height, img_width, _ = img.shape
  width_ratio = img_width/width
  height_ratio = img_height/height

  # run model on darknet style image to get detections
  copy_image_from_bytes(darknet_image, img_resized.tobytes())
  detections = detect_image(network, class_names, darknet_image)
  free_image(darknet_image)
  return detections, width_ratio, height_ratio

第二段程序代码则是如何在Colab上运作实时影像的部分,由于是Javascript的程序所以我也不多作介绍了,有兴趣的可以在自己研究。

# import dependencies
from IPython.display import display, Javascript, Image
from google.colab.output import eval_js
from google.colab.patches import cv2_imshow
from base64 import b64decode, b64encode
import cv2
import numpy as np
import PIL
import io
import html
import time
import matplotlib.pyplot as plt
%matplotlib inline

# function to convert the JavaScript object into an OpenCV image
def js_to_image(js_reply):
  """
  Params:
          js_reply: JavaScript object containing image from webcam
  Returns:
          img: OpenCV BGR image
  """
  # decode base64 image
  image_bytes = b64decode(js_reply.split(',')[1])
  # convert bytes to numpy array
  jpg_as_np = np.frombuffer(image_bytes, dtype=np.uint8)
  # decode numpy array into OpenCV BGR image
  img = cv2.imdecode(jpg_as_np, flags=1)

  return img

# function to convert OpenCV Rectangle bounding box image into base64 byte string to be overlayed on video stream
def bbox_to_bytes(bbox_array):
  """
  Params:
          bbox_array: Numpy array (pixels) containing rectangle to overlay on video stream.
  Returns:
        bytes: Base64 image byte string
  """
  # convert array into PIL image
  bbox_PIL = PIL.Image.fromarray(bbox_array, 'RGBA')
  iobuf = io.BytesIO()
  # format bbox into png for return
  bbox_PIL.save(iobuf, format='png')
  # format return string
  bbox_bytes = 'data:image/png;base64,{}'.format((str(b64encode(iobuf.getvalue()), 'utf-8')))

  return bbox_bytes

# JavaScript to properly create our live video stream using our webcam as input
def video_stream():
  js = Javascript('''
    var video;
    var div = null;
    var stream;
    var captureCanvas;
    var imgElement;
    var labelElement;
    
    var pendingResolve = null;
    var shutdown = false;
    
    function removeDom() {
       stream.getVideoTracks()[0].stop();
       video.remove();
       div.remove();
       video = null;
       div = null;
       stream = null;
       imgElement = null;
       captureCanvas = null;
       labelElement = null;
    }
    
    function onAnimationFrame() {
      if (!shutdown) {
        window.requestAnimationFrame(onAnimationFrame);
      }
      if (pendingResolve) {
        var result = "";
        if (!shutdown) {
          captureCanvas.getContext('2d').drawImage(video, 0, 0, 640, 480);
          result = captureCanvas.toDataURL('image/jpeg', 0.8)
        }
        var lp = pendingResolve;
        pendingResolve = null;
        lp(result);
      }
    }
    
    async function createDom() {
      if (div !== null) {
        return stream;
      }

      div = document.createElement('div');
      div.style.border = '2px solid black';
      div.style.padding = '3px';
      div.style.width = '100%';
      div.style.maxWidth = '600px';
      document.body.appendChild(div);
      
      const modelOut = document.createElement('div');
      modelOut.innerHTML = "<span>Status:</span>";
      labelElement = document.createElement('span');
      labelElement.innerText = 'No data';
      labelElement.style.fontWeight = 'bold';
      modelOut.appendChild(labelElement);
      div.appendChild(modelOut);
           
      video = document.createElement('video');
      video.style.display = 'block';
      video.width = div.clientWidth - 6;
      video.setAttribute('playsinline', '');
      video.onclick = () => { shutdown = true; };
      stream = await navigator.mediaDevices.getUserMedia(
          {video: { facingMode: "environment"}});
      div.appendChild(video);

      imgElement = document.createElement('img');
      imgElement.style.position = 'absolute';
      imgElement.style.zIndex = 1;
      imgElement.onclick = () => { shutdown = true; };
      div.appendChild(imgElement);
      
      const instruction = document.createElement('div');
      instruction.innerHTML = 
          '<span style="color: red; font-weight: bold;">' +
          'When finished, click here or on the video to stop this demo</span>';
      div.appendChild(instruction);
      instruction.onclick = () => { shutdown = true; };
      
      video.srcObject = stream;
      await video.play();

      captureCanvas = document.createElement('canvas');
      captureCanvas.width = 640; //video.videoWidth;
      captureCanvas.height = 480; //video.videoHeight;
      window.requestAnimationFrame(onAnimationFrame);
      
      return stream;
    }
    async function stream_frame(label, imgData) {
      if (shutdown) {
        removeDom();
        shutdown = false;
        return '';
      }

      var preCreate = Date.now();
      stream = await createDom();
      
      var preShow = Date.now();
      if (label != "") {
        labelElement.innerHTML = label;
      }
            
      if (imgData != "") {
        var videoRect = video.getClientRects()[0];
        imgElement.style.top = videoRect.top + "px";
        imgElement.style.left = videoRect.left + "px";
        imgElement.style.width = videoRect.width + "px";
        imgElement.style.height = videoRect.height + "px";
        imgElement.src = imgData;
      }
      
      var preCapture = Date.now();
      var result = await new Promise(function(resolve, reject) {
        pendingResolve = resolve;
      });
      shutdown = false;
      
      return {'create': preShow - preCreate, 
              'show': preCapture - preShow, 
              'capture': Date.now() - preCapture,
              'img': result};
    }
    ''')

  display(js)
  
def video_frame(label, bbox):
  data = eval_js('stream_frame("{}", "{}")'.format(label, bbox))
  return data

最后的流程如下:取得影像,将影像转换成特定格式并且辨识,将结果绘制到特定图层,将图层覆盖上去并且更新画面的内容。

# 开启影像串流
video_stream()
# 标题
label_html = 'Capturing...'
# 初始化参数
bbox = ''
count = 0 

while True:

  # 显示并取得影像
  js_reply = video_frame(label_html, bbox)
  if not js_reply:
      break

  # 将影像转换成OpenCV的格式
  frame = js_to_image(js_reply["img"])

  # 建立边界框的底图
  bbox_array = np.zeros([480,640,4], dtype=np.uint8)

  # 进行辨识
  detections, width_ratio, height_ratio = darknet_helper(frame, width, height)

  # 绘制边界框于刚刚建立的bbox_array
  for label, confidence, bbox in detections:
    left, top, right, bottom = bbox2points(bbox)
    left, top, right, bottom = int(left * width_ratio), int(top * height_ratio), int(right * width_ratio), int(bottom * height_ratio)
    bbox_array = cv2.rectangle(bbox_array, (left, top), (right, bottom), class_colors[label], 2)
    bbox_array = cv2.putText(bbox_array, "{} [{:.2f}]".format(label, float(confidence)),
                      (left, top - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5,
                      class_colors[label], 2)

  bbox_array[:,:,3] = (bbox_array.max(axis = 2) > 0 ).astype(int) * 255
  # 将 bbox_array转换成可以输入到画面上的 byte 格式
  bbox_bytes = bbox_to_bytes(bbox_array)
  
  # 更新bbox这样下一次画面中的画面才会更新
  bbox = bbox_bytes

结语

到这边你已经将基础观念都已经摸熟了,包括怎么去Transfer Learning的基本概念以及 YOLOv4如何在Colab上运作,接下来我们就进入Transfer Learning的实作的部分吧!

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