【Paper】 Mask R-CNN 논문 핵심 정리

Mask R-CNN 논문의 핵심만 직접 정리해 적어놓은 내용입니다.

Mask R-CNN 논문 핵심 정리
이전 게시물도 참고하시는 것을 추천드립니다.

1. FCN (Fully Convolutional Network)

  • Pixel wise Classification : 이미지 각 pixel에 대한 Classification 수행
    = Pixel Grid by Grid로 Softmax값이 추출된다.
  • Segmentation Annotation 기법 2가지 방법
    • 1 depth를 이용하는 첫번째 방법
    • Class 갯수 depth를 이용하는 두번째 방법
    • coco - mask 데이터의 다각형 꼭지점 값 이용.

drawing

  • Encoder & Decoder 기법
    • Dimension Reduction 하여 응축된 정보를 가지고 (위치 정보 소실) 다시 복원(Upsampling) 하는 과정을 거친다.

drawing

  • FCN architecture : 사진 속 필기 확인하기
    • De-Convolution : convolution 기법을 반대로 수행하는 것이 아니다. 보간법(bilinear interpolation)을 사용한다. 이것을 통해서 복구를 하는 것이다. 최대한 비슷하게…

drawing

  • Feature Map을 혼합한 Sementic Segmentation
    • FCN 32 :
      (width, hight) -> (width/32, hight/32) -> (width, hight)
    • FCN 16 :
      (width, hight) -> (width/32, hight/32)x2 + (width/16, hight/16) -> (width, hight)
    • FCN 8 :
      (width, hight) -> (width/32, hight/32)x4 + (width/16, hight/16)x2 + (width/8, hight/8)-> (width, hight)

drawing

2. Mask RCNN

  • Mask R-CNN 핵심
    • Faster Rcnn + FCN
    • ROI Align
    • Binary Mask Rediction
    • 비교적 빠른 수행 시간
    • 비교적 높은 정확도
    • 직관적이고 상대적으로 쉬운 구현
  • 이전 게시물 핵심 이미지 - Mask-RCNN을 한눈에 표현함

drawing

  • ROI Pooling의 문제점
    • 아래 이미지 참조. quantization을 2번 거친다. (= 실수의 반올림을 2번 한다.)
    • 아래의 이미지의 2번쨰 quantization(B1)은 아래와 같은 과정을 거칠 수도 있지만,
      Grid의 나눔을 불균등하게 하는 방법(B2)도 있다. (B1 - 5 = 2 + 2 + (1 버림) OR B2- 5 = 3 + 2)
    • 때문에 정확한 Pooling이 이뤄지지 못한다.

    drawing

2-1 ROI Align - ROI Pooling 문제점 해결

  • ROI Align : 이전 Post 55 슬라이드 부터 참조. 가장 중요한 부분 첨부
    • 순서 정리(꼭 이 사이트 그림 참조)
      1. Input-EachROI. output-7x7(nxn) Pooled Feature.
      2. nxn등분(첫번째 quantization)
      3. 각4등분(두번째 quantization)->Bilinear interpolation
      4. 각4등분에 대해 Max Pooling->(nxn) Pooled Feature Map 획득.
      5. 아래 이미지 참조 (아래 빨간 메모 꼭 읽기)

drawing

  • ROI Pooling 대신에 ROI Align을 적용해서 AP 성능 향상을 3~4% 할 수 있었다.

2-2 Backbone과 Loss function

  • Fearture Extractor (BackBone) : Resnet + Feature Pyramid Network

  • Totoal Loss = L_cls + L_bbox + L_mask
    • L_cls = Multiclass cross-entropy loss (Softmax)
    • L_bbox = Smooth L1 loss
    • L_mask = Binary cross-entropy loss(해당 픽셀이 Classification에서 찾 Object class인지 아닌지(Sigmoid 사용))
  • Mask Prediction
    • 7 x 7 x 2048이 14 x 14 x 256으로 Upsampling된다.
    • 14 x 14 x 256은 14 x 14 x 80이 되어서, COCO 80개 Class에 대한 이진 Binary mask prediction값이 나온다. (아래 오른쪽 이미지의 Prediced Mask)
    • Prediced Mask(14 x 14)를 Upsampling하여 Resized Mask가 되도록 만든다.

drawing

2-3 성능

  • Instance Segmentation에서 좋은 성능이 나오는 것을 확인할 수 있다.

drawing

【Keras】Keras기반 Yolo3 - Google Open Image 학습시키기

이전 게시물에서 공부했던 Keras 기반 Yolo3를 이용해 Google Open Image 데이터 학습을 시켜보자.

Google Open Image 학습시키기 - Keras기반 Yolo3 이전의 Pascal VOC, MS coco 데이터 셋에 대해서는 이전 게시물 참조

1. Google Open Image 고찰 정리

  • 600개의 object 카테고리,
  • Object Detection Dataset 170만개 train 4만 validation 12만개 test
  • 하지만 데이터셋 용량이 너무 크다보니, COCO로 pretrained된 모델만 전체적으로 공유되는 분위기이다.
  • Version 4 : 2018 Kaggle Challenge Detection Bounding Box
  • Version 5 : 2019 Kaggle Challenge Detection Bounding Box + Instace Segmentation
  • Version 6 : 2020.02 https://storage.googleapis.com/openimages/web/index.html
  • Google Open Image Dataset 특징 : COCO와 비슷하지만, 용량은 훨씬 많다. (아래 이미지 확인)

drawing

  • 아래의 사진처럼, Download를 받아보면 Box의 정보가 CSV 파일로 되어있는 것을 확인할 수 있다. (특징 : Xmin Xmax Ymin Ymax가 0~1사이값으로 정규화가 되어있기 때문에 Image w,h를 곱해줘야 정확한 box위치라고 할 수 있다.)

drawing

  • 아래의 사진처럼, Metadate의 Class Names를 확인해보면 다음과 같이 ClassID와 Class 이름이 mapping되어 있는 것을 확인할 수 있다. (나중에 사용할 예정)

  • Class 구조(Hierarchy_bounding box label). (Download 사이트의 맨 아래로 가면 볼 수 있다.)

drawing

2. OIDv4 Toolkit

  • OIDv4 Toolkit Git 사이트
  • 방대한 Open Image를 전체 다운로드 받는 것은 힘드므로, 원하는 양의 이미지, 원하는 클래스의 이미지만 다운로드 할 수 있게 해주는 툴이다.
  • Xmin Xmax Ymin Ymax가 0~1사이값을 좌표로 자동 변환을 해준다.
  • 설치
      $ cd ~/DLCV/data
      $ git clone https://github.com/EscVM/OIDv4_ToolKit.git
      $ pip3 install -r requirements.txt
    
  • 다운로드 - 아래의 1,2,3,4 순서과정 주의하기
      $ python3 main.py downloader --classes Apple Orange --type_csv validation
    
      아래와 같은 예시로 다운로드 된다. 
       main_folder
      │   main.py
      │
      └───OID
          │   file011.txt
          │   file012.txt
          │
          └───csv_folder
          |    │   class-descriptions-boxable.csv         1. open image Metadata에 있는 class 이름
          |    │   validation-annotations-bbox.csv        2. open image의 annotation csv파일 그대로 다운로드
          |
          └───Dataset
              |
              └─── test
              |
              └─── train
              |
              └─── validation                         3. 위의 anotation csv 파일을 참조해서 apple과 orange에 관련된 이미지만 아래와 같이 다운로드 
                  |
                  └───Apple
                  |     |
                  |     |0fdea8a716155a8e.jpg
                  |     |2fe4f21e409f0a56.jpg
                  |     |...
                  |     └───Labels
                  |            |
                  |            |0fdea8a716155a8e.txt  4. 사과에 대한 내용만 annotation으로 csv 파일형식으로 txt파일로 만들어 놓는다. 
                  |            |2fe4f21e409f0a56.txt
                  |            |...
                  |
                  └───Orange
                      |
                      |0b6f22bf3b586889.jpg
                      |0baea327f06f8afb.jpg
                      |...
                      └───Labels
                              |
                              |0b6f22bf3b586889.txt
                              |0baea327f06f8afb.txt
                              |...
    
  • 위의 명령어에서 validation은 Google Open Image에서 validation에 있는 데이터 중에 사과와 오렌지에 관한 것만 다운로드를 해준다.
  • 직접 쳐서 다운로드 하고 어떻게 구성되어 있는지 직접 확인하기.

drawing

  • Keras-Yolo3에서 데이터를 활용하기 위한 변환 과정 (이미지 있는 코드 적극 활용 위해)
    1. OIDv4 Toolkit으로 데이터 다운로드
    2. VOC XML 타입으로 변경
    3. Keras yolo용 CSV로 변환

3. 데이터 다운로드 및 전처리 해보기(Keras-yolo3 용)

  • 아래 전체 과정 요약
    1. 내가 가진 데이터 전처리(패키지가 원하는 형식으로)
    2. Data Genereator에 넣기(torch같은 경우 TensorDataset, DataLoader를 이용해서)
    3. create_model 하기. (torch.nn 모듈을 사용해서 정의한 신경망 모델 클래스로, 객체 생성하기)
    4. Check point, log 생성을 위한 작업하기.(Keras 같은 경우 keras.callbacks 모듈의 함수 이용)
    5. model을 위한 optimizer, Loss function(criterion) 설정하기.
    6. model.fit_generator를 이용해서 학습 및 가중치 갱신 시키기. (from keras.models import Model에 있는 맴버함수이다.)
    7. .h5 형식의 Inference를 위한 가중치 파일을 얻을 수 있다.
    8. .h5 형식 파일을 이용해서, yolo.py함수의 YOLO 클래스를 이용해 객체 생성
    9. YOLO.detect_image를 이용해서 객체 탐지 수행
  1. 실습을 위한 데이터 다운로드
     $ python3 main.py downloader --classes Football Football_helmet Fish Shark Shellfish  --type_csv train --multiclasses 1 --limit 300
     # multiclasses : 다양한 class의 이미지를 위에처럼 다른 폴더에 넣는게 아니라 하나의 폴더에 묶어서 다운로드
     # limit : 카테고리당, 최대 이미지 수
     # Football_helmet라고 쳐도 되고, 'Football helmet'라고 해도 된다.
    
  2. Label 폴더의 annotation.txt 파일을 모두 VOC XML 파일로 바꾼다.
    • Label 폴더의 annotation.txt 파일들이 각각 이미지 하나에 txt파일 이미지 하나로 맴핑된다.
    • 이것은 VOC XML 파일 형식과 매우 유사한 것이다. (이미지 하나당, XML 파일 하나)
    • 따라서 annotation.txt 파일을 XML 파일로 바꾼다.
    • Git에 돌아니는, oid_to_pascal_voc_xml.py 파일을 이용할 것이다. 해당 파일을 보면 알겠듯이, OIDv4_ToolKit repository가 있는 그 위치에 파일이 존재해야한다.
     $ cd ~/DLCV/data/util
     $ cp oid_to_pascal_voc_xml.py ~/DLCV/data/OIDv4_ToolKit
     $ cd ~/DLCV/data/OIDv4_ToolKit
     $ python oid_to_pascal_voc_xml.py 
    
    • 아래처럼 xml파일이 /train/ballnfish 파일 바로 아래에 jpg파일과 같은 위치에 생성된 것을 알 수 있다.

    drawing

  3. 이전 Post 내용(라쿤데이터셋 전처리)을 활용해서 위의 XML파일을 CSV로 전환
    • 새로운 위치에 이미지와 xml파일 옮기기
     $ cd ~/DLCV/data
     $ mkdir ballnfish
     $ cd ./ballnfish
     $ mkdir annotation
     $ mkdir images
     $ cd ~/DLCV/data/OIDv4_ToolKit/OID/Dataset/train/ballnfish 
     $ cp *.jpg  ~/DLCV/data/ballnfish/images
     $ cp *.xml  ~/DLCV/data/ballnfish/annotation
    
    • images에 들어가면 이미지가 1499개 있다. (하나의 카테코리당 300개 이미지)
    • 이전에 했던 라쿤데이터셋 전처리 함수 이용하기 xml_to_csv 함수 찾아 이용.
    • 여기서부터 참조 : /DLCV/Detection/yolo/KerasYolo_OpenImage_학습및_Detection.ipynb
  • Open Image Dataset의 Object Detection 학습 및 Inference
    • Open Image Dataset에서 Football 관련 Object, Fish관련 Object를 추출 후 학습 데이터 세트 생성.
    • 이를 이용하여 Object Detection 수행.
# annotation과 image 디렉토리 설정. annotation디렉토리에 있는 파일 확인. 
import os
from pathlib import Path

HOME_DIR = str(Path.home())

ANNO_DIR = os.path.join(HOME_DIR, 'DLCV/data/ballnfish/annotations')
IMAGE_DIR = os.path.join(HOME_DIR, 'DLCV/data/ballnfish/images')
print(ANNO_DIR)

files = os.listdir(ANNO_DIR)
print('파일 개수는:',len(files))
print(files)
!cat /home/sb020518/DLCV/data/ballnfish/annotations/e5e4aba50208e92f.xml
<annotation>
  <folder>vallnfish</folder>
  <filename>e5e4aba50208e92f.jpg</filename>
  <path>/home/sb020518/DLCV/data/OIDv4_ToolKit/OID/Dataset/train/vallnfish/e5e4aba50208e92f.jpg</path>
  ...
  <object>
    <name>Football_helmet</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>685</xmin>
      <ymin>86</ymin>
      <xmax>778</xmax>
      <ymax>197</ymax>
    </bndbox>
  </object>
</annotation>
import glob
import xml.etree.ElementTree as ET

classes_map = {'Football':0, 'Football_helmet':1, 'Fish':2, 'Shark':3, 'Shellfish':4 }

def xml_to_csv(path, output_filename):
    xml_list = []
    # xml 확장자를 가진 모든 파일의 절대 경로로 xml_file할당. 
    with open(output_filename, "w") as train_csv_file:
        for xml_file in glob.glob(path + '/*.xml'):
            # xml 파일을 parsing하여 XML Element형태의 Element Tree를 생성하여 object 정보를 추출. 
            tree = ET.parse(xml_file)
            root = tree.getroot()
            # 파일내에 있는 모든 object Element를 찾음. 
            full_image_name = os.path.join(IMAGE_DIR, root.find('filename').text)
            value_str_list = ' '
            for obj in root.findall('object'):
                xmlbox = obj.find('bndbox')
                class_name = obj.find('name').text
                x1 = int(xmlbox.find('xmin').text)
                y1 = int(xmlbox.find('ymin').text)
                x2 = int(xmlbox.find('xmax').text)
                y2 = int(xmlbox.find('ymax').text)
                # 
                class_id = classes_map[class_name]
                value_str = ('{0},{1},{2},{3},{4}').format(x1, y1, x2, y2, class_id)
                # object별 정보를 tuple형태로 object_list에 저장. 
                value_str_list = value_str_list+value_str+' '
        
            train_csv_file.write(full_image_name+' '+ value_str_list+'\n')
        # xml file 찾는 for loop 종료 
xml_to_csv(ANNO_DIR, os.path.join(ANNO_DIR,'ballnfish_anno.csv'))
print(os.path.join(ANNO_DIR,'ballnfish_anno.csv'))
!cat /home/sb020518/DLCV/data/ballnfish/annotations/ballnfish_anno.csv
/home/sb020518/DLCV/data/ballnfish/images/6d3552b1e28f05c0.jpg  339,304,631,472,2 
/home/sb020518/DLCV/data/ballnfish/images/379a8df86070df59.jpg  361,312,498,445,1 76,39,108,77,1 120,42,171,92,1 
/home/sb020518/DLCV/data/ballnfish/images/ba993dcfb21071c7.jpg  177,345,753,678,2 
/home/sb020518/DLCV/data/ballnfish/images/1a51f73f96d2e88b.jpg  50,294,778,598,3 
/home/sb020518/DLCV/data/ballnfish/images/823873ff2c000a52.jpg  126,127,912,484,2 
/home/sb020518/DLCV/data/ballnfish/images/022ab76a12ee9808.jpg  0,0,1023,767,4 445,408,771,692,4 
/home/sb020518/DLCV/data/ballnfish/images/222e79f02c455fc5.jpg  64,5,942,613,3 
/home/sb020518/DLCV/data/ballnfish/images/0eade5d90ecc799e.jpg  111,35,139,53,1 135,85,175,116,1 170,25,200,42,1 225,42,255,71,1 264,100,309,149,1 280,46,304,67,1 479,37,510,71,1 552,55,588,81,1 604,81,637,113,1 709,69,746,109,1 
/home/sb020518/DLCV/data/ballnfish/images/de93f11430812481.jpg  445,256,566,346,2 264,443,307,509,2 553,320,658,376,2 796,251,865,289,2 
/home/sb020518/DLCV/data/ballnfish/images/94798692e8a62998.jpg  131,162,997,741,2 
/home/sb020518/DLCV/data/ballnfish/images/aff435118770ffce.jpg  355,208,753,530,3 

4. 모델 구성하고 학습시키기

  • 라쿤 데이터 학습과는 조금 많이 다르게 학습시킨다.
  • 좀 더 하나하나를 함수화 해서 체계적으로 코드를 구성해 나갈 계획이다.
import cv2
import matplotlib.pyplot as plt
%matplotlib inline

plt.imshow(cv2.cvtColor(cv2.imread('/home/sb020518/DLCV/data/ballnfish/images/a9b21c816203e357.jpg'), cv2.COLOR_BGR2RGB))
import numpy as np
import keras.backend as K
from keras.layers import Input, Lambda
from keras.models import Model
from keras.optimizers import Adam
from keras.callbacks import TensorBoard, ModelCheckpoint, ReduceLROnPlateau, EarlyStopping
import sys, os

LOCAL_PACKAGE_DIR = os.path.abspath("./keras-yolo3")
sys.path.append(LOCAL_PACKAGE_DIR)

from yolo3.model import preprocess_true_boxes, yolo_body, tiny_yolo_body, yolo_loss
from yolo3.utils import get_random_data
from train import get_classes, get_anchors
from train import create_model, data_generator, data_generator_wrapper

BASE_DIR = os.path.join(HOME_DIR, 'DLCV/Detection/yolo/keras-yolo3')

## 학습을 위한 기반 환경 설정. annotation 파일 위치, epochs시 저장된 모델 파일, Object클래스 파일, anchor 파일.
annotation_path = os.path.join(ANNO_DIR, 'ballnfish_anno.csv')
log_dir = os.path.join(BASE_DIR, 'snapshots/ballnfish/')
classes_path = os.path.join(BASE_DIR, 'model_data/ballnfish_classes.txt')
anchors_path = os.path.join(BASE_DIR,'model_data/yolo_anchors.txt')

