Group Study (2022-2023)/Machine Learning

[Machine Learning] 6주차 스터디 - Object Detection: YOLOv1~v2

sooking87 2022. 11. 15. 13:56

Introduction

download1

One-Stage Detector

이미지가 들어오면 Conv & FC Layers를 거져서 output을 만들게 되고 이를 통해서 Classification과 Box Regression을 진행한다.

Two-Stage Detector

원본 이미지 -> Region Proposal -> 객체가 있을 것 같은 영역을 찾아낸다.

원본 이밎 -> Classification를 통해서 Feature map을 뽑아낸 후 -> Proposed Regions를 Feature map에 투영시켜서 Classification과 Box Regression을 진행

YOLO

Main Contribution

  1. object detection 을 regression problem으로 관점 전환
  2. Unified Architecture: 하나의 신경망으로 classification & localization 예측
  3. DPM, R-CNN 모델보다 속도 개선
  4. 여러 도메인에서 object detection 가능

Unified Detection

download1

 

one-stage detection으로 통합

  1. input image를 S x S 그리드로 나눈다.
  2. Box Regression + Class Probability을 구함

download2



하나의 그리드 셀에서 예측할 BBox를 2개, 클래스의 종류를 20이라고 할 때

Box Regression 진행 과정

하나의 그리드 셀을 기준으로 예측한 첫 번째 BBox는 진한 파란색 박스이고, 여기서 나온 output으로는 BBox의 정 중앙 좌표일 x, y 그리고 input의 w, h를 셀만큼 나누어서 정규화한 w, h(0~1) 그리고 물체가 BBox 내에 있는지 없는지 나타내는 Pc이다.

Classification

각 클래스에 대해서 해당 객체가 어떤 클래스에 해당할지에 대한 확률을 나타낸다.

download3

Network Design

download1

 

주로 사용한 모델의 구조는 GoogleNet. Conv Layer를 많이 쌓으면 연산량이 증가해서 중간에 Reduction Layer를 추가한 것을 확인할 수 있다.

Training Stage

어떻게 학습을 하는지에 대해서 다룸.

download2



실제 객체가 있는 박스를 Groundtruth라고 하는데, 여기서 박스의 중앙에 점을 포함하고 있는 셀이 responsible한 셀이 되는 것이다.

download3



학습을 할 때에는 한 가지 BBox를 사용을 하고, 여기서 BBox가 결정되는 기준이 IoU이다. Groundtruth와 겹치는 부분이 가장 많은 박스로 선택.

-> IoU가 가장 큰 값을 가진 박스는 노란색 박스이고, 여기서 스칼라값을 1로 표시하여 Loss Function에 반영이 되게 하였고, 다른 스칼라값은 0으로 두어 Loss Function에 반영이 되지 않게 하였다.

download1



첫 번째는 모든 셀에 대해서 B개(예측하고자 했던 BBox 개수)의 BBox 좌표와 GT(GroundTruth) Box좌표의 오차를 구하는 공식

두 번째를 모든 셀에 대해서 B개의 확률값과 GT값의 오차를 구하는 공식

모든 셀의 confidence score와 정답 값의 오차를 구하는 공식

첫 번째 스칼라값: 몇 번째의 셀이 가장 예측력이 좋은 BBox를 표현

두 번째 스칼라값: 물체가 나타났는지(1) 아닌지(0)를 표현

세 번째 람다값: 어떤 BBox의 손실을 더 반영을 할 것인지를 표현

즉, 그리드 셀에 객체가 존재하는 경우의 오차와 예측 박스로 선정된 경우에만 오차를 학습한다.

Inference Stage(예측 단계)

첫 번째 BBox의 클래스 확률값과 학습 과정에서 구했던 Class Probability를 곱해서 각 바운딩 박스마다 구하게 된다.

download1

 

이 과정대로 진행을 하게 된다면 BBox의 개수가 많아지게 되어, NMS(Non-Maximum Suppression) 알고리즘이 적용된다.

download2

 

이 알고리즘을 통해 클래스 별로 비교를 해서 가장 예측력이 좋은 BBox만 남길 수 있다. 코드를 보면 특정 값을 넘지 않으면 0으로 처리

download1



-> 예측값이 높은 BBox를 기준으로 내림차순 정렬

