본문 바로가기

8. OpenCV | CV

8/4(금) IT K-DT(105일차) / 5.FasterR-CNN실습 (1)

 

5. Faster R-CNN 실습

버스, 트럭 등의 차량이 지나가는 영상에 인식 개체에 네모 박스를 구현 // 아래: 예시 사진

 

# 필요한 라이브러리와 모듈 설치

import os
import cv2
import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from ipywidgets import interact
from torch.utils.data import DataLoader
from torchvision import models, transforms
from torchvision.utils import make_grid
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor # Fast R-CNN 모듈
from util import CLASS_NAME_TO_ID, visualize # util.py에서 가져올 예정

 

Pycharm으로 util.py 파일을 생성한 후, 아래와 같이 코드 입력

 

import os
import cv2
import torch

CLASS_NAME_TO_ID = {'Bus': 0, 'Truck': 1}
CLASS_ID_TO_NAME = {0: 'Bus', 1: 'Truck'}
BOX_COLOR = {'Bus': (200, 0, 0), 'Truck': (0, 0, 200)}
TEXT_COLOR = (255, 255, 255) # 흰색

# 여기까지 작성 후 실행해서 build를 complie

 

# data_dir 안의 df.csv를 DataFrame으로 만들어줌

data_dir = './DataSet/'
data_df = pd.read_csv(os.path.join(data_dir, 'df.csv')) 
data_df

# ImageID : 파일의 이름
# LabelName: 차종
# XMin, XMax, YMin, YMax: 차량에 대한 bounding box의 네 꼭지점 의미. 
# 왼쪽 상단부터 시계방향으로 차례대로 XMin, XMax, YMin, YMax.

 

 

# listdir에서 끝부분이 jpg인 index 0번 파일을 가져와서 image_files라는 list에 담아줌

index = 0
image_files = [fn for fn in os.listdir('./DataSet/train/') if fn.endswith('jpg')] 
# endswith(): 끝부분이 동일한 것을 찾음
image_file = image_files[index]
image_file

 

 

# index 0번인 image_file를 가져와서 경로와 합쳐줌

image_path = os.path.join('./DataSet/train/', image_file)
image_path

 

 

# index 0번 사진의 시각화 후 shape 출력

image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image.shape  # height 170, width 256, channel 3(컬러) 라는 의미

 

 

# 시각화

plt.imshow(image)

 

 

# image파일의 확장명을 제외한 부분을 image_id로 가져옴

image_id = image_file.split('.')[0]
image_id

 

 

# DataFrame의 ImageID와 파일이름이 일치하는 data를 가져옴

meta_data = data_df[data_df['ImageID'] == image_id]
meta_data

# 0000599864fd15b3이 파일이름인 것의 data를 가져옴

 

 

# data의 LabelName(차종)이 무엇인지 알고싶은 경우

cate_names = meta_data['LabelName'].values
cate_names # LabelName은 Bus

 

 

# data의 Bounding Box의 정보를 알고싶은 경우

bboxes = meta_data[['XMin', 'XMax', 'YMin', 'YMax']].values
bboxes

 

 

# data의 height, width를 알고싶은 경우

img_H, img_W, _ = image.shape # 맨 마지막 channel부분은 언더바 처리
img_H, img_W

 

 

# 모듈을 직접 만들어서 사용
# Pycharm을 사용할 예정

class_ids = [CLASS_NAME_TO_ID[cate_name] for cate_name in cate_names] # from util import CLASS_NAME_TO_ID 
class_ids # bus이므로 0이 출력

 

 

# X_Cen (X 전체 길이의 중앙값), Y_Cen(Y 전체 길이의 중앙값), W(너비), H(높이)로 좌표를 바꾸어보기 
# (YOLO 모델에 들어가는 형식)

unnorm_bboxes = bboxes.copy() # array([[0.34375 , 0.90875 , 0.156162, 0.650047]]) // 
# 'XMin', 'XMax', 'YMin', 'YMax'

 # XMax와 YMin의 순서 변경 // 'XMin', 'YMin', 'XMax', 'YMax'
