학습된 yolo(욜로) 모델을 가져오는 방법을 알아보겠습니다.
예제는 darknet으로 만들어진 욜로 모델을 기준으로 합니다.
(이 글은 아래 포스팅을 읽고 왔다는 전제로 작성 되었습니다.)
1) 학습된 모델 다운로드
darknet에서도 cfg(환경파일)과 Weight 파일을 다운로드하여 사용하시면 되는데요.
해당 링크 접속 후 커서를 조금 내리면 아래와 같은 화면을 볼 수 있습니다.
빨간 사각형 부분이 다운 받는 링크입니다.
tensorflow 기반 모델과 다르게 config 파일이 첫 번째 인자, weight파일이 두 번째 인자로 들어옵니다.
2) Yolov3 기본 네트워크 구조
이번 포스팅은 yolo 사용법 tutorial이지만 모델 구조를 이해할 필요가 있습니다.
아래 3가지 모델 중 YOLOv3-416을 사용할 건데요.
입력 이미지 크기 416x416 이미지를 인풋으로 하는 모델을 의미합니다.
욜로 모델은 총 3개 output(82번 레이어, 94번 레이어, 106번 레이어)을 결괏값으로 출력합니다.
피쳐 정보들을 조합하여 찾고자 하는 개체 위치를 찾는다고 이해하면 되는데요.
좀 더 깊이 들어가 보자면 각 output은 13x13, 26x26, 52x52 크기의 feature 맵입니다.
각 featuare 맵은 한 픽셀당 3개의 박스 정보를 가지고 있습니다.
박스를 상세히 보면 array로 구성되어 있는데요.
앞 4개 값은 바운딩 박스 좌표 값입니다. 이 중 Tx, Ty값이 바운딩 박스의 center좌표, Tw, Th는 각각 weight, height입니다.
Po는 정답 확신도를 나타내는 confidence score입니다.
Class Scores는 80개로 이루어져 있습니다. 구 이유는 욜로 모델이 코코데이터 셋으로 학습했기 때문입니다.
(코코데이터셋 클래스가 80개임)
Class Scores는 클래스별 정답일 확률을 나타내는데 이중 가장 큰 값이 정답 라벨이겠죠?
마찬가지로 26x26 피쳐 맵, 52x52 피쳐 맵에서도 같은 원리가 적용됩니다.
픽셀 값이 큰 피쳐 맵일수록 상대적으로 더 작은 객체를 탐지하는데 집중한다고 보시면 됩니다.
정리해보겠습니다. 각 output은 13x13x3x85, 26x26x3x85, 52x52x3x85입니다.
하지만 실제로 모델이 뱉어내는 값은 507x 85, 2028x85, 8112x85이니 실제 코드에서 당황하지 마세요!
3) Yolov3 예제 코드
(1) weight와 config 파일을 기반으로 모델 생성
import os
import cv2
import numpy as np
weights_path = '여러분의파일경로'
config_path = '여러분의파일경로'
cv_net_yolo = cv2.dnn.readNetFromDarknet(config_path, weights_path)
모델 생성 코드는 다운 받은 weigths, config파일의 경로를 기입해주면 됩니다.
(2) 출력 레이어의 네임 값 찾기
layer_names = cv_net_yolo.getLayerNames()
outlayer_names = [layer_names[i-1] for i in cv_net_yolo.getUnconnectedOutLayers()]
getlayerNames() 함수는 욜로 모델 레이어 이름을 출력하는데요.
실제로 필요한 건 output layer 이름입니다.
이를 추출하기 위해선. getUnconnectedOutLayers() 함수를 통해 output layer 번호를 먼저 추출합니다.
위 번호를 기준으로 인덱싱을 적용하면 필요한 output layer 이름을 알 수 있죠.
굳이 왜 이름을 뽑아야 하는지는 아래 코드를 보시면 이해가 되실 겁니다.
img = cv2.imread('이미지경로')
# 해당 모델은 416 x 416이미지를 받음으로 img 크기정해줘야함.
cv_net_yolo.setInput(cv2.dnn.blobFromImage(img, scalefactor=1/255.0, size=(416, 416), swapRB=True, crop=False))
cv_outs = cv_net_yolo.forward(outlayer_names) # layer이름을 넣어주면 그 layer이름에 해당하는 output을 return 하게 됨.
분석하고자 하는 이미지를 불러오고 intput으로 넣습니다.
관심 가져야 하는 부분은 size를 (416,416)으로 고정했다는 건데요.
최초에 416모델을 쓰기로 했기 때문입니다.
scalefactor인자로 1/255.0을 넣어줌으로써 픽셀 값을 0~1 사이로 만들어 줍니다.
forward함수 인자로 outputlayer 이름값을 넣어주면 상응하는 output을 반환하는데요.
3개를 넣었으니 ndarray 3개를 반환합니다.
output shape은 아래와 같습니다.
507은 13x13x3 = 507
202는 26x26x3 = 2028
8112는 52x52x 3 = 8112로 앞서 배운 이론과 일치하죠?
(3) 바운딩 박스 그리기
output에서 우리가 원하는 bounding box 정보를 추출하겠습니다.
자세한 설명은 주석을 참고 바랍니다.
# rows , cols는 1/255.0 스케일링된 값을 추후 복원시키는 용도로 사용
rows = img.shape[0]
cols = img.shape[1]
conf_threshold = 0.5 # confidence score threshold
# for loop를 돌면서 추출된 값을 담을 리스트 세팅
class_ids = []
confidences = []
boxes = []
for _, out in enumerate(cv_outs):
for _, detection in enumerate(out):
# detection => 4(bounding box) + 1(objectness_score) + 80(class confidence)
class_scores = detection[5:] # 인덱스 5음 부터는 80개의 score 값
class_id = np.argmax(class_scores) # 80개중에 최대값이 있는 index 값
confidence = class_scores[class_id] # 최대값 score
if confidence > conf_threshold:
# 바운딩 박스 중심 좌표 and 박스 크기
cx = int(detection[0] * cols) # 0~1 사이로 리사이즈 되어있으니 입력영상에 맞는 좌표계산을 위해 곱해줌.
cy = int(detection[1] * rows)
bw = int(detection[2] * cols)
bh = int(detection[3] * rows)
# 바운딩 박스를 그리기 위해선 좌상단 점이 필요함(아래는 그 공식)
left = int(cx - bw / 2)
top = int(cy - bh / 2)
class_ids.append(class_id) # class id값 담기
confidences.append(float(confidence)) # confidence score담기
boxes.append([left, top, bw, bh]) # 바운딩박스 정보 담기
80개 라벨과 인덱스 번호로 맵핑된 데이터를 세팅합니다. 추 후 바운딩 박스를 그릴 때 사용할 예정입니다.
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' }
라벨 정보는 아래 깃헙 링크에서 확인할 수 있습니다.
import matplotlib.pyplot as plt
nms_threshold = 0.4 # nonmaxsuppression threshold
# opencv 제공하는 nonmaxsuppression 함수를 통해 가장 확률 높은 바운딩 박스 추출
idxs = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)
draw_img = img.copy() #그림그리기 위한 도화지
# 같은 라벨별로 같은 컬러를 사용하기 위함/ 사용할때마다 랜덤하게 컬러 설정
colors = np.random.uniform(0, 255, size=(len(labels_to_names_seq),3))
if len(idxs) > 0: # 바운딩박스가 아예 없을 경우를 대비하여 1개 이상일때만 실행하도록 세팅
for i in idxs:
box = boxes[i]
left = box[0]
top = box[1]
width = box[2]
height = box[3]
caption = f"{labels_to_names_seq[class_ids[i]]}: {confidences[i]:.2})"
label = colors[class_ids[i]]
cv2.rectangle(draw_img, (int(left), int(top), int(width), int(height)), color=label, thickness=2)
cv2.putText(draw_img, caption, (int(left), int(top-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, label, 2, cv2.LINE_AA)
img_rgb = cv2.cvtColor(draw_img, cv2.COLOR_BGR2RGB) #
plt.figure(figsize=(12, 12))
plt.imshow(img_rgb)
* 추가 설명
NMSBoxes 값을 출력을 해보면 인덱스를 출력하는데요.
boxes 리스트에 넣은 바운딩 박스 정보 들 중 최종적으로 선택한 박스 인덱스 값을 의미합니다.
즉 4번째, 17번째, 6번째... 바운딩 박스를 선택한 거죠.
output:
* yolov5에 대해 궁금하다면 아래 포스팅을 참조하기 바랍니다.
'머신러닝,딥러닝 > opencv' 카테고리의 다른 글
[파이썬 opencv] 오픈 cv에서 selectroi 사용하는 방법 (0) | 2022.12.09 |
---|---|
[파이썬 opencv] 오픈cv 를 통해 비디오(영상) 출력하는 방법 (0) | 2022.12.02 |
[파이썬 opencv] 오픈 cv에서 detection model 실행하는 방법 (0) | 2022.10.21 |
[오픈 cv] 트랙바란? 트랙바 사용방법(for opencv 초보자) (0) | 2022.09.14 |
[파이썬 opencv] 두 이미지 합치는 방법(with 크기 다른 이미지) (0) | 2022.09.13 |
댓글