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

Pytorch深度學習框架X NVIDIA JetsonNano應用-YOLOv5辨識台灣即時路況 (繁體)

作者

嘉鈞

難度

理論困難,實作普通

材料表

  1. NVIDIA Jestson Nano Developer Kit
  2. 64 GB SD卡
  3. 電源供應器
  4. 無線網卡(可以用網路線代替)

前景提要

當時YOLOv4出了沒多久YOLOv5就悄然推出了,但是你可以發現v5其實不是v4的團隊 (Alexey Bochkovskiy, Chien-Yao Wang, Hong-Yuan Mark Liao) 所做,更不是YOLO之父 (Joseph Redmon) 所做,而是一間名為Ultralytics LLC的公司所開發 ( 之前一直有發布關於YOLO轉成PyTorch的Github ),在YOLOv5發布之前也沒發布論文來佐證YOLOv5,很多人對它的存在感到懷疑也鬧出很大的風波。不過它是基於PyTorch實現,架構與YOLOv3、v4的DarkNet環境截然不同,對於再修改跟開發上比較簡單,接著就跟我一起利用Jetson Nano來完成YOLOv5的實作吧!

如果你想了解更多可以看看Ultralytics LLC出面說明YOLOv4與YOLOv5差異-https://blog.roboflow.com/yolov4-versus-yolov5/,而Ultralytics也有推出基於YOLO的APP ( 僅限IOS ),現在也更新到YOLOv5了。

1_i_detection_ff8706754bd4d3940fabdae383c8e0c5b256f55e.png

訓練環境

先前都直接用原生的系統來裝套件,有時候不同的專案會需要不同的版本,為了將其獨立開來建議是使用虛擬環境來安裝比較合適。今天會稍微介紹一下虛擬環境的部分,我在Windows上常會使用Anaconda而Jetson Nano上因為Anaconda不支援aarch64 (Arm64) 的核心所以要另外編譯,非常麻煩!所以我直接使用Virtualenv ( 另一個輕便的虛擬環境套件)。

安裝 virtualenv以及virtualenvwrapper:

$ sudo pip3 install virtualenv virtualenvwrapper

修改環境變數:

$ nano ~/.bashrc
export WORKON_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
export /usr/local/bin/virtualenvwrapper.sh

建置虛擬環境:

$ mkvirtualenv yolov5

開啟虛擬環境:

$ workon yolov5

可以看到前面會多一個括弧(env_name)就是你目前的環境名稱:

2_virtualenv_d74f444ff3ca13d77873f97ed6e5549cf439eff7.png

接下來先安裝git,因為要下載YOLOv5:

$ sudo apt-get install git-all -y

下載YOLOv5 的Github:

$ git clone https://github.com/ultralytics/yolov5.git

接著安裝所需套件,可以先打開requirements.txt來看看所需套件,不管是Raspberry Pi 還是 Jetson系列,安裝PyTorch或OpenCV都有特定的方式或來源,所以我在嘗試別人的Github時都會分別開來安裝。

3_required_txt_2f245ed24e4e4056812b0479103c013e0574053e.png

像是這邊可以看到Cython、Numpy都在安裝PyTorch的時候會一起安裝,而OpenCV因為原生就有所以用Link的方式就可以了,所以我們先來處理比較特別的OpenCV跟PyTorch。

4_nano_pytorch_5556ce099eebfaf86e22457c78052bdfb8de7d78.png

這是官方提供的教學PyTorch for Jetson - version 1.6.0 now available,首先在YOLOv5提出的安裝套件可以看到建議是1.6以上,目前只有JetPack4.4才能支援PyTorch 1.6哦!請特別注意自己的JetPack版本

5_nano_jetpack_a6e897bebccca25a0114e550fb3513415f8d18ef.png

安裝PyTorch

$ wget https://nvidia.box.com/shared/static/9eptse6jyly1ggt9axbja2yrmj6pbarc.whl -O torch-1.6.0-cp36-cp36m-linux_aarch64.whl
$ sudo apt-get install python3-pip libopenblas-base libopenmpi-dev 
$ pip3 install Cython
$ pip3 install numpy torch-1.6.0-cp36-cp36m-linux_aarch64.whl

安裝torchvision