class_names = get_classes(classes_path)
num_classes = len(class_names)
anchors = get_anchors(anchors_path)
print(class_names, num_classes)
print(anchors)
['Football', 'Football_helmet', 'Fish', 'Shark', 'Shellfish'] 5
[[ 10.  13.]
 [ 16.  30.]
 [ 33.  23.]
 [ 30.  61.]
 [ 62.  45.]
 [ 59. 119.]
 [116.  90.]
 [156. 198.]
 [373. 326.]]
  • yolo 모델 학습을 위한 전반적인 파라미터를 config 클래스로 설정하고 필요시 이를 수정하여 학습.
# csv annotation 파일을 읽어서 lines 리스트로 만듬. 
with open(annotation_path) as f:
    lines = f.readlines()

# 추후에 사용할 config 정보를 json, xml 등으로 적을 수 있겠지만, 여기서는 class변수 하나에 config에 대한 정보를 모아 놓았다. 
# 해당 cell의 맨 아래와 같이 맴버변수를 사용해서 환경변수 데이터를 불러올 예정이다.
class config:
    #tiny yolo로 모델로 초기 weight 학습 원할 시 아래를 tiny-yolo.h5로 수정. 
    initial_weights_path=os.path.join(BASE_DIR, 'model_data/yolo.h5' )
    # input_shape는 고정. 
    input_shape=(416, 416) 
    # epochs는 freeze, unfreeze 2 step에 따라 설정. 
    first_epochs=50
    first_initial_epochs=0
    second_epochs=100
    second_initial_epochs=50
    # 학습시 batch size, train,valid건수, epoch steps 횟수  
    batch_size = 2
    val_split = 0.1   
    num_val = int(len(lines)*val_split)
    num_train = len(lines) - num_val
    train_epoch_steps = num_train//batch_size 
    val_epoch_steps =  num_val//batch_size
    
    anchors = get_anchors(anchors_path)
    class_names = get_classes(classes_path)
    num_classes = len(class_names)
    # epoch시 저장된 weight 파일 디렉토리 
    log_dir = os.path.join(BASE_DIR, 'snapshots/ballnfish/')  
    
print('Class name:', config.class_names,'\nNum classes:', config.num_classes)
Class name: ['Football', 'Football_helmet', 'Fish', 'Shark', 'Shellfish'] 
Num classes: 5
  • create_generator 함수 만들기
    • csv 파일을 입력 받아서 train 데이터와 valid 데이터 처리를 위한 data_generator_wrapper객체를 각각 생성.
    • train용, valid 용 data_generator_wrapper는 Yolo 모델의 fit_generator()학습시 인자로 입력됨.
def create_generator(lines):
    
    train_data_generator = data_generator_wrapper(lines[:config.num_train], config.batch_size, 
                                                  config.input_shape, config.anchors, config.num_classes)
    
    valid_data_generator = data_generator_wrapper(lines[config.num_train:], config.batch_size, 
                                                  config.input_shape, config.anchors, config.num_classes)
    
    return train_data_generator, valid_data_generator
  • create_yolo_model 함수 만들기
    • YOLO 모델 또는 tiny yolo 모델 반환. 초기 weight값은 pretrained된 yolo weight값으로 할당.
# anchor 개수에 따라 tiny yolo 모델 또는 yolo 모델 반환. 
# tiny yolo 를 위한 config파일은 나중에 만들어 사용할 에정이다.
def create_yolo_model():
    is_tiny_version = len(config.anchors)==6 
    if is_tiny_version:
        model = create_tiny_model(config.input_shape, config.anchors, config.num_classes, 
            freeze_body=2, weights_path=config.initial_weights_path)
    else:
        model = create_model(config.input_shape, config.anchors, config.num_classes, 
            freeze_body=2, weights_path=config.initial_weights_path)
        
    return model 
  • callback 객체들을 생성.
    • Keras에서 많이 사용하는 checkpoint 저장 방식인듯 하다. 우선 이것도 모르지만 넘어가자.
# Tensorboard, ModelCheckpoint, ReduceLROnPlateau, EarlyStopping callback 반환
def create_callbacks():
    logging = TensorBoard(log_dir=config.log_dir)
    checkpoint = ModelCheckpoint(config.log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5',
        monitor='val_loss', save_weights_only=True, save_best_only=True, period=3)  # epoch 3마다 weight 저장
    reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)
    early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)
    # early_stopping에 의해 loss가 항상 비슷하면 학습을 자동 중단한다. patience를 늘리면 더 학습이 진행된다.
    
    #개별 callback들을 한꺼번에 list로 묶어서 반환
    return [logging, checkpoint, reduce_lr, early_stopping]
  • 위의 작업으로 모델 구성을 마쳤다. 학습 수행을 시작한다 {:.lead}
    • GPU : $ watch -d -n 1 nvidia-smi
    • CPU : $ vmstat 3
      us : 사용자 사용량 wa : wait cpu가 놀고 있는 량

drawing

   
# create_generator(), create_model(), create_callbacks() 수행. 
train_data_generator, valid_data_generator = create_generator(lines)
ballnfish_model = create_yolo_model()
callbacks_list = create_callbacks()

# 최초 모델은 주요 layer가 freeze되어 있음. 안정적인 loss를 확보하기 위해 주요 layer를 freeze한 상태로 먼저 학습. 
print('First train 시작' )
ballnfish_model.compile(optimizer=Adam(lr=1e-3), loss={'yolo_loss': lambda y_true, y_pred: y_pred})

#1단계 학습 완료 모델 저장. 
# 아래의 과정을 통해서 foward -> backpropagation -> weight 갱신 -> weight 저장
ballnfish_model.fit_generator(
                train_data_generator, 
                steps_per_epoch=config.train_epoch_steps,
                validation_data=valid_data_generator, 
                validation_steps=config.val_epoch_steps,
                epochs=config.first_epochs, 
                initial_epoch=config.first_initial_epochs, 
                callbacks=callbacks_list)
ballnfish_model.save_weights(log_dir + 'trained_weights_stage_1.h5')

# 2단계 학습 시작. 
# create_model() 로 반환된 yolo모델에서 trainable=False로 되어 있는 layer들 없이, 모두 True로 만들고 다시 학습 (뭔지 정확히 모르겠음)
# 모든 layer를 trainable=True로 설정하고 학습 수행. 
for i in range(len(ballnfish_model.layers)):
    ballnfish_model.layers[i].trainable = True
    
print('Second train 시작' )
ballnfish_model.compile(optimizer=Adam(lr=1e-4), loss={'yolo_loss': lambda y_true, y_pred: y_pred}) 
ballnfish_model.fit_generator(train_data_generator, steps_per_epoch=config.train_epoch_steps,
    validation_data=valid_data_generator, validation_steps=config.val_epoch_steps,
    epochs=config.second_epochs, initial_epoch=config.second_initial_epochs,
    callbacks=callbacks_list)

# 최종 학습 완료 모델 저장. 
ballnfish_model.save_weights(log_dir + 'trained_weights_final.h5')
  • 최종 학습된 모델을 로딩하여 Object Detection 수행.
    • 이제 위에서 만든 구성 및 모듈 다 필요없다.
    • 가장 마지막에 만들어진 Weight 파일인 /keras-yolo3/snapshots/ballnfish/trained_weights_final.h5 를 가지고 학습을 시작하자
from yolo import YOLO
#keras-yolo에서 image처리를 주요 PIL로 수행. 
from PIL import Image

LOCAL_PACKAGE_DIR = os.path.abspath("./keras-yolo3")
sys.path.append(LOCAL_PACKAGE_DIR)0

