지난 게시글에 올렸던 Fast-RCNN 과는 딱봐도 차이점을 느낄 수 있습니다.

보시다시피 Fast R-CNN보다 더 빠르다고 이름에서부터 강하게 주장하고 있습니다.

이제 어떻게 Faster R-CNN이 더 빠른지 알아보도록 하겠습니다.

 

Fast R-CNN에서의 문제점

 - Selective Search 알고리즘으로 RoI를 계산하는 시간 소모가 크다.

 

 


Faster R-CNN

Faster R-CNN은 위에서 언급한 Fast R-CNN의 문제점을 해결하기 위해 RPN (Region Proposal Network)를 제안했습니다.

Faster R-CNN

위의 사진은 Faster R-CNN의 구조입니다.

1. 전체 이미지에서 Feature Map을 추출한다.

2. Feature Map을 RPN에 넣어 RoI를 추출해냅니다.

3. RPN에서 추출한 RoI를 Feature Map에 Projection 시켜줍니다.

4. 그 다음, RoI Pooling을 실행하여 Classification을 진행합니다.

 

 

 


Anchor Box

RPN에 들어가기전 Anchor Box에 대해 알아보겠습니다.

Anchor Box는 원본 이미지를 일정 간격의 grid로 나누어 각 grid를 Bounding Box로 간주합니다.

각 grid마다 9개의 Box가 생성되며 아래의 사진과 같이 여러 크기의 Box가 생성됩니다.

Anchor Box

즉, 오른쪽 원본 이미지에는 8x8x9 = 576개의 box가 생기게 됩니다.

 

 


RPN (Region Proposal Network)

RPN은 Fast R-CNN에서 Selective Search를 개선하기 위해 나왔습니다.

RPN

논문상의 RPN은 설명이 부족하다고 느꼈기 때문에 타블로그를 참고하여 작성하였습니다.

 

 

RPN Structure

RPN 동작 알고리즘

1. Feature Map을 Input Data로 받습니다.

2. 3x3 Convolution, 256 channel (Intermediate Layer)를 통과시켜 줍니다. 여기서 padding=1로 설정하여 사이즈를 유지시켜줍니다.

3. Classification과 Bounding Box Regression을 예측해주기 위해 1x1 Convolution을 통과시켜 줍니다. 각각, 18, 36 channel을 갖습니다.

    - 18 = 2(Objectness) x 9(Anchor)

    - 36 = 4(Bounding Box Regressor) x 9(Anchor)

4. 이후, class score에 따라 상위 N개의 region proposals만을 추출하고, NMS(Non maximum suppression)을 적용하여 일부 region proposals만들 Fast R-CNN에 전달하게 됩니다.

 

 


Multi-task Loss

출처 - https://herbwood.tistory.com/10

 

 


Training

이 부분은 코드 꼭 보기!

 

1) 먼저 Anchor generation layer에서 생성된 anchor box와 원본 이미지의 ground truth box를 사용하여 Anchor target layer에서 RPN을 학습시킬 positive/negative 데이터셋을 구성합니다. 이를 활용하여 RPN을 학습시킵니다. 이 과정에서 pre-trained된 VGG16 역시 학습됩니다. 

 

2) Anchor generation layer에서 생성한 anchor box와 학습된 RPN에 원본 이미지를 입력하여 얻은 feature maps를 사용하여 proposals layer에서 region proposals를 추출합니다. 이를 Proposal target layer에 전달하여 Fast R-CNN 모델을 학습시킬 positive/negative 데이터셋을 구성합니다. 이를 활용하여 Fast R-CNN을 학습시킵니다. 이 때 pre-trained된 VGG16 역시 학습됩니다. 

 

3) 앞서 학습시킨 RPN과 Fast R-CNN에서 RPN에 해당하는 부분만 학습(fine tune)시킵니다. 세부적인 학습 과정은 1)과 같습니다. 이 과정에서 두 네트워크끼리 공유하는 convolutional layer, 즉 pre-trained된 VGG16은 고정(freeze)합니다.  

 

4) 학습시킨 RPN(3)번 과정)을 활용하여 추출한 region proposals를 활용하여 Fast R-CNN을 학습(fine tune)시킵니다. 이 때 RPN과 pre-trained된 VGG16은 고정(freeze)합니다.

- 출처: https://herbwood.tistory.com/10

 

 


Code

zebras.jpg
0.35MB

https://www.youtube.com/watch?v=4yOcsWg-7g8

이곳에 친절하게 코드 설명과 작성하는 것이 나와 있어서 작성 및 정리를 해보겠습니다.

모든 코드가 들어있지는 않으며 아래 github에 전체 코드가 있습니다.

 