나머지 BBox는 BBox#12와의 IoU가 높아서 NMS에 의해서 제거된다.

download1



만약 탐지해야되는 객체가 여러 개일 경우, 예측값이 높은 BBox끼리 IoU를 비교를 할 텐데, 이 때의 IoU는 낮은 값 또는 0의 값을 가지게 되므로 NMS에 의해서 제거되지 않는다.

서로 다른 Object가 있을 때

download2

성능

속도: Fast YOLO >> YOLO >> DPM, R-CNN

성능: Faster R-CNN >> Fast R-CNN >> YOLO >> DPM

Limitaion

  1. 작은 물체에 대해서 탐지 성능이 낮음 -> BBox가 작게 되어서 IoU의 값 차이가 작아지게 되기 때문에
  2. 일반화된 지식이랑 다르게 객체 비율이 달라지면 detection 성능이 낮아짐.

YOLO9000: Better, Faster, Stronger

Main Contribution

  • YOLOv2제안: YOLOv1의 단점을 개선하여 연산을 빠르게 정확도는 높임
  • YOLO9000제안: Detection dataset의 적은 Class 개수로 인한 예측 가능한 class 개수의 증가. -> 존재하지 않은 클래스에 대한 예측도 가능해짐
  • 새로운 classification network인 Darknet-19를 통해서 성능 향상

Better(Model: YOLOv2)

  1. batch normalization

    이전 layer의 파라미터 변화로 인해 현재 layer의 입력 분포가 바뀌는 현상을 방지하기 위해서 사용. -> mini batch 사용하여 학습 시, 빠른 수렴 가능 + 정규화 효과를 통해서 overfitting 발생 X
  2. high resolution classifier

    YOLOv1에서는 classification에서는 저해상도를 사용해서 pretrain을 시킨다. 그 이후 task에서는 고해상도 사진을 넣고 Fine tuning 하는 과정을 거쳤는데, 이렇게 된다면 네트워크가 피팅되는데 시간이 걸린다.

    download1


    Sol. 똑같이 저해상도 이미지로 pretrain을 한 다음 마지막 10 에포크 정도를 고해상도 이미지로 Fine-tunning 하게 한다.
  3. Convolutional with anchor boxes
    YOLOv2와 v1과의 차이

    download3


    그리드 별로 anchor box 5개를 예측하되 anchor box의 원점은 grid cell 내에 존재하도록 예측해야된다.

    output의 경우는 v1과 마찬가지고 x, y, w, h, Pc(클래스 예측 확률값)으로 나온다.
  4. download2
  5. dimension clusters
    download1


    생성한 anchor box를 기준으로 클러스터링을 수행하게 된다.
  6. download2


    K-means clustering에서 IoU를 사용하는 이유는 Euclidean distance를 사용하는 것 보다는 IoU를 사용하는 것이 더 정확한 결과값을 가져오기 때문이다.
  7. anchor box를 어떻게 정의하는지를 말하고 있다.
  8. direct location prediction
  9. download1


    시그모이드 함수를 사용함으로써 grid cell 내에 중심에 위치하도록(grid cell을 벗어나지 않도록) 하였다.
  10. fine-grained features
    download2


    26 _ 26_ 256 featuure map을 4등분하여 이를 stacking 하듯이 연결을 해서 이 feature map과 low reso feature map과 사이즈가 같아졌으므로 concatenate를 한다.
  11. 이 과정을 통해서 Passthrough layer를 추가하여 High reso feature map을 Low reso feature map과 합치는 층이다.
  12. Multi-scale training
    download1
  13. FC층을 때고, one by one conv를 통해서 예측을 한다. 이렇게 되면 input 사이즈가 변해도 상관이 없게 된다.

Faster(Model: YOLOv2)

download2

 

Global Average Pooling -> 채널별로 average pooling을 사용해서 1차원 벡터로 만들기 위함이다. 이를 통해서 Fc Layer를 사용할 때보다 params 개수가 크게 감소하였다.

Stronger(Model: YOLO9000)

  1. Hierarchical Classification

ImageNet의 클래스 구조가 트리 기반이 아님

-> Hierarchical Classification 학습을 통해서 WordTree를 생성한다. (강아지 > 푸들, 골든 리트리버 ,,) 이렇게 트리 기반 클래스 구조가 만들어 졌을 때, 상위 요소도 ground truth label로 propagate를 시킨다.

  1. Dataset Conbination ith WordTree

