개발자로 후회없는 삶 살기

데이터 청년 캠퍼스 PART.14, 15, 16일차(Object Detection) 본문

[대외활동]/[데이터 청년 캠퍼스]

데이터 청년 캠퍼스 PART.14, 15, 16일차(Object Detection)

몽이장쥰 2023. 2. 1. 12:22

서론

cv의 꽃 Object Detection을 공부해 보고 실습해 보겠습니다.

 

본론

- 객체 검출을 위한 전략

Region Proposal Stage : 객체를 잠재적으로 포함할 Region을 식별하는 과정, Region을 mini-images로 고려합니다.

 

 

-> Regression 문제

입력 : Region의 w, h/ 출력 : bounding Box의 w*, h*

 

- Region Proposals(RP)의 이해

=> SelectiveSearch(SS)

 

RP는 전체 이미지에서 객체가 있을 거 같은 픽셀을 찾는 것으로 SS는 객체의 위치를 잡는 데 사용하는 알고리즘(RP)입니다. object 인식이나 검출을 위한 가능한 후보 영역을 알아낼 수 있는 방법을 제공하는 것을 목표로 합니다.

 

-> SS 과정

1) 입력 영상에 대해 segmentation을 실시해서 이를 기반으로 후보 영역을 찾기 위한 seed를 설정합니다. 왼쪽 상단 그림처럼 초기에는 엄청나게 많은 후보들이 만들어집니다.

2) 이를 적절하게 통합해 segmentation 하면 후보 영역의 개수가 줄어들고 결과적으로 box의 후보 개수도 줄어듭니다.

 

 

결과적으로 결합된 크기의 후보군이 생기고 그 후보를 small images로 하여 RP을 하는 방법입니다.

 

> 단점 : RP 과정이 실제 object detection과 별도로 이루어지기 때문에, SS를 사용하면 end-to-end로 학습이 불가능하고, 실시간 적용에도 어려움이 있습니다.

 

- 실습

1. SS segmentation 

원본 이미지로부터 segmentation을 수행

 

2. RP

위에서 구한 최종 Segmentation에서 Region Proposal을 반환합니다.

 

print(candidates)

# 결과
[
    [0, 0, 142, 290],
    [212, 0, 84, 429],
    [159, 210, 86, 177],
    [67, 268, 82, 230],
    [162, 302, 128, 193],
    [57, 0, 145, 154],
    [212, 0, 84, 429],
    [212, 0, 84, 429],
    [162, 302, 128, 213],
    [154, 226, 136, 289],
    [27, 268, 122, 230],
    [212, 0, 84, 429],
    [57, 0, 161, 154],
    [212, 0, 84, 429],
    [0, 268, 149, 230],
    [154, 226, 145, 289],
    [154, 181, 145, 334],
    [0, 268, 149, 247],
    [53, 125, 165, 154],
    [53, 125, 168, 154],
    [21, 113, 116, 258],
    [21, 113, 116, 310],
    [53, 125, 168, 379],
    [0, 249, 149, 266],
    [154, 181, 145, 334],
    [57, 0, 162, 154],
    [57, 0, 162, 154],
    [154, 181, 145, 334],
    [0, 249, 149, 266],
    [154, 181, 145, 334],
    [0, 0, 147, 388],
    [57, 0, 168, 168],
    [21, 0, 204, 423],
    [0, 0, 149, 515],
    [154, 181, 145, 334],
    [0, 0, 149, 515],
    [35, 125, 190, 390],
    [35, 125, 190, 390],
    [35, 125, 190, 390],
    [0, 0, 149, 515],
    [0, 0, 149, 515],
    [21, 0, 275, 429],
    [154, 181, 145, 334],
    [154, 0, 145, 515],
    [154, 0, 145, 515],
    [0, 0, 149, 515],
    [0, 0, 149, 515],
    [0, 0, 225, 515],
    [0, 0, 299, 515],
    [21, 0, 275, 429],
    [0, 0, 296, 429],
    [0, 0, 299, 515]
]