https://github.com/ajw1587/Pytorch_Study/blob/main/Faster_R_CNN_RPN.ipynb

 

GitHub - ajw1587/Pytorch_Study

Contribute to ajw1587/Pytorch_Study development by creating an account on GitHub.

github.com

herbwood 이분의 블로그가 굉장히 도움이 많이 됐습니다.

 

 

 

 

 


1) 먼저 이미지의 truth box를 확인해 봅니다. 또한 이미지를 800x800으로 resize 해주면서 좌표값들도 비율에 따라 조절해줍니다.

# BBox, labels 선언해주기 - x1, y1, x2, y2
origin_bbox = np.array([[200, 802, 623, 1094], [597, 715, 1038, 1070], 
                  [1088, 719, 1452, 1077], [1544, 791, 1914, 1083]]) 
labels = np.array([1, 1, 1, 1]) # 0: background, 1: zebra

copy_img = np.copy(origin_img)
for i in range(len(origin_bbox)):
  cv.rectangle(copy_img, (origin_bbox[i][0], origin_bbox[i][1]), (origin_bbox[i][2], origin_bbox[i][3]), color=(255, 0, 0), thickness=10)

plt.imshow(copy_img)
plt.show()
# Resize BBox
w_ratio = 800/origin_img.shape[1]
h_ratio = 800/origin_img.shape[0]

print('w_ratio : {}'.format(w_ratio))
print('h_ratio : {}\n'.format(h_ratio))

ratio_list = [w_ratio, h_ratio, w_ratio, h_ratio]
bbox = []

for box in origin_bbox:
  box = [int(a*b) for a,b in zip(box, ratio_list)]
  bbox.append(box)

bbox = np.array(bbox)
print(bbox)

copy_img = np.copy(img)
for i in range(len(bbox)):
  cv.rectangle(copy_img, (bbox[i][0], bbox[i][1]), (bbox[i][2], bbox[i][3]), color=(255, 0, 0), thickness=5)
plt.imshow(copy_img)
plt.show()

 

 

Create Extractor Model

2) VGG16을 통해 feature map을 추출해줍니다.

# Feature extractor

model = torchvision.models.vgg16(pretrained=True).to(device)
features = list(model.features)
print(len(features))
for i, layer in enumerate(features):
  print('{}th Layer : {}'.format(i, layer))
# Collect required layers
test_img = torch.zeros((1, 3, 800, 800)).float()
print(test_img.shape)

req_features = []
output = test_img.clone().to(device)

for feature in features:
  output = feature(output)
  if output.size()[2] < 800//16:
    break
  req_features.append(feature)
  output_channels = output.size()[1]

# Create Extractor Model
extractor = nn.Sequential(*req_features)

# test
transform = transforms.Compose([transforms.ToTensor()])
imgTensor = transform(img).to(device)
imgTensor = imgTensor.unsqueeze(0)
output_feature = extractor(imgTensor)

print(output_feature.shape)
# Visualize output_feature
imgs = output_feature.data.cpu().numpy().squeeze(0)
fig = plt.figure(figsize=(12, 4)) # figsize=전체사이즈
no = 1

for i in range(5):
  fig.add_subplot(1, 5, no)
  plt.imshow(imgs[i], cmap='gray')
  no += 1

plt.show()

 

 

Generate Anchors Boxes

3) Anchor generation layer에서는 anchor box를 생성하는 역할을 합니다. 이미지의 크기가 800x800이며, sub-sampling ratio=1/16이므로, 총 22500(=50x50)개의 anchor box를 생성해야 합니다. 서로 다른 scale과 aspect ratio를 가지는 9개의 anchor box를 생성해줍니다.anchor_boxes변수에 전체 anchor box의 좌표(x1, y1, x2, y2)를 저장합니다(anchor_boxes 변수의 크기는 (22500, 4)입니다). 

# Create Anchors Boxes Center
ctr = np.zeros((2500, 2)) # (x, y)

index = 0
for i in range(len(ctr_x)):
  for j in range(len(ctr_y)):
    ctr[index, 0] = ctr_x[i] - 8
    ctr[index, 1] = ctr_y[j] - 8
    index += 1
  
print(ctr.shape)
print(ctr[:10, :])

# Create Anchors Boxes
ratios = [0.5, 1, 2]
scales = [8, 16, 32]
sub_sample = 16

anchor_boxes = np.zeros(((feature_size * feature_size * 9), 4))
index = 0

