
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의 부분