segmentation 이후 RP한 후보군의 X, Y, W, H 좌표가 나타납니다.

 

- IoU(Intersection over Union)의 이해

성능 평가 척도로 두 박스가 겹친 비율이 얼마나 되느냐를 나타내고 합집합(Union) 분에 교집합(Intersection)의 비율이 완전히 겹치면 1, 겹치지 않으면 0을 반환합니다.

 

-> Non-Max suppression(nms)

RP를 한 후 small images를 분류, 회귀 모델에 넣을 텐데 RP가 많을 것이고 하나의 객체에 겹치는 것도 많을 것입니다. 따라서 객체가 있을 확률이 가장 높은 RP를 제외하고 나머지는 없앱니다. 그때 IoU가 가장 높은 박스만 남깁니다.

 

- mAP(mean Average Precision)

합성곱 신경망의 모델 성능 평가는 mAP를 이용해야 합니다. 우선 Precision과 Average Precision에 대해 이해해 보겠습니다.

 

1) Precision

정확도를 의미하며 검출 결과들 중 옳게 검출한 비율을 의미합니다.

 

2) Average Precision

Precision-recall 그래프에서 그래프 선 아래쪽의 면적으로 계산되며 높으면 그 알고리즘의 성능이 전체적으로 우수하다는 의미입니다. 컴퓨터 비전 분야에서 물체인식 알고리즘의 선능은 대부분 AP로 평가합니다.

 

-> mAP 정의

물체 클래스가 여러 개인 경우 각 클래스당 AP를 구한 다음 그것을 모두 합하고 물체 클래스의 개수로 나눈 값으로 알고리즘의 전체 성능을 평가합니다.

 

 

- RCNN 학습으로 IoU, RP 개념 이해하기

이제부터는 앞에서 배운 개념을 활용하여 GT가 있을 때 RP를 찾고 RP 중 IoU가 가장 높은 RP를 찾는 실습을 해보겠습니다.

 

1) 데이터 불러오기

for ix, (im, bbs, labels, fpath) in enumerate(ds): 
  if(ix == 1):
    break


  print(ix) # 이미지 행 번호
  print(im.shape) # 이미지 shape
  print(bbs) # 그 이미지의 bbs
  print(labels) # 라벨
  print(fpath) # 캐글 데이터의 저장 경로
  
# 결과
0
(170, 256, 3)
[[88, 26, 232, 110]]
['Bus']
images/images/0000599864fd15b3.jpg

# 결과 2
0
(170, 256, 3)
[[88, 26, 232, 110]]
['Bus']
images/images/0000599864fd15b3.jpg
1
(170, 256, 3)
[[70, 24, 178, 74], [179, 34, 255, 69]]
['Truck', 'Truck']
images/images/00006bdb1eb5cd74.jpg
2
(144, 256, 3)
[[40, 38, 95, 101]]
['Bus']
images/images/00010bf498b64bab.jpg

ds는 파이토치의 dataset 모듈로 현재 트럭, 버스 데이터가 들어있습니다. enumerate로 불렀을 때 반환값을 확인해 보았습니다. 결과를 보면 하나의 GT에 여러 개의 바운딩 박스가 있는 경우가 있습니다. 이것은 하나의 GT에 여러개의 트럭이 있는 경우입니다.

 

2) GT에서 후보 구하기

FPATHS, GTBBS, CLSS, DELTAS, ROIS, IOUS = [], [], [], [], [], []
N = 500
for ix, (im, bbs, labels, fpath) in enumerate(ds):
  if(ix==N):
    break
  H, W, _ = im.shape
  candidates = extract_candidates(im)
  candidates = np.array([(x,y,x+w,y+h) for x,y,w,h in candidates])
  
print(candidates)