unnorm_bboxes[:, [1,2]] = unnorm_bboxes[:, [2,1]]

# XMax, YMax -= XMin, YMin
# XMax - XMin, YMax - YMin으로 변경
# W, H이 됨
unnorm_bboxes[:, 2:4] -= unnorm_bboxes[:, 0:2]

# XMin, YMin += (W, H / 2)
# XMin + (W/2), YMin + (H/2)으로 변경.
# X_Cen, Y_Cen이 됨.
unnorm_bboxes[:, 0:2] += (unnorm_bboxes[:, 2:4]/2)

unnorm_bboxes
# X_Cen, Y_Cen, W, H

 

 

# bounding box가 적용

unnorm_bboxes[:, [0,2]] *= img_W
unnorm_bboxes[:, [1,3]] *= img_H
unnorm_bboxes

# 만약 출력값이 크게 나온다면, 다시 위에서부터 실행해보기

 

 

<util.py> 파일 內 아래와 같이 코드를 입력 함수를 생성

 

def visualize_bbox(image, bbox, class_name, color=BOX_COLOR, thickness=2):
    x_center, y_center, w, h = bbox
    x_min = int(x_center - w/2)
    y_min = int(y_center - h/2)
    x_max = int(x_center + w/2)
    y_max = int(y_center + h/2)
    cv2.rectangle(
        image,
        (x_min, y_min),
        (x_max, y_max),
        color=color[class_name],
        thickness=thickness
    )
    ((text_width, text_height), _) = cv2.getTextSize(
        class_name,
        cv2.FONT_HERSHEY_SIMPLEX,
        0.4,
        1
    )
    cv2.rectangle(image, (x_min, y_min - int(1.3 * text_height)),
                  (x_min + text_width, y_min), color[class_name], -1)
    cv2.putText(
        image,
        text=class_name,
        org=(x_min, y_min - int(0.3 * text_height)),
        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
        fontScale=0.4,
        color=TEXT_COLOR
    )
    return image



def visualize(image, bboxes, category_ids): 
# bboxes: 바운딩 박스 좌표 / category_ids: 차종이 확인 가능한 번호
    img = image.copy()
    for bbox, category_id in zip(bboxes, category_ids):
        class_name = CLASS_ID_TO_NAME[category_id]
        img = visualize_bbox(img, bbox, class_name)
    return img

 

# 위의 사진의 버스에 bounding box가 그려지도록  visualize 함수를 생성

canvas = visualize(image, unnorm_bboxes, class_ids)
plt.figure(figsize=(6,6))
plt.imshow(canvas)
plt.show()

 

 

# interact를 이용해서 조절 바를 통해 화면이 바뀌면서 자동으로 인식을 해주는 기능 구현
# 위에서 작성했던 단계를 show_sample이라는 함수에 하나로 담기