ImageNet + COCO 데이터셋 + ImageNet Detection을 합쳐서 9000개의 class label을 생성했다.

  1. Joint Classification and detection
  • detection dataset과 classification dataset의 개수 차이가 크므로 oversampling으로 개수를 맞춥니다.
  • detection dataset(훈련 데이터셋): propagate the loss(classification + bbox regression)
  • classification dataset: propagate only classification loss
  • COCO에 있는 데이터로 detection 학습 -> 9000개 객체에 대한 구분이 가능하다.

간단한 YOLO 구현

데이터셋 준비

자동차 인식 데이터셋 을 준비한다.

import pandas as pd
import numpy as np
import cv2
import matplotlib.pyplot as plt

box = pd.read_csv(
    './data/train_solution_bounding_boxes (1).csv')
box.head()

>>>
image    xmin    ymin    xmax    ymax
0    vid_4_1000.jpg    281.259045    187.035071    327.727931    223.225547
1    vid_4_10000.jpg    15.163531    187.035071    120.329957    236.430180
2    vid_4_10040.jpg    239.192475    176.764801    361.968162    236.430180
3    vid_4_10020.jpg    496.483358    172.363256    630.020260    231.539575
4    vid_4_10060.jpg    16.630970    186.546010    132.558611    238.386422
  • testing_images folder: 테스트 데이터셋의 이미지
  • training_images folder: 훈련 데이터셋의 이미지
  • train_solution_bounding_boxes (1).csv: training_images의 객체가 있는 BBox의 위치(image(이미지 이름), xmin, ymin, xmax, ymax)

불러온 데이터셋 확인

sample = cv2.imread(
    './data/training_images/vid_4_1000.jpg')
sample = cv2.cvtColor(sample, cv2.COLOR_BGR2RGB)
point = box.iloc[0]
pt1 = (int(point['xmin']), int(point['ymax']))
pt2 = (int(point['xmax']), int(point['ymin']))
cv2.rectangle(sample, pt1, pt2, color=(255, 0, 0), thickness=2)
plt.imshow(sample)

download1

 

cvtColor의 역할: 이미지 파일을 OpenCV 함수인 imread()를 통해서 열 때, BGR로 되어있는 색깔 순서를 RGB로 바꾸어 주는 역할을 한다.

YOLO 구현

# Yolo 로드
# 훈련된 가중치와 네트워크 구성을 저장하고 있는 파일을 불러옴.
net = cv2.dnn.readNet("./yolov3.weights",
                      "./yolov3.cfg")
# 클래스 카테고리를 가져옴.
classes = []
with open("./darknet/data/coco.names", "r") as f:
    classes = [line.strip() for line in f.readlines()]
# 레이어 이름 가져오기
layer_names = net.getLayerNames()
# 만들어진 층에서 (13x13, 26x26, 52x52) Layer에 직접 접근해서 feature map 정보를 직접 가져와야 한다.
output_layers = [layer_names[i - 1] for i in net.getUnconnectedOutLayers()]