# 결과
[[ 69   0 180  71]
 [ 87  76 226 131]
 [  0 133 255 170]
 [  0 133 255 170]
 [ 87  76 226 132]
 [  0 133 255 170]
 [ 46 114 243 149]
 [  0  97  87 133]
 [  0  97  87 146]
 [  0  65 190 101]
 [ 69   0 180  71]
 [ 87  76 226 146]
 [  0   0 102  79]
 [  0   0 102  79]
 [  0   0 180  79]
 [161   0 255  72]
 [  0 113 243 149]
 [  0  65 190 146]
 [  0  65 226 146]
 [161   0 255  80]
 [161   0 255  80]
 [  0   0 255  80]
 [142  46 255 118]
 [142  46 255 147]
 [  0 113 253 149]
 [  0 133 255 170]
 [  0   0 255  80]
 [  0   0 255  86]
 [  0  65 253 149]
 [  0   0 255  86]
 [142  46 255 147]
 [142  46 255 147]
 [  0  46 255 149]
 [  0   0 255 149]
 [  0   0 255 149]
 [  0   0 255 149]
 [  0   0 255 149]
 [  0   0 255 149]
 [  0   0 255 149]
 [  0   0 255 149]
 [  0   0 255 149]
 [  0   0 255 170]
 [  0   0 255 170]]

하나의 GT에서 수많은 RP가 나옵니다. 

 

3) IoU 구하기

FPATHS, GTBBS, CLSS, DELTAS, ROIS, IOUS = [], [], [], [], [], []
N = 500
for ix, (im, bbs, labels, fpath) in enumerate(ds): 
    if(ix==1): # 일단 첫번째 500 이미지만 사용한다.
        break
    H, W, _ = im.shape
    candidates = extract_candidates(im) # 이 이미지에서 라벨에 해당하는 RP를 뽑아낸다.(RP로 객체 후보 = small image를 뽑아내는 거지)
    candidates = np.array([(x,y,x+w,y+h) for x,y,w,h in candidates])

    ious, rois, clss, deltas = [], [], [], []
    ious = np.array([[extract_iou(candidate, _bb_) for candidate in candidates] for _bb_ in bbs]).T

    print(ious)
    
# 결과
[[0.36111111]
 [0.66468254]
 [0.08943037]
 [0.66666667]
 [0.10455259]
 [0.10455259]
 [0.06493189]
 [0.12301957]
 [0.        ]
 [0.31404959]
 [0.13081583]
 [0.11846675]
 [0.13643332]
 [0.06493189]
 [0.12904785]
 [0.11092646]
 [0.10455259]
 [0.11092646]
 [0.11092646]
 [0.07052527]
 [0.67752443]
 [0.32555216]
 [0.07052527]
 [0.07052527]
 [0.26537017]
 [0.55177447]
 [0.11846675]
 [0.55177447]
 [0.26537017]
 [0.26537017]
 [0.11092646]
 [0.23981786]
 [0.12879884]
 [0.12879884]
 [0.36893796]
 [0.        ]
 [0.19701333]
 [0.07052527]
 [0.36893796]
 [0.19701333]
 [0.19701333]
 [0.28068221]
 [0.28068221]
 [0.28068221]
 [0.28068221]
 [0.28068221]
 [0.28068221]
 [0.28068221]]

수 많은 RP의 IoU를 전부 구합니다.

 

len(candidates), len(ious)

# 결과
(48, 48)

후보 모두의 IoU를 구한 것이므로 개수가 동일합니다.

 

4) 가장 큰 IoU의 처리

for jx, candidate in enumerate(candidates):
    cx,cy,cX,cY = candidate # gt 하나의 후보를 하나씩 뽑아서 수 많은 RP의의 IoU를 전부 구한다.(아까 그중 제일 큰 애를 )
    candidate_ious = ious[jx]

    # 한 gt의 한 후보의 iou를 구함 한 후보의 좌표가(cx,cy,cX,cY)/ iou가 candidate_ious
    best_iou_at = np.argmax(candidate_ious)

    # 여러개의 RP중 iou가 가장 큰 것을 찾음
    best_iou = candidate_ious[best_iou_at]

    # 그 녀석(가장 IoU가 높은 RP)의 GT에서의 bbs를 구하고 그게 
    # 하나의 GT(bbs가 여러개 있을 수 있음)에서 여러개의 RP 중 IoU가 가장 높은 RP의 bbs임
    best_bb = _x,_y,_X,_Y = bbs[best_iou_at]

    print(bbs)

