YOLO는 현재 v3 모델까지 나온 상황이며 v3 코드를 다루겠다
!wget https://pjreddie.com/media/files/yolov3.weights
YOLOv3를 사용하기에 앞서 가중치를 받아야 한다
import os
import scipy.io
import scipy.misc
import numpy as np
import pandas as pd
import PIL
import struct
import cv2
from numpy import expand_dims
import tensorflow as tf
from skimage.transform import resize
from tensorflow.keras import backend as K
from tensorflow.keras.layers import Input, Lambda, Conv2D, BatchNormalization, LeakyReLU, ZeroPadding2D, UpSampling2D
from tensorflow.keras.models import load_model, Model
from tensorflow.keras.layers import add, concatenate
from tensorflow.keras.preprocessing.image import load_img
from tensorflow.keras.preprocessing.image import img_to_array
import matplotlib.pyplot as plt
from matplotlib.pyplot import imshow
from matplotlib.patches import Rectangle
%matplotlib inline
PATH = 'Image/'
모델을 사용하기에 패키지를 선언한다.
def _conv_block(inp, convs, skip=True):
x = inp #입력레이어
count = 0
for conv in convs:
if count == (len(convs) - 2) and skip:
skip_connection = x
count += 1
if conv['stride'] > 1: x = ZeroPadding2D(((1,0),(1,0)))(x) # stride가 2이상이면 padding
x = Conv2D(conv['filter'], #필터사이즈
conv['kernel'], #커널사이즈
strides=conv['stride'], #스트라이드
padding='valid' if conv['stride'] > 1 else 'same', #패딩을 1이면 valid 2이상 부터 sasme
name='conv_' + str(conv['layer_idx']), #레이어 이름 설정
use_bias=False if conv['bnorm'] else True)(x) #use_bias 설정
if conv['bnorm']: x = BatchNormalization(epsilon=0.001, name='bnorm_' + str(conv['layer_idx']))(x) #배치노말라이제이션 설정
if conv['leaky']: x = LeakyReLU(alpha=0.1, name='leaky_' + str(conv['layer_idx']))(x) #리키렐루 설정
return add([skip_connection, x]) if skip else x
YOLO v3는 darknet 구조를 따르기 때문에 100개가 넘는 층으로 되있다
그렇기에 일일이 선언하기에는 코드가 길어지고 많은 시간이 필요로 하기 때문에 모델 생성을 함수로 선언하여 진행한다.
def make_yolov3_model():
input_image = Input(shape=(None, None, 3)) # 이미지 크기가 각각 다르므로 None, None으로 설정
# Layer 0 => 4
x = _conv_block(input_image, [{'filter': 32, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 0},
{'filter': 64, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 1},
{'filter': 32, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 2},
{'filter': 64, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 3}])
# Layer 5 => 8
x = _conv_block(x, [{'filter': 128, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 5},
{'filter': 64, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 6},
{'filter': 128, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 7}])
# Layer 9 => 11
x = _conv_block(x, [{'filter': 64, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 9},
{'filter': 128, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 10}])
# Layer 12 => 15
x = _conv_block(x, [{'filter': 256, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 12},
{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 13},
{'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 14}])
# Layer 16 => 36
for i in range(7):
x = _conv_block(x, [{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 16+i*3},
{'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 17+i*3}])
skip_36 = x
# Layer 37 => 40
x = _conv_block(x, [{'filter': 512, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 37},
{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 38},
{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 39}])
# Layer 41 => 61
for i in range(7):
x = _conv_block(x, [{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 41+i*3},
{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 42+i*3}])
skip_61 = x
# Layer 62 => 65
x = _conv_block(x, [{'filter': 1024, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 62},
{'filter': 512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 63},
{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 64}])
# Layer 66 => 74
for i in range(3):
x = _conv_block(x, [{'filter': 512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 66+i*3},
{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 67+i*3}])
# Layer 75 => 79
x = _conv_block(x, [{'filter': 512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 75},
{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 76},
{'filter': 512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 77},
{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 78},
{'filter': 512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 79}], skip=False)
# Layer 80 => 82
yolo_82 = _conv_block(x, [{'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 80},
{'filter': 255, 'kernel': 1, 'stride': 1, 'bnorm': False, 'leaky': False, 'layer_idx': 81}], skip=False)
# Layer 83 => 86
x = _conv_block(x, [{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 84}], skip=False)
x = UpSampling2D(2)(x)
x = concatenate([x, skip_61])
# Layer 87 => 91
x = _conv_block(x, [{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 87},
{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 88},
{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 89},
{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 90},
{'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 91}], skip=False)
# Layer 92 => 94
yolo_94 = _conv_block(x, [{'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 92},
{'filter': 255, 'kernel': 1, 'stride': 1, 'bnorm': False, 'leaky': False, 'layer_idx': 93}], skip=False)
# Layer 95 => 98
x = _conv_block(x, [{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 96}], skip=False)
x = UpSampling2D(2)(x)
x = concatenate([x, skip_36])
# Layer 99 => 106
yolo_106 = _conv_block(x, [{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 99},
{'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 100},
{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 101},
{'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 102},
{'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 103},
{'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 104},
{'filter': 255, 'kernel': 1, 'stride': 1, 'bnorm': False, 'leaky': False, 'layer_idx': 105}], skip=False)
model = Model(input_image, [yolo_82, yolo_94, yolo_106]) #앵커 사이즈 3개 이므로 3개의 출력
return model
YOLOv3는 106개의 층을 사용하기 때문에 for문으로 이전에 만들었던 Conv_block 함수를 호출하여 모델을 생성한다.
출력은 3개로 되있고 각각 13 , 26 , 52의 층을 가진다
net_h, net_w = 416, 416 # 입력할 이미지 크기
# obj_trresh : 객체와 객체가 아닌 객체를 구분하는 임계값
obj_thresh = 0.5
# nms_thresh : 두 개의 객체가 얼마나 겹치는지 확인하는 임계값
nms_thresh = 0.45 #
anchors = [[116,90, 156,198, 373,326], [30,61, 62,45, 59,119], [10,13, 16,30, 33,23]] #앵커박스 설정
#80개의 라벨데이터
labels = ["person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", \
"boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", \
"bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", \
"backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", \
"sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", \
"tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", \
"apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", \
"chair", "sofa", "pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop", "mouse", \
"remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", \
"book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"]
이번에는 학습에 필요한 변수들, 라벨, 앵커박스를 선언합니다.
YOLO v3 넘오면서 이미지 크기가 바뀌게 됬는데 YOLO v1에서는 이미지의 크기가 448 x 448 였고 이미지를 나눌때 중간 셀이 4개가 나오게 되고 중간 셀이 많게 되면 인식률이 떨어진다. 이를 해결하기 위해 이미지 크기를 416 x 416으로 수정 하여 중간셀이 1개만 나오게 했다.
이미지 크기를 설정하는 변수 밑에는 각각 객체를 판별할 수 있는 임계값, 두 객체간의 얼마나 겹쳐는지 확인하는 임계값, 앵커박스를 설정했다.
라벨이라는 클래스(classes)는 80개 정도로 설정 되었는데 이미지넷의 라벨은 1000개 정도이지만 ms coco는 80개다.
그래서 yolo는 80개의 라벨 값을 가지고 있다.
# 코코 데이터의 80개의 클래스 예측
yolov3 = make_yolov3_model()
# 코코모델로 학습된 가중치(weight) 불러오기
weight_reader = WeightReader('yolov3.weights')
weight_reader.load_weights(yolov3)
이제는 선언된 함수를 이용하여 모델을 만들고 가중치를 부른다.
그러면 아까 다운받았던 파일에 가중치를 불러오게 된다.
from numpy import expand_dims
def load_image_pixels(filename, shape):
# 이미지 불러오기 및 이미지 사이즈 받기
image = load_img(PATH+filename)
width, height = image.size
# 이미지를 416 x 416 크기로 다시 불러오기
image = load_img(PATH+filename, target_size=shape)
# 이미지를 numpy로 변경
image = img_to_array(image)
# 이미지를 0 ~ 1 사이의 값으로 변경
image = image.astype('float32')
image /= 255.0
# 이미지에 차원을 추가합니다.
image = expand_dims(image, 0)
return image, width, height #리사이즈 이미지, 원래 크기를 리턴
이미지를 불러오는 함수다.
함수를 호출을 하게되면 이미지를 2번 불러오는데
처음에는 원본을 가져오고 크기를 기억을 하고 416x416 크기로 다시 이미지를 불러온다
416 x 416로 불러온 이미지와 원래 이미지의 크기를 반환한다
class BoundBox:
def __init__(self, xmin, ymin, xmax, ymax, objness = None, classes = None):
self.xmin = xmin
self.ymin = ymin
self.xmax = xmax
self.ymax = ymax
self.objness = objness
self.classes = classes
self.label = -1
self.score = -1
def get_label(self):
if self.label == -1:
self.label = np.argmax(self.classes)
return self.label
def get_score(self):
if self.score == -1:
self.score = self.classes[self.get_label()]
return self.score
바운딩 박스를 클래스로 지정했다
박스의 크기를 저장하고 라벨과 정확도를 저장하는 클래스다.
#박스간의 교집합 계산
def _interval_overlap(interval_a, interval_b):
x1, x2 = interval_a
x3, x4 = interval_b
if x3 < x1:
if x4 < x1:
return 0
else:
return min(x2,x4) - x1
else:
if x2 < x3:
return 0
else:
return min(x2,x4) - x3
박스와 객체와의 교집합 계산하는 함수다
IoU에서 계산에 사용되는 함수며 설명은 IoU할 때 하겠습니다.
#2개의 객체를 IoU를 통해서 비교 함수
def bbox_iou(box1, box2):
intersect_w = _interval_overlap([box1.xmin, box1.xmax], [box2.xmin, box2.xmax])
intersect_h = _interval_overlap([box1.ymin, box1.ymax], [box2.ymin, box2.ymax])
intersect = intersect_w * intersect_h
w1, h1 = box1.xmax-box1.xmin, box1.ymax-box1.ymin
w2, h2 = box2.xmax-box2.xmin, box2.ymax-box2.ymin
union = w1*h1 + w2*h2 - intersect
return float(intersect) / union
오브젝트 디텍션의 평가요소인 IoU(Intersection Over Union)입니다.
IoU는 교집합 영역 넓이 / 합집합 영역 넓이를 구하는 평가 방법입니다.
해당 작업을 하는 이유는 하나의 객체에서 하나의 박스만 나오는 것이 아닌 많은 양의 박스들이 나오게 됩니다.
그래서 박스와 객체간의 겹침이 많은 것을 찾기 위해서 사용하는 것입니다.
def do_nms(boxes, nms_thresh):
if len(boxes) > 0:
nb_class = len(boxes[0].classes) #80개 라벨(클래스)
else:
return
#for문으로 80개의 라벨(클래스) 파악
for c in range(nb_class):
# 80개의 라벨(클래스)를 순위를 매긴다.
sorted_indices = np.argsort([-box.classes[c] for box in boxes])
# 박스가 생성된 객체 수 --> len(sorted_indices)
for i in range(len(sorted_indices)):
index_i = sorted_indices[i]
# box에 데이터가 없다면 continue
if boxes[index_i].classes[c] == 0: continue
for j in range(i+1, len(sorted_indices)):
index_j = sorted_indices[j]
# 순위를 매긴 데이터이므로 뒤에 있는 값들은 IoU를 통해 크게 일치하는 데이터들만 0으로 처리한다.
if bbox_iou(boxes[index_i], boxes[index_j]) >= nms_thresh:
boxes[index_j].classes[c] = 0
객채들을 NMS(Non-Maximum Suppression)를 처리를 해야합니다.
객체의 많은 박스와 여러 라벨들이 측정이 되는데 그 중에서 가장 높은걸 제외하고 전부 0으로 처리합니다.
#바운딩 박스 후보와 라벨(클래스)를 예측합니다.
def decode_netout(netout, anchors, obj_thresh, net_h, net_w):
grid_h, grid_w = netout.shape[:2] # shape (13, 13)
nb_box = 3
netout = netout.reshape((grid_h, grid_w, nb_box, -1)) # shape (13, 13, 3, 85)
nb_class = netout.shape[-1] - 5 # size 80
boxes = []
netout[..., :2] = _sigmoid(netout[..., :2])
netout[..., 4:] = _sigmoid(netout[..., 4:])
netout[..., 5:] = netout[..., 4][..., np.newaxis] * netout[..., 5:]
netout[..., 5:] *= netout[..., 5:] > obj_thresh
for i in range(grid_h*grid_w):
row = i / grid_w
col = i % grid_w
for b in range(nb_box):
# 객체를 확인하는 점수
objectness = netout[int(row)][int(col)][b][4]
# 객체일 경우가 obj_thresh 보다 낮으면 컨티뉴
if(objectness.all() <= obj_thresh): continue
# x, y, w, h의 크기를 구합니다.
x, y, w, h = netout[int(row)][int(col)][b][:4]
x = (col + x) / grid_w # 중앙에 위치하고, 이미지의 넓이
y = (row + y) / grid_h # 중앙에 위치하고, 이미지의 높이
w = anchors[2 * b + 0] * np.exp(w) / net_w # 이미지의 넓이
h = anchors[2 * b + 1] * np.exp(h) / net_h # 이미지의 높이
# 클래스(라벨)의 확률을 저장한다
classes = netout[int(row)][col][b][5:]
box = BoundBox(x-w/2, y-h/2, x+w/2, y+h/2, objectness, classes)
boxes.append(box)
return boxes
박스와 라벨을 예측하는 함수 입니다.
이 함수를 통하여 객체에 대해 박스를 그리고, 라벨을 씌웁니다.
하나의 객체에 대하여 여러 박스와 라벨을 만드는 작업입니다.
def correct_yolo_boxes(boxes, image_h, image_w, net_h, net_w):
# 가로 세로 크기 중 큰쪽을 자릅니다.
if (float(net_w)/image_w) < (float(net_h)/image_h):
new_w = net_w
new_h = (image_h*net_w)/image_w
else:
new_h = net_w
new_w = (image_w*net_h)/image_h
#바운딩 박스의 크기를 조절
for i in range(len(boxes)):
x_offset, x_scale = (net_w - new_w)/2./net_w, float(new_w)/net_w
y_offset, y_scale = (net_h - new_h)/2./net_h, float(new_h)/net_h
boxes[i].xmin = int((boxes[i].xmin - x_offset) / x_scale * image_w)
boxes[i].xmax = int((boxes[i].xmax - x_offset) / x_scale * image_w)
boxes[i].ymin = int((boxes[i].ymin - y_offset) / y_scale * image_h)
boxes[i].ymax = int((boxes[i].ymax - y_offset) / y_scale * image_h)
decode_netout 함수에서 그렸던 박스들을 이미지의 원래 크기에 맞혀 바꿔주는 함수입니다.
코드에서 예측에 사용했던 크기는 416x416이지만 실제 이미지의 크기는 해당 이미지 보다 크거나 작기 때문에 크기를 맞춰주는 함수입니다.
from matplotlib.patches import Rectangle
def draw_boxes(filename, v_boxes, v_labels, v_scores):
# 이미지를 불러오기
data = plt.imread(PATH+filename)
# 이미지 출력
plt.imshow(data)
ax = plt.gca()
# 박스 그리기
for i in range(len(v_boxes)):
box = v_boxes[i]
# 박스의 좌표를 얻기
y1, x1, y2, x2 = box.ymin, box.xmin, box.ymax, box.xmax
# 상자의 높이와 넓이 구하기
width, height = x2 - x1, y2 - y1
# 바운딩박스 모양만들기
rect = Rectangle((x1, y1), width, height, fill=False, color='red')
# 바운딩박스 그리기
ax.add_patch(rect)
# 라벨과 정확도를 이미지에 표시
label = "%s (%.3f)" % (v_labels[i], v_scores[i])
plt.text(x1, y1, label, color='red')
plt.axis('off')
# plot 표현
plt.show()
박스를 이미지에 그려주는 함수입니다.
이전에 예측해서 저장했던 박스의 좌표를 이미지에 그립니다.
def get_boxes(boxes, labels, thresh):
v_boxes, v_labels, v_scores = list(), list(), list()
# 모든 box데이터 검수
for box in boxes:
# 모든 라벨 값 검수
for i in range(len(labels)):
# box의 라벨 값이 임계값(0.6) 이상인 데이터만 사용
if box.classes[i] > thresh:
# 리턴할 리스트에 데이터 추가하기
v_boxes.append(box)
v_labels.append(labels[i])
v_scores.append(box.classes[i]*100)
# 하나의 이미지에 여러개의 박스가 있기 때문에 break는 하지 않습니다.
return v_boxes, v_labels, v_scores
설정된 박스의 임계값이 0.6이상인 값들을 추출하는 함수 입니다.
박스정보, 라벨, 점수를 반환을 하는 것입니다.
for j in os.listdir(PATH) :
if 'jpg' in j :
# 디텍션에 사용할 이미지 크기 정하기
input_w, input_h = 416, 416
# 폴더 안에 이미지 파일 이름 지정
photo_filename = j
# 이미지 프리프로세싱 하기
image, image_w, image_h = load_image_pixels(photo_filename, (net_w, net_w))
# 예측값 만들기
yolos = yolov3.predict(image)
# 리스트로된 배열을 요약
print([a.shape for a in yolos])
# 앵커박스 정의
anchors = [[116,90, 156,198, 373,326], [30,61, 62,45, 59,119], [10,13, 16,30, 33,23]]
# 오브젝트 디텍션의 임계값
class_threshold = 0.6
boxes = list()
for i in range(len(yolos)):
# decode_netout으로 예측한 이미지, 앵커박스, 객체구별임계값, 이미지 크기를 보낸다
boxes += decode_netout(yolos[i][0], anchors[i], obj_thresh, net_h, net_w)
# 바운딩 박스 크기 수정
correct_yolo_boxes(boxes, image_h, image_w, net_h, net_w)
# 최대값이 아닌 상자들을 0으로 초기화
do_nms(boxes, nms_thresh)
# 오브젝트 디텍션 데이터 얻기
v_boxes, v_labels, v_scores = get_boxes(boxes, labels, class_threshold)
# 찾은 객체 라벨과 스코어 출력
for i in range(len(v_boxes)):
print(v_labels[i], v_scores[i])
# 이미지의 박스를 같이 출력
draw_boxes(photo_filename, v_boxes, v_labels, v_scores)
for문을 이용하여 이미지를 입력받아 위에서 선언한 함수들을 호출하여 이미지에 박스와 라벨을 붙여 출력을 하게 됩니다.
실제로 코드를 실행하여 결과를 확인해보세요
작성자 김강빈 kkb08190819@gmail.com / 이원재 ondslee0808@gmail.com
'【4】이미지 분류를 넘어, Object Detection 모델 > R-CNN 이론 + 실습' 카테고리의 다른 글
faster R-CNN (이론 + 실습) (0) | 2020.02.19 |
---|