# 이미지 가져오기
img = cv2.imread(
    './data/training_images/vid_4_10000.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
height, width, channels = img.shape

Feature Map 정보를 사용해서 3개의 scale output layer에서 object detection 정보를 수집(결과를 받아온다.)

# 이미지를 blob으로 변환한다.
blob = cv2.dnn.blobFromImage(
    img, 1/256, (416, 416), (0, 0, 0), swapRB=True, crop=False)
net.setInput(blob)

# outs는 출력으로 탐지된 개체에 대한 모든 정보와 위치를 제공한다.
outs = net.forward(output_layers)

Blob은 이미지에서 특징을 잡아내고 크기를 조정하는데 사용된다.

  • 320 x 320: 작고 정확도는 떨어지지만 속도가 빠름
  • 609 x 609: 정확도는 더 높지만 속도는 느림
  • 416 x 416: 중간

cv2.dnn.blobFromImage(image, scalefactor=None, size=None, mean=None, swapRB=None, crop=None, ddepth=None) -> retval

  • image: 입력 영상 / 이미지
  • scalefactor: 입력 영상 픽셀 값에 곱할 값. 기본값은 1.

    입력 이미지의 픽셀값을 0255로 했는지, 01로 정규화해서 이용했는지에 맞게 scalefacter를 곱했을 때 0~1이 나오도록 값을 결정해준다.
  • size: 출력 영상의 크기. 기본값은 (0, 0).
  • mean: 입력 영상 각 채널에서 뺄 평균 값. 기본값은 (0, 0, 0, 0).
  • swapRB: R과 B 채널을 서로 바꿀 것인지를 결정하는 플래그. 기본값은 False.
  • crop: 크롭(crop) 수행 여부. 기본값은 False.
  • ddepth: 출력 블롭의 깊이. CV_32F 또는 CV_8U. 기본값은 CV_32F.

정보를 화면에 표시

# 정보를 화면에 표시
class_ids = []
confidences = [] # 0에서 1까지의 탐지에 대한 신뢰도
boxes = [] # 감지된 개체를 둘러싼 사각형의 좌표
for out in outs:
    for detection in out:
        scores = detection[5:]
        class_id = np.argmax(scores)
        confidence = scores[class_id]
        # 일단 신뢰도 0.5가 넘는 boxes 좌표만 가져오기
        if confidence > 0.5:
            # Object detected
            center_x = int(detection[0] * width)
            center_y = int(detection[1] * height)
            w = int(detection[2] * width)
            h = int(detection[3] * height)
            # 좌표
            x = int(center_x - w / 2)
            y = int(center_y - h / 2)
            boxes.append([x, y, w, h])
            confidences.append(float(confidence))
            class_ids.append(class_id)

# 동일한 개체에 대한 여러 박스 좌표들 중 제일 신뢰도가 높은 좌표를 제외하고 나머지는 제거하기 위한 코드(NMS)
indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)

font = cv2.FONT_HERSHEY_PLAIN
colors = np.random.uniform(0, 255, size=(len(boxes), 3))

# 박스, label, confidence를 image 위에 그리는 코드
for i in indexes.flatten():
    x, y, w, h = boxes[i]
    print(x, y, w, h)
    label = str(classes[class_ids[i]])
    confidence = str(round(confidences[i], 2))
    color = colors[i]
    cv2.rectangle(img, (x, y), ((x+w), (y+h)), color, 2)
    cv2.putText(img, label + " " + confidence,
                (x, y+20), font, 2, (0, 255, 0), 2)

plt.imshow(img)

Total

def predict_yolo(img_path):
  # 이미지 가져오기
  img = cv2.imread(img_path)
  img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
  height, width, channels = img.shape

  blob = cv2.dnn.blobFromImage(img, 1/256, (416, 416), (0, 0, 0), swapRB=True, crop=False)
  net.setInput(blob)
  outs = net.forward(output_layers)

  class_ids = []
  confidences = []
  boxes = []
  for out in outs:
      for detection in out:
          scores = detection[5:]
          class_id = np.argmax(scores)
          confidence = scores[class_id]
          if confidence > 0.5:
              # Object detected
              center_x = int(detection[0] * width)
              center_y = int(detection[1] * height)
              w = int(detection[2] * width)
              h = int(detection[3] * height)
              # 좌표
              x = int(center_x - w / 2)
              y = int(center_y - h / 2)
              boxes.append([x, y, w, h])
              confidences.append(float(confidence))
              class_ids.append(class_id)

  indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)

  font = cv2.FONT_HERSHEY_PLAIN
  colors = np.random.uniform(0, 255, size=(len(boxes), 3))
  if len(indexes) > 0:
    for i in indexes.flatten():
        x, y, w, h = boxes[i]
        print(x, y, w, h)
        label = str(classes[class_ids[i]])
        confidence = str(round(confidences[i], 2))
        color = colors[i]
        cv2.rectangle(img, (x, y), ((x+w), (y+h)), color, 2)
        cv2.putText(img, label + " " + confidence, (x, y+20), font, 2, (0, 255, 0), 2)

    plt.imshow(img)

  else:
    print('탐지된 물체가 없습니다.')

테스트

import glob
import random

paths = glob.glob('./data/testing_images/*.jpg')

img_path = random.choice(paths)

predict_yolo(img_path)

>>>
39 101 20 27
354 186 13 10
315 189 28 22
352 183 13 9
336 177 9 5
324 180 14 7
328 178 10 5

download2