@interact(index=(0, len(image_files)-1))
def show_sample(index=0):
    image_file = image_files[index]
    image_path = os.path.join('./DataSet/train/',image_file)
    image = cv2.imread(image_path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    
    image_id = image_file.split('.')[0]
    meta_data = data_df[data_df['ImageID'] == image_id]
    cate_names = meta_data['LabelName'].values
    bboxes = meta_data[['XMin', 'XMax', 'YMin', 'YMax']].values
    img_H, img_W, _ = image.shape
    class_ids = [CLASS_NAME_TO_ID[cate_name] for cate_name in cate_names]
    
    unnorm_bboxes = bboxes.copy()
    unnorm_bboxes[:, [1,2]] = unnorm_bboxes[:, [2,1]]
    unnorm_bboxes[:, 2:4] -= unnorm_bboxes[:, 0:2]
    unnorm_bboxes[:, 0:2] += (unnorm_bboxes[:, 2:4]/2)
    
    unnorm_bboxes[:,[0,2]] *= img_W
    unnorm_bboxes[:,[1,3]] *= img_H
    # 시각화
    canvas = visualize(image, unnorm_bboxes, class_ids) # 이미지, box좌표, 클래스(0 or 1)
    plt.figure(figsize=(6,6))
    plt.imshow(canvas)
    plt.show()

 

 

class Detection_dataset():
    def __init__(self, data_dir, phase, transformer=None):
        self.data_dir = data_dir
        self.phase = phase
        self.data_df = pd.read_csv(os.path.join(self.data_dir, 'df.csv'))
        self.image_files = [fn for fn in os.listdir(os.path.join(self.data_dir, phase)) if fn.endswith('jpg')]
        self.transformer = transformer
    
    def __len__(self):
        return len(self.image_files)
    
    def __getitem__(self, index):
        filename, image = self.get_image(index)
        bboxes, class_ids = self.get_label(filename)
        img_H, img_W, _ = image.shape
        if self.transformer:
            image = self.transformer(image)
            _, img_H, img_W = image.shape
        
        bboxes[:, [0,2]] *= img_W
        bboxes[:, [1,3]] *= img_H
        
        target = {}
        target['boxes'] = torch.Tensor(bboxes).float()
        target['labels'] = torch.Tensor(class_ids).long()
        
        return image, target, filename
        
    def get_image(self, index):
        filename = self.image_files[index]
        image_path = os.path.join(self.data_dir, self.phase, filename)
        image = cv2.imread(image_path)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        return filename, image
    
    def get_label(self, filename):
        image_id = filename.split('.')[0]
        meta_data = data_df[data_df['ImageID'] == image_id]
        cate_names = meta_data['LabelName'].values
        class_ids = [CLASS_NAME_TO_ID[cate_name] for cate_name in cate_names]
        
        bboxes = meta_data[['XMin', 'XMax', 'YMin', 'YMax']].values
        bboxes[:, [1,2]] = bboxes[:, [2,1]]
        return bboxes, class_ids

 

data_dir = './DataSet'
dataset = Detection_dataset(data_dir=data_dir, phase='train', transformer=None)

dataset[0]

 

 

index = 20
image, target, filename = dataset[index]

target, filename

 

 

boxes = target['boxes'].numpy()
class_ids = target['labels'].numpy()
# 찾는 object가 2개임
n_obj = boxes.shape[0]
bboxes = np.zeros(shape=(n_obj, 4), dtype=np.float32)
bboxes[:, 0:2] = (boxes[:, 0:2] + boxes[:, 2:4]) / 2
bboxes[:, 2:4] = boxes[:, 2:4] - boxes[:, 0:2]
canvas = visualize(image, bboxes, class_ids)
plt.figure(figsize=(6, 6))
plt.imshow(canvas)
plt.show()

 

 

# Detection_dataset 클래스 사용하여 dataset을 만들고 적용해보기
# 위에서 작성했던 단계를 show_sample이라는 함수에 하나로 담기

@interact(index=(0, len(image_files)-1))
def show_sample(index=0):
    image, target, filename = dataset[index]
    
    boxes = target['boxes'].numpy()
    class_ids = target['labels'].numpy()

    n_obj = boxes.shape[0] # 객체 갯수 알아보기
    bboxes = np.zeros(shape=(n_obj, 4), dtype=np.float32) # boxes를 검은 화면으로 만들어주기
    bboxes[:, 0:2] = (boxes[:, 0:2] + boxes[:, 2:4]) / 2
    bboxes[:, 2:4] = boxes[:, 2:4] - boxes[:, 0:2]
    
    canvas = visualize(image, bboxes, class_ids)
    
    plt.figure(figsize=(6,6))
    plt.imshow(canvas)
    plt.show()

 

 

IMAGE_SIZE = 448
# [0.48 5, 0.456, 0.406],[0.229, 0.224, 0.225]값을 줬을 때 가장 성능이 좋다라는 논문이 있음
# Faster R-CNN 사용 시 pre-trained으로 사용된 이미지넷 데이터셋의 학습 시에 얻어낸 값
 
transformer = transforms.Compose([
    transforms.ToTensor(),
    transforms.Resize(size=(IMAGE_SIZE, IMAGE_SIZE)),
    transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225]) 
    # 평균, 표준편차... 데이터 사이를 스케일링.
])