for c in ctr:
  ctr_y, ctr_x = c
  for i in range(len(ratios)):
    for j in range(len(scales)):

      h = sub_sample * scales[j] * np.sqrt(ratios[i])
      w = sub_sample * scales[j] * np.sqrt(1./ratios[i])

      anchor_boxes[index, 0] = ctr_x - w/2.
      anchor_boxes[index, 1] = ctr_y - h/2.
      anchor_boxes[index, 2] = ctr_x + w/2.
      anchor_boxes[index, 3] = ctr_y + h/2.
      index += 1
  
print(anchor_boxes.shape)
print(anchor_boxes[:10, :])

 

 

Target Anchors

4) 이미지를 벗어나는 Anchor Box들을 없애주고 IoU 값들을 계산해줍니다.

# Choose anchor boxes inside the image

index_inside = np.where(
    (anchor_boxes[:, 0] >= 0) &
    (anchor_boxes[:, 1] >= 0) &
    (anchor_boxes[:, 2] <= 800) &
    (anchor_boxes[:, 3] <= 800))[0]
  
print(index_inside.shape)

valid_anchor_boxes = anchor_boxes[index_inside]
print(valid_anchor_boxes.shape)


# show anchor boxes images
img_clone3 = np.copy(img)
img_clone4 = cv.copyMakeBorder(img_clone3, 400, 400, 400, 400, cv.BORDER_CONSTANT, value=(255, 255, 255))
img_clone5 = np.copy(img_clone4)

for i in range(len(valid_anchor_boxes)):
# for i in range(1):
  x1 = int(valid_anchor_boxes[i][0])
  y1 = int(valid_anchor_boxes[i][1])
  x2 = int(valid_anchor_boxes[i][2])
  y2 = int(valid_anchor_boxes[i][3])

  cv.rectangle(img_clone5, (x1+400, y1+400), (x2+400, y2+400), color=(255, 0, 0), thickness=1)

plt.figure(figsize=(10,10))
plt.subplot(121), plt.imshow(img_clone4)
plt.subplot(122), plt.imshow(img_clone5)
plt.show()
# Calculate IoUs
ious = np.empty((len(valid_anchor_boxes), 4), dtype=np.float32)
ious.fill(0)
img_clone5 = np.copy(img)

# anchor boxes
for i, anchor_box in enumerate(valid_anchor_boxes):
  xa1, ya1, xa2, ya2 = anchor_box
  anchor_area = (xa2 - xa1) * (ya2 - ya1)

  # ground truth boxes
  for j, gt_box in enumerate(bbox):
    xb1, yb1, xb2, yb2 = gt_box
    box_area = (xb2 - xb1) * (yb2 - yb1)

    inter_x1 = max([xb1, xa1])
    inter_y1 = max([yb1, ya1])
    inter_x2 = min([xb2, xa2])
    inter_y2 = min([yb2, ya2])

    if (inter_x1 < inter_x2) and (inter_y1 < inter_y2):
      inter_area = (inter_x2 - inter_x1) * (inter_y2 - inter_y1)
      iou = inter_area / (anchor_area + box_area - inter_area)
    else:
      iou = 0
    
    ious[i,j] = iou

 

Positive / Negative Anchor Boxes

5) RPN을 학습시키기 위한 Positive, Negative Anchor Box를 나눠줍니다.

# Anchor Boxes의 ious 중 가장 큰 값을 가지는 iou 뽑기

# 각 Truth Box 별 가장 높은 iou값의 위치
gt_argmax_ious = ious.argmax(axis=0)
print(gt_argmax_ious)

# 각 Truth Box 별 가장 높은 iou값
gt_max_ious = ious[gt_argmax_ious, np.arange(ious.shape[1])]
print(gt_max_ious)

# 가장 높은 iou값들 추출
gt_argmax_ious = np.where(ious == gt_max_ious)[0]
print(gt_argmax_ious)
# 이미지 내에 있는 8940개의 Anchor Box가 각각 어떤 Truth Box와 연관이 많이 되어있는지
argmax_ious = ious.argmax(axis=1)
max_ious = ious[np.arange(len(index_inside)), argmax_ious]

# Label 정해주기
label = np.empty(len(index_inside), dtype = np.int32)
label.fill(-1)

pos_iou_threshold = 0.7
neg_iou_threshold = 0.3

label[gt_argmax_ious] = 1
label[max_ious >= pos_iou_threshold] = 1
label[max_ious < neg_iou_threshold] = 0
# First set the label=-1 and locations=0 of the 22500 anchor boxes, 
# and then fill in the locations and labels of the 8940 valid anchor boxes
# NOTICE: For each training epoch, we randomly select 128 positive + 128 negative 
# from 8940 valid anchor boxes, and the others are marked with -1