ballnfish_yolo = YOLO(model_path='/home/younggi.kim999/DLCV/Detection/yolo/keras-yolo3/snapshots/ballnfish/trained_weights_final.h5',
            anchors_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/yolo_anchors.txt',
            classes_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/ballnfish_classes.txt')
  • 이미지 Object Detection
    • 위에서 정의한 모델을 가지고 ballnfish_yolo.detect_image(img) 를 사용해서 detection 수행해 나가자!
football_list = ['f1b492a9bce3ac9a.jpg', '1e6ff631bb0c198b.jpg', '97ac013310bda756.jpg',
                'e5b1646c395aecfd.jpg', '53ef241dad498f6c.jpg', '02ccbf5ddaaecedb.jpg' ]
for image_name in football_list:
    img = Image.open(os.path.join(IMAGE_DIR, image_name))
    detected_img = ballnfish_yolo.detect_image(img)
    plt.figure(figsize=(8, 8))
    plt.imshow(detected_img)
    
helmet_list = ['1fed5c930211c6e0.jpg', '011a59a160d7a091.jpg', 'd39b46aa4bc0c165.jpg', '7e9eb7eba80e34e7.jpg', '9c27811a78b74a48.jpg']
for image_name in helmet_list:
    img = Image.open(os.path.join(IMAGE_DIR, image_name))
    detected_img = ballnfish_yolo.detect_image(img)
    plt.figure(figsize=(8, 8))
    plt.imshow(detected_img)
fish_list = ['25e42c55bfcbaa88.jpg', 'a571e4cdcfbcb79e.jpg', '872c435491f2b4d3.jpg', 
             'bebac23c45451d93.jpg', 'eba7caf07a26829b.jpg', 'dc607a2989bdc9dc.jpg' ]
for image_name in fish_list:
    img = Image.open(os.path.join(IMAGE_DIR, image_name))
    detected_img = ballnfish_yolo.detect_image(img)
    plt.figure(figsize=(8, 8))
    plt.imshow(detected_img)
shark_list = ['d92290f6c04dd83b.jpg', '3a37a09ec201cdeb.jpg', '32717894b5ce0052.jpg', 'a848df5dbed78a0f.jpg', '3283eafe11a847c3.jpg']
for image_name in shark_list:
    img = Image.open(os.path.join(IMAGE_DIR, image_name))
    detected_img = ballnfish_yolo.detect_image(img)
    plt.figure(figsize=(8, 8))
    plt.imshow(detected_img)
shell_list=['5cc89bc28084e8e8.jpg',  '055e756883766e1f.jpg', '089354fc39f5d82d.jpg', '80eddfdcb3384458.jpg']
for image_name in shell_list:
    img = Image.open(os.path.join(IMAGE_DIR, image_name))
    detected_img = ballnfish_yolo.detect_image(img)
    plt.figure(figsize=(8, 8))
    plt.imshow(detected_img)

drawing

5. 영상 Object Detection

  • 어렵지 않게 cv2.VideoCapture 함수와 ballnfish_yolo.detect_image(img) 를 사용해서 Detection 수행하면 끝!
import cv2
import time

def detect_video_yolo(model, input_path, output_path=""):
    
    start = time.time()
    cap = cv2.VideoCapture(input_path)
    
    #codec = cv2.VideoWriter_fourcc(*'DIVX')
    codec = cv2.VideoWriter_fourcc(*'XVID')
    vid_fps = cap.get(cv2.CAP_PROP_FPS)
    vid_size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    vid_writer = cv2.VideoWriter(output_path, codec, vid_fps, vid_size)
    
    frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    print('총 Frame 갯수:', frame_cnt, '원본 영상 FPS:',vid_fps, '원본 Frame 크기:', vid_size)
    index = 0
    while True:
        hasFrame, image_frame = cap.read()
        if not hasFrame:
            print('프레임이 없거나 종료 되었습니다.')
            break
        start = time.time()
        # PIL Package를 내부에서 사용하므로 cv2에서 읽은 image_frame array를 다시 PIL의 Image형태로 변환해야 함.  
        image = Image.fromarray(image_frame)
        # 아래는 인자로 입력된 yolo객체의 detect_image()로 변환한다.
        detected_image = model.detect_image(image)
        # cv2의 video writer로 출력하기 위해 다시 PIL의 Image형태를 array형태로 변환 
        result = np.asarray(detected_image)
        index +=1
        print('#### frame:{0} 이미지 처리시간:{1}'.format(index, round(time.time()-start,3)))
        
        vid_writer.write(result)
    
    vid_writer.release()
    cap.release()
    print('### Video Detect 총 수행시간:', round(time.time()-start, 5))
detect_video_yolo(ballnfish_yolo, '../../data/video/NFL01.mp4', '../../data/output/NFL_yolo_01.avi')
!gsutil cp ../../data/output/NFL_yolo_01.avi gs://my_bucket_dlcv/data/output/NFL_yolo_01.avi
detect_video_yolo(ballnfish_yolo, '../../data/video/FishnShark01.mp4', '../../data/output/FishnShark_yolo_01.avi')
!gsutil cp ../../data/output/FishnShark_yolo_01.avi gs://my_bucket_dlcv/data/output/FishnShark_yolo_01.avi
  • 분석결과 : 영상의 FPS가 작다. 그래서 빠르게 움직이는 물체들을 잡는 능력이 부족하다.
  • 영상의 FPS가 커지면, Model의 Inference 속도도 굉장히 빨라져야한다….
  • 쉽지 않은 문제이다.

drawing

drawing

【Keras】Keras기반 Yolo3 -라쿤 데이터 학습시키기 + GPU 학습 유의사항

GPU Object Detection시 유의 사항을 간단히 알아보고, 이전 게시물에서 공부했던 Keras 기반 Yolo3를 이용해 데이터 학습을 시켜보자.

GPU 학습 유의사항/ 라쿤 데이터 학습시키기 - Keras기반 Yolo3

1. GPU 학습 시킬 떄 주의 사항

  1. 대량의 이미지 학습시 메모리 문제
    • 알다시피 개별 이미지가 Tensor, narray로 변환해서 NeuralNet에 집어넣을떄 이미지를 Batch로 묶어 처리한다.
    • 이 Batch가 그래픽 카드 메모리 사용량의 많은 부분을 좌우한다.
      (1000장 - 10장 배치 * 100번 = 1 에폭)
    • GPU 사용 % 자주 확인하기. Image를 loading하는 것은 CPU에서 한다. Batch가 크면 GPU는 바쁘고 CPU는 논다. Batch가 작으면 GPU는 놀고 CPU는 바쁘다. 근데 Object Detection은 GPU에서 할게 너무 많아서 Batch를 작게하는게 대부분이다. (Batch = 1~16)
  2. Genrator와 Batch
    • 특히 fit_generator를 확인해보면, Python gererator를 사용한다. gerator, next, yield에 대해서 알아보자. 코딩도장 generator 설명
    • 배치 사이즈를 크게 늘린다고 학습이나 추론이 빨라지지 않는다. 100장의 이미지를 한방에 끌어오는것도 힘들지만, 그 이미지를 네트워크에 넣는다 하더라도 어딘가에서 병목이 걸릴건 당연하다. 따라서 배치 사이즈를 어떻게 설정하지는 CPU core, GPU 성능 등을 파악해서 균형있게 맞춰야 한다.(Heuristic 하게 Turning)
    • 한쪽은 열심히 일하고, 한쪽은 기다리는 현상이 일어나지 않게끔. 이렇게 양쪽이 일을 쉬지 않고 균형있게 배치하는 것이 필요하다.
    • 이러한 Batch를 Keras에서는 (fit_generator 함수가 사용하는) DataGenerator, flow_from_drectory 가 해준다. 알다시피 Torch는 TensorDataset, DataLoader가 해준다.
    • 이런 generator를 사용하기 위해서 Keras와 같은 경우에 다음과 같은 코드를 사용한다.

            from keras.preprocessing.image import ImageDataGenerator
      
            train_datagen = ImageDataGerator(rescale=1/255) # 이미지 정규화
            train_gernator = train_datagen.flow_from_director('학습용 이미지 Directory', targer_size = (240,240), batch_size-10, class_mode='categoricl') 
            valid_datagen = ImageDataGerator(rescale=1/255) 
            valid_gernator = valid_datagen.flow_from_director('검증용 이미지 Directory', targer_size = (240,240), batch_size-10, class_mode='categoricl')
      
            # 모델을 구성하고
            model = Sequential()
            model.add(Conv2D(32, kernel_size=(3,3), activation='relu', input_shape=(240,240,3)))
            model.add ...
            ...
            ...
            model.add(Dense(3, activation='softmax'))
            model.compile(loss='categorial_crossentropy', optimizer='adam', metrixs=['accuracy'])
      
            # 학습을 시킨다.
            model.fit_generator(train_generator, steps_per_epoch=100, epochs=10, validation_data=valid_generator, validation_steps=2)
      
    • 이렇게 fit_generator을 돌리면 data pipeline이 만들어 진다. train_generator가 디렉토리에 가서 배치만큼의 파일을 읽어와서 yeild를 하여 모델에 데이터를 넣는다.

    drawing

2. qqwweee/Keras-Yolo3로 Training 하기 위한 분석

주의사항{:.lead}

  • 아래의 일렬의 과정들은 패키지의 있는 함수들을 전적으로 사용합니다.
  • 따라서 그냥 보면 흐름을 이해하지 못할 수도 있습니다.
  • 꼭 패키지의 코드들과 같이 보는 것을 추천합니다.
  • 만약 케라스를 잘 모른다면, 그냥 다른 Data를 학습시킬때는 이런 일렬의 과정들이 있구나
    정도의 감을 잡는 것도 아주 좋은 공부 일 수 있습니다.
  • 아래 전체 과정 요약
    1. 내가 가진 데이터 전처리(패키지가 원하는 형식으로)
    2. Data Genereator에 넣기(torch같은 경우 TensorDataset, DataLoader를 이용해서)
    3. create_model 하기. (torch.nn 모듈을 사용해서 정의한 신경망 모델 클래스로, 객체 생성하기)
    4. Check point, log 생성을 위한 작업하기.(Keras 같은 경우 keras.callbacks 모듈의 함수 이용)
    5. model을 위한 optimizer, Loss function(criterion) 설정하기.
    6. model.fit_generator를 이용해서 학습 및 가중치 갱신 시키기. (from keras.models import Model에 있는 맴버함수이다.)
    7. .h5 형식의 Inference를 위한 가중치 파일을 얻을 수 있다.
    8. .h5 형식 파일을 이용해서, yolo.py함수의 YOLO 클래스를 이용해 객체 생성
    9. YOLO.detect_image를 이용해서 객체 탐지 수행
  • 지금까지는 1. Pretrained된 Weight값을 가져와서 Inference를 수행하거나(Tensorflow(SSD, Yolo), Keras-yolo3) 2. OpenCV DNN 모듈을 사용하거나 해왔다.
  • 참고 : 1개의 객체만 검출한다면 100장이어도 충분하다. 근데 Class가 여러개면 수 많은 이미지 데이터가 필요하다.
  • <weight 가중치 파일 형식 정리>
    • keras weight(케라스 가중치) 파일은 파일 형식이 .h5파일이다.
    • Tensorflow에서 Inference를 위한 파일 형식은 .pb 파일이다. .ckpt파일에는 학습에 대한check point에 대한 정보를 담고 있다.
    • PyTorch에서는 .pt 파일이 torch.save(model.state_dict()를 이용해서 저장한 weight파일이다.
  • qqwweee/keras-yolo3에 적힌 Training 방법 정리
    1. voc(xml 파일사용)을 annotation파일로 바꾸는 convert_annotation.py 파일을 살펴보면 최종적으로 ‘%s_%s.txt’%(year, image_set) 파일을 만드는 것을 확인할 수 있다.
    2. Readme의 내용을 따르면,
      각 한줄 : image_file_path box1 box2 … boxN
      boxK의 format : x_min,y_min,x_max,y_max,class_id (no space) 이다.
       path/to/img1.jpg 50,100,150,200,0 30,50,200,120,3
       path/to/img2.jpg 120,300,250,600,2
       ...
      
    3. 일단 pretrained된 weight를 가져와서 학습시키는 것이 좋으니, conver.py를 실행해서 darknet weight를 keras weight로 바꿔준다.
    4. train.py를 사용해서 학습을 한다. 내부 코드 적극 활용할 것.
    5. train.py에 들어가야하는 Option을 설정하는 방법은 원래의 default option이 무엇인지 확인하면 된다.

drawing

  • train.py을 적극적으로 이용할 계획이다. 아래와 같은 방법으로. 함수는 아래와 같이 import를 해서 사용할 것이고, main함수의 내용은 소스 코드 그대로 복사해 가져와서 사용할 예정이다.
          from train import get_classes, get_anchors
          from train import create_model, data_generator 
    

3. 라쿤 데이터셋으로 학습시키고 Inference하기

3-1 데이터 전처리 하기

  • Raccoon 데이터 세트 download 후
    • 이미지와 annoation 디렉토리를 제외하고 모두 삭제합니다.
    • images에 이미지가 있고 annoation에는 각이미지에 해당하는 xml파일 203개가 존재한다.
  • 아래의 내용들은 전처리를 하기에 아주 좋은 함수들과 모듈들이 잘 사용되고 있으므로 잘 알아두고 나중에도 꼭 잘 활용하자
# annotation과 image 디렉토리 설정. annotation디렉토리에 있는 파일 확인. 
import os
from pathlib import Path

HOME_DIR = str(Path.home())

ANNO_DIR = os.path.join(HOME_DIR, 'DLCV/data/raccoon/annotations')
IMAGE_DIR = os.path.join(HOME_DIR, 'DLCV/data/raccoon/images')
print(ANNO_DIR)

files = os.listdir(ANNO_DIR)
print('파일 개수는:',len(files))  # 200개
print(files)  # Dicrectory에 있는 파일 이름 전부가 list 변수에 저장된다.
import glob
import xml.etree.ElementTree as ET

def xml_to_csv(path, output_filename):
    """
    path : annotation Detectory
    filename : ouptut file name
    """
    xml_list = []
    # xml 확장자를 가진 모든 파일의 절대 경로로 xml_file할당. 
    with open(output_filename, "w") as train_csv_file:
        for xml_file in glob.glob(path + '/*.xml'):
            # path에 있는 xml파일중 하나 하나를 가져온다. 
            tree = ET.parse(xml_file) 
            root = tree.getroot()
            # 파일내에 있는 모든 object Element를 찾음. 
            full_image_name = os.path.join(IMAGE_DIR, root.find('filename').text)
            value_str_list = ' '
            # find all <object>인것 다 찾는다
            for obj in root.findall('object'): 
                xmlbox = obj.find('bndbox')
                x1 = int(xmlbox.find('xmin').text)
                y1 = int(xmlbox.find('ymin').text)
                x2 = int(xmlbox.find('xmax').text)
                y2 = int(xmlbox.find('ymax').text)
                # 단 하나의 class_id raccoon
                class_id = 0
                value_str = ('{0},{1},{2},{3},{4}').format(x1, y1, x2, y2, class_id)
                value_str_list = value_str_list+value_str+' ' 
                # box1 box2 ......
                # object별 정보를 tuple형태로 object_list에 저장. 
            train_csv_file.write(full_image_name+' '+ value_str_list+'\n') # image_file_path box1 box2 ... boxN \n ... image_file_path
        # xml file 찾는 for loop 종료 
xml_to_csv(ANNO_DIR, os.path.join(ANNO_DIR,'raccoon_anno.csv'))
print(os.path.join(ANNO_DIR,'raccoon_anno.csv'))
/home/sb020518/DLCV/data/raccoon/annotations/raccoon_anno.csv

3-2 전처리한 데이터로 학습시키기 - pretrain 모델가져오기

drawing

  • 위의 파일 내부의 함수들을 import할 예정이다. import방법은 아래와 같다. 참조하자
  • 사진의 model.py, utils.py, keras-yolo3의 train.py파일을 아래와 같이 import할 것이다.
import sys

LOCAL_PACKAGE_DIR = os.path.abspath("./keras-yolo3")
sys.path.append(LOCAL_PACKAGE_DIR)

from yolo3.model import preprocess_true_boxes, yolo_body, tiny_yolo_body, yolo_loss
from yolo3.utils import get_random_data 
# get_random_data - Image Augmentation 하기 위한 모듈 - Augmentation을 할 때 Bounding Box 처리도 해준다.
import numpy as np
import keras.backend as K
from keras.layers import Input, Lambda
from keras.models import Model
from keras.optimizers import Adam
from keras.callbacks import TensorBoard, ModelCheckpoint, ReduceLROnPlateau, EarlyStopping
from train import get_classes, get_anchors
from train import create_model, data_generator, data_generator_wrapper

BASE_DIR = os.path.join(HOME_DIR, 'DLCV/Detection/yolo/keras-yolo3')

## 학습을 위한 기반 환경 설정. annotation 파일 위치, epochs시 저장된 모델 파일, Object클래스 파일, anchor 파일.
annotation_path = os.path.join(ANNO_DIR, 'raccoon_anno.csv')
log_dir = os.path.join(BASE_DIR, 'snapshots/000/')  # tensorboard, epoch 당 weight 저장 장소로 사용할 될 예정이다.
classes_path = os.path.join(BASE_DIR, 'model_data/raccoon_class.txt')    # 내가 만듬 한줄 - raccoon
# tiny yolo로 모델을 학습 원할 시 아래를 yolo_anchors.txt' -> tiny_yolo_anchors.txt'로 수정. 
anchors_path = os.path.join(BASE_DIR,'model_data/yolo_anchors.txt')      # 그대로 사용

class_names = get_classes(classes_path)
num_classes = len(class_names)   # 1개
anchors = get_anchors(anchors_path)

# 아래는 원본 train.py에서 weights_path 변경을 위해 임의 수정. 최초 weight 모델 로딩은 coco로 pretrained된 모델 로딩. 
# tiny yolo로 모델을 학습 원할 시 아래를 model_data/yolo.h5' -> model_data/tiny-yolo.h5'로 수정. 
model_weights_path = os.path.join(BASE_DIR, 'model_data/yolo.h5' )

input_shape = (416,416) # yolo-3 416 을 사용하므로 이렇게 정의. 라쿤 이미지들 wh는 모두 다르다.

is_tiny_version = len(anchors)==6 # default setting
# create_tiny_model(), create_model() 함수의 인자 설정을 원본 train.py에서 수정. 
if is_tiny_version:
    model = create_tiny_model(input_shape, anchors, num_classes,
        freeze_body=2, weights_path=model_weights_path)
else:
    # create_model 은 해당 패키지의 tarin.py 내부에 있는 클래스를 사용했다. 이 함수는 keras 모듈이 많이 사용한다. 우선 모르는 건 pass하고 넘어가자.
    model = create_model(input_shape, anchors, num_classes,
        freeze_body=2, weights_path=model_weights_path) # make sure you know what you freeze

# epoch 마다 call back 하여 모델 파일 저장.
# 이것 또한 Keras에서 많이 사용하는 checkpoint 저장 방식인듯 하다. 우선 이것도 모르지만 넘어가자.
logging = TensorBoard(log_dir=log_dir)
checkpoint = ModelCheckpoint(log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5',
    monitor='val_loss', save_weights_only=True, save_best_only=True, period=3)
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)

Create YOLOv3 model with 9 anchors and 1 classes.
Load weights /home/sb020518/DLCV/Detection/yolo/keras-yolo3/model_data/yolo.h5.
Freeze the first 249 layers of total 252 layers.

3-3 전처리한 데이터로 학습시키기 - 학습과 검증 데이터로 나누고 학습!

  • 학습 전에 GPU정리를 하자.
  • P100에서 40분정도 걸린다.
  • 1 epoch당 weight가 저장되는 것을 확인할 수 있다.

drawing

  • 데이터가 적기 때문에, 최종 Loss값이 큰 것을 확인할 수 있다. 하지만 그래도 Inference 성능이 나쁘지 않다. Class 갯수가 1개 이므로..
val_split = 0.1  # train data : val_data = 9 : 1

with open(annotation_path) as f:
    # 이러니 annotation 파일이 txt이든 csv이든 상관없었다.
    lines = f.readlines()

# 랜덤 시드 생성 및 lines 셔플하기
np.random.seed(10101)
np.random.shuffle(lines)
np.random.seed(None)

# 데이터셋 나누기
num_val = int(len(lines)*val_split)
num_train = len(lines) - num_val

# 여기서 부터 진짜 학습 시작! 
# create_model() 로 반환된 yolo모델에서 trainable=False로 되어 있는 layer들 제외하고 학습
if True:
    # optimizer와 loss 함수 정의
    # 위에서 사용한 create_model 클래스의 맴버함수를 사용한다. 
    model.compile(optimizer=Adam(lr=1e-3), loss={
        # use custom yolo_loss Lambda layer.
        'yolo_loss': lambda y_true, y_pred: y_pred})

    batch_size = 2
    print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
    
    # foward -> backpropagation -> weight 갱신 -> weight 저장
    # checkpoint 만드는 것은 뭔지 모르겠으니 pass...
    model.fit_generator(
            data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
            steps_per_epoch=max(1, num_train//batch_size),
            validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes),
            validation_steps=max(1, num_val//batch_size),
            epochs=50,
            initial_epoch=0,
            callbacks=[logging, checkpoint])
    model.save_weights(log_dir + 'trained_weights_stage_1.h5')

# create_model() 로 반환된 yolo모델에서 trainable=False로 되어 있는 layer들 없이, 모두 True로 만들고 다시 학습
if True:
    for i in range(len(model.layers)):
        model.layers[i].trainable = True
    model.compile(optimizer=Adam(lr=1e-4), loss={'yolo_loss': lambda y_true, y_pred: y_pred}) # recompile to apply the change
    print('Unfreeze all of the layers.')

    batch_size = 4 # note that more GPU memory is required after unfreezing the body
    print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
    model.fit_generator(
        data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
        steps_per_epoch=max(1, num_train//batch_size),
        validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes),
        validation_steps=max(1, num_val//batch_size),
        epochs=100,
        initial_epoch=50,
        callbacks=[logging, checkpoint, reduce_lr, early_stopping])
    model.save_weights(log_dir + 'trained_weights_final.h5')
Train on 180 samples, val on 20 samples, with batch size 2.
Epoch 1/50
90/90 [==============================] - 501s 6s/step - loss: 933.7634 - val_loss: 111.7494
Epoch 2/50
66/90 [=====================>........] - ETA: 1:58 - loss: 89.7261


KeyboardInterrupt: 
# YOLO 객체 생성. 
import sys
import argparse
#keras-yolo에서 image처리를 주요 PIL로 수행. 
from PIL import Image

LOCAL_PACKAGE_DIR = os.path.abspath("./keras-yolo3")
sys.path.append(LOCAL_PACKAGE_DIR)

from yolo import YOLO, detect_video
raccoon_yolo = YOLO(
            model_path=os.path.join(HOME_DIR,'DLCV/Detection/yolo/keras-yolo3/snapshots/000/trained_weights_final.h5'),
            anchors_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/yolo_anchors.txt',
            classes_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/raccoon_class.txt')
import matplotlib
import matplotlib.pyplot as plt

img = Image.open(os.path.join(IMAGE_DIR, 'raccoon-171.jpg'))

plt.figure(figsize=(12, 12))
plt.imshow(img)
detected_img = raccoon_yolo.detect_image(img)

plt.figure(figsize=(12, 12))
plt.imshow(detected_img)
  • 이제 모델구성까지 모두 끝났다. 이제 raccoon_yolo.detect_image(img) 만 잘사용하면 밑의 내용은 끝이다.

drawing

3-4 임의의 16개의 원본 이미지를 추출하여 Object Detected된 결과 시각화

import numpy as np
np.random.seed(0)

# 랜덤하게 16개의 이미지 파일만 선택.
# 랜덤 숫자 만을 이용하지 말고 아래와 같은 방법을 이용하면, 알아서 random하게 파일을 선택해준다. 
all_image_files = glob.glob(IMAGE_DIR + '/*.jpg')
all_image_files = np.array(all_image_files)
file_cnt = all_image_files.shape[0]
show_cnt = 16
show_indexes = np.random.choice(file_cnt, show_cnt)
show_files = all_image_files[show_indexes]
print(show_files)

# 16개의 
fig, axs = plt.subplots(figsize=(24,24) , ncols=4 , nrows=4)
for i , filename in enumerate(show_files):
    print(filename)
    row = int(i/4)
    col = i%4
    img = Image.open(os.path.join(IMAGE_DIR, filename))
    detected_image = raccoon_yolo.detect_image(img)
    axs[row][col].imshow(detected_image)
    

drawing

4. 위의 내용들을 다시 활용 - Video Object Detection 수행.

  • video frame 가져오는 것은 cv2 사용.
  • 해당 프래임 이미지를 narray에서 PIL image로 변환해서, model.detect_image(image) 수행
import cv2
import time

def detect_video_yolo(model, input_path, output_path=""):
    
    start = time.time()
    cap = cv2.VideoCapture(input_path)
    
    #codec = cv2.VideoWriter_fourcc(*'DIVX')
    codec = cv2.VideoWriter_fourcc(*'XVID')
    vid_fps = cap.get(cv2.CAP_PROP_FPS)
    vid_size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    vid_writer = cv2.VideoWriter(output_path, codec, vid_fps, vid_size)
    
    frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    print('총 Frame 갯수:', frame_cnt, '원본 영상 FPS:',vid_fps, '원본 Frame 크기:', vid_size)
    index = 0
    while True:
        hasFrame, image_frame = cap.read()
        if not hasFrame:
            print('프레임이 없거나 종료 되었습니다.')
            break
        start = time.time()
        # PIL Package를 내부에서 사용하므로 cv2에서 읽은 image_frame array를 다시 PIL의 Image형태로 변환해야 함.  
        image = Image.fromarray(image_frame)
        # 아래는 인자로 입력된 yolo객체의 detect_image()로 변환한다.
        detected_image = model.detect_image(image)
        # cv2의 video writer로 출력하기 위해 다시 PIL의 Image형태를 array형태로 변환 
        result = np.asarray(detected_image)
        index +=1
        print('#### frame:{0} 이미지 처리시간:{1}'.format(index, round(time.time()-start,3)))
        
        vid_writer.write(result)
    
    vid_writer.release()
    cap.release()
    print('### Video Detect 총 수행시간:', round(time.time()-start, 5))
detect_video_yolo(raccoon_yolo, '../../data/video/jack_and_raccoon.mp4', '../../data/output/jack_and_raccoon_yolo_01.avi')

drawing

5. 위의 내용들을 반복 - tiny yolo로 학습.

  • 위에서 했던 내용을 통채로 함수로 만들자.
import numpy as np
import keras.backend as K
from keras.layers import Input, Lambda
from keras.models import Model
from keras.optimizers import Adam
from keras.callbacks import TensorBoard, ModelCheckpoint, ReduceLROnPlateau, EarlyStopping
from train import get_classes, get_anchors
from train import create_model, create_tiny_model, data_generator, data_generator_wrapper

def train_yolo(pretrained_path, annotation_path,classes_path, anchors_path,log_dir,trained_model_name, b_size, epochs_cnt):      
        
        print('pretrained_path:', pretrained_path)
        class_names = get_classes(classes_path)
        num_classes = len(class_names)
        anchors = get_anchors(anchors_path)

        input_shape = (416,416) # multiple of 32, hw
        # tiny yolo여부를 anchor 설정 파일에서 자동으로 알 수 있음. anchor갯수가 6개이면 tiny yolo
        is_tiny_version = len(anchors)==6 # default setting
        
        # create_tiny_model(), create_model() 함수의 인자 설정을 원본 train.py에서 수정.
        if is_tiny_version:
            model = create_tiny_model(input_shape, anchors, num_classes,
                freeze_body=2, weights_path=pretrained_path)
        else:
            model = create_model(input_shape, anchors, num_classes,
                freeze_body=2, weights_path=pretrained_path) # make sure you know what you freeze

        logging = TensorBoard(log_dir=log_dir)
        checkpoint = ModelCheckpoint(log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5',
            monitor='val_loss', save_weights_only=True, save_best_only=True, period=3)
        reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)
        early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)

        val_split = 0.1
        with open(annotation_path) as f:
            lines = f.readlines()
        np.random.seed(10101)
        np.random.shuffle(lines)
        np.random.seed(None)
        num_val = int(len(lines)*val_split)
        num_train = len(lines) - num_val

        # Train with frozen layers first, to get a stable loss.
        # Adjust num epochs to your dataset. This step is enough to obtain a not bad model.
        if True:
            model.compile(optimizer=Adam(lr=1e-3), loss={
                # use custom yolo_loss Lambda layer.
                'yolo_loss': lambda y_true, y_pred: y_pred})

            batch_size = b_size
            print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
            model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
                    steps_per_epoch=max(1, num_train//batch_size),
                    validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes),
                    validation_steps=max(1, num_val//batch_size),
                    epochs=int(epochs_cnt/2),
                    initial_epoch=0,
                    callbacks=[logging, checkpoint])
            model.save_weights(log_dir + trained_model_name+'_stage_1.h5')

        # Unfreeze and continue training, to fine-tune.
        # Train longer if the result is not good.
        if True:
            for i in range(len(model.layers)):
                model.layers[i].trainable = True
            model.compile(optimizer=Adam(lr=1e-4), loss={'yolo_loss': lambda y_true, y_pred: y_pred}) # recompile to apply the change
            print('Unfreeze all of the layers.')

            batch_size = b_size # note that more GPU memory is required after unfreezing the body
            print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
            model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
                steps_per_epoch=max(1, num_train//batch_size),
                validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes),
                validation_steps=max(1, num_val//batch_size),
                epochs=epochs_cnt,
                initial_epoch=int(epochs_cnt/2),
                callbacks=[logging, checkpoint, reduce_lr, early_stopping])
            model.save_weights(log_dir + trained_model_name+'_final.h5')
BASE_DIR = os.path.join(HOME_DIR, 'DLCV/Detection/yolo/keras-yolo3')
# keras-yolo3에서 convert 된 yolo-tiny pretrained 모델을 사용해야 함. 
pretrained_path = os.path.join(BASE_DIR, 'model_data/yolo-tiny.h5')
annotation_path = os.path.join(ANNO_DIR,'raccoon_anno.csv')
classes_path = os.path.join(BASE_DIR, 'model_data/raccoon_class.txt')
anchors_path = os.path.join(BASE_DIR, 'model_data/tiny_yolo_anchors.txt')
log_dir = os.path.join(BASE_DIR,'snapshots/000/')
trained_model_name = 'raccoon'
b_size=4
epochs_cnt = 100

train_yolo(pretrained_path, annotation_path,classes_path, anchors_path, log_dir,trained_model_name, b_size, epochs_cnt)
raccoon_tiny_yolo = YOLO(model_path=os.path.join(HOME_DIR,'DLCV/Detection/yolo/keras-yolo3/snapshots/000/raccoon_final.h5'),
            anchors_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/tiny_yolo_anchors.txt',
            classes_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/raccoon_class.txt')
# 16개의 이미지를 Detect해보자. 
import numpy as np
np.random.seed(0)

# 모든 이미지 파일중에서 임의의 16개 파일만 설정. 
all_image_files = glob.glob(IMAGE_DIR + '/*.jpg')
all_image_files = np.array(all_image_files)
file_cnt = all_image_files.shape[0]
show_cnt = 16

show_indexes = np.random.choice(file_cnt, show_cnt)
show_files = all_image_files[show_indexes]
print(show_files)
fig, axs = plt.subplots(figsize=(24,24) , ncols=4 , nrows=4)

for i , filename in enumerate(show_files):
    print(filename)
    row = int(i/4)
    col = i%4
    img = Image.open(os.path.join(IMAGE_DIR, filename))
    detected_image = raccoon_tiny_yolo.detect_image(img)
    axs[row][col].imshow(detected_image)
    
detect_video_yolo(raccoon_tiny_yolo, '../../data/video/jack_and_raccoon.mp4', '../../data/output/jack_and_raccoon_tiny_yolo_01.avi')

drawing

음… tiny yolo… 성능이 심각하게 안 좋다.

【Keras】Keras기반 YOLO3 - 오픈 소스 코드로 Inference

심플하면서 좋은 성능을 보유하는 YOLO - Keras open source 코드를 공부해보자

Keras기반 YOLO3 오픈 소스 코드 공부 및 활용

1. Keras 기반 YOLO

  • DarkNet은 c언어 기반의 하나의 딥러닝 프레임워크라고 할 수 있다.
  • 따라서 Yolo를 python에서 사용하기 위해서는 Tensorflow, keras, torch에서 Yolo모델을 지원해주어야 한다.
  • Tenworflow Object Detection API에서는 YOLO를 지원하지 않고, keras에서 위의 코드를 이용해서 구현할 수 있다.
  • keras를 사용함에 있어서 source 코드가 Tensorflow보다 훨씬 이해하기가 쉽다. 그래서 Customization도 상대적으로 쉽다.

  • keras 기반 유명한 Yolo 오픈소스 코드 비교

drawing

2. qqwweee/keras-yolo3 분석하기

drawing

  1. Yolo.py : train, inference 대부분이 이 파일에 있는 Yolo Class의 메소드 함수를 사용한다.
  2. yolo3 Directory : model.py와 utils.py에 필요한 함수가 정의되어 있고, Yolo.py 내부 함수들이 이것을 이용한다.
  3. train.py : 학습에 사용
  4. convert.py : darkNet weight를 받아서 Keras에서 사용할 수 있는 Weight 파일로 바꿔준다.
  5. coco_annotation.py : coco data format을 csv로 바꿔준다.
  6. voc_annotation.py : voc xml파일 포멧을 csv로 바꿔준다.
  7. yolo_video.py : Inferene 할 수 있따. 하기 전에 cumstomization이 필요하다.
  8. 이 코드는 Setup이 안돼 있어서 손수 몇가지 작업을 해주어야 한다.
  9. keras : 2.2 version. Tensorflow : 1.3 version
  10. model_data : 환경구성 파일 class id 와 mapping, yolo_anchors - Default anchors가 들어가 있다.
    csv 파일 format 예시
    path/to/img1.jpg 50,100,150,200,0 30,50,200,120,3
    path/to/img2.jpg 120,300,250,600,2

3. keras-yolo3 패키지를 이용하여 Yolo 수행

  • 앞으로 우리는 오픈소스 패키지를 이용할 것이다. 이떄 패키지의 .py파일을 그대로 이용하는게 아니라, 파일 내부의 클래스와 맴버 함수를 하나하나 적극적으로 사용해 나갈 것이다.
  • 물론 맴버함수 하나하나 전부 알고 사용한다면 좋겠지만, 정말 필요한 핵심적인 맴버 함수(YOLO.detect_image)를 사용하면 이 함수가 알아서 주변의 맴버 함수 이것저것을 사용해서 결국 Detection을 수행한다.
  • 차근차근 코드를 처보고 익히면서 공부해보자.
  • 참고 자료 : /DLCV/Detection/yolo/KerasYolo_이미지와_영상_Detection.ipynb

  • Yolo를 사용하는 딥러닝 모든 딥러닝 프레임워크 들은, 다크넷에서 Pretrained된 yolo/tiny-yolo weights 모델을 다운로드해서 사용한다.
  • pytorch_YoloV3 package만 봐도 다크넷사이트에서 pretrained 모델을 다운로드해서 처리하는 것을 확인할 수 있다.
  • 다운로드한 다크넷 weight 파일을 기반으로 keras-yolo3에서 사용할 수 있는 weight 파일로 변환 후 이를 이용하여 Object Detection 수행할 것이다. 아래에 어떻게 그렇게 하는지 나와 있다.
import os
import sys
import random
import math
import time
import numpy as np
import tensorflow as tf
import matplotlib
import matplotlib.pyplot as plt
%cd ./keras-yolo3  
from yolo import YOLO  # yolo.py 파일의 YOLO class를 가져온다.
/home/sb020518/DLCV/Detection/yolo/keras-yolo3


Using TensorFlow backend.

3-1 Local Directory 상에서 yolo package를 import 함.

  • 위와 같이 파일이 있는 위치에 가서 import를 해도 되지만 아래와 같은 방법도 있다. 즉
  • keras-yolo3는 setup을 제공하지 않으므로 local directory상에서 바로 package를 import함.
  • 이를 위해 keras-yolo3를 system path에 추가. 그래서 아래 2줄이 필요한 것이다. 없다며 import 불가능
  • keras-yolo3 디렉토리에 있는 yolo.py에서 YOLO class를 import하여 사용.

  • 점프 투 파이썬 내용 참조하기(python package directory path add 하기)
    • import sys; sys.path’ [내부의 위치들이 파이썬 라이브러리가 설치되어 있는 디렉터리들의 목록을 보여준다.]
    • sys.path.append(“모듈이 있는 절대 위치 ex) C:/doit/mymod”) 그 후 import mod3를 하면 정상적으로 import되는 것을 확인할 수 있다.!
print(sys.path)  # 파이썬 라이브러리가 설치되어 있는 디렉터리 확인 가능
LOCAL_PACKAGE_DIR = os.path.abspath("./keras-yolo3")
sys.path.append(LOCAL_PACKAGE_DIR)

from yolo import YOLO  # yolo.py 파일의 YOLO class를 가져온다.
# YOLO 클래스는 model_path, achors_path, classes_path를 model_data 밑에 파일로 가짐. 변경을 위해서는 yolo.py 파일에서 YOLO 클래스코드를  직접 변경 필요. 

print(LOCAL_PACKAGE_DIR)
!ls /home/chulmin.kwon45/DLCV/Detection/yolo/keras-yolo3
!cat /home/chulmin.kwon45/DLCV/Detection/yolo/keras-yolo3/yolo.py
/home/sb020518/DLCV/Detection/yolo/keras-yolo3/keras-yolo3
ls: cannot access '/home/chulmin.kwon45/DLCV/Detection/yolo/keras-yolo3': No such file or directory
cat: /home/chulmin.kwon45/DLCV/Detection/yolo/keras-yolo3/yolo.py: No such file or directory

YOLO 객체를 사용하기 위한 모델 파일 설정 및 소스 코드 변경

  • 다크넷에서 Yolo V3 Weight 모델 파일을 다운로드 받은 뒤 이를 keras-yolo3용으로 모델 파일 변경
  • convert.py 파일을 이용해서 아래와 같은 작업을 거치면 된다. GPU를 사용하기도 하지만 나는 CPU밖에 없으므로 CPU로도 처리가 가능하다. 대략 5분정도 걸린다.
  • model_data 디렉토리 밑에 yolo_anchors.txt, coco_classes.txt 가 있는지 확인.

  • keras-yolo3 디렉토리 밑에서 아래 명령어로 다크넷에서 Yolov3 weight를 다운로드 받고 이를 keras-yolo3용으로 모델 파일 변경
  • cfg 파일은 이미 git 레포지토리에 있었다. pjreddie(YOLO 저자)사이트에서 받은 파일과 거의 똑같다. batch등 값만 아주 조금 다르다.
$ wget https://pjreddie.com/media/files/yolov3.weights
$ python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5
  • convert.py를 수행하면 yolo anchor값이 yolo_anchors.txt 파일이 자동으로 생성됨.
  • coco label과 클래스 매핑은 0부터 매핑함.(0 => person)
  • yolo_anchors : Default anchor box정보가 들어가 있다.(9개) - 너비 x 높이, 너비 x 높이…

아래 코드 설명

  • YOLO 객체 생성. config는 default로 keras-yolo3 디렉토리에 있는 yolov3.cfg를 적용
  • 중요한 인자를 YOLO 생성자 인자로 넣어준다. 그래서 yolo class가 생성된다.
  • 인자를 아무것도 넣지 않아도, _default에 의해서 맴버변수가 정의가 자동으로 정의되지만, 우리에게 맞게 아래와 같이 설정

drawing

  • self.__dict__.update에 의해서 yolo 생성자의 값이 자동으로 분배 및 저장된다.
    예를 들어서 self.model_path, self.anchors_path, self.classes_path, self.score 가 위에 정의되어 있는 dictionary 변수를 읽어서 자동으로 맴버변수를 정의해준다.
  • 새로 넣어주는 생성자 인자에서 path값을 절대경로로 넣어줘야 한다. 상대경로 안된다!! 아래의 코드들을 보면 왜 안되는지 알 수 있다. (os.path.expanduser 라는 API를 이용하기 때문이다. 이 모듈은 home directory 부터 시작하는 ~부터 시작하는 path를 적어줘야 한다.)
  • yolo class 생성자에 의해서 yolo.py의 _get_class, _get_anchors, generate 맴버함수를 사용한다.
  • 이미지를 PIL을 사용한다. 지금 PIL은 거의 죽은 패키지이다. 근데 이 패키지는 PIL을 사용하기 때문에 우리도 어쩔 수 없이 이것을 사용해야 한다. 보통은 openCV등으로 img read를 해서 array형태로 받아와야하는데…
import sys
import argparse
from yolo import YOLO, detect_video
#keras-yolo에서 image처리를 주요 PIL로 수행. 
from PIL import Image


config_dict = {}
yolo = YOLO(model_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/yolo.h5',
            anchors_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/yolo_anchors.txt',
            classes_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/coco_classes.txt',
           score=0.55)
# score=0.55를 지우면 원래 _default대로 0.3으로 지정된다.
/home/sb020518/DLCV/Detection/yolo/keras-yolo3/model_data/yolo.h5 model, anchors, and classes loaded.

단일 이미지 Object Detection

# 원본 이미지 보기 # Path 설정 조심하기 ~/DLCV로 하면 안되더라.
from PIL import Image
img = Image.open(os.path.join('/home/sb020518/DLCV/data/image/beatles01.jpg'))

plt.figure(figsize=(12, 12))
plt.imshow(img)

drawing

  • yolo.detect_image() 메소드는 PIL package를 이용하여 image 작업 수행. keras-yolo3/font 디렉토리를 상위 디렉토리로 복사 해야함. (나는 아무 문제 없었음)
  • 만약 font문제가 있다면, 해결방법은 다음과 같다.
// detect_image 메소드의 ImageFont.truetype에러. 
~~yolo/keras-yolo3 $ cp -rf font ..
// keras-yolo3 패키지 내부의 font 폴더를 아래의 디렉토리(지금 나의 주피터 노트북파일(KerasYolo_이미지와_영상_Detection)이 있는 위치로 내려준다.)

img = Image.open(os.path.join('/home/sb020518/DLCV/data/image/beatles01.jpg'))
detected_img = yolo.detect_image(img)

plt.figure(figsize=(12, 12))
plt.imshow(detected_img)
(416, 416, 3)
Found 11 boxes for img
car 0.56 (314, 233) (351, 267)
car 0.70 (502, 230) (572, 291)
car 0.79 (471, 235) (503, 261)
car 0.93 (383, 222) (400, 238)
car 0.94 (432, 225) (452, 242)
car 0.96 (138, 246) (256, 334)
person 0.71 (606, 228) (627, 293)
person 0.98 (395, 274) (551, 554)
person 0.98 (588, 272) (735, 557)
person 1.00 (48, 254) (189, 556)
person 1.00 (260, 263) (377, 537)
9.930654634023085

drawing

4. Video Object Detection 수행

  • yolo_video.py를 사용하면 inference를 이미지든 비디오든 쉽게 수행할 수 있지만, 이것은 cv2.imshow를 사용한다. 이것은 jupyter에서 보이지 않는다. (OpenCV Windows Frame 인터페이스 - 이전 게시물)
  • 따라서 우리는 새롭게 우리가 다시 정의해서 사용해보자.
  • 나는 CPU만을 사용하기 때문에 하나의 이미지당 2초가 걸린다. GPU를 쓰는 사람들은 0.05초 정도 걸린다.

drawing

import cv2
import time

def detect_video_yolo(class_yoloModel, input_path, output_path=""):
    
    start = time.time()
    cap = cv2.VideoCapture(input_path)
    
    #codec = cv2.VideoWriter_fourcc(*'DIVX')
    codec = cv2.VideoWriter_fourcc(*'XVID')
    vid_fps = cap.get(cv2.CAP_PROP_FPS)
    vid_size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    vid_writer = cv2.VideoWriter(output_path, codec, vid_fps, vid_size)
    
    frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    print('총 Frame 갯수:', frame_cnt, '원본 영상 FPS:',vid_fps)
    index = 0
    while True:
        hasFrame, image_frame = cap.read()
        if not hasFrame:
            print('프레임이 없거나 종료 되었습니다.')
            break
        start = time.time()
        # PIL Package를 내부에서 사용하므로 cv2에서 읽은 image_frame array를 다시 PIL의 Image형태로 변환해야 함.  
        image = Image.fromarray(image_frame)
        # 아래는 인자로 입력된 yolo객체의 detect_image()로 변환한다.
        detected_image = class_yoloModel.detect_image(image)
        # cv2의 video writer로 출력하기 위해 다시 PIL의 Image형태를 array형태로 변환 
        result = np.asarray(detected_image)
        index +=1
        if index%5 == 0:
            print('#### frame:{0} 1장 이미지 처리시간:{1}, 진행률:{2:4.2f}%'.format(index, round(time.time()-start,3), index/frame_cnt*100 ))
        
        vid_writer.write(result)
    
    vid_writer.release()
    cap.release()
    print('### Video Detect 총 수행시간:', round(time.time()-start, 5))
detect_video_yolo(yolo, '/home/sb020518/DLCV/data/video/Night_Day_Chase.mp4', '/home/sb020518/DLCV/data/output/Night_Day_Chase_yoloKeras_01.avi')
총 Frame 갯수: 1383 원본 영상 FPS: 28.282227867912656
(416, 416, 3)
Found 2 boxes for img
motorbike 0.99 (559, 177) (862, 459)
person 0.87 (549, 113) (870, 451)
2.561595087056048
2.6086239949800074
#### frame:5 5장 이미지 처리시간:13.065, 진행률:0.36%
(416, 416, 3)

drawing

5. tiny Yolo를 이용하여 이미지와 영상 object detection 수행.

  • tiny yolo weights파일은 https://pjreddie.com/media/files/yolov3-tiny.weights 에서 다운로드 받을 수 있음.
  • 다운로드 받은 tiny-yolo weight파일을 keras-yolo3에서 사용할 수 있게 Convert 수행 후 YOLO객체에서 로딩하여 사용
  • 확실히 tiny Yolo는 이미지에 정말 확실한!! 객체만 찾는다.
  • 그럼에도 불구하고 PIL을 사용해서 그런지 (?) 수행속도가 0.03정도 나온다. (논문 공식 속도인 220 FPS 까지는 안된다.)
$ wget https://pjreddie.com/media/files/yolov3-tiny.weights
$ !python convert.py yolov3-tiny.cfg ./model_data/yolov3-tiny.weights model_data/yolo-tiny.h5
  • tiny yolo weight 파일과 tiny와 맞는 anchor 파일, coco 클래스 파일을 YOLO 객체 생성 시 인자로 입력
tiny_yolo = YOLO(model_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/yolo-tiny.h5',
            anchors_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/tiny_yolo_anchors.txt',
            classes_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/coco_classes.txt')
  • 단일 이미지 Object Detection
img = Image.open(os.path.join('../../data/image/beatles01.jpg'))
detected_img = tiny_yolo.detect_image(img)

plt.figure(figsize=(12, 12))
plt.imshow(detected_img)
  • Video Object Detection
detect_video_yolo(tiny_yolo, '../../data/video/secretlife_pet.mp4', '../../data/output/secretlife_pet_yolo01.avi')

【Python-Module】 YOLO 수행하기 - OpenCV DNN 모듈

주의 사항 : 지금까지의 OpenCV DNN 모듈 이용과는 흐름이 다르다. 이전 Post를 통해서 YOLO의 이론에 대해서 공부했다. OpenCV DNN 모델을 사용해서 YOLO를 수행해보자.

OpenCV DNN 모듈을 사용해서 YOLO 수행하기
주의 사항 : 지금까지의 OpenCV DNN 모듈 이용과는 흐름이 다르다.
OpenCV 사용과정 요약은 이전 Post 참조

참고 파일 (conda tf113 환경)
/DLCV/Detection/yolo/OpenCV_Yolo_이미지와_영상_Detection.ipynb

1. OpenCV DNN Yolo 사용방법

  • 핵심 사항
    • 지금까지는 Tensorflow 기반 pretrain 모델을 가져왔다.
    • Tensorflow에서 Yolo를 지원하지 않는다. Yolo의 창시자 사이트에 가서 weight와 conf파일을 받아와야 한다.
  • DarkNet 구성 환경 및 Yolo 아키텍처에 따라서 사용자가 직접 Object Detection 정보를 추출해야한다.
    • 아래와 같이 사용자가 Scale이 다른 Feature Map에서 Detection결과 추출과 NMS를 직접 해야한다. OpenCV에서 해주지 않는다.
    • 82번 94번 106번 (13 x 13, 26 x 26, 52 x 52) Layer에 직접 접근해서 Feature Map정보를 직접 가져와야 한다.
    • NMS 도 직접 해야한다. OpenCV NMS 함수를 호출해야한다.
    • 이때 Feature Map정보에서 Depth인 4(bounding box regression) + 1(confidence=Objectness Score(객체인지 아닌지)) + 80(class logistic 확률값).
      • 80개의 값 중에서 가장 높은 값을 가지는 위치 인덱스가 객체의 Label값이다.
      • 4개의 값이 t인 (offset값인지 알았는데… 좌표값이다.) 근데 그 좌표값은 (center_X, center_Y, width, hight)이다. 이 값을 좌상단 우하단(X1 Y1 X2 Y2) 좌표로 변환을 해줘야 한다.

drawing

  • Pretrained 된 Inferecne 모델 로딩 주의 사항
    • 대부분이 Darknet 사이트에서 직접 weight와 config파일을 다운받와야 한다.
    • 그것을 통해서 cv.dnn.readNetFromDarknet(confg, weight)
      (지금까지는 cv.dnn.readNetFromTensorflow(weight, confg) 했었지만.. )
  • OpenCV로 Yolo inference 구현 절차 (위의 내용 전체 요약)

drawing

2. OpenCV DNN Yolo Inference 구현 코드 - 단일 이미지

- 아래의 코드에서 계속 yolo-416 과 tiny-yolo 를 이용
import cv2
import matplotlib.pyplot as plt
%matplotlib inline

img = cv2.imread('../../data/image/avengers2.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

print('image shape:', img.shape)
plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
image shape: (365, 550, 3)

drawing

  • Darknet Yolo사이트에서 coco로 학습된 Inference모델와 환경파일을 다운로드 받은 후 이를 이용해 OpenCV에서 Inference 모델 생성
    • https://pjreddie.com/darknet/yolo/ 에 다운로드 URL 있음.
    • YOLOv3-416 사용할 에정
    • pretrained 모델은 wget https://pjreddie.com/media/files/yolov3.weights 에서 다운로드
    • pretrained 모델을 위한 환경 파일은 https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg 에서 다운로드
    • wget https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg?raw=true -O ./yolov3.cfg
    • 주의 필요 : readNetFromDarknet(config파일, weight파일)로 config파일 인자가 weight파일 인자보다 먼저 옴. 주의 필요.
  • readNetFromDarknet(config파일, weight파일)을 이용하여 yolo inference network 모델을 로딩
!pwd
# /home/sb020518/DLCV/Detection/yolo

import os
#가급적 절대 경로 사용. 
CUR_DIR = os.path.abspath('.')
weights_path = os.path.join(CUR_DIR, './pretrained/yolov3.weights')
config_path =  os.path.join(CUR_DIR, './pretrained/yolov3.cfg')
#config 파일 인자가 먼저 옴. 
cv_net_yolo = cv2.dnn.readNetFromDarknet(config_path, weights_path)
/home/sb020518/DLCV/Detection/yolo
  • COCO class id와 class 명 매핑
labels_to_names_seq = {0:'person',1:'bicycle',2:'car',3:'motorbike',4:'aeroplane',5:'bus',6:'train',7:'truck',8:'boat',9:'traffic light',10:'fire hydrant',
                        11:'stop sign',12:'parking meter',13:'bench',14:'bird',15:'cat',16:'dog',17:'horse',18:'sheep',19:'cow',20:'elephant',
                        21:'bear',22:'zebra',23:'giraffe',24:'backpack',25:'umbrella',26:'handbag',27:'tie',28:'suitcase',29:'frisbee',30:'skis',
                        31:'snowboard',32:'sports ball',33:'kite',34:'baseball bat',35:'baseball glove',36:'skateboard',37:'surfboard',38:'tennis racket',39:'bottle',40:'wine glass',
                        41:'cup',42:'fork',43:'knife',44:'spoon',45:'bowl',46:'banana',47:'apple',48:'sandwich',49:'orange',50:'broccoli',
                        51:'carrot',52:'hot dog',53:'pizza',54:'donut',55:'cake',56:'chair',57:'sofa',58:'pottedplant',59:'bed',60:'diningtable',
                        61:'toilet',62:'tvmonitor',63:'laptop',64:'mouse',65:'remote',66:'keyboard',67:'cell phone',68:'microwave',69:'oven',70:'toaster',
                        71:'sink',72:'refrigerator',73:'book',74:'clock',75:'vase',76:'scissors',77:'teddy bear',78:'hair drier',79:'toothbrush' }
labels_to_names = {1:'person',2:'bicycle',3:'car',4:'motorcycle',5:'airplane',6:'bus',7:'train',8:'truck',9:'boat',10:'traffic light',
                    11:'fire hydrant',12:'street sign',13:'stop sign',14:'parking meter',15:'bench',16:'bird',17:'cat',18:'dog',19:'horse',20:'sheep',
                    21:'cow',22:'elephant',23:'bear',24:'zebra',25:'giraffe',26:'hat',27:'backpack',28:'umbrella',29:'shoe',30:'eye glasses',
                    31:'handbag',32:'tie',33:'suitcase',34:'frisbee',35:'skis',36:'snowboard',37:'sports ball',38:'kite',39:'baseball bat',40:'baseball glove',
                    41:'skateboard',42:'surfboard',43:'tennis racket',44:'bottle',45:'plate',46:'wine glass',47:'cup',48:'fork',49:'knife',50:'spoon',
                    51:'bowl',52:'banana',53:'apple',54:'sandwich',55:'orange',56:'broccoli',57:'carrot',58:'hot dog',59:'pizza',60:'donut',
                    61:'cake',62:'chair',63:'couch',64:'potted plant',65:'bed',66:'mirror',67:'dining table',68:'window',69:'desk',70:'toilet',
                    71:'door',72:'tv',73:'laptop',74:'mouse',75:'remote',76:'keyboard',77:'cell phone',78:'microwave',79:'oven',80:'toaster',
                    81:'sink',82:'refrigerator',83:'blender',84:'book',85:'clock',86:'vase',87:'scissors',88:'teddy bear',89:'hair drier',90:'toothbrush',
                    91:'hair brush'}
  • 3개의 scale Output layer에서 결과 데이터 추출
  • 82번 94번 106번 (13 x 13, 26 x 26, 52 x 52) Layer에 직접 접근해서 Feature Map정보를 직접 가져와야 한다.
#전체 Darknet layer에서 13x13 grid, 26x26, 52x52 grid에서 detect된 Output layer만 filtering
layer_names = cv_net_yolo.getLayerNames()
# layer_names : ['conv_0', 'bn_0', 'relu_1', 'conv_1', 'bn_1', 'relu_2', 'conv_ ....
outlayer_names = [layer_names[i[0] - 1] for i in cv_net_yolo.getUnconnectedOutLayers()]
# cv_net_yolo.getUnconnectedOutLayers() : [[200], [227], [254]]

print('output_layer name:', outlayer_names)

# Yolov3 416는 416 x 416 Input을 받는다. 원본 이미지 배열을 사이즈 (416, 416)으로, BGR을 RGB로 변환하여 배열 입력
# blobFromImage를 사용해서 CNN에 넣어야할 이미지 전처리를 쉽게 한다.
# DarkNet에서는 scalefactor를 꼭 처리해줘야한다!
out = cv2.dnn.blobFromImage(img, scalefactor=1/255.0, size=(416, 416), swapRB=True, crop=False)
print('type : ', type(out))
print('shape :', out.shape)
print('size :', out.size)

 # 말그대로 신경망에 넣을 사진만 Setting 해준다. 
cv_net_yolo.setInput(out)  
# Object Detection 수행하여 결과를 cv_out으로 반환 
cv_outs = cv_net_yolo.forward(outlayer_names)  # inference를 돌려서 원하는 layer의 Feature Map 정보만 뽑아 낸다. 
print('cv_outs type : list // cv_outs length :', len(cv_outs))
print("cv_outs[0] : 첫번째 FeatureMap 13 x 13 x 85, cv_outs[1] : 두번째 FeatureMap 26 x 26 x 85 ")

# bounding box의 테두리와 caption 글자색 지정
green_color=(0, 255, 0)
red_color=(0, 0, 255)

output_layer name: ['yolo_82', 'yolo_94', 'yolo_106']
type :  <class 'numpy.ndarray'>
shape : (1, 3, 416, 416)
size : 519168
cv_outs type : list // cv_outs length : 3
cv_outs[0] : 첫번째 FeatureMap 13 x 13 x 85, cv_outs[1] : 두번째 FeatureMap 26 x 26 x 85 
  • 위에서 뽑은 FeatureMap 정보를 사용해서…
    • 3개의 scale output layer에서 Object Detection 정보를 모두 수집.
    • center와 width,height좌표는 모두 좌상단, 우하단 좌표로 변경.
  • 아래 과정을 통해서 ((13 x 13) + (26 x 26) + (52 x 52)) x 3(anchor 갯수) = 10,647 개의 예측한 Bounding Box중에서 confidence가 threshold 보다 높은 Bounding box만을 추출 했다.
  • 아래 코드의 결과 값으로 나오는 (507, 85) = (13 x 13 x 3(하나의 cell마다 3개의 Anchor에 대한), 85) 이다
import numpy as np

# 원본 이미지를 네트웍에 입력시에는 (416, 416)로 resize 함. 
# 이후 결과가 출력되면 resize된 이미지 기반으로 bounding box 위치가 예측 되므로 이를 다시 원복하기 위해 원본 이미지 shape정보 필요
rows = img.shape[0]
cols = img.shape[1]

conf_threshold = 0.5
nms_threshold = 0.4   # 이 값이 클 수록 box가 많이 사라짐. 조금만 겹쳐도 NMS로 둘 중 하나 삭제하므로

class_ids = []
confidences = []
boxes = []

# 3개의 개별 output layer별로 Detect된 Object들에 대해서 Detection 정보 추출 및 시각화 
for ix, output in enumerate(cv_outs):
    print('output shape:', output.shape)
    # Detected된 Object별 iteration
    for jx, detection in enumerate(output):
        # class score는 detetection배열에서 5번째 이후 위치에 있는 값. 즉 6번쨰~85번째 까지의 값
        scores = detection[5:]
        # scores배열에서 가장 높은 값을 가지는 값이 class confidence, 그리고 그때의 위치 인덱스가 class id
        class_id = np.argmax(scores)
        confidence = scores[class_id] # 5번쨰 값은 objectness score이다. 객체인지 아닌지의 확률이다. 6번쨰~85번째 까지의 값이 그 객체일 확률 값이다. 

        # confidence가 지정된 conf_threshold보다 작은 값은 제외 
        if confidence > conf_threshold:
            print('ix:', ix, 'jx:', jx, 'class_id', class_id, 'confidence:', confidence)
            # detection은 scale된 좌상단, 우하단 좌표를 반환하는 것이 아니라, detection object의 중심좌표와 너비/높이를 반환
            # 원본 이미지에 맞게 scale 적용 및 좌상단, 우하단 좌표 계산
            center_x = int(detection[0] * cols)
            center_y = int(detection[1] * rows)
            width = int(detection[2] * cols)
            height = int(detection[3] * rows)
            left = int(center_x - width / 2)
            top = int(center_y - height / 2)
            # 3개의 개별 output layer별로 Detect된 Object들에 대한 class id, confidence, 좌표정보를 모두 수집
            class_ids.append(class_id)
            confidences.append(float(confidence))

            boxes.append([left, top, width, height])
output shape: (507, 85)
ix: 0 jx: 324 class_id 0 confidence: 0.780293
ix: 0 jx: 325 class_id 0 confidence: 0.70145583
ix: 0 jx: 328 class_id 0 confidence: 0.95964307
ix: 0 jx: 331 class_id 0 confidence: 0.8294637
ix: 0 jx: 367 class_id 0 confidence: 0.83721125
ix: 0 jx: 370 class_id 0 confidence: 0.62093467
output shape: (2028, 85)
ix: 1 jx: 978 class_id 0 confidence: 0.9954639
ix: 1 jx: 1262 class_id 0 confidence: 0.8263151
ix: 1 jx: 1274 class_id 0 confidence: 0.8833774
ix: 1 jx: 1331 class_id 0 confidence: 0.7529699
ix: 1 jx: 1334 class_id 0 confidence: 0.9874747
ix: 1 jx: 1337 class_id 0 confidence: 0.5676544
ix: 1 jx: 1340 class_id 0 confidence: 0.99014115
ix: 1 jx: 1343 class_id 0 confidence: 0.9779411
ix: 1 jx: 1352 class_id 0 confidence: 0.59539807
ix: 1 jx: 1361 class_id 0 confidence: 0.62599
ix: 1 jx: 1409 class_id 0 confidence: 0.97582513
ix: 1 jx: 1412 class_id 0 confidence: 0.9940848
ix: 1 jx: 1415 class_id 0 confidence: 0.62239736
ix: 1 jx: 1418 class_id 0 confidence: 0.8629691
ix: 1 jx: 1421 class_id 0 confidence: 0.84953487
ix: 1 jx: 1487 class_id 0 confidence: 0.80823416
output shape: (8112, 85)
ix: 2 jx: 3848 class_id 0 confidence: 0.85432345
ix: 2 jx: 3851 class_id 0 confidence: 0.9162883
ix: 2 jx: 4007 class_id 0 confidence: 0.53006333
  • 위에서 뽑은 몇가지 Box에 대해서 NMS적용하기
    • cv2.dnn.NMSBoxes 모듈을 사용하기 위한 파라미터는 위에서 잘 만들어 놓았다,
    • 파라메터 : (bounding box의 좌상단 우하단 4개의 좌표, 같은 인덱스에 위치한 confidence값)
    • output은 진짜 필요한 Bounding box의 indexes를 반환한다!
conf_threshold = 0.5
nms_threshold = 0.4
idxs = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)

print(idxs) # 이 index의 box만 살아남았음을 의미한다.
[[ 6]
 [17]
 [12]
 [ 2]
 [23]
 [ 8]
 [21]]
  • NMS로 최종 filtering된 idxs를 이용하여 boxes, classes, confidences에서 해당하는 Object정보를 추출하고 시각화.
# cv2의 rectangle()은 인자로 들어온 이미지 배열에 직접 사각형을 업데이트 하므로 그림 표현을 위한 별도의 이미지 배열 생성. 
draw_img = img.copy()

# NMS로 최종 filtering된 idxs를 이용하여 boxes, classes, confidences에서 해당하는 Object정보를 추출하고 시각화.
if len(idxs) > 0:
    for i in idxs.flatten():
        box = boxes[i]
        left = box[0]
        top = box[1]
        width = box[2]
        height = box[3]
        # labels_to_names 딕셔너리로 class_id값을 클래스명으로 변경. opencv에서는 class_id + 1로 매핑해야함.
        caption = "{}: {:.4f}".format(labels_to_names_seq[class_ids[i]], confidences[i])
        #cv2.rectangle()은 인자로 들어온 draw_img에 사각형을 그림. 위치 인자는 반드시 정수형.
        cv2.rectangle(draw_img, (int(left), int(top)), (int(left+width), int(top+height)), color=green_color, thickness=2)
        cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, red_color, 1)
        print(caption)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)

person: 0.9955
person: 0.9941
person: 0.9901
person: 0.9596
person: 0.9163
person: 0.8834
person: 0.8082

drawing

3. 2에서 한 작업을 함수로 def해서 사용하기 - 단일 이미지

def get_detected_img(cv_net, img_array, conf_threshold, nms_threshold, use_copied_array=True, is_print=True):
    
    # 원본 이미지를 네트웍에 입력시에는 (416, 416)로 resize 함. 
    # 이후 결과가 출력되면 resize된 이미지 기반으로 bounding box 위치가 예측 되므로 이를 다시 원복하기 위해 원본 이미지 shape정보 필요
    rows = img_array.shape[0]
    cols = img_array.shape[1]
    draw_img = None
    if use_copied_array:
        draw_img = img_array.copy()
    else:
        draw_img = img_array
    
    #전체 Darknet layer에서 13x13 grid, 26x26, 52x52 grid에서 detect된 Output layer만 filtering
    layer_names = cv_net.getLayerNames()
    outlayer_names = [layer_names[i[0] - 1] for i in cv_net.getUnconnectedOutLayers()]
    
    # 로딩한 모델은 Yolov3 416 x 416 모델임. 원본 이미지 배열을 사이즈 (416, 416)으로, BGR을 RGB로 변환하여 배열 입력
    cv_net.setInput(cv2.dnn.blobFromImage(img_array, scalefactor=1/255.0, size=(416, 416), swapRB=True, crop=False))
    start = time.time()
    # Object Detection 수행하여 결과를 cvOut으로 반환 
    cv_outs = cv_net.forward(outlayer_names)
    layerOutputs = cv_net.forward(outlayer_names)
    # bounding box의 테두리와 caption 글자색 지정
    green_color=(0, 255, 0)
    red_color=(0, 0, 255)

    class_ids = []
    confidences = []
    boxes = []

    # 3개의 개별 output layer별로 Detect된 Object들에 대해서 Detection 정보 추출 및 시각화 
    for ix, output in enumerate(cv_outs):
        # Detected된 Object별 iteration
        for jx, detection in enumerate(output):
            scores = detection[5:]
            class_id = np.argmax(scores)
            confidence = scores[class_id]
            # confidence가 지정된 conf_threshold보다 작은 값은 제외 
            if confidence > conf_threshold:
                #print('ix:', ix, 'jx:', jx, 'class_id', class_id, 'confidence:', confidence)
                # detection은 scale된 좌상단, 우하단 좌표를 반환하는 것이 아니라, detection object의 중심좌표와 너비/높이를 반환
                # 원본 이미지에 맞게 scale 적용 및 좌상단, 우하단 좌표 계산
                center_x = int(detection[0] * cols)
                center_y = int(detection[1] * rows)
                width = int(detection[2] * cols)
                height = int(detection[3] * rows)
                left = int(center_x - width / 2)
                top = int(center_y - height / 2)
                # 3개의 개별 output layer별로 Detect된 Object들에 대한 class id, confidence, 좌표정보를 모두 수집
                class_ids.append(class_id)
                confidences.append(float(confidence))
                boxes.append([left, top, width, height])
    
    # NMS로 최종 filtering된 idxs를 이용하여 boxes, classes, confidences에서 해당하는 Object정보를 추출하고 시각화.
    idxs = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)
    if len(idxs) > 0:
        for i in idxs.flatten():
            box = boxes[i]
            left = box[0]
            top = box[1]
            width = box[2]
            height = box[3]
            # labels_to_names 딕셔너리로 class_id값을 클래스명으로 변경. opencv에서는 class_id + 1로 매핑해야함.
            caption = "{}: {:.4f}".format(labels_to_names_seq[class_ids[i]], confidences[i])
            #cv2.rectangle()은 인자로 들어온 draw_img에 사각형을 그림. 위치 인자는 반드시 정수형.
            cv2.rectangle(draw_img, (int(left), int(top)), (int(left+width), int(top+height)), color=green_color, thickness=2)
            cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, red_color, 1)

    if is_print:
        print('Detection 수행시간:',round(time.time() - start, 2),"초")
    return draw_img
  • 방금 위에서 정의한 함수를 사용해서 Detection을 수행해보자!
  • Open CV에서 Yolo를 시행하는 것이 생각보다 Detection 수행시간이 긴것을 알 수 있다.
  • 그럼에도 불구하고 검출 성능은 나쁘지 않다.
  • 이것은 OpenCV에서 YOLO가 최적화되어 있지 않기 때문이라고 한다.