$ sudo apt-get install libjpeg-dev zlib1g-dev
$ git clone --branch v0.7.0 https://github.com/pytorch/vision torchvision
$ cd torchvision
$ export BUILD_VERSION=0.7.0 
$ sudo python setup.py install

這部分大概10分鐘內能搞定,可以透過導入函式庫查看版本來確認是否安裝成功:

6_nano_pytorch_check_623e049e44666977bcd931f3faa7395d51e61fa2.png

接著要找到原生的OpenCV位置:

$ sudo find / -name cv2

7_nano_findcv_b6127d7b472ae28245028ed6cfa65485e7826e08.png

尋找 .so 檔案,大家應該都一樣會在 /usr/lib/python3.6/dist-packages/cv2/python-3.6/ 裡面:

8_nano_getcv_5ddb91bb730a701f067e182e18b3379c87d56dcd.png

接著就要建立連結,使用ln 指令:

$ ln -s /usr/lib/python3.6/dist-packages/cv2/python-3.6/cv2.cpython-36m-aarch64-linux-gnu.so ~/.virtualenvs/{your env}/cv2.cpython-36m-aarch64-linux-gnu.so

9_nano_link_cv_87a472b8115a855145c434b0d75cacd1cb07a10b.png

確認是否安裝成功:

10_nano_check_cv_8a3bf0b6de371d76358c3327ac73236bdbfbfa1f.png

剩下還沒安裝的套件整理一下會變成下面

$ pip3 install matplotlib pillow pyyaml tensorboard tqdm scipy

其中scipy可能原本就有了,由於它安裝要好一陣子所以我建議如果有先執行看看,不行再安裝。

執行YOLOv5的範例程式

使用Github範例程式 detect.py,範例程式主要會用到的引數

--source

圖片、影片目錄或是 0 開啟攝影機

--iou_thred

信心指數的閥值,雖然低一些可框出越多東西但準確度就不敢保證

--weights

權重,有s、m、l、x之分

我們可以下載訓練好的weights,可以利用作者的download_weight.sh來下載,他需要用到util資料夾的程式所以我有移動到上一層目錄,嫌麻煩的當然也可以直接到他的GoogleDrive下載。

$ cd weights/
$ mv download_weights.sh ..
$ cd ..
$ bash download_weights.sh

接著可以執行detec.py,我們先使用官網提供的範例圖來測試:

$ python detect.py --source inference/images/ --weights weights/yolov5s.pt

運行結果如下:

11_run_yolo5_f0cef8c6c417449160eb20bc37f77e026423e76a.png

對兩張圖片進行推論,耗費時間為18秒:

12_1_bus_org_e98a8475267f82103134c8a942612e95ba83bd07.jpg 12_2_bus_res_d8626ac69e4ee58ee4a40d59b29bd250e87fa46f.jpg
13_1_zidane_org_244323d057afd16e6e741fdb44d7f0cb207098f2.jpg 13_2_zidane_res_83adc5eaec9ed424964e55f0995d0aecc295504f.jpg

還可以接上相機做即時影像偵測:

14_yolo5_camera_03fa40683e07eda488a2c4ac294ac04070d01360.png

比較各模型差異

這邊我們拿YOLOv3跟YOLOv5來做比較,可以注意到v3-tiny雖然秒數最少但運行結果不盡理想;然後v5-s目前看起來秒數少,框出來的物品多準確度也蠻高的;v5-l 、v3-spp準確度高但是也會框到一些錯誤的物品。

15_yolo35_compared_e8e8242e89f451a362e9808f5baa84b38c8d01f0.png

拿YOLOv5來應用在即時路況影像

對於Jetson 系列的開發版相當多人會拿來做自駕車專案,而YOLO所訓練的coco_2017數據集也能用於偵測人、車,所以我們先直接拿pre-trained model來實際運行看看,第一步是要取得到即時路況影像,這樣類型的影像直接用手機路就太過時了,所以我們來玩點不一樣的,我們可以到到下列這個網站獲取「即時影像監視器

https://tw.live/

這個網站有各式各樣的台灣路況可以查看,而這些都是即時影像。

16_taiwain_road_conditions_430ae9711c4737f041bf83953123466d1d3a22e1.png

仔細看了一下西門町的路口監視器畫面比較清晰也人多,所以最後我選擇西門町的即時影像,接下來就要考慮一個大問題了~我該如何將直播影片給下載下來!