anchor_labels = np.empty((len(anchor_boxes),), dtype=label.dtype)
anchor_labels.fill(-1)
anchor_labels[index_inside] = label

anchor_locations = np.empty((len(anchor_boxes),) + anchor_boxes.shape[1:], dtype=anchor_locs.dtype)
anchor_locations.fill(0)
anchor_locations[index_inside, :] = anchor_locs

 

RPN

6) RPN을 정의해 줍니다.

# Send the features of the input image to the Region Proposal Network (RPN), 
# predict 22500 region proposals (ROIs)

in_channels = 512
mid_channels = 512
n_anchor = 9

conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1).to(device)
conv1.weight.data.normal_(0, 0.01)
conv1.bias.data.zero_()

# bounding box regressor
reg_layer = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0).to(device)
reg_layer.weight.data.normal_(0, 0.01)
reg_layer.bias.data.zero_()

# classifier (object or not)
cls_layer = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0).to(device)
cls_layer.weight.data.normal_(0, 0.01)
cls_layer.bias.data.zero_()

 

7) RPN의 LOSS를 정의 및 계산해줍니다.

rpn_score : RPN에 feature map을 input으로 넣어준 classifiacition ouput값

rpn_loc : RPN에 feature map을 input으로 넣어준 box regression ouput값

# For classification we use cross-entropy loss
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_score.long().to(device), ignore_index = -1)

# For Loc Loss
# only positive samples
pos = gt_rpn_score > 0
mask = pos.unsqueeze(1).expand_as(rpn_loc)

# take those bounding boxes whick have positive labels
mask_loc_preds = rpn_loc[mask].view(-1, 4)
mask_loc_targets = gt_rpn_loc[mask].view(-1, 4)

x = torch.abs(mask_loc_targets.cpu() - mask_loc_preds.cpu())
rpn_loc_loss = ((x < 1).float() * 0.5 * x ** 2) + ((x >= 1).float() * (x - 0.5))

# Combining both the rpn_cls_loss and rpn_reg_loss
rpn_lambda = 10
N_reg = (gt_rpn_score > 0).float().sum()
rpn_loc_loss = rpn_loc_loss.sum() / N_reg
rpn_loss = rpn_cls_loss + (rpn_lambda * rpn_loc_loss)

 

Proposal Layer

 8) Proposal Layer는 class scores, bounding box regressor와 anchor boxes를 추출하는 작업을 해줍니다.

먼저 score 변수에 저장된 objectness score를 내림차순으로 정렬한 후

objectness score 상위N(n_train_pre_nms=12000)개의 anchor box에 대하여 Non maximum suppression 알고리즘을 수행합니다.

 

# Send the 22500 ROIs predicted by RPN to Fast RCNN to predict bbox + classifications
# First use NMS (Non-maximum supression) to reduce 22500 ROI to 2000

nms_thresh = 0.7  # non-maximum supression (NMS) 
n_train_pre_nms = 12000 # no. of train pre-NMS
n_train_post_nms = 2000 # after nms, training Fast R-CNN using 2000 RPN proposals
n_test_pre_nms = 6000
n_test_post_nms = 300 # During testing we evaluate 300 proposals,
min_size = 16


# the labelled 22500 anchor boxes
# format converted from [x1, y1, x2, y2] to [ctrx, ctry, w, h]

anc_height = anchor_boxes[:, 3] - anchor_boxes[:, 1]
anc_width = anchor_boxes[:, 2] - anchor_boxes[:, 0]
anc_ctr_y = anchor_boxes[:, 1] + 0.5 * anc_height
anc_ctr_x = anchor_boxes[:, 0] + 0.5 * anc_width


# ctr_y = dy predicted by RPN * anchor_h + anchor_cy
# ctr_x similar
# h = exp(dh predicted by RPN) * anchor_h
# w similar

ctr_y = dy * anc_height[:, np.newaxis] + anc_ctr_y[:, np.newaxis]
ctr_x = dx * anc_width[:, np.newaxis] + anc_ctr_x[:, np.newaxis]
h = np.exp(dh) * anc_height[:, np.newaxis]
w = np.exp(dw) * anc_width[:, np.newaxis]


roi = np.zeros(pred_anchor_locs_numpy.shape, dtype=anchor_locs.dtype)
roi[:, 0::4] = ctr_x - 0.5 * w
roi[:, 1::4] = ctr_y - 0.5 * h
roi[:, 2::4] = ctr_x + 0.5 * w
roi[:, 3::4] = ctr_y + 0.5 * h

 

9) Non maximum suppression(select 2000 bounding boxes)

score 값이 높은걸 기준으로 IoU를 계산해주고 일정 이상값 이상이면 제외해줍니다.