위의 nms 개념에서 말했듯이 가장 IoU가 큰 RP를 찾아서 best_bb라고 지정합니다.

 

5) 지역 제안 및 오프셋의 실측 정보 가져오기

        if best_iou > 0.3: clss.append(labels[best_iou_at]) # IoU가 가장 높은 RP의 IoU가 0.3을 넘으면 clss에 라벨 (['Bus'])을 넣고
        else : clss.append('background') # 안 넘으면 background라고 한다.

        # delta를 계산한다. 이게 GT의 bbs와 후보 x, y, w, h의 차이네
        delta = np.array([_x-cx, _y-cy, _X-cX, _Y-cY]) / np.array([W,H,W,H])
        deltas.append(delta)
        
        rois.append(candidate / np.array([W,H,W,H]))

IoU가 지정한 threshhold보다 크면 해당 라벨을 append하고 아니면 background를 append합니다. 즉 잘못 구한 RP라는 뜻입니다. delta는 실제 GT의 bbs와 RP의 좌표의 차이입니다.

 

6) 학습 결과

test 결과입니다. 학습은 vgg16을 백본으로 하였습니다.

 

RCNN 실습.ipynb
2.96MB

 

- Fast RCNN 실습

이번에는 RCNN의 입력 이미지를 매번 feature map으로 구하고 RP을 하는 문제점을 개선한 Fast RCNN에 대해서 알아보겠습니다. Fast RCNN은 한 번에 feature를 가져오고 roi pooling을 해서 속도 문제를 해결합니다.

 

=> RoI Pooling

입력 이미지를 conv하여 feature map을 찾고 feature map에서 RP을 하여 단 한 번의 conv만 진행한 후 만들어진 여러 개의 RP을 RoI Pooling하면 입력 크기를 맞추는 역할을 합니다.

 

-> 코드 실습

from torchvision.ops import RoIPool
class FRCNN(nn.Module):
    def __init__(self):
        super().__init__()
        rawnet = torchvision.models.vgg16_bn(pretrained=True)
        for param in rawnet.features.parameters():
            param.requires_grad = True
        self.seq = nn.Sequential(*list(rawnet.features.children())[:-1])
        
        # torchvision의 RoIPool 모듈을 사용합니다.
        self.roipool = RoIPool(7, spatial_scale=14/224)
        feature_dim = 512*7*7
        self.cls_score = nn.Linear(feature_dim, len(label2target))
        self.bbox = nn.Sequential(
              nn.Linear(feature_dim, 512),
              nn.ReLU(),
              nn.Linear(512, 4),
              nn.Tanh(),
            )
        self.cel = nn.CrossEntropyLoss()
        self.sl1 = nn.L1Loss()

파이토치의 ROIPool 모듈을 활용하기만 하면 됩니다.

 

# 기존 RCNN

class RCNN(nn.Module):
    def __init__(self):
        super().__init__()
        feature_dim = 25088
        self.backbone = vgg_backbone
        self.cls_score = nn.Linear(feature_dim, len(label2target))
        self.bbox = nn.Sequential(
              nn.Linear(feature_dim, 512),
              nn.ReLU(),
              nn.Linear(512, 4),
              nn.Tanh(),
            )
        self.cel = nn.CrossEntropyLoss()
        self.sl1 = nn.L1Loss()

기본 RCNN 코드와 비교해 보면 모델 구성시에 RoI라는 차이가 있습니다.

 

 

결론

이번 시간에는 CV의 꽃이라고 불리는 객체 탐지의 구조와 평가 지표를 알아보았습니다. 이 개념을 이해해야 더 나아갈 수 있으므로 제 것으로 만드는 것이 매우 중요함을 느낍니다.

Comments