Darknet이라는 네트워크는 객체 검출만을 위한 네트워크가 아닌 신경망처럼 일반적인 네트워크를 구성하기 위한 방법론이다.
전용함수가 있는 것이 아니라 신경망 출력함수만 있어 충분한 이해와 해석이 필요하다.
객체 검출
import cv2
import numpy as np
from matplotlib import pyplot as plt
modelConfiguration = "yolov3.cfg"
modelWeights = "yolov3.weights"
net = cv2.dnn.readNetFromDarknet(modelConfiguration, modelWeights)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
yolov3.weights은 학습 시킬 CNN의 weight 를 가지고 있다.
yolov3.cfg는 일반 txt파일로 yolo의 크기가 정해져 있지 않고 다양한 형태로 학습 시킬 수 있다. 이런 설정 구조를 가지고 있다. 이 두 파일이 있어야 네트워크를 구성할 수 있다.
readNetFromDarknet : 다크넷을 로딩하기 위해
아래 두가지는 셋팅 함수이다.
cap = cv2.VideoCapture('people.jpg')
#cap = cv2.VideoCapture('cars.jpg')
hasFrame, frame = cap.read()
print(frame.shape)
# (1080, 1924 , 3) 3개의 rgb채널
VideoCapture 함수로 이미지를 읽을 수 있다. 0을 넣으면 카메라의 캠으로 부터 연속적으로 영상을 받을 수 있다. 다양한 환경에서 테스트 하기 위해 사용!
hasFrame, frame = cap.read() 의 첫번째 파라미터 hasFrame에는 이미지가 있는지 없는지, 두번째 파라미터는 frame이 나온다.
# yolo3는 416x416크기가 입력이 되어야 함, [0,0,0]은 평균, RB채널 swap
#
blob = cv2.dnn.blobFromImage(frame, 1/255, (416, 416), [0,0,0], True, crop=False) #
print(blob.shape)
net.setInput(blob)
#(1, 3, 416, 416)
이미지를 읽었으면 CNN을 통과시키고 다크넷을 사용하기 위해서는 전처리 과정이 필요하다. 전처리 과정은 blobFromImage함수를 사용한다. 파라미터의 frame은 칼라가 들어가고(3채널), 1/255는 스케일링 값, 다크넷이 사용하는 입력영상의 크기, 전체 이미지의 rgb 평균값, 다크넷은 rgb 포맷을 사용하기 때문에 ture로 주면 r,b채널을 스왑한다.마지막은 이미지 크기가 맞지 않을까 크롭할 것인지 이다.
CNN으로 다룰때 모든 입력은 4차원이 되어야한다. 이유는 다수의 이미지가 들어갈 수 있기 때문이다.
blob.shape의 결과는 이미지수, 채널, 영상의 크기로 나타난다.
setInput은 blob을 input 이미지로 쓰겠다는 의미이다.
%%time
outs = net.forward(['yolo_82', 'yolo_94', 'yolo_106'])
forward는 다크넷을 통과시켜서 출력 결과를 얻는 방법이다.
(네트워크를 통과시켜 아웃풋을 얻어냄)
영상의 크기에 따라서 오래걸릴 수 있지만 고속 알고리즘이어서 기존의 객체 검출 알고리즘 보다 수행속도가 빠르다.
%%time 매 셀을 실행시킬 때마다 실행 속도를 알 수 있다.
yolo3는 입력 영상이 주어지면 여러가지 필터를 통해 결과가 출력되는데 리그레션 된 값이 출력이 된다.
아웃풋 출력층이 세개가 되는데 첫번째는 영상의 크기를 잘게, 두번째는 조금더 잘게, 세번째는 다시한번 잘게 쪼갠 출력층이다. 여기서 레이어의 인덱스 번호는 각각 82, 94, 106이다.
출력 결과가 행렬로 나오기 때문에 outs의 행렬을 잘 이해할 필요가 있다.
print(len(outs))
print(outs[0].shape) # 507x 85 85 = (cx,cy,w,h + conf+ 클래스확률80) 507 = 13x13*3
print(outs[1].shape) # 2028x85 2028 = 26x26*3
print(outs[2].shape) # 8112x85
출력물을 확인해보면 위에서 outs은 출력층을 3개로 주었기 때문에 list의 원소개수가 3개이고, 3개의 행렬이 나오게 된다 .
3개의 행렬은 각각
507x 85, 2028x85, 8112x85 인데
첫번째 행렬은 82번째 레이어이고
85의 의미는
85 = (cx,cy,w,h + conf+ 클래스확률80) 로
검출된 위치의 좌표, 사각형의 폭과 높이, 셀에서의 객체일지 아닐지 확률, 나머지 80개의 대한 확률값으로 총 85 개로 구성이 되어있다. 이런 85개가 507개가 있다는 의미이다.
507 = 13x13*3 의미인데
yolo의 기본적인 알고리즘 설명
- 입력 영상이 들어오면 셀로 구분을 하고
- 각각의 셀은 바운딩 박스의 좌표(위치좌표, 폭, 높이), 객체일지 아닐지 확률값, 클래스 수만큼의 확률값 (80개)로 셀 하나의 대해서
(입력이 들어오면 셀을 중심으로 바운딩 박스의 크기 예측 후 확률값 계산, 이때 각각의 셀마다 85개의 출력값이 나오게 된다.)
yolo3는 각각의 셀에서 바운딩 박스가 3개가 생성될 것으로 추정하기 때문에 결국 한 셀마다 3 * 13(13으로 셀을 나눌 경우) * 13이 된다. 그리고 한개의 셀은 85개의 값으로 이루어져 있다.
따라서 507의 의미는 13133을 한 값이다.
결국 정리하면 507개의 바운딩 박스 위치가 출력되고 바운딩 박스마다 각각의 확률값과 오브젝트일 확률값 등이 나오게 된다.
두번째 행렬은 94번째 레이어를 의미하고
2028인 의미는 1313이 아닌 2626으로 더 잘게 영상을 쪼갠다.
그리고 각각의 셀은 세개의 바운딩 박스를 예측사기 때문에 *3 으로
26 * 26 * 3 = 2028개의 바운딩 박스
세번째 행렬은 52*52로 더더 잘게 영상을 쪼갠다.
52 * 52 * 3 = 8112가 된다.
전체적으로 보면 세개의 네트워크를 통과시켰을때 생성되는 바운딩 박스의 위치는
507 + 2028 + 8112 개의 바운딩 박스가 만들어 지게 된다.
이렇게 만들어진 바운딩 박스 중에서 어떤 바운딩 박스는 객체이고 어떤 바운딩 박스는 객체가 아닐것이다.
이제는 생성되어 있는 클래스는 해석해서 각각의 바운딩 박스 위치를 그리고 클래스 확률을 표시해보자
np.set_printoptions(formatter={'float_kind': lambda x: "{0:0.3f}".format(x)})
outs[0][300][0] * 1924, outs[0][300][1] * 1080
#(1377.4365618228912, 587.3750638961792) -> 중심점
outs[0][300][2] * 1924, outs[0][300][3] * 1080
#(408.1667233109474, 227.79508709907532) -. 폭 높이
p = outs[0][300][5:] #영상의 확률값
#앞에 다섯번째 부터 마지막까지의 확률값 80개
p
numpy 옵션중에 확률값을 보기 좋게 (소숫점 셋째자리까지 출력) 해주는 옵션이 "{0:0.3f}".format(x) 이다. 위의 셋팅은 global하게 적용된다.
outs[0][0]으로 출력할 경우 앞에 4개의 값은 중심값과 영상의 폭과 높이 인데 이 값은 0~1로 정규화가 되어있다. 이 값을 사각형 좌표로 확대를 하기 위해서는 영상의 폭과 높이를 곱해줘야한다. 읽은 영상의 기준으로 살펴보면 높이가 1080이고 폭이 1924이므로 각각을 곱해주면 영상의 중심점과 폭 높이를 구할 수 있다.
확률값은 p = outs[0][300][5:] 앞에 다섯번째 부터 마지막까지의 확률값 80개를 구할 수 있따.
아웃풋 행렬을 계산해서 객체를 추출하는 방법을 살펴보자!
우선 80개의 클래스가 무엇을 의미하는지 알아야한다.
classesFile = "coco.names"
classes = None
with open(classesFile, 'rt') as f:
classes = f.read().rstrip('\\n').split('\\n')
print(classes)
다크넷은 클래스 레이블에 대한 이름을 주지 않는다.
coco.names라는 80개의 클래스의 이름을 기록해놓은 텍스트 파일을 읽어서 레이블을 알 수 있다.
def drawPred(frame, classId, conf, left, top, right, bottom):
cv2.rectangle(frame, (left, top), (right, bottom), (255, 178, 50), 3)
#label = '%.2f' % conf
label = '%s:%.2f' % (classes[classId], conf)
labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
top = max(top, labelSize[1])
cv2.rectangle(frame, (left, top - round(1.5*labelSize[1])), (left + round(1.5*labelSize[0]), top + baseLine), (255, 255, 255), cv2.FILLED)
cv2.putText(frame, label, (left, top), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0,0,0), 1)
def postprocess(frame, outs):
confThreshold = 0.5 #Confidence threshold
nmsThreshold = 0.4 #Non-maximum suppression threshold
frameHeight = frame.shape[0]
frameWidth = frame.shape[1]
# Scan through all the bounding boxes output from the network and keep only the
# ones with high confidence scores. Assign the box's class label as the class with the highest score.
classIds = []
confidences = []
boxes = []
for out in outs: # 3번 loop
for detection in out: # 507번, 2028, 8112,, detection = 85차원
scores = detection[5:] # 80개의 확률값을 가져옴
classId = np.argmax(scores) #확률값이 가장 높은 곳의 인덱스를 가져옴
confidence = scores[classId] # 확률값
if confidence > confThreshold:
center_x = int(detection[0] * frameWidth)
center_y = int(detection[1] * frameHeight)
width = int(detection[2] * frameWidth)
height = int(detection[3] * frameHeight)
left = int(center_x - width / 2)
top = int(center_y - height / 2)
classIds.append(classId) #검출된 객체의 아이디 값을 누적
confidences.append(float(confidence)) #확률값을 누적
boxes.append([left, top, width, height]) #박스도 누적 (4개의 좌표)
# Perform non maximum suppression to eliminate redundant overlapping boxes with
# lower confidences.
# nms가 너무 낮으면 겹치는 객체를 감지하지 못함
# nms가 너무 크면 같은 객체에 대해서 box 많이 사용
indices = cv2.dnn.NMSBoxes(boxes, confidences, confThreshold, nmsThreshold)
for i in indices:
i = i[0]
box = boxes[i]
left = box[0]
top = box[1]
width = box[2]
height = box[3]
drawPred(frame, classIds[i], confidences[i], left, top, left + width, top + height)
postprocess 함수는 위에서 출력했던 output행렬을 계산해서 어떤것이 사각형이고, 사각형 크기가 어떻게 되고, 어떤 오브젝트가 있는지 알 수 있다. 이 함수를 잘 이해해야한다.
파라미터로는 이미지 영상이 입력과, 세개의 행렬이 들어온다.
전체적으로 outs은 행렬 세개를 의미하기때문에 행렬 세개가 돌고 첫번째 루프를 돌때는 각각의 행렬의 row 수 , 507, 2028,8112번이 들어가고 detection은 85차원의 데이터가 들어온다.
confThreshold을 주는 이유는 최대 확률값이 0.5보다 작으면 신뢰성이 없다고 판단!
confThreshold이 낮으면 많은 오브젝트가 검출되게된다.
좌표는 0~1로 정규화 되어있기 때문에 실제 input영상에서의 위치를 구하기 위해서는 이미지의 폭과 높이를 곱해준다.
left와 top을 다시 계산하는 이유는 폭과 높이의 연산을 하기 위해서 필요하다.
유사한 위치에서 동일한 객체가 여러개 나올 수 있다. 따라서 박스를 그룹핑 할 필요가 있다. NMS : 확률값이 있을때 가장 확률값이 큰 사각형만 찾아서 대표 사각형으로 만드는 작업
NMSBoxes라는 함수를 사용! 파라미터는 사각형 좌표, 확률값, 한계점(확률값이 이것보다 작은지 큰지),nms한계점이 들어간다.
- nms한계점?반대로 너무 크면 같은 객체에 대해서 box 를 많이 사용함
- nms가 너무 낮으면 겹치는 객체를 감지하지 못함
위치에 있는 박스 좌표를 가져와서 의미 있는 박스를 그림
drawPred함수로 박스를 그림. 파라미터는 (입력영상, 클래스 아이디, 확률값, left, top , left+width, top+height)
→ 박스를 치고, 검출된 객체의 이름과 확률값을 출력하는 함수
postprocess(frame, outs)
img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
plt.imshow(img)
프레임의 출력 값으로 그림을 그리고 프레임을 다시 rgb로 변환해서 plt.imshow함수 적용
영상이 큰 경우 줄여서 보여주기 때문에 잘 보이지 않는다.
영상이 큰 경우는 open.cv 함수를 사용하는 것이 좋다.
cv2.imshow('frame',frame)
k = cv2.waitKey(-1)
cv2.destroyAllWindows()
'Other > Computer vision' 카테고리의 다른 글
[CV] OpenPose (0) | 2021.12.17 |
---|---|
[CV] ImageNet을 이용한 인식 (0) | 2021.12.17 |
[CV] CNN 활용한 영상 인식 (0) | 2021.12.17 |
[CV] 사람의 시각을 닮은 신경망 CNN (0) | 2021.12.17 |
[CV] 전이 학습 (0) | 2021.12.17 |