# take all the roi boxes
x1 = roi[:, 0]
y1 = roi[:, 1]
x2 = roi[:, 2]
y2 = roi[:, 3]

# find the areas of all the boxes
areas = (x2 - x1 + 1) * (y2 - y1 + 1)


# take the indexes of order the probability score in descending order
# non maximum suppression
# 첫번째를 기준으로 iou값을 이용해 0.7 이하 값들로 다시 계산해준다.
order = order.argsort()[::-1] # 위에서 정렬을 해줬는데 왜 order에 argsort를 해주지?
keep = []

while (order.size > 0):
  i = order[0] # take the 1st elt in roder and append to keep
  keep.append(i)

  xx1 = np.maximum(x1[i], x1[order[1:]])
  yy1 = np.maximum(y1[i], y1[order[1:]])
  xx2 = np.minimum(x2[i], x2[order[1:]])
  yy2 = np.minimum(y2[i], y2[order[1:]])

  w = np.maximum(0.0, xx2 - xx1 + 1)
  h = np.maximum(0.0, yy2 - yy1 + 1)

  inter = w * h
  ovr = inter / (areas[i] + areas[order[1:]] - inter)
  inds = np.where(ovr <= nms_thresh)[0]
  order = order[inds + 1]

keep = keep[:n_train_post_nms] # while training/testing, use accordingly
roi = roi[keep]

 

Proposal Target Layer

 10) Fast R-CNN 모델을 학습시키기 위한 유용한 sample을 선택하는 것입니다. 학습을 위해 128개의 sample을 mini-batch로 구성합니다. 이 때 Proposal layer에서 얻은 anchor box 중 ground truth box와의 IoU 값이 0.5 이상인 box를 positive sample로, 0.5 미만인 box를 negative sample로 지정합니다(IoU를 구하는 과정은 코드를 참고하시기 바랍니다). 전체 mini-batch sample 중 1/4, 즉 32개가 positive sample이 되도록 구성합니다. 

# select the foreground rois as pre the pos_iou_thresh
# and n_sample x pos_ratio (128 x 0.25 = 32) foreground samples

pos_roi_per_image = 32
pos_index = np.where(max_iou >= pos_iou_thresh)[0]
pos_roi_per_this_image = int(min(pos_roi_per_image, pos_index.size))

if pos_index.size > 0:
  pos_index = np.random.choice(
      pos_index, size=pos_roi_per_this_image, replace=False)  


# Select background negative samples
# similarly we do for negative(background) region proposals

neg_index = np.where((max_iou < neg_iou_thresh_hi) &
                     (max_iou >= neg_iou_thresh_lo))[0]
neg_roi_per_this_image = n_sample - pos_roi_per_this_image
neg_roi_per_this_image = int(min(neg_roi_per_this_image, neg_index.size))

if neg_index.size > 0:
  neg_index = np.random.choice(
    neg_index, size = neg_roi_per_this_image, replace=False)

 

RoI pooling

11) Feature extractor를 통해 얻은 feature map과 Proposal Target layer에서 추출한 region proposals을 활용하여 RoI pooling을 수행합니다. 이 때 output feature map의 크기가 7x7이 되도록 설정합니다.

size = (7, 7)
adaptive_max_pool = nn.AdaptiveMaxPool2d(size[0], size[1])

output = []
rois = indices_and_rois.data.float()
rois[:, 1:].mul_(1/16.0) # sub-sampling ratio
rois = rois.long()
num_rois = rois.size(0)

for i in range(num_rois):
  roi = rois[i]
  im_idx = roi[0]
  im = output_feature.narrow(0, im_idx, 1)[..., roi[1]:(roi[3]+1), roi[2]:(roi[4]+1)]
  tmp = adaptive_max_pool(im)
  output.append(tmp[0])

output = torch.cat(output, 0)

 

 

 

 

 

 

 

 

참고:

 - https://arxiv.org/abs/1506.01497

 - https://yeomko.tistory.com/17

 - https://herbwood.tistory.com/10

 - https://herbwood.tistory.com/11

 - https://www.youtube.com/watch?v=4yOcsWg-7g8 ← 이거 꼭 보기! ★

'Deep Learning > Vision' 카테고리의 다른 글

06. Vision Transform (작성중)  (0) 2022.05.13
05. Yolo 버전별 비교  (0) 2022.05.13
03. Fast RCNN [Vision, Object Detection]  (0) 2022.01.21
02. SPP-Net [Vision, Object Detection]  (0) 2021.11.17
01. R-CNN [Vision, Object Detection]  (0) 2021.11.11

+ Recent posts