17_ximending_f22d13459fd5b8b5320b125a25e1e8ffeafdb09f.png

其實你可以發現它從Youtube直播影片連動過來的,所以我寫了這支程式用來擷取Youtube影像直播,主要利用pafy跟vlc來下載mp4影片,並且利用moviepy來剪輯預設秒數,首先先安裝相關套件:

$ pip install pafy youtube-dl python-vlc moviepy

因為moviepy跟影片有關要安裝相關的編碼格式,Windows本身就有了但是Linux需要額外安裝,透過pip安裝在虛擬環境中是行不通的,目前安裝在本身的環境:

$ sudo apt install ffmpeg

接著就是主要程式的部分:

def capture_video(opt):

    f_name = 'org.mp4'      # 下載影片名稱
    o_name = opt.output     # 裁剪影片名稱     
    sec = opt.second        # 欲保留秒數
    video = pafy.new(opt.url)   # 取得Youtube影片
    
    r_list = video.allstreams   # 取得串流來源列表

    print_div()
    for i,j in enumerate(r_list): print( '[ {} ] {} {}'.format(i,j.title,j))
    idx = input('\nChoose the resolution : ')
    
    if idx:
        ### 選擇串流來源
        trg = r_list[int(idx)]
        print_div('您選擇的解析度是: %s'%(trg))
        
        ### 下載串流
        vlcInstance = vlc.Instance()
        player = vlcInstance.media_player_new()    # 創建一個新的MediaPlayer實例
        media = vlcInstance.media_new(trg.url)     # 創建一個新的Media實例
        media.get_mrl()
        media.add_option(f"sout=file/ts:{f_name}") # 將媒體直接儲存 
        player.set_media(media)                    # 設置media_player將使用的媒體
        player.play()                              # 播放媒體
        time.sleep(1)                              # 等待資訊跑完

        ### 進度條、擷取影片
        clock(sec)                                 # 播放 sec 秒 同時運行 進度條
        cut_video(f_name, sec, o_name)             # 裁切影片,因為停n秒長度不一定是n

        ### 關閉資源
        player.stop()                              # 關閉撥放器以及釋放媒體

其餘副函式,大部分是美觀用,像是用來取得終端機視窗大小以及打印分隔符號等,在clock的部分費了些心思寫了類似tqdm的進度條,最後cut_video就是剪取影片,從第0秒到第n秒:

### 取得terminal視窗大小
def get_cmd_size():
    import shutil
    col, line = shutil.get_terminal_size()
    return col, line

### 打印分隔用
def print_div(text=None):
    col,line = get_cmd_size()
    col = col-1
    if text == None:
        print('-'*col)
    else:
        print('-'*col)
        print(text)
        print('-'*col)

### 計時、進度條
def clock(sec, sign = '#'):
    col, line = get_cmd_size()
    col_ = col - 42
    bar_bg = ' '*(col_)
    print_div()

    for i in range(sec+1):
        
        bar_idx = (col_//sec*(i+1))
        bar = ''
        for t in range(col_): bar += sign if t <= bar_idx else bar_bg[t]
        
        percent = int(100/sec*(i))
        end = '\n' if i==sec else '\r'
        print('Download Stream Video [{:02}/{:02}s] [{}] ({:02}%)'.format(i, sec, bar, percent), end=end)
        time.sleep(1)

### 擷取特定秒數並儲存
def cut_video(name, sec, save_name):
    print_div()
    print('Cutting Video Used Moviepy\n')
    ffmpeg_extract_subclip(name, 0, sec, targetname=save_name)
    print_div(f'save file {save_name}')

為了使用更方便,我增加了argparse命令列選項,「-u」為Youtube連結;「-o」為輸出影片名稱;「-s」為輸出影片秒數:

if __name__=='__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-u', '--url', help='youtube url')
    parser.add_argument('-o', '--output', type=str, default='sample.mp4' , help='save file path\name')
    parser.add_argument('-s', '--second',type=int, default=10 , help='video length')
    opt = parser.parse_args()
    capture_video(opt)

執行結果:

$ python capture_livestream.py -u 'https://www.youtube.com/watch?v=iVpOdRU0r9s&feature=emb_title&ab_channel=%E8%87%BA%E5%8C%97%E5%B8%82%E6%94%BF%E5%BA%9CTaipeiCityGovernment' -o test.mp4 -s 10