import cv2
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import time


# image 로드 
img = cv2.imread('../../data/image/john_wick01.jpg')

#coco dataset 클래스명 매핑

import os
#가급적 절대 경로 사용. 
CUR_DIR = os.path.abspath('.')
weights_path = os.path.join(CUR_DIR, './pretrained/yolov3.weights')
config_path =  os.path.join(CUR_DIR, './pretrained/yolov3.cfg')

# tensorflow inference 모델 로딩
cv_net_yolo = cv2.dnn.readNetFromDarknet(config_path, weights_path)
    
conf_threshold = 0.5
nms_threshold = 0.4
# Object Detetion 수행 후 시각화 
draw_img = get_detected_img(cv_net_yolo, img, conf_threshold=conf_threshold, nms_threshold=nms_threshold, use_copied_array=True, is_print=True)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
Detection 수행시간: 2.8 초

drawing

4. 3에서 def한 함수사용해서 YOLO - video object detection

  • 지금까지 ouput을 .avi형식으로 저장했지만, .mp4로 저장해도 상관없고, mp4를 사용하면 Video모듈을 사용해서 볼수도 있다.
  • avi를 사용하면 Video 모듈을 사용할 수 없고, 다운로드해서 봐야한다는 점을 깨달았으니 잘 알아두자.