transformed_dataset = Detection_dataset(data_dir=data_dir, phase='train', transformer=transformer)

index = 0
image, target, filename = transformed_dataset[index]
image.shape

 

 

np_image = make_grid(image, normalize=True).cpu().permute(1,2,0).numpy()
np_image.shape

 

 

boxes = target['boxes'].numpy()
class_ids = target['labels'].numpy()

n_obj = boxes.shape[0]
bboxes = np.zeros(shape=(n_obj, 4), dtype=np.float32)
bboxes[:, 0:2] = (boxes[:, 0:2] + boxes[:, 2:4]) / 2
bboxes[:, 2:4] = boxes[:, 2:4] - boxes[:, 0:2]

canvas = visualize(np_image, bboxes, class_ids)

plt.figure(figsize=(6, 6))
plt.imshow(canvas)
plt.show()
# 머신러닝에서는 사진이 찌그러져도 detection하는데는 문제가 되지 않는다.

 

 

# collate_fn: DataLoader에서 데이터를 배치 사이즈씩 나누고, 
# 각 배치에 대하여 해당 함수를 실행한 후에 반환하라고 하는 콜백함수

def collate_fn(batch):
    image_list = []
    target_list = []
    filename_list = []
    
    for img, target, filename in batch:
        image_list.append(img)
        target_list.append(target)
        filename_list.append(filename)
        
    return image_list, target_list, filename_list

 

BATCH_SIZE = 8 #2의 배수로 주는게 좋음

trainset = Detection_dataset(data_dir=data_dir, phase='train', transformer=transformer)
trainloader = DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn)

 

for index, batch in enumerate(trainloader):
    images = batch[0]
    targets = batch[1]
    filenames = batch[2]
    
    if index == 0:
        break
        
print(targets)
print(filenames)

 

 

def build_dataloader(data_dir, batch_size=4, image_size=448):
    transformer = transforms.Compose([
        transforms.ToTensor(),
        transforms.Resize(size=(IMAGE_SIZE, IMAGE_SIZE)),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    dataloaders = {}
    train_dataset = Detection_dataset(data_dir=data_dir, phase='train', transformer=transformer)
    dataloaders['train'] = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
    
    val_dataset = Detection_dataset(data_dir=data_dir, phase='val', transformer=transformer)
    dataloaders['val'] = DataLoader(val_dataset, batch_size=1, shuffle=False, collate_fn=collate_fn)
    return dataloaders

 

data_dir = './DataSet/'
dloaders = build_dataloader(data_dir)
# train모드 4바퀴, val 1바퀴. 배치가 하나니깐.
for phase in ['train', 'val']:
    for index, batch in enumerate(dloaders[phase]):
        images = batch[0]
        targets = batch[1]
        filenames = batch[2]
        print(f'{phase}: {targets}')
        if index == 0:
            break

 

 

model = models.detection.fasterrcnn_resnet50_fpn(pretrained=True)
model

 

 

# 모델 수정해주는 함수
def build_model(num_classes):
    model = models.detection.fasterrcnn_resnet50_fpn(pretrained=True)
    in_features = model.roi_heads.box_predictor.cls_score.in_features
    model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
    
    return model

 

NUM_CLASSES = 2
model = build_model(num_classes = NUM_CLASSES)
model

# 8은 2개 객체당 각각 4개의 좌표값을 말하는것이라서 
# 바운딩박스 2개의 총 좌표가 8개여서 out_features가 8인것임.
# 아래처럼 2개의 레이어가, 하나의 out_features가 다음 레이어의 in_features와 다른경우는 
# 두개가 연속적으로 이어진 레이어가 아니고, 각각 값을 받아서 out_features를 반환함. 
# 따라서 이 모델의 예측값은 cls_score, bbox_pred 2가지의 값이 리스트에 담겨서 반환됨.
#     (box_predictor): FastRCNNPredictor(
#      (cls_score): Linear(in_features=1024, out_features=2, bias=True)
#      (bbox_pred): Linear(in_features=1024, out_features=8, bias=True)

아래 빨간색 블록이 수정된 out_features의 부분