18_capture_stream_video_904f0e42e24cc5be0c26967b7fbf772d3596f55b.png

接著可以直接執行範例程式來運行看看:

$ python detect.py --source test.mp4 --weights yolov5s.pt

19_run_sample_code_via_stream_13c5b21919906761142ac7d7447abb2efc8447dd.png

結果如下:

20_streamvideo_result_92df510440035ed007b81cdf0773c17b0a7ef33a.png

使用Jetson Nano運行平均0.165秒一幀,10秒的影片總共耗費83秒完成,這邊提供運行完的影片給大家參考:

個人覺得這樣的影片無法評估Nano效能是好是壞,所以我修改了一下範例程式將它變成即時影像辨識的方式,程式中稍微計算了FPS大概在5左右,一個順暢的影片FPS至少要在30以上,所以可以看到有些許的卡頓,當然我用遠端也有可能造成更多的Delay:

修改的內容相當簡單,就是將原本要讀取照片或影片的部分擷取出來,改成只有照片,並且在一開始讀檔的方式改成用OpenCV讀取影像,最終修改後的程式如下:

import argparse
import os
import platform
import shutil
import time
from pathlib import Path
import numpy as np

import cv2
import torch
import torch.backends.cudnn as cudnn
from numpy import random

from models.experimental import attempt_load
from utils.datasets import LoadStreams, LoadImages
from utils.general import (
    check_img_size, non_max_suppression, apply_classifier, scale_coords,
    xyxy2xywh, plot_one_box, strip_optimizer, set_logging)
from utils.torch_utils import select_device, load_classifier, time_synchronized

# Image process
###############################################################################

def img_preprocess(img0, img_size=640):
    
    # Padded resize
    img = letterbox(img0, new_shape=img_size)[0]

    # Convert
    img = img[:, :, ::-1].transpose(2, 0, 1)  # BGR to RGB, to 3x416x416
    img = np.ascontiguousarray(img)

    # cv2.imwrite(path + '.letterbox.jpg', 255 * img.transpose((1, 2, 0))[:, :, ::-1])  # save letterbox image
    return img, img0

# Get target size
###############################################################################

def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True):
    # Resize image to a 32-pixel-multiple rectangle https://github.com/ultralytics/yolov3/issues/232
    shape = img.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better test mAP)
        r = min(r, 1.0)

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
    if auto:  # minimum rectangle
        dw, dh = np.mod(dw, 64), np.mod(dh, 64)  # wh padding
    elif scaleFill:  # stretch
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]  # width, height ratios

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return img, ratio, (dw, dh)

def print_div(text):

    print(text, '\n')
    print('='*40,'\n')