def do_detected_video(cv_net, input_path, output_path,conf_threshold,nms_threshold, is_print):
    
    cap = cv2.VideoCapture(input_path)

    codec = cv2.VideoWriter_fourcc(*'XVID')

    vid_size = (round(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    vid_fps = cap.get(cv2.CAP_PROP_FPS)

    vid_writer = cv2.VideoWriter(output_path, codec, vid_fps, vid_size) 

    frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    print('총 Frame 갯수:', frame_cnt)

    green_color=(0, 255, 0)
    red_color=(0, 0, 255)
    while True:
        hasFrame, img_frame = cap.read()
        if not hasFrame:
            print('더 이상 처리할 frame이 없습니다.')
            break
        
        returned_frame = get_detected_img(cv_net, img_frame, conf_threshold=conf_threshold, nms_threshold=nms_threshold, \
                                          use_copied_array=False, is_print=is_print)
        vid_writer.write(returned_frame)
    # end of while loop

    vid_writer.release()
    cap.release()
do_detected_video(cv_net_yolo, '../../data/video/John_Wick_small.mp4', '../../data/output/John_Wick_small_yolo01.mp4', conf_threshold,
                  nms_threshold, True)
!gsutil cp ../../data/output/John_Wick_small_yolo01.avi gs://my_bucket_dlcv/data/output/John_Wick_small_yolo01.avi

5. 지금까지의 모든 작업 tiny Yolo로 다시 수행하기

  • 속도가 많이 빠르지만, 정확도가 많~이 떨어진다.
  • tiny yolo의 pretrained된 weight파일은 wget https://pjreddie.com/media/files/yolov3-tiny.weights 에서 download 가능.
  • config 파일은 wget https://github.com/pjreddie/darknet/blob/master/cfg/yolov3-tiny.cfg?raw=true -O ./yolov3-tiny.cfg 로 다운로드
import cv2
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import time

# image 로드 
img = cv2.imread('../../data/image/beatles01.jpg')

#coco dataset 클래스명 매핑

import os
#가급적 절대 경로 사용. 
CUR_DIR = os.path.abspath('.')
weights_path = os.path.join(CUR_DIR, './pretrained/yolov3-tiny.weights')
config_path =  os.path.join(CUR_DIR, './pretrained/yolov3-tiny.cfg')

# tensorflow inference 모델 로딩
cv_net_yolo = cv2.dnn.readNetFromDarknet(config_path, weights_path)

#tiny yolo의 경우 confidence가 일반적으로 낮음. 
conf_threshold = 0.3
nms_threshold = 0.4
# Object Detetion 수행 후 시각화 
draw_img = get_detected_img(cv_net_yolo, img, conf_threshold=conf_threshold, nms_threshold=nms_threshold, use_copied_array=True, is_print=True)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
  • tiny yolo로 영상 Detection
do_detected_video(cv_net_yolo, '../../data/video/John_Wick_small.mp4', '../../data/output/John_Wick_small_tiny_yolo01.avi', conf_threshold,
                  nms_threshold, True)
!gsutil cp ../../data/output/John_Wick_small_tiny_yolo01.avi gs://my_bucket_dlcv/data/output/John_Wick_small_tiny_yolo01.avi

【Paper】 YOLO (You Only Live Once) V1 V2 V3 핵심정리

One Stage Detector의 시작이라고 할 수 있는 Yolo, SSD에 대해서 다시 공부하고 정리해본다.

YOLO (You Only Live Once)

1. Yolo의 이해

drawing

  • V3의 abstract에서 COCO dataset에 대해서 0.5 IOU를 기준으로 하면 RetinaNet보다 성능도 좋고 빠르다고 변명하고 있다.
  • RetinaNet의 장점 - 작은 Object에 대해서도 Bounding Box Object Detection을 잘 수행한 한다는 것을 할 수 있다.
  • Version에 따른 특징
    • Version1 : 빠른 Detection 시간. 그러나 낮은 정확도. 그 뒤 SSD가 더 좋은 성능과 속도
    • Version2 : 수행시간과 성능 모두 개선. SSD보다 빠르지만 성능은 아주 조금 개선
    • Version3 : 수행시간은 조금 느려졌으나 성능은 대폭 개선

2. YOLO Version 1

  • 학습 절차
    1. 어떤 입력 이미지 크기든 상관없이, 이미지를 S x S Grid Cell(7 x 7)로 나눈다.
    2. 하나의 Cell이 하나의 Object에 대한 Detection을 수행한다.
    3. 각 Grid Cell이 2개의 Bounding Box 후보를 기반으로 Object의 Bounding Box를 예측한다.
      (Anchor Box 기반은 아니다. Cell을 기반으로 한다. 2개의 Box를 활용해 Cell의 Classificatino, Box 추측을 수행한다.)

drawing

  • 위 이미지 설명
    • S x S x (B x 5 + C) : S x S = 7 x 7 , B = bounding box + Confidence Score(아래의 값이 학습 위한 GT) , C = Class 갯수
    • Confidence Score : (Object 인지, 아닌지에 대한 확률 = Objectness Score) x (Grid Cell에 대한 Bounding Box와 GT Box와의 IOU값) 이 나오도록 학습된다.
    • bounding box : x, y, w, h 는 Grid Cell에 대한 예측되 나와야 하는 Bounding Box 좌표값이다.
    • 20개의 Class에 대한 Softmax값이 나오도록 학습되는 값이다. Bounding Box가 2개인데 (20개 Class)x2 인 40개가 있는게 아니고 20개만 있다. 그 이유는 한 Grid Cell에 대해서 한 Object만 Detect하겠다는 의지를 담고 있다. (Yolo V1의 단점이라고 할 수 있다. Version2에서 바뀐다.)

drawing

  • 위 이미지 설명
    • 위위 이미지의 흐름을 통해서 첫번째 보라색 박스의 결과가 추출 된다.
    • 가운데 위 : 7 x 7 x 2 개의 bounding box값 [or에 대한 offset값], Confidence 값
    • 가운데 아래 : Class에 대한 20개 Softmax값에 대해, 가장 큰 확률값을 가지는 Object probability 값.
    • NMS 과정을 거쳐, 오른쪽 최종 결과를 추출한다. (NMS 과정 정리 이전 게시물)
      <NMS 과정>
      1. 높은 confidence를 가지는 Bbox를 정렬
      2. 높은 Confidence Score의 Box부터 겹치는 다른 Box를 모두 조사하여 특정 IOU 이상인 Box를 모두 제거 한다(IOU Threshold > 0.4, [주의] 이 값이 낮을 수록 많은 Box가 제거 된다. )
      3. 위의 과정을 반복해서 최종적으로 남은 Box만 return 한다.
  • YOLO v1 문제점
    • Detection 시간은 빠르나 성능은 떨어짐. 특히 작은 물체에 대한 성능 나쁨.
    • Anchor Box를 사용하지 않으므로, 어떤 힌트가 없다. Cell에 대해서 Bounding Box를 단순 예측 한다.
      • 추가 보충 설명 : Anchor box하고 유사한 후보 box를 사용하기는 한다. 7x7 형태의 Grid Cell 마다 중심점 만으로는 오보젝트의 형태를 제대로 예측하기 어렵습니다. 후보 bounding box와 같이 일정한 bounding box 형태가 일단 있어야 합니다. 그리고 이 후보 bounding box와 Ground Truth bounding box 간의 offset값을 학습할 수 있어야 합니다. 결국은 학습된 offset값에 기반해서 새로운 이미지 오브젝트의 offset을 예측하는 것입니다. anchor box도 이와 비슷한 방식이지만, V1은 많은 anchor box가 기준이 되는 게 아니라 개별 Grid Cell이 기준이 되어서 후보 bounding box와 GT Bounding box의 offset을 학습하기 때문에 anchor box 기반 예측이라고 간주하기가 조금은 어렵다.
    • 하나의 Cell에서 하나의 Object만 예측하므로, 겹친 Object는 Detect하지 못한다.

3. YOLO Version 2

  • 핵심 특징
    1. Darknet 19
    2. Grid Cell 당 Anchor Box 수 : 5
    3. Anchor box 사용 - 결정 방법 : K-means Clustering. Anchor box 크기와 ratio를 정하는 방법. GT Box를 그룹핑 한 후, 그것을 이용해서 Anchor box 크기와 ratio 결정.
    4. Output Peature Map 크기 : 13 x 13
    5. Batch Normaliztion 을 적극적으로 활용하기 시작
    6. High Resolution Classifier : 처음에 Input 224 x 224로 학습하다가, input 448 x 448로 Fine tuning 하여 성능향상을 얻음.
    7. 서로 다른 크기의 Image들 섞어서 배치하여 작은 Object도 Detect할 수 있도록 노력.

drawing

  • Network 구조도
    • Version 1에서 사용되던 Fully connected 구조가 사라짐

drawing

  • Anchor Box
    • 1개의 Cell에서 여러개의 Anchor를 통해 개별 Cell에서 여러개 Object Detection이 가능해졌다. 이전에는 Cell 중심 Detection이었다면, 이제는 Anchor 중심 Detecion을 수행한다.
    • K-Means Clustering 을 통해 데이터 세트의 GT의 크기와 Ratio에 대해서 군집화 분류를 하고, 학습에 사용할 Anchor Box의 크기와 Ratio를 선정했다.
    • 위의 사진을 보면 K-Means Clustering의 결과로 5개 색깔의 묶음이 나왔다. 1묶음이 1개의 Anchor Box의 평균사이즈가 되어 총 5개의 Anchor box를 Yolo2에서 사용한다.

drawing

  • Output
    • 13 x 13 의 최종 Feature Map에는 5개의 Anchor Box에 대한 정보가 들어가 있다.
    • 1개의 Anchor box에 대해서 25개의 정보를 가진다. 4(bounding box regression) + 1(Objectness Score x IOU)) + 20(class softmax)

3. YOLO Version 3

drawing

  • 핵심 정리
    • DarkNet 53 : ResNet을 사용하면 속도가 느린것을 할 수 있다. ResNet과 같이 Classification에서 사용하는 Backbone을 사용하는게 아니라, 실시간성을 높이기 위해서 자신들만의 Classification Baxkcone인 Darknet을 만드는 것을 알 수 있다. (Bn Ops/BFLOP/s 를 보아도 수행 성능이 좋음을 확인할 수 있다.)
    • Grid Cell 당 Anchor Box 수 : Output Feature map 당 3개. 서로 다른 크기와 스케일로 총 9개
    • Anchor box 사용 - 결정 방법 : K-means Clustering
    • Output Peature Map 크기 : 13 x 13, 26 x 26, 52 x 52 (3개의 다른 크기의 Ouput - Multi Scale Detection 가능)
    • Feature Map Scalling 기법 : FPN(Feature Pyramid Network)

drawing

  • 주요 특징
    • FPN와 유사한 기법을 적용하여 3개의 Feature Map Output에서(13x13 26x26 52x52), 각각 3개의 서로 다른 크기의 scale을 가진 anchor box로 Detection
    • Darknet 53 을 통해서 좀 더 강한 Sementic한 Feature Map추출
    • Multi Labels (사람 - 여성 - 빨간 옷 입음 - 커피 듬 이런 것) 예측하기 위해 Softmax가 아닌 Sigmoid 기반의 Logistic Regression Classifier로 개별 Object의 Multi labels 예측
    • 수행 속도는 Version2보다는 줄었지만 수행 성능을 높아 졌다.

drawing

  • 네트워크 구조도
    • input은 416 x 416 의 이미지가 들어간다. 원본이미지가 이것보다 크면 그 이미지를 Resize한다.
    • Skip Connection 과 Upsampling(초록색)이 적극적으로 활용되는 것을 볼 수 있다.
    • 82번 Layer에서 나오는 결과가 13 x 13 -> Anchor 3개 사용. 1번 2번 3번
    • 94번 Layer에서 나오는 결과가 26 x 26 -> Anchor 3개 사용. 4번 5번 6번
    • 106번 Layer에서 나오는 결과가 52 x 52 -> Anchor 3개 사용. 7번 8번 9번
    • 구조도를 보면 모르겠지만, 구조도 모양을 조금 바꾸면 FPN과 똑같다.
      Feature Pyramid Network 사진도 간략히 참고해 놓음.

drawing

  • Ouput
    • Output된 결과가 Box 하나당 4(bounding box regression) + 1(Objectness Score x IOU) + 80(class Logistic 값)이다.
    • Box는 총 9개니까 (4 x 1 x 80) x 9 의 ouput feature map depth
    • Multi Scale Detection에서 좋은 성능을 내었다.
  • Training
    • Multi-scale Training : 다양한 크기의 이미지를 배치로써 학습시킨다.
    • Data Augmentation 적극 이용

drawing

  • Location Prediction(아래 사진)
    • b는 ground True와 동일하게 내가 예측해야하는 박스의 크기와 좌표
      = 이게 Inference에서 최종 Output으로 나와야 한다. (아직 헷갈림)
    • c는 Anchor 박스의 좌표
    • p는 Anchor 박스의 크기
    • t는 offset 값 = 신경망의 역할 (아직 헷갈림)
    • 좌표 offset으로 0~1사이 값인 시그모이드 값 사용. Cell의 중심으로 부터bounding box가 너무 옮겨지지 않도록 해야한다.
    • Objectness Score = object이냐 아니냐 확률 x GT와 나의 최종 예측 boundingbox와의 IOU 이 나와야 한다.

drawing

  • Logistic Classifier 사용
    • Softmax사용하지 않음. 20개의 class가 있다면 다 합쳐서 1이 나오게 하는 softmax값 사용하지 않는다.
    • 보통은 Logistic Classifier 값(0.3, 0.6, 0.7)를 Softmax로 바꾸는데 (0.26, 0.35, 0.39) 이런 행동을 하지 않는다.
    • 그래서 사진과 같이 woman 이면서 person일 수 있다고 할 수 있다. = Multi Labels 예측 가능.
  • 결론
    • coco에서 IOU를 0.5~0.95를 사용하지 않고, 0.5만 고려한다면 Yolo3는 성능이 RetinaNet과 비슷하면서 속도가 굉장히 빠르다.
    • Focal Loss 를 사용해봤지만, 성능 향상을 그리 많이 되지 않았다. (이미 Focal Loss가 추구하는 방향이 Yolo에 들어가 있으므로)
    • RetinaNet은 Focal Loss와 FPN를 적극적으로 활용하고 있는 것은 안다.

drawing

【Tensorflow】SSD Inference 수행하기

Tensorflow v1.3 API로, SSD Inference 수행하기

/DLCV/Detection/ssd/Tensorflow_SSD_이미지와_영상_Detection.ipynb 참조

/DLCV/Detection/ssd/SSD_Face_Detection.md

이번 post를 참고해서, Tensorflofw inference 흐름 파악하기

1. Tensorflow로 SSD - Object Detection 수행하기

  • 이전 Post에서 받은,
    download 받은 SSD+Inception, SSD+Mobilenet 그래프 모델을 이용하여 Tensorflow에서 이미지와 비디오 Object Detectio 수행

  • SSD+Inception, SSD+Mobilenet의 config파일은 각각 /DLCV/Detection/ssd/pretrained/ “graph.pbtxt”, “graph2.pbtxt” 이다.
    하지만 여기서는 graph.pbtxt와 graph2.pbtxt를 사용할 필요가 없다. pb 파일만 필요하다^^

#### ms coco 클래스 아이디와 클래스 명 매핑
labels_to_names = {1:'person',2:'bicycle',3:'car',4:'motorcycle',5:'airplane',6:'bus',7:'train',8:'truck',9:'boat',10:'traffic light',
                    11:'fire hydrant',12:'street sign',13:'stop sign',14:'parking meter',15:'bench',16:'bird',17:'cat',18:'dog',19:'horse',20:'sheep',
                    21:'cow',22:'elephant',23:'bear',24:'zebra',25:'giraffe',26:'hat',27:'backpack',28:'umbrella',29:'shoe',30:'eye glasses',
                    31:'handbag',32:'tie',33:'suitcase',34:'frisbee',35:'skis',36:'snowboard',37:'sports ball',38:'kite',39:'baseball bat',40:'baseball glove',
                    41:'skateboard',42:'surfboard',43:'tennis racket',44:'bottle',45:'plate',46:'wine glass',47:'cup',48:'fork',49:'knife',50:'spoon',
                    51:'bowl',52:'banana',53:'apple',54:'sandwich',55:'orange',56:'broccoli',57:'carrot',58:'hot dog',59:'pizza',60:'donut',
                    61:'cake',62:'chair',63:'couch',64:'potted plant',65:'bed',66:'mirror',67:'dining table',68:'window',69:'desk',70:'toilet',
                    71:'door',72:'tv',73:'laptop',74:'mouse',75:'remote',76:'keyboard',77:'cell phone',78:'microwave',79:'oven',80:'toaster',
                    81:'sink',82:'refrigerator',83:'blender',84:'book',85:'clock',86:'vase',87:'scissors',88:'teddy bear',89:'hair drier',90:'toothbrush',
                    91:'hair brush'}

1-1 SSD + Inception으로 단일 이미지 Object Detection

import numpy as np
import tensorflow as tf
import cv2
import time
import matplotlib.pyplot as plt
%matplotlib inline


#inference graph를 읽음. .
with tf.gfile.FastGFile('./pretrained/ssd_inception_v2_coco_2017_11_17/frozen_inference_graph.pb', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    
with tf.Session() as sess:
    # Session 시작하고 inference graph 모델 로딩 
    sess.graph.as_default()
    tf.import_graph_def(graph_def, name='')
    
    # 입력 이미지 생성 및 BGR을 RGB로 변경 
    img = cv2.imread('../../data/image/beatles01.jpg')
    draw_img = img.copy()
    rows = img.shape[0]
    cols = img.shape[1]
    inp = cv2.resize(img, (300, 300))
    # OpenCV로 입력받은 BGR 이미지를 RGB 이미지로 변환 
    inp = inp[:, :, [2, 1, 0]] 

    start = time.time()
    # Object Detection 수행. 
    out = sess.run([sess.graph.get_tensor_by_name('num_detections:0'),
                    sess.graph.get_tensor_by_name('detection_scores:0'),
                    sess.graph.get_tensor_by_name('detection_boxes:0'),
                    sess.graph.get_tensor_by_name('detection_classes:0')],
                   feed_dict={'image_tensor:0': inp.reshape(1, inp.shape[0], inp.shape[1], 3)})
    
    green_color=(0, 255, 0)
    red_color=(0, 0, 255)
    
    # Bounding Box 시각화 
    # Detect된 Object 별로 bounding box 시각화 
    num_detections = int(out[0][0])
    for i in range(num_detections):
        # class id와 object class score, bounding box정보를 추출
        classId = int(out[3][0][i])
        score = float(out[1][0][i])
        bbox = [float(v) for v in out[2][0][i]]
        if score > 0.4:
            left = bbox[1] * cols
            top = bbox[0] * rows
            right = bbox[3] * cols
            bottom = bbox[2] * rows
            # cv2의 rectangle(), putText()로 bounding box의 클래스명 시각화 
            cv2.rectangle(draw_img, (int(left), int(top)), (int(right), int(bottom)), green_color, thickness=2)
            caption = "{}: {:.4f}".format(labels_to_names[classId], score)
            print(caption)
            cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, red_color, 1)
    
    print('Detection 수행시간:',round(time.time() - start, 2),"초")
    
img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
        

drawing

1-2 위에서 한작업 함수로 def 하기. Session 내부에 있는 일부 작업만 def한 것!

def get_tensor_detected_image(sess, img_array, use_copied_array):
    
    rows = img_array.shape[0]
    cols = img_array.shape[1]
    if use_copied_array:
        draw_img = img_array.copy()
    else:
        draw_img = img_array
    
    inp = cv2.resize(img_array, (300, 300))
    inp = inp[:, :, [2, 1, 0]]  # BGR2RGB
    
    start = time.time()
    # Object Detection 수행. 
    out = sess.run([sess.graph.get_tensor_by_name('num_detections:0'),
                    sess.graph.get_tensor_by_name('detection_scores:0'),
                    sess.graph.get_tensor_by_name('detection_boxes:0'),
                    sess.graph.get_tensor_by_name('detection_classes:0')],
                   feed_dict={'image_tensor:0': inp.reshape(1, inp.shape[0], inp.shape[1], 3)})
    
    green_color=(0, 255, 0)
    red_color=(0, 0, 255)
    
    # Bounding Box 시각화 
    # Detect된 Object 별로 bounding box 시각화 
    num_detections = int(out[0][0])
    for i in range(num_detections):
        # class id와 object class score, bounding box정보를 추출
        classId = int(out[3][0][i])
        score = float(out[1][0][i])
        bbox = [float(v) for v in out[2][0][i]]
        if score > 0.4:
            left = bbox[1] * cols
            top = bbox[0] * rows
            right = bbox[3] * cols
            bottom = bbox[2] * rows
            # cv2의 rectangle(), putText()로 bounding box의 클래스명 시각화 
            cv2.rectangle(draw_img, (int(left), int(top)), (int(right), int(bottom)), green_color, thickness=2)
            caption = "{}: {:.4f}".format(labels_to_names[classId], score)
            cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, red_color, 1)
    
    print('Detection 수행시간:',round(time.time() - start, 3),"초")
    return draw_img