# Detect func
###############################################################################
def detect(save_img=False):
    print_div('INTIL')
    out, source, weights, view_img, save_txt, imgsz = \
        opt.output, opt.source, opt.weights, opt.view_img, opt.save_txt, opt.img_size

    # Initialize
    print_div('GET DEVICE')
    set_logging()
    device = select_device(opt.device)
    half = device.type != 'cpu'  # half precision only supported on CUDA
    
    # Load model
    print_div('LOAD MODEL')
    model = attempt_load(weights, map_location=device)  # load FP32 model
    imgsz = check_img_size(imgsz, s=model.stride.max())  # check img_size
    if half:
        model.half()  # to FP16

    # Second-stage classifier
    print_div('LOAD MODEL_CLASSIFIER')
    classify = False
    if classify:
        modelc = load_classifier(name='resnet101', n=2)  # initialize
        modelc.load_state_dict(torch.load('weights/resnet101.pt', map_location=device)['model'])  # load weights
        modelc.to(device).eval()

    # Get names and colors
    print_div('SET LABEL COLOR')
    names = model.module.names if hasattr(model, 'module') else model.names
    colors = [[random.randint(0, 255) for _ in range(3)] for _ in range(len(names))]

    # Run inference
    ###############################################################################
    print_div("RUN INFERENCE")
    
    img = torch.zeros((1, 3, imgsz, imgsz), device=device)  # init img
    _ = model(img.half() if half else img) if device.type != 'cpu' else None  # run once

    video_path = source
    cap = cv2.VideoCapture(video_path)

    print_div('Start Play VIDEO')
    while cap.isOpened():
        
        ret, frame = cap.read()
        t0 = time.time()
        
        if not ret: 
            print_div('No Frame')
            break

        fps_t1 = time.time()

        img, img0 = img_preprocess(frame)   # img: Resize , img0:Orginal
        img = torch.from_numpy(img).to(device)
        img = img.half() if half else img.float()  # uint8 to fp16/32
        img /= 255.0  # 0 - 255 to 0.0 - 1.0
        if img.ndimension() == 3:
            img = img.unsqueeze(0)

        # Inference
        t1 = time_synchronized()
        pred = model(img, augment=opt.augment)[0]

        # Apply NMS : 取得每項預測的數值
        pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, classes=opt.classes, agnostic=opt.agnostic_nms)
        t2 = time_synchronized()

        # Apply Classifier : 取得該數值的LAbel
        if classify:
            pred = apply_classifier(pred, modelc, img, img0)
        
        # Draw Box
        for i, det in enumerate(pred):

            s = '%gx%g ' % img.shape[2:]  # print string
            gn = torch.tensor(img0.shape)[[1, 0, 1, 0]]  # normalization gain whwh
            if det is not None and len(det):
                # Rescale boxes from img_size to im0 size
                det[:, :4] = scale_coords(img.shape[2:], det[:, :4], img0.shape).round()

                # Print results
                for c in det[:, -1].unique():
                    n = (det[:, -1] == c).sum()  # detections per class
                    s += '%g %ss, ' % (n, names[int(c)])  # add to string

                # Write results
                for *xyxy, conf, cls in reversed(det):

                    label = '%s %.2f' % (names[int(cls)], conf)
                    plot_one_box(xyxy, img0, label=label, color=colors[int(cls)], line_thickness=3)

        # Print Results(inference + NMS)
        print_div('%sDone. (%.3fs)' % (s, t2 - t1))

        # Draw Image
        x, y, w, h = (img0.shape[1]//4), 25, (img0.shape[1]//2), 30
        cv2.rectangle(img0, (x, 10),(x+w, y+h), (0,0,0), -1)
        
        rescale = 0.5
        re_img0 = (int(img0.shape[1]*rescale) ,int(img0.shape[0]*rescale))

        cv2.putText(img0, '{} | inference: {:.4f}s | fps: {:.4f}'.format(opt.weights[0], t2-t1, 1/(time.time()-t0)),(x+20, y+20),cv2.FONT_HERSHEY_SIMPLEX,1,(0,0,255),2)
        cv2.imshow('Stream_Detected', cv2.resize(img0, re_img0) )

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

    # After break
    cap.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--weights', nargs='+', type=str, default='yolov5s.pt', help='model.pt path(s)')
    parser.add_argument('--source', type=str, default='inference/images', help='source')  # file/folder, 0 for webcam
    parser.add_argument('--output', type=str, default='inference/output', help='output folder')  # output folder
    parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)')
    parser.add_argument('--conf-thres', type=float, default=0.4, help='object confidence threshold')
    parser.add_argument('--iou-thres', type=float, default=0.5, help='IOU threshold for NMS')
    parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    parser.add_argument('--view-img', action='store_true', help='display results')
    parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
    parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --class 0, or --class 0 2 3')
    parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
    parser.add_argument('--augment', action='store_true', help='augmented inference')
    parser.add_argument('--update', action='store_true', help='update all models')
    opt = parser.parse_args()
    print(opt)

    with torch.no_grad():
        if opt.update:  # update all models (to fix SourceChangeWarning)
            for opt.weights in ['yolov5s.pt', 'yolov5m.pt', 'yolov5l.pt', 'yolov5x.pt']:
                detect()
                strip_optimizer(opt.weights)
        else:
            detect()

結語

YOLOv5縱使不是正統、最快的YOLO但是基於PyTorch實做的YOLO讓我們修改更加的方便,以往在DarkNet上運行現在只要裝好PyTorch基本就可以執行,檔案大小也差非常多,邊緣裝置的負擔也不會太大!大家可以去體驗看看YOLOv5的方便性、輕便性,下一篇文章中我會教大家如何使用YOLOv5來訓練自己的數據!

相關文章

在Jetson Nano (TX1/TX2)上使用Anaconda与PyTorch 1.1.0

https://zhuanlan.zhihu.com/p/64868319

YOLOv5 github

https://github.com/ultralytics/yolov5

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