1-3 위에서 만든 함수 사용해서 Inference 수행하기

#inference graph를 읽음. .
with tf.gfile.FastGFile('./pretrained/ssd_inception_v2_coco_2017_11_17/frozen_inference_graph.pb', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    
with tf.Session() as sess:
    # Session 시작하고 inference graph 모델 로딩 
    sess.graph.as_default()
    tf.import_graph_def(graph_def, name='')
    
    # 입력 이미지 생성, Object Detection된 image 반환, 반환된 image의 BGR을 RGB로 변경 
    img = cv2.imread('../../data/image/beatles01.jpg')
    draw_img = get_tensor_detected_image(sess, img, True)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)

2. tensorflow로 SSD+ Inception 기반 video Object Detection 수행

  • 위에서 만든 함수 사용
video_input_path = '../../data/video/Night_Day_Chase.mp4'
# linux에서 video output의 확장자는 반드시 avi 로 설정 필요. 
video_output_path = '../../data/output/Night_Day_Chase_tensor_ssd_incep01.avi'

cap = cv2.VideoCapture(video_input_path)

codec = cv2.VideoWriter_fourcc(*'XVID')

vid_size = (round(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
vid_fps = cap.get(cv2.CAP_PROP_FPS)
    
vid_writer = cv2.VideoWriter(video_output_path, codec, vid_fps, vid_size) 

frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print('총 Frame 갯수:', frame_cnt, 'FPS:', vid_fps )

green_color=(0, 255, 0)
red_color=(0, 0, 255)

# SSD+Inception inference graph를 읽음. .
with tf.gfile.FastGFile('./pretrained/ssd_inception_v2_coco_2017_11_17/frozen_inference_graph.pb', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    
with tf.Session() as sess:
    # Session 시작하고 inference graph 모델 로딩 
    sess.graph.as_default()
    tf.import_graph_def(graph_def, name='')
    index = 0
    while True:
        hasFrame, img_frame = cap.read()
        if not hasFrame:
            print('더 이상 처리할 frame이 없습니다.')
            break

        draw_img_frame = get_tensor_detected_image(sess, img_frame, False)
        vid_writer.write(draw_img_frame)
    # end of while loop

vid_writer.release()
cap.release()  

  • SSD가 CPU에서 매우 빠르고 잘 되는 것을 확인할 수 있다.

drawing

!gsutil cp ../../data/output/Night_Day_Chase_tensor_ssd_incep01.avi gs://my_bucket_dlcv/data/output/Night_Day_Chase_tensor_ssd_incep01.avi

3. SSD+Mobilenet Video Object Detection

  • 아래 코드는 커널을 재 기동후 바로 수행해야 합니다.
  • Graph를 가져오는 것에 대해서 위에서 했던 것과 중첩되면 오류가 날 수 있다.
  • inceptionV2 과 성능차이가 없지만, 수행시간이 CPU에서 더더 빠른 것을 확인할 수 있다.

3-1 함수정의하기

labels_to_names = {1:'person',2:'bicycle',3:'car',4:'motorcycle',5:'airplane',6:'bus',7:'train',8:'truck',9:'boat',10:'traffic light',
                    11:'fire hydrant',12:'street sign',13:'stop sign',14:'parking meter',15:'bench',16:'bird',17:'cat',18:'dog',19:'horse',20:'sheep',
                    21:'cow',22:'elephant',23:'bear',24:'zebra',25:'giraffe',26:'hat',27:'backpack',28:'umbrella',29:'shoe',30:'eye glasses',
                    31:'handbag',32:'tie',33:'suitcase',34:'frisbee',35:'skis',36:'snowboard',37:'sports ball',38:'kite',39:'baseball bat',40:'baseball glove',
                    41:'skateboard',42:'surfboard',43:'tennis racket',44:'bottle',45:'plate',46:'wine glass',47:'cup',48:'fork',49:'knife',50:'spoon',
                    51:'bowl',52:'banana',53:'apple',54:'sandwich',55:'orange',56:'broccoli',57:'carrot',58:'hot dog',59:'pizza',60:'donut',
                    61:'cake',62:'chair',63:'couch',64:'potted plant',65:'bed',66:'mirror',67:'dining table',68:'window',69:'desk',70:'toilet',
                    71:'door',72:'tv',73:'laptop',74:'mouse',75:'remote',76:'keyboard',77:'cell phone',78:'microwave',79:'oven',80:'toaster',
                    81:'sink',82:'refrigerator',83:'blender',84:'book',85:'clock',86:'vase',87:'scissors',88:'teddy bear',89:'hair drier',90:'toothbrush',
                    91:'hair brush'}

def get_tensor_detected_image(sess, img_array, use_copied_array):
    
    rows = img_array.shape[0]
    cols = img_array.shape[1]
    if use_copied_array:
        draw_img = img_array.copy()
    else:
        draw_img = img_array
    
    inp = cv2.resize(img_array, (300, 300))
    inp = inp[:, :, [2, 1, 0]]  # BGR2RGB
    

    start = time.time()
    # Object Detection 수행. 
    out = sess.run([sess.graph.get_tensor_by_name('num_detections:0'),
                    sess.graph.get_tensor_by_name('detection_scores:0'),
                    sess.graph.get_tensor_by_name('detection_boxes:0'),
                    sess.graph.get_tensor_by_name('detection_classes:0')],
                   feed_dict={'image_tensor:0': inp.reshape(1, inp.shape[0], inp.shape[1], 3)})
    
    green_color=(0, 255, 0)
    red_color=(0, 0, 255)
    
    # Bounding Box 시각화 
    # Detect된 Object 별로 bounding box 시각화 
    num_detections = int(out[0][0])
    for i in range(num_detections):
        # class id와 object class score, bounding box정보를 추출
        classId = int(out[3][0][i])
        score = float(out[1][0][i])
        bbox = [float(v) for v in out[2][0][i]]
        if score > 0.3:
            left = bbox[1] * cols
            top = bbox[0] * rows
            right = bbox[3] * cols
            bottom = bbox[2] * rows
            # cv2의 rectangle(), putText()로 bounding box의 클래스명 시각화 
            cv2.rectangle(draw_img, (int(left), int(top)), (int(right), int(bottom)), green_color, thickness=2)
            caption = "{}: {:.4f}".format(labels_to_names[classId], score)
            cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, red_color, 1)
    
    print('Detection 수행시간:',round(time.time() - start, 3),"초")
    return draw_img

3-2 정의한 함수사용해서 비디오 Detection 수행하기

import numpy as np
import tensorflow as tf
import cv2
import time
import matplotlib.pyplot as plt
%matplotlib inline

video_input_path = '../../data/video/Night_Day_Chase.mp4'
# linux에서 video output의 확장자는 반드시 avi 로 설정 필요. 
video_output_path = '../../data/output/Night_Day_Chase_tensor_ssd_mobile_01.avi'

cap = cv2.VideoCapture(video_input_path)

codec = cv2.VideoWriter_fourcc(*'XVID')

vid_size = (round(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
vid_fps = cap.get(cv2.CAP_PROP_FPS)
    
vid_writer = cv2.VideoWriter(video_output_path, codec, vid_fps, vid_size) 

frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print('총 Frame 갯수:', frame_cnt, 'FPS:', vid_fps )

green_color=(0, 255, 0)
red_color=(0, 0, 255)

# SSD+Mobilenet inference graph를 읽음. .
with tf.gfile.FastGFile('./pretrained/ssd_mobilenet_v2_coco_2018_03_29/frozen_inference_graph.pb', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    
with tf.Session() as sess:
    # Session 시작하고 inference graph 모델 로딩 
    sess.graph.as_default()
    tf.import_graph_def(graph_def, name='')
    index = 0
    while True:
        hasFrame, img_frame = cap.read()
        if not hasFrame:
            print('더 이상 처리할 frame이 없습니다.')
            break

        draw_img_frame = get_tensor_detected_image(sess, img_frame, False)
        vid_writer.write(draw_img_frame)
    # end of while loop

vid_writer.release()
cap.release()  

!gsutil cp ../../data/output/Night_Day_Chase_tensor_ssd_mobile_01.avi gs://my_bucket_dlcv/data/output/Night_Day_Chase_tensor_ssd_mobile_01.avi

4. Tensorflow Inference.pb 파일가져와 사용하기(Face Detection)

  • WiderFace 데이터세트로 Pretrained된 Tensorflow graph 모델을 다운로드 받아 이를 이용해 Face Detection 수행.

  • Github : https://github.com/yeephycho/tensorflow-face-detection

  • SSD + MobileNet기반으로 Pretrained된 모델 다운로드

  • https://github.com/yeephycho/tensorflow-face-detection/raw/master/model/frozen_inference_graph_face.pb

drawing

4-1 SSD + Mobilenet Pretrained된 모델 로딩하여 Face Detection

import numpy as np
import tensorflow as tf
import cv2
import time
import matplotlib.pyplot as plt
%matplotlib inline
        
def get_tensor_detected_image(sess, img_array, use_copied_array):
    
    rows = img_array.shape[0]
    cols = img_array.shape[1]
    if use_copied_array:
        draw_img = img_array.copy()
    else:
        draw_img = img_array
    
    inp = cv2.resize(img_array, (300, 300))
    inp = inp[:, :, [2, 1, 0]]  # BGR2RGB
    
    start = time.time()
    # Object Detection 수행. 
    out = sess.run([sess.graph.get_tensor_by_name('num_detections:0'),
                    sess.graph.get_tensor_by_name('detection_scores:0'),
                    sess.graph.get_tensor_by_name('detection_boxes:0'),
                    sess.graph.get_tensor_by_name('detection_classes:0')],
                   feed_dict={'image_tensor:0': inp.reshape(1, inp.shape[0], inp.shape[1], 3)})
    
    green_color=(0, 255, 0)
    red_color=(0, 0, 255)
    
    # Bounding Box 시각화 
    # Detect된 Object 별로 bounding box 시각화 
    num_detections = int(out[0][0])
    for i in range(num_detections):
        # class id와 object class score, bounding box정보를 추출
        classId = int(out[3][0][i])
        score = float(out[1][0][i])
        bbox = [float(v) for v in out[2][0][i]]
        if score > 0.4:
            left = bbox[1] * cols
            top = bbox[0] * rows
            right = bbox[3] * cols
            bottom = bbox[2] * rows
            # cv2의 rectangle(), putText()로 bounding box의 클래스명 시각화 
            cv2.rectangle(draw_img, (int(left), int(top)), (int(right), int(bottom)), green_color, thickness=2)
            caption = "face: {:.4f}".format(score)
            cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, red_color, 2)
    
    print('Detection 수행시간:',round(time.time() - start, 3),"초")
    return draw_img

#inference graph를 읽음. .
with tf.gfile.FastGFile('./pretrained/frozen_inference_graph_face.pb', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    
with tf.Session() as sess:
    # Session 시작하고 inference graph 모델 로딩 
    sess.graph.as_default()
    tf.import_graph_def(graph_def, name='')
    
    # 입력 이미지 생성, Object Detection된 image 반환, 반환된 image의 BGR을 RGB로 변경 
    img = cv2.imread('../../data/image/EPL01.jpg')
    draw_img = get_tensor_detected_image(sess, img, True)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)

drawing

4-2 tensorflow로 SSD+ Inception 기반 video Object Detection 수행

from IPython.display import clear_output, Image, display, Video, HTML
Video('../../data/video/InfiniteWar01.mp4')
video_input_path = '../../data/video/InfiniteWar01.mp4'
# linux에서 video output의 확장자는 반드시 avi 로 설정 필요. 
video_output_path = '../../data/output/InfiniteWar01_ssd.avi'

cap = cv2.VideoCapture(video_input_path)

codec = cv2.VideoWriter_fourcc(*'XVID')

vid_size = (round(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
vid_fps = cap.get(cv2.CAP_PROP_FPS)
    
vid_writer = cv2.VideoWriter(video_output_path, codec, vid_fps, vid_size) 

frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print('총 Frame 갯수:', frame_cnt, 'FPS:', vid_fps )

green_color=(0, 255, 0)
red_color=(0, 0, 255)

# SSD+Inception inference graph를 읽음. .
with tf.gfile.FastGFile('./pretrained/frozen_inference_graph_face.pb', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    
with tf.Session() as sess:
    # Session 시작하고 inference graph 모델 로딩 
    sess.graph.as_default()
    tf.import_graph_def(graph_def, name='')
    index = 0
    while True:
        hasFrame, img_frame = cap.read()
        if not hasFrame:
            print('더 이상 처리할 frame이 없습니다.')
            break

        draw_img_frame = get_tensor_detected_image(sess, img_frame, False)
        vid_writer.write(draw_img_frame)
    # end of while loop

vid_writer.release()
cap.release()  

Detection 수행시간: 0.307 초
Detection 수행시간: 0.294 초
Detection 수행시간: 0.296 초
Detection 수행시간: 0.304 초
Detection 수행시간: 0.296 초
Detection 수행시간: 0.294 초
Detection 수행시간: 0.293 초
Detection 수행시간: 0.31 초
더 이상 처리할 frame이 없습니다.
!gsutil cp ../../data/output/InfiniteWar01_ssd.avi gs://my_bucket_dlcv/data/output/InfiniteWar01_ssd.avi

drawing

【Python-Module】 SSD 수행하기 - OpenCV DNN 모듈

이전 Post를 통해서 SSD의 이론에 대해서 공부했다. OpenCV DNN 모델을 사용해서 SSD를 수행해보자.

OpenCV DNN 모듈을 사용해서 SSD 수행하기
/DLCV/Detection/SSD/OpenCV_SSD_이미지와_영상_Detection.ipynb 참조
OpenCV 사용과정 요약은 이전 Post 참조

1. OpenCV DNN 패키지를 이용하여 SSD 수행하기

  • Tensorflow 에서 Pretrained 된 모델 파일을 OpenCV에서 로드하여 이미지와 영상에 대한 Object Detection 수행.
### 입력 이미지로 사용될 이미지 보기
import cv2
import matplotlib.pyplot as plt
%matplotlib inline

img = cv2.imread('../../data/image/avengers.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

print('image shape:', img.shape)
plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
image shape: (330, 499, 3)

drawing

1-1 Tensorflow에서 Pretrained 된 Inference모델(Frozen graph)와 환경파일을 다운로드 받은 후 이를 이용해 OpenCV에서 Inference 모델 생성

  • https://github.com/opencv/opencv/wiki/TensorFlow-Object-Detection-API 에 다운로드 URL 있음.
  • Inception-SSD v2 2017_11_17 이것 사용할 예정
  • pretrained 모델은 http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2017_11_17.tar.gz 에서 다운로드 후 압축 해제
  • pretrained 모델을 위한 환경 파일은 https://github.com/opencv/opencv_extra/blob/master/testdata/dnn/ssd_inception_v2_coco_2017_11_17.pbtxt 에서 다운로드
  • download된 모델 파일과 config 파일을 인자로 하여 inference 모델을 DNN에서 로딩함.
# %cd /home/sb020518/DLCV/Detection/ssd
# !mkdir pretrained 
# %cd ./pretrained
# !wget http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2017_11_17.tar.gz
# !wget https://raw.githubusercontent.com/opencv/opencv_extra/master/testdata/dnn/ssd_inception_v2_coco_2017_11_17.pbtxt
# !tar -xvf ssd*.tar.gz
# !mv ssd_inception_v2_coco_2017_11_17.pbtxt graph.pbtxt
# cd ../
!pwd
!ls pretrained/ssd_inception_v2_coco_2017_11_17
/home/sb020518/DLCV/Detection/ssd
checkpoint		   model.ckpt.data-00000-of-00001  model.ckpt.meta
frozen_inference_graph.pb  model.ckpt.index		   saved_model
cv_net = cv2.dnn.readNetFromTensorflow('./pretrained/ssd_inception_v2_coco_2017_11_17/frozen_inference_graph.pb', 
                                     './pretrained/graph.pbtxt')
  • coco 데이터 세트의 클래스id별 클래스명 지정.
  • 왜 90번 까지 하는지는 여기 참조
  • 90번까지 해야할지 91번까지 해야할지는 실험적으로 확인하는 수 밖에 없다.
labels_to_names = {1:'person',2:'bicycle',3:'car',4:'motorcycle',5:'airplane',6:'bus',7:'train',8:'truck',9:'boat',10:'traffic light',
                    11:'fire hydrant',12:'street sign',13:'stop sign',14:'parking meter',15:'bench',16:'bird',17:'cat',18:'dog',19:'horse',20:'sheep',
                    21:'cow',22:'elephant',23:'bear',24:'zebra',25:'giraffe',26:'hat',27:'backpack',28:'umbrella',29:'shoe',30:'eye glasses',
                    31:'handbag',32:'tie',33:'suitcase',34:'frisbee',35:'skis',36:'snowboard',37:'sports ball',38:'kite',39:'baseball bat',40:'baseball glove',
                    41:'skateboard',42:'surfboard',43:'tennis racket',44:'bottle',45:'plate',46:'wine glass',47:'cup',48:'fork',49:'knife',50:'spoon',
                    51:'bowl',52:'banana',53:'apple',54:'sandwich',55:'orange',56:'broccoli',57:'carrot',58:'hot dog',59:'pizza',60:'donut',
                    61:'cake',62:'chair',63:'couch',64:'potted plant',65:'bed',66:'mirror',67:'dining table',68:'window',69:'desk',70:'toilet',
                    71:'door',72:'tv',73:'laptop',74:'mouse',75:'remote',76:'keyboard',77:'cell phone',78:'microwave',79:'oven',80:'toaster',
                    81:'sink',82:'refrigerator',83:'blender',84:'book',85:'clock',86:'vase',87:'scissors',88:'teddy bear',89:'hair drier',90:'toothbrush',
                    91:'hair brush'}

1-2 이미지를 preprocessing 수행하여 Network에 입력하고 Object Detection 수행 후 결과를 이미지에 시각화

# 원본 이미지 (633, 806)를 네트웍에 입력시에는 (300, 300)로 resize 함. 
# 이후 결과가 출력되면 resize된 이미지 기반으로 bounding box 위치가 예측 되므로 이를 다시 원복하기 위해 원본 이미지 shape정보 필요
rows = img.shape[0]
cols = img.shape[1]
# cv2의 rectangle()은 인자로 들어온 이미지 배열에 직접 사각형을 업데이트 하므로 그림 표현을 위한 별도의 이미지 배열 생성. 
draw_img = img.copy()

# 원본 이미지 배열을 사이즈 (300, 300)으로, BGR을 RGB로 변환하여 배열 입력
cv_net.setInput(cv2.dnn.blobFromImage(img,  size=(300, 300), swapRB=True, crop=False))
# Object Detection 수행하여 결과를 cv_out으로 반환 
cv_out = cv_net.forward()
print(cv_out.shape)

# bounding box의 테두리와 caption 글자색 지정
green_color=(0, 255, 0)
red_color=(0, 0, 255)

# detected 된 object들을 iteration 하면서 정보 추출
for detection in cv_out[0,0,:,:]:
    score = float(detection[2])
    class_id = int(detection[1])
    # detected된 object들의 score가 0.3 이상만 추출
    if score > 0.3:
        # detected된 object들은 image 크기가 (300, 300)으로 scale된 기준으로 예측되었으므로 다시 원본 이미지 비율로 계산
        left = detection[3] * cols
        top = detection[4] * rows
        right = detection[5] * cols
        bottom = detection[6] * rows
        # labels_to_names 딕셔너리로 class_id값을 클래스명으로 변경. opencv에서는 class_id + 1로 매핑해야함.
        caption = "{}: {:.4f}".format(labels_to_names[class_id], score)
        
        #cv2.rectangle()은 인자로 들어온 draw_img에 사각형을 그림. 위치 인자는 반드시 정수형.
        cv2.rectangle(draw_img, (int(left), int(top)), (int(right), int(bottom)), color=green_color, thickness=2)
        cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, red_color, 2)
        print(caption, class_id)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
(1, 1, 100, 7)

drawing

1-3 위에서 한 작업 그대로 함수로 def

import time

def get_detected_img(cv_net, img_array, score_threshold, use_copied_array=True, is_print=True):
    
    rows = img_array.shape[0]
    cols = img_array.shape[1]
    
    draw_img = None
    if use_copied_array:
        draw_img = img_array.copy()
        #draw_img = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)
    else:
        draw_img = img_array
    
    cv_net.setInput(cv2.dnn.blobFromImage(img_array, size=(300, 300), swapRB=True, crop=False))
    
    start = time.time()
    cv_out = cv_net.forward()
    
    green_color=(0, 255, 0)
    red_color=(0, 0, 255)

    # detected 된 object들을 iteration 하면서 정보 추출
    for detection in cv_out[0,0,:,:]:
        score = float(detection[2])
        class_id = int(detection[1])
        # detected된 object들의 score가 0.4 이상만 추출
        if score > score_threshold:
            # detected된 object들은 image 크기가 (300, 300)으로 scale된 기준으로 예측되었으므로 다시 원본 이미지 비율로 계산
            left = detection[3] * cols
            top = detection[4] * rows
            right = detection[5] * cols
            bottom = detection[6] * rows
            # labels_to_names 딕셔너리로 class_id값을 클래스명으로 변경. opencv에서는 class_id + 1로 매핑해야함.
            caption = "{}: {:.4f}".format(labels_to_names[class_id], score)

            #cv2.rectangle()은 인자로 들어온 draw_img에 사각형을 그림. 위치 인자는 반드시 정수형.
            cv2.rectangle(draw_img, (int(left), int(top)), (int(right), int(bottom)), color=green_color, thickness=2)
            cv2.putText(draw_img, caption, (int(left), int(top - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, red_color, 2)
    if is_print:
        print('Detection 수행시간:',round(time.time() - start, 2),"초")

    return draw_img

drawing

# 위에서 def 한 함수 사용해서 Detection 수행하기.
# 0.23초 지린다. 
img = cv2.imread('../../data/image/EPL01.jpg')

#coco dataset 클래스명 매핑

# tensorflow inference 모델 로딩
cv_net = cv2.dnn.readNetFromTensorflow('./pretrained/ssd_inception_v2_coco_2017_11_17/frozen_inference_graph.pb', 
                                     './pretrained/graph.pbtxt')
# Object Detetion 수행 후 시각화 
draw_img = get_detected_img(cv_net, img, score_threshold=0.4, use_copied_array=True, is_print=True)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
Detection 수행시간: 0.27 초

2. Video Object Detection 수행

원본 영상 보기

from IPython.display import clear_output, Image, display, Video, HTML
Video('../../data/video/John_Wick_small.mp4')
  • VideoCapture와 VideoWriter 설정하기
  • VideoCapture를 이용하여 Video를 frame별로 capture 할 수 있도록 설정
  • VideoCapture의 속성을 이용하여 Video Frame의 크기 및 FPS 설정.
  • VideoWriter를 위한 인코딩 코덱 설정 및 영상 write를 위한 설정
  • 총 Frame 별로 iteration 하면서 Object Detection 수행. 개별 frame별로 단일 이미지 Object Detection과 유사
  • video detection 전용 함수 생성.
def do_detected_video(cv_net, input_path, output_path, score_threshold, is_print):
    
    cap = cv2.VideoCapture(input_path)

    codec = cv2.VideoWriter_fourcc(*'XVID')

    vid_size = (round(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    vid_fps = cap.get(cv2.CAP_PROP_FPS)

    vid_writer = cv2.VideoWriter(output_path, codec, vid_fps, vid_size) 

    frame_cnt = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    print('총 Frame 갯수:', frame_cnt, )

    green_color=(0, 255, 0)
    red_color=(0, 0, 255)
    while True:
        hasFrame, img_frame = cap.read()
        if not hasFrame:
            print('더 이상 처리할 frame이 없습니다.')
            break
        
        returned_frame = get_detected_img(cv_net, img_frame, score_threshold=score_threshold, use_copied_array=True, is_print=True)
        vid_writer.write(returned_frame)
    # end of while loop

    vid_writer.release()
    cap.release()
do_detected_video(cv_net, '../../data/video/John_Wick_small.mp4', '../../data/output/John_Wick_small_ssd01.avi', 0.4, True)
!gsutil cp ../../data/output/John_Wick_small_ssd01.avi gs://my_bucket_dlcv/data/output/John_Wick_small_ssd01.avi

3. SSD+Mobilenet으로 Object Detection 수행.

  • https://github.com/opencv/opencv/wiki/TensorFlow-Object-Detection-API 에 다운로드 URL 있음.
  • weight파일은 http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v2_coco_2018_03_29.tar.gz 에서 다운로드

  • 수행결과 : 위에서는 SSD+inceptionV2를 사용했다. 확실히 MobileNet의 속도가 훨씬 빠른것을 확인할 수 있다.
# !mkdir pretrained; cd pretrained
# !wget  http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v2_coco_2018_03_29.tar.gz
# !wget https://raw.githubusercontent.com/opencv/opencv_extra/master/testdata/dnn/ssd_mobilenet_v2_coco_2018_03_29.pbtxt
# cd ssd_mobilenet_v2_coco_2018_03_29; mv ssd_mobilenet_v2_coco_2018_03_29.pbtxt graph.pbtxt
cv_net_mobile = cv2.dnn.readNetFromTensorflow('./pretrained/ssd_mobilenet_v2_coco_2018_03_29/frozen_inference_graph.pb', 
                                     './pretrained/ssd_mobilenet_v2_coco_2018_03_29/graph.pbtxt')
#### 영상 Detection
do_detected_video(cv_net_mobile, '../../data/video/John_Wick_small.mp4', '../../data/output/John_Wick_small_ssd_mobile01.avi', 0.2, True)
!gsutil cp ../../data/output/John_Wick_small_ssd_mobile01.avi gs://my_bucket_dlcv/data/output/John_Wick_small_ssd_mobile01.avi
#### Image Detection
img = cv2.imread('../../data/image/avengers.jpg')

#coco dataset 클래스명 매핑

cv_net_mobile = cv2.dnn.readNetFromTensorflow('./pretrained/ssd_mobilenet_v2_coco_2018_03_29/frozen_inference_graph.pb', 
                                     './pretrained/ssd_mobilenet_v2_coco_2018_03_29/graph.pbtxt')
# Object Detetion 수행 후 시각화 
draw_img = get_detected_img(cv_net_mobile, img, score_threshold=0.4, use_copied_array=True, is_print=True)

img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)

【Paper】 SSD(SIngle Shot Multibox Detector)

One Stage Detector의 시작이라고 할 수 있는 Yolo, SSD에 대해서 다시 공부하고 정리해본다.

SSD(SIngle Shot Multibox Detector)

1. Objecet Detection History 그리고 SSD

drawing

  • Region Proposal + Detection 을 한방에 하는 One Stage detector
  • Yolo가 처음 나와서 Faster RCNN보다 4배 속도는 빠르지만 정확도는 떨어졌다.
  • SSD는 Faster RCNN보다 mAP보다 좋고, Yolo보다 빠른 object detection 모델이다. 한때 SOTA
  • 그 후 Yolo에서 SSD 기법을 차용하며 지금은 version 4 까지 나와서 속도와 정확도 모두를 가진 모델이 되었다.

  • SSD Network의 핵심
    • BackBone을 통과시키고 점점 featrue map resolution을 낮춰가면서 그 모든 Feature map에 대해서 Detection + Box regressoin을 수행한다.
    • 핵심은 2가지이다
      1. Multi Scale Feature Layer
      2. Default (Anchor) Box : 수행 성능과 속도의 2마리 토끼를 잡게 해준 포인트!

2. Multi Scale Feature Layer

  • 과거 Multi Scale Detector = Image Pyramid를 이용하여 Sliding Windows 기법을 수행한다.

drawing

  • SSD는 이 Image Pyramid 기법을 차용해서 Object Detection을 수행한다. Feature Map을 convNet을 통과시켜 크기를 줄이고 줄여가며 각각 Detection을 수행한다.
  • 아래의 이미지에서 32x32에서는 작은 object, 4x4에서는 큰 object를 Detect 할 수 있다.

drawing

3. Default (Anchor) Box

  • Faster RCNN의 RPN Network에 대해서 다시 생각해보자. Anchor 박스를 잘 활용해서 좋은 Detection을 수행할 수 있었다. 하지만 RPN에서도 그리고 Classification의 뒷단(Object Detection)에서도 Bounding Box Regression이 2번 수행되는 것은 약간 불 필요하지 않나? 라는 생각을 할 수 있다.

  • Anchor Box 개념은 One stage Detector에서도 좋은 성능 향상을 가져와 주었다.

  • Anchor Box 개념을 RPN 뿐만 아니라 뒷단의 Object Detection에서도 수행하면 좋지 않을까? 라는 생각을 하게 되었다. 그래서 Yolo와 SSD에서 Default Anchor Box를 사용하게 되었다.

  • Anchor 박스를 통해서 학습해야 하는 것.

    1. Anchor box와 겹치는 Feature 맵 영역의 obeject Class 분류
    2. GT Box 위치를 예측할 수 있도록 수정된 좌표

drawing

  • 위의 사진에서 1 ~ 6 번까지 흐름 순서대로 따라가며 공부하기
  • 확실히 이해가 안되면 이번 Faster RCNN - Anchor box POST 확인해서 먼저 공부하고 다시 위의 사진 공부하기

4. SSD architecture 그리고 Default Box

drawing

  • SSD input은 512x512 or 300x300 이다.
  • Object Detion 단으로 넘어가는 Feature Map의 크기가 38x38 19x19 10x10 5x5 3x3 1x1 Feature map 결과들이다.
  • SSD 에서 논문에서는 Dafault Box라는 용어를 사용한다. Anchor Box와 똑같은 것이다.

  • 위 사진의 그림 중 윗 중간 부분에 Classifier : Conv, 3x3x(4x(classes+4)) 라고 적혀 있는 것을 확인할 수 있다. 이것에 대해서 자세하게 생각해보면 다음과 같다.
    • 3x3x(4x(classes+4)) : 3x3 필터를 사용한다. 위,위 사진을 확인하면 Locallization과 classification에서 3x3 필터를 사용하는 것처럼.
    • 3x3x(4x(classes+4)) : 4개의 Anchor 박스
    • 3x3x(4x(classes+4)) : 각 class에 대한 Softmax 값(= confidence 값)(20+1(classes And background)), Bounding Box 좌표값.

    • 3x3x(4x(classes+4)) 이것은 Kernel의 갯수이자, Detection 결과 Output의 Depth 값이다. (ouput 결과의 width hight는 38x38 19x19 10x10 5x5 3x3 1x1 일 것이다.) 당연히 3x3 Kernel의 Depth는 각 Feature Map(38x38 19x19 10x10 5x5 3x3 1x1)의 Depth값을 따르게 된다.
  • 그렇게 해서 만들어지는 Box의 갯수는 다음과 같고 이것을 NMS 처리를 한다.

drawing

5. Matching 전략과 Loss 함수

  • Matching 전략 : Ground True Bounding box와 겹치는 IOU가 0.5이상인 Anchor Box들을 찾는다. 이 Anchor Box를 Matching 된 Anchor Box라고 한다. 그런 Matching Anchor Box 만을 이용해서 Classification과 Bounding Box Regression에 대한 학습을 해나간다.

  • Loss 함수 :

    • Multi Tast Loss를 사용했다. Faster RCNN의 Loss와 동일하다.
    • N matching 된 Default Box의 갯수로써, 정규화에 사용한다.

drawing

6. Design Choice 별 Performance (아래 이미지 참조)

  • 1:2 rate의 box를 사용했을 때 vs 1:3 rate의 box를 사용했을 때

  • atrous : dilated convolution

  • Data Augmentation을 했을 때의 역할이 매우 컸다.

  • Yolo에서의 작은 Object Detection을 못하는 문제를 해결하기 위해 노력했다.
    이 문제를 SSD에서는 Data Augmentation을 이용해서 해결하려고 엄청 노력을 했다.

  • 논문에서 계속 나오는 용어

    • SSD300 : 300x300 이미지 Input
    • SSD512 : 512x512 이미지 Input

drawing

Pagination


© All rights reserved By Junha Song.