1. 객체 탐지 (Object Detection)
- 컴퓨터 비전과 이미지 처리와 관련된 컴퓨터 비전 기술로써, 디지털 이미지와 비디오로 특정한 계열의 시맨틱 객체 인스턴스를 감지하는 일
- 얼굴 검출, 보행자 검출 등이 포함
- 데이터확인
🟠. 컴퓨터 비전의 Task 비교 |
|
◼ 모듈 설치
import os
import cv2
import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
from ipywidgets import interact # Jupyter 노트북에서 인터랙티브한 위젯을 만들기 위한 interact
from torch.utils.data import DataLoader # 데이터 로딩을 위한 DataLoader 클래스
from torchvision import models, transforms # torchvision의 pre-trained 모델과 이미지 변환을 위한 transforms
from torchvision.utils import make_grid # 이미지를 그리드 형태로 변환하는 make_grid 함수
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor # Faster R-CNN 모델에서 객체 감지기(예측기)
|
◼ 파일 불러오기
data_dir = './DataSet/'
data_df = pd.read_csv(os.path.join(data_dir, 'df.csv'))
data_df
|
24062개의 데이터가 있는 것을 확인할 수 있음 |
◼ './DataSet/train/' 디렉토리 내의 모든 .jpg 파일을 [ image_files ] 에 리스트로 저장
image_files = [fn for fn in os.listdir('./DataSet/train/') if fn.endswith('jpg')]
image_files
|
◼ [ image_files ] 첫번째 이미지 이름
image_file = image_files[0]
image_file
|
'0000599864fd15b3.jpg' |
◼[ image_files ] 첫번째 이미지 확인하기
image_path = os.path.join('./DataSet/train/', image_file)
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
plt.imshow(image)
|
◼ 첫번째 이미지 정보 불러오기
image_id = image_file.split('.')[0]
meta_data= data_df[data_df['ImageID'] == image_id]
meta_data
|
◼ 첫번째 이미지 'LabelName' 가져오기
cate_names = meta_data['LabelName'].values
cate_names
|
◼ 첫번째 이미지 x, y 최소값, 최대값좌표 배열로 가져오기
bboxes = meta_data[['XMin', 'XMax', 'YMin', 'YMax']].values
bboxes
|
array([[0.34375 , 0.90875 , 0.156162, 0.650047]]) |
- 잠깐 파이참에서 작업
- 프로젝트 생성
-두가지 설치
<<<<< 잠깐 파이참에서 작업 >>>>>
◼ 클래스와 해당 ID, 박스 색상 및 텍스트 색상을 정의
# util.py
import os
import cv2
import torch
CLASS_NAME_TO_ID = {'Bus': 0, 'Truck': 1} # 'Bus' 클래스는 ID 0으로, 'Truck' 클래스는 ID 1로 매핑
CLASS_ID_TO_NAME = {0: 'Bus', 1: 'Truck'} # 0은 'Bus'로, ID 1은 'Truck'으로 매핑
BOX_COLOR = {'Bus': (200, 0, 0), 'Truck': (0, 0, 200)} # 'Bus'는 (200, 0, 0)
# 'Bus'는 빨간색, 'Truck'은 파란색으로 정의
TEXT_COLOR = (255,255,255) # 텍스트의 색상을 정의하는 튜플 : 흰색
|
<<<<< 다시 주피터 노트북>>>>
◼ cate_names 리스트에 있는 각 클래스 이름을 해당하는 클래스 ID로 변환하여 class_ids 리스트를 생성
from util import CLASS_NAME_TO_ID
class_ids = [CLASS_NAME_TO_ID[cate_name] for cate_name in cate_names]
class_ids
|
[0] ------------------------------------------- 클래스가 bus이기 때문에 0이 출력 |
◼ bboxes라는 NumPy 배열의 복사본을 unnorm_bboxes라는 새로운 변수에 저장
unnorm_bboxes = bboxes.copy()
unnorm_bboxes
|
array([[0.34375 , 0.90875 , 0.156162, 0.650047]]) |
◼ x, y 좌표 수정 (1)
# XMin, XMax, Ymin, Ymax -> XMin, Ymin, XMax, Ymax
unnorm_bboxes[:, [1,2]] = unnorm_bboxes[:, [2, 1]]
unnorm_bboxes
|
array([[0.34375 , 0.156162, 0.90875 , 0.650047]]) ------------------------------------------------------------------------ unnorm_bboxes는 배열의 각 행이 [XMin, YMin, XMax, YMax] 형식으로 되어 있는 경계 상자를 포함하고 있습니다. unnorm_bboxes[:, 2:4]는 XMax와 YMax 열을 선택하고, unnorm_bboxes[:, 0:2]는 XMin과 YMin 열을 선택하여, 두 열의 차이를 계산합니다. |
◼ x, y 좌표 수정 (2)
# XMin, Ymin, XMax, Ymax -> XMin, Ymin, W, H
unnorm_bboxes[:, 2:4] -= unnorm_bboxes[:, 0:2]
unnorm_bboxe
|
array([[0.34375 , 0.156162, 0.565 , 0.493885]]) ----------------------------------------------------------------
|
◼ 경계 상자의 좌표를 변환
# XMin, Ymin, W, H -> X_Cen, Y_Cen, W, H
# X_Cen(XMin + (W/2)), Y_Cen(YMin + (H/2)), W, H
unnorm_bboxes[:, 0:2] += (unnorm_bboxes[:, 2:4] / 2)
unnorm_bboxes
|
array([[0.62625 , 0.4031045, 0.565 , 0.493885 ]]) -------------------------------------------------------------------- 이번에는 좌표를 왼쪽 상단 모서리 (XMin, YMin)에서 중심 좌표 (CX, CY)로 변환
|
◼ 이미지의 높이와 너비를 추출
img_H, img_W, _ = image.shape
img_H, img_W
|
(170, 256) ---------------------------------------------------------
|
◼ 상자의 좌표를 이미지 크기에 맞게 조정하는 작업
# img중 실제 x,y 값 및 box 사이즈
unnorm_bboxes[:, [0, 2]] *= img_W
unnorm_bboxes[:, [1, 3]] *= img_H
unnorm_bboxes
|
array([[160.32 , 68.527765, 144.64 , 83.96045 ]]) ---------------------------------------------------------------------------
|
<<<<< 잠깐 파이참에서 작업 >>>>>
◼ 이미지를 시각화하여 객체 감지 결과를 표시하는 두 가지 함수를 정의
# 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)
def visualize_bbox(image, bbox, class_name, color = BOX_COLOR, thinkess = 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=thinkess)
((text_width, text_height), _) = cv2.getTextSize(class_name, cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.4, thickness=1)
cv2.rectangle(image, (x_min, y_min - int(1.3 * text_height)), (x_min + text_height, 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):
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
|
- 주석
# util.py
import os
import cv2
import torch
CLASS_NAME_TO_ID = {'Bus': 0, 'Truck': 1} # 'Bus' 클래스는 ID 0으로, 'Truck' 클래스는 ID 1로 매핑
CLASS_ID_TO_NAME = {0: 'Bus', 1: 'Truck'} # 0은 'Bus'로, ID 1은 'Truck'으로 매핑
BOX_COLOR = {'Bus': (200, 0, 0), 'Truck': (0, 0, 200)} # 'Bus'는 (200, 0, 0)
# 'Bus'는 빨간색, 'Truck'은 파란색으로 정의
TEXT_COLOR = (255,255,255) # 텍스트의 색상을 정의하는 튜플 : 흰색
def visualize_bbox(image, bbox, class_name, color = BOX_COLOR, thinkess = 2):
"""
이미지에 경계 상자와 클래스 이름을 시각화합니다.
Parameters:
- image (numpy.ndarray): 경계 상자를 그릴 이미지.
- bbox (list or tuple): 경계 상자의 [x_center, y_center, w, h] 정보.
- class_name (str): 경계 상자에 해당하는 객체의 클래스 이름.
- color (dict, optional): 클래스별 색상 딕셔너리. 기본값은 BOX_COLOR.
- thickness (int, optional): 경계 상자의 두께. 기본값은 2.
Returns:
- numpy.ndarray: 경계 상자가 그려진 이미지.
"""
# 경계 상자의 중심 좌표와 너비, 높이에서 상자 좌표를 계산합니다.
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=thinkess)
# 클래스 이름의 텍스트 크기를 계산합니다.
((text_width, text_height), _) = cv2.getTextSize(class_name, cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.4, thickness=1)
# 클래스 이름 배경을 그립니다. (텍스트 위에 사각형을 그려 배경을 생성)
cv2.rectangle(image, (x_min, y_min - int(1.3 * text_height)), (x_min + text_height, 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):
"""
여러 개의 경계 상자와 해당 클래스 ID를 이미지에 시각화합니다.
Parameters:
- image (numpy.ndarray): 경계 상자를 그릴 이미지.
- bboxes (list of lists or tuples): 각 경계 상자의 [x_center, y_center, w, h] 정보가 담긴 리스트.
- category_ids (list of int): 각 경계 상자에 대한 클래스 ID 리스트.
Returns:
- numpy.ndarray: 경계 상자와 클래스 이름이 시각화된 이미지.
"""
img = image.copy() # 원본 이미지를 복사하여 수정 작업을 진행
# 각 경계 상자와 클래스 ID를 순회하면서 시각화합니다.
for bbox, category_id in zip(bboxes, category_ids):
class_name = CLASS_ID_TO_NAME[category_id] # 클래스 ID를 클래스 이름으로 변환
img = visualize_bbox(img, bbox, class_name) # 경계 상자와 클래스 이름을 이미지에 그리기
return img
|
<<<<< 다시 쥬피터에서 작업 >>>>
◼ 이미지와 바운딩 박스 표기
from util import CLASS_NAME_TO_ID, CLASS_ID_TO_NAME, visualize
# 이미지와 경계 상자를 시각화하여 결과를 반환합니다.
canvas = visualize(image, unnorm_bboxes, class_ids)
plt.figure(figsize=(6, 6))
plt.imshow(canvas)
plt.show()
|
◼ image_files에 있는 모든 영상의 객체를 바운딩 박스로 표기 (1)
# interact를 사용하여 image_files에 있는 모든 영상의 객체를 바운딩 박스로 표기
@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)
plt.figure(figsize=(6, 6))
plt.imshow(canvas)
plt.show()
|
인덱스별로 사진을 불러와 객체를 탐지하고 어떤 차인지 분석해서 바운딩박스로 표시 |
- 주석
# interact를 사용하여 image_files에 있는 모든 영상의 객체를 바운딩 박스로 표기
@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)
# 이미지를 읽어와서 RGB 형식으로 변환합니다.
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 이미지 파일 이름에서 ID를 추출합니다.
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
# 클래스 이름을 클래스 ID로 변환합니다.
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]] # XMax, XMin 순서 변경
unnorm_bboxes[:, 2:4] -= unnorm_bboxes[:, 0:2] # 너비와 높이 계산
unnorm_bboxes[:, 0:2] += (unnorm_bboxes[:, 2:4] / 2) # 중심 좌표 계산
unnorm_bboxes[:, [0, 2]] *= img_W # X 좌표를 이미지 너비에 맞게 스케일 조정
unnorm_bboxes[:, [1, 3]] *= img_H # Y 좌표를 이미지 높이에 맞게 스케일 조정
# 경계 상자와 클래스 ID를 사용하여 이미지를 시각화합니다.
canvas = visualize(image, unnorm_bboxes, class_ids)
# 시각화된 이미지를 출력합니다.
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
|
- 주석
class Detection_dataset():
def __init__(self, data_dir, phase, transformer=None):
"""
데이터셋 초기화
:param data_dir: 데이터가 저장된 디렉토리 경로
:param phase: 데이터의 phase (train, val, test 등)
:param transformer: (선택적) 이미지 변환기 (데이터 증강 등)
"""
self.data_dir = data_dir
self.phase = phase
# CSV 파일에서 데이터 프레임을 읽어옴
self.data_df = pd.read_csv(os.path.join(self.data_dir, 'df.csv'))
# 지정된 phase 디렉토리에서 jpg 파일 목록을 가져옴
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: 이미지 파일의 개수
"""
return len(self.image_files)
def __getitem__(self, index):
"""
주어진 인덱스에 해당하는 이미지와 레이블을 반환
:param index: 요청된 데이터의 인덱스
:return: 이미지, 타겟 딕셔너리 (바운딩 박스와 레이블), 파일 이름
"""
filename, image = self.get_image(index)
bboxes, class_ids = self.get_label(filename)
img_H, img_W, _ = image.shape
# 이미지가 변환기(transformer)를 사용할 경우 적용
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):
"""
주어진 인덱스에 해당하는 이미지를 로드
:param index: 요청된 이미지의 인덱스
:return: 파일 이름, 이미지 배열
"""
filename = self.image_files[index]
image_path = os.path.join(self.data_dir, self.phase, filename)
# 이미지 읽기 및 RGB로 변환
image = cv2.imread(image_path)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
return filename, image
def get_label(self, filename):
"""
주어진 파일 이름에 해당하는 레이블을 가져옴
:param filename: 이미지 파일 이름
:return: 바운딩 박스 배열, 클래스 ID 배열
"""
# 파일 이름에서 image_id 추출
image_id = filename.split('.')[0]
# 데이터 프레임에서 해당 image_id에 대한 메타데이터 필터링
meta_data = self.data_df[self.data_df['ImageID'] == image_id]
# 클래스 이름을 클래스 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]] # YMin과 YMax 스왑
return bboxes, class_ids
|
- 데이터 확인
◼ dataset 객체에 index를 넣어 바운딩 박스를 표현하는 interact
@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)
bboxes[:, 0:2] = (boxes[:, 0:2] + boxes[:, 2:4]) / 2
bboxes[:, 2:4] = boxes[:, 2:4] - boxes[:, 0:2]
canvas = visualize(image, unnorm_bboxes, class_ids)
plt.figure(figsize=(6, 6))
plt.imshow(canvas)
plt.show()
|
◼ transformer 만들기 _ Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
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])
])
|
|
◼ Detection_dataset을 이용하여 변환된 데이터셋을 생성하기
data_dir='./Dataset/'
transformed_dataset = Detection_dataset(data_dir=data_dir, phase='train', transformer=transformer)
|
|
◼ transformed_dataset에서 20번 인덱스에 해당하는 데이터를 가져와
image, target, filename 변수에 할당하고, image 텐서의 형태(shape)를 출력
image, target, filename = transformed_dataset[20]
image.shape
|
torch.Size([3, 448, 448]) |
◼ 이미지 또는 그리드 형태의 데이터를 시각화하기 위해 make_grid 함수를 사용하고,
이를 NumPy 배열로 변환하여 그 형태를 출력
#make_grid() : 이미지 또는 그리드 형태의 데이터를 시각화하기 위해 사용하는 함수
np_image = make_grid(image, normalize=True).cpu().permute(1, 2, 0).numpy()
np_image.shape
|
(448, 448, 3) |
◼ collate_fn() : 데이터 로더가 배치 단위로 데이터를 나눌 때 어떻게 처리할지를 정의하는 함수
# collate_fn: 파이토치에서 데이터 로더에서 사용하는 함수. 배치 단위로 데이터를 나눌 때 사용
# 데이터 로더가 배치로 나눌 때 어떻게 처리할 지 정의함
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
|
|
◼ Detection_dataset 데이터셋을 배치 단위로 로드하는 방법
BATCH_SIZE = 8
trainset = Detection_dataset(data_dir, phase='train', transformer = transformer)
tranloader = DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn)
|
|
◼ DataLoader에서 데이터를 배치 단위로 가져와 각 배치의 이미지, 타겟, 파일 이름을 처리하는 예제
for index, batch in enumerate(tranloader):
images = batch[0]
targets = batch[1]
filenames = batch[2]
if index == 0:
break
print(filenames)
|
['54bac9dc5019ba10.jpg', 'a2241c80b7d0f3aa.jpg', '366078debdbff8c9.jpg', '2f41a05de15ac60c.jpg', '91258b32cbde58d5.jpg', 'ce0733da0a1cc673.jpg', '1931566d310ee29c.jpg', '4288d6901bd9841a.jpg'] --------------------------------------------------------------------------------------------------------- 8개 확인 |
◼ build_dataloader ( ) : 데이터 로더를 생성하여 학습(train)과 검증(validation) 데이터로 나누는 작업을 수행
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
|
|
◼ build_dataloader ( )를 호출하여
학습(train)과 검증(val) 데이터로더를 생성한 후, 각 데이터로더에서 첫 번째 배치의 파일 이름을 출력하는 예제
data_dir='./Dataset/'
dloaders = build_dataloader(data_dir)
for phase in ['train','val']:
for index, batch in enumerate(dloaders[phase]):
images = batch[0]
targets = batch[1]
filenames = batch[2]
print(filenames)
if index == 0:
break
|
|
2. Two-Stage모델
- 탐색 영역을 찾는 Region Proposal과 해당 영역을 분류하는 Detection 두 가지 과정이 순차적으로 수행되는 방법
1. 위치를 찾는 문제(Localization)
* 하나의 이미지 안에서 물체가 있을 법한 위치를 찾아 나열하는 과정에 대한 정보를 제안
2. 분류 문제(Classification)
* 각각의 위치에 대해 class를 분류
* 이미지 내의 사물에 존재하는 bounding box를 예측하는 regression을 사용
1. R-CNN |
|
2. Fast R-CNN |
|
3. Faster R-CNN |
|
◼ Faster R-CNN 모델 객체 생성
model = models.detection.fasterrcnn_resnet50_fpn(pretrained = True)
model
|
|
◼ Faster R-CNN 모델을 학습 모드로 설정
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
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
|
|
◼ build_model 함수를 사용하여 NUM_CLASSES에 맞게 조정된 Faster R-CNN 모델을 생성하기
NUM_CLASSES = 2
model = build_model(num_classes=NUM_CLASSES)
model
|
|
◼ Faster R-CNN 모델을 학습 모드로 설정하고,
데이터 로더에서 배치를 가져와 모델에 입력하여 손실(loss)을 계산
phase = 'train'
model.train()
for index, batch in enumerate(dloaders[phase]):
images = batch[0]
targets = batch[1]
filenames = batch[2]
image = list(image for image in images)
targets = [{k: v for k, v in t.items()} for t in targets]
loss = model(images, targets)
if index == 0:
break
loss
# loss_classifier: 객체 탐지 모델에서 분류기 손실 함수. 객체의 종류를 예측하는 사용
# loss_box_reg: 객체 위치를 예측하는 박스 회귀 모델의 손실 함수. 예측된 경계 상자의 위치와 실제 객체의 위치 사이의 차이를 줄이기 위해 사용
# loss_objectness: 객체 탐지 모델에서 사용되는 객체 존재 여부를 예측하는데 사용되는 손실함수. 각 경계 상자에 대해 해당 상자에 객체가 존재하는지 여부를 예측하고 실제와 비교하여 학습
# loss_rpn_box_reg: RPN의 박스 회귀 손실 함수. 객체 후보 영역을 제안하고 이 후보 영역의 경계 상자를 조정하기 위해 사용
|
{'loss_classifier': tensor(0.6151, grad_fn=<NllLossBackward0>), 'loss_box_reg': tensor(0.0834, grad_fn=<DivBackward0>), 'loss_objectness': tensor(0.2546, grad_fn=<BinaryCrossEntropyWithLogitsBackward0>), 'loss_rpn_box_reg': tensor(0.0511, grad_fn=<DivBackward0>)} --------------------------------------------------------------------------------------------
|
◼ 학습하기
from collections import defaultdict
def train_one_epoch(dataloaders, model, optimizer, device):
train_loss = defaultdict(float)
val_loss = defaultdict(float)
model.train()
for phase in ['train', 'val']:
for index, batch in enumerate(dataloaders[phase]):
images = batch[0]
targets = batch[1]
filenames = batch[2]
images = list(image for image in images)
targets = [{k: v for k, v in t.items()} for t in targets]
with torch.set_grad_enabled(phase == 'train'):
loss = model(images, targets)
total_loss = sum(each_loss for each_loss in loss.values())
if phase == 'train':
optimizer.zero_grad()
total_loss.backward()
optimzer.step()
if (index > 0) and (index % VERBOSE_FREQ == 0):
text = f"{index}/{len(dataloaders[phase])} - "
for k, v in loss.items():
text += f'{k}: {v.item():.4f} '
print(text)
for k, v in loss.items():
train_loss[k] += v.item()
train_loss['total_loss'] += total_loss.item()
else:
for k, v in loss.items():
val_loss[k] += v.item()
val_loss['total_loss'] += total_loss.item()
for k in train_loss.keys():
train_loss[k] /= len(dataloaders['train'])
val_loss[k] /= len(dataloaders['val'])
return train_loss, val_loss
|
◼ train_one_epoch() : 모델을 한 에폭 동안 학습하고 평가하는 함수
학습과 검증 단계 모두에서 손실(loss)을 계산하고, 학습 과정에서의 손실을 출력하며,
최종적으로 평균 손실 값을 반환
from collections import defaultdict
def train_one_epoch(dataloaders, model, optimizer, device):
train_loss = defaultdict(float)
val_loss = defaultdict(float)
model.train()
for phase in ['train', 'val']:
for index, batch in enumerate(dataloaders[phase]):
images = batch[0]
targets = batch[1]
filenames = batch[2]
images = list(image for image in images)
targets = [{k: v for k, v in t.items()} for t in targets]
with torch.set_grad_enabled(phase == 'train'):
loss = model(images, targets)
total_loss = sum(each_loss for each_loss in loss.values())
if phase == 'train':
optimizer.zero_grad()
total_loss.backward()
optimzer.step()
if (index > 0) and (index % VERBOSE_FREQ == 0):
text = f"{index}/{len(dataloaders[phase])} - "
for k, v in loss.items():
text += f'{k}: {v.item():.4f} '
print(text)
for k, v in loss.items():
train_loss[k] += v.item()
train_loss['total_loss'] += total_loss.item()
else:
for k, v in loss.items():
val_loss[k] += v.item()
val_loss['total_loss'] += total_loss.item()
for k in train_loss.keys():
train_loss[k] /= len(dataloaders['train'])
val_loss[k] /= len(dataloaders['val'])
return train_loss, val_loss
|
|
- 주석
from collections import defaultdict
def train_one_epoch(dataloaders, model, optimizer, device):
# 학습 손실과 검증 손실을 저장할 딕셔너리 초기화
train_loss = defaultdict(float)
val_loss = defaultdict(float)
# 모델을 학습 모드로 설정
model.train()
# 'train'과 'val' 단계에서 반복
for phase in ['train', 'val']:
# 각 배치에 대해 데이터 로더에서 반복
for index, batch in enumerate(dataloaders[phase]):
images = batch[0] # 현재 배치의 이미지 텐서
targets = batch[1] # 현재 배치의 타겟 데이터
filenames = batch[2] # 현재 배치의 파일 이름 (일반적으로 디버깅에 사용)
# 이미지를 리스트 형태로 변환 (Faster R-CNN 모델 요구 사항)
images = list(image for image in images)
# 타겟 데이터를 사전 형태로 변환 (모델 요구 사항)
targets = [{k: v for k, v in t.items()} for t in targets]
# 학습 단계에서는 기울기 계산을 활성화, 검증 단계에서는 비활성화
with torch.set_grad_enabled(phase == 'train'):
loss = model(images, targets) # 모델에 입력하여 손실 계산
# 손실 값을 합산하여 총 손실 계산
total_loss = sum(each_loss for each_loss in loss.values())
# 학습 단계의 경우
if phase == 'train':
optimizer.zero_grad() # 기울기 초기화
total_loss.backward() # 총 손실에 대한 기울기 계산
optimizer.step() # 기울기를 사용하여 모델의 매개변수 업데이트
# 일정 간격으로 학습 상태 출력
if (index > 0) and (index % VERBOSE_FREQ == 0):
text = f"{index}/{len(dataloaders[phase])} - "
for k, v in loss.items():
text += f'{k}: {v.item():.4f} '
print(text)
# 학습 손실 값을 누적
for k, v in loss.items():
train_loss[k] += v.item()
train_loss['total_loss'] += total_loss.item()
# 검증 단계의 경우
else:
# 검증 손실 값을 누적
for k, v in loss.items():
val_loss[k] += v.item()
val_loss['total_loss'] += total_loss.item()
# 손실 값의 평균을 계산하여 반환
for k in train_loss.keys():
train_loss[k] /= len(dataloaders['train'])
val_loss[k] /= len(dataloaders['val'])
return train_loss, val_loss
|
◼ 객체 감지 모델을 준비하고 학습을 위한 설정
data_dir = './DataSet/' is_cuda = False
NUM_CLASSES = 2
IMAGE_SIZE = 448
BATCH_SIZE = 8
VERBOSE_FREQ = 100
DEVICE = torch.device('cuda' if torch.cuda.is_available and is_cuda else 'cpu')
dataloaders = build_dataloader(data_dir=data_dir, batch_size=BATCH_SIZE, image_size=IMAGE_SIZE)
model = build_model(num_classes=NUM_CLASSES)
model = model.to(DEVICE)
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
|
|
◼ 1번만 돌리고 확인
num_epochs = 1
train_losses = []
val_losses = []
for epoch in range(num_epochs):
train_loss, val_loss = train_one_epoch(dataloaders, model, optimizer, DEVICE)
train_losses.append(train_loss)
val_losses.append(val_loss)
print(f"epoch:{epoch+1}/{num_epochs} - Train Loss:{train_loss['total_loss']:.4f}, Val Loss:{val_loss['total_loss']:.4f}")
|
◼ 학습된 모델 불러오기
def load_model(ckpt_path, num_classes, device):
checkpoint = torch.load(ckpt_path, map_location=device)
model = build_model(num_classes=num_classes)
model.load_state_dict(checkpoint)
model = model.to(device)
model.eval()
return model
model = load_model(ckpt_path='./trained_model/model_40.pth', num_classes=NUM_CLASSES, device=DEVICE)
model
|
3. One-Stage 모델
- Region Proposal과 detection이 한 번에 수행
1 . YOLO(You only Look Once) |
2015년에 제안된 개체 검출 모델로 이미지 전체를 단일 그리드로 나누고, 각 그리드 셀마다 여러개의 바운딩 박스와 클래스를 예측하는 방식 |
4. Confidence Threshold
* 객체 탐지와 같은 작업에서 사용되는 개념
* 객체 탐지 모델은 이미지에서 객체의 위치를 찾아내는 작업을 수행
* 모델은 주어진 이미지 내에서 다양한 위치에 개해 객체가 존재하는지 예측하고 각 객체에 대한 바운딩 박스와 해당 객체에 대한 신뢰도(Confidence Score)를 출력
1. Confidence Threshold :신뢰도를 조절하는 기준 값
예) Confidence Threshold를 0.6로 설정하면 모델은 신뢰도가 0.6이상인 객체만을 선택하게 됨
2. Confidence Threshold를 적절하게 설정해야 객체 탐지의 정확도를 높을 수 있음
너무 낮은 Confidence Threshold를 설정하면 신뢰성이 낮은 결과를 포함할 수 있고,
너무 높은 Confidence Threshld를 설정하면 싱뢰성이 높은 객체조차 누락
5. Non-Maximum Suppression(NMS)
* 중복된 결과를 제거하여 겹치지 않는 객체를 식별하는데 사용
* NMS가 작동하는 순서
1. 객체 탐지 모델 실행
2. 이미지를 입력받아 마운딩 박스와 신로도를 출력)
3. 바운딩 박스 필터링(겹치는 바운딩 박스들 중에서 가장 확실한 바운딩 박스만 남기고 나머지 겹치는 바운딩 박스를 제거 -> IoU 지표를 사용)
6. IoU(Intersection over Union
* 객체 탐지나 세그멘테이션과 같은 컴퓨터 비전에서 모델이 예츧한 결과와 실제 라벨 사이의 정확도를 측정하는 지표
* 바운딩 박스나 세그멘테이션 마스트가 얼마나 겹치는지를 측정하여, 예측 결과의 정확성을 평가하는데 사용
* 0과 1상이의 값으로 1에 가까울수록 예측 결과가 정확하고 겹치는 영역이 많다는것을 의미
* 계산 방법
1. 영역 A과 B의 겹치는 영역 계산 -> 공통 부분을 계산 ->교집합 계산(얼마나 겹쳐져 있는지)
2. 합집합 계산(두 영역의 전체크기)
3. 교집합을 합집합으로 나눔 -> 교집합 / 합집합(IoU 계산)
◼ 예측 설정하고 보기
from torchvision.ops import nms
def postprocess(prediction, conf_thres=0.3, IoU_threshold=0.3):
pred_box = prediction['boxes'].cpu().detach().numpy()
pred_label = prediction['labels'].cpu().detach().numpy()
pred_conf = prediction['scores'].cpu().detach().numpy()
valid_index = pred_conf > conf_thres
pred_box = pred_box[valid_index]
pred_label = pred_label[valid_index]
pred_conf = pred_conf[valid_index]
valid_index = nms(torch.tensor(pred_box.astype(np.float32)), torch.tensor(pred_conf), IoU_threshold)
pred_box = pred_box[valid_index.numpy()]
pred_label = pred_label[valid_index.numpy()]
pred_conf = pred_conf[valid_index.numpy()]
return np.concatenate((pred_box, pred_conf[:, np.newaxis], pred_label[:, np.newaxis]), axis=1)
pred_images = []
pred_labels = []
for index, (images, _, filenames) in enumerate(dataloaders['val']):
images = list(image.to(DEVICE) for image in images)
filename = filenames[0]
image = make_grid(images[0].cpu().detach(), normalize=True).permute(1, 2, 0).numpy()
image = (image * 255).astype(np.uint8)
with torch.no_grad():
prediction = model(images)
prediction = postprocess(prediction[0])
prediction[:, 2].clip(min=0, max=image.shape[1])
prediction[:, 3].clip(min=0, max=image.shape[0])
xc = (prediction[:, 0] + prediction[:, 2])/2
yc = (prediction[:, 1] + prediction[:, 3])/2
w = prediction[:, 2] - prediction[:, 0]
h = prediction[:, 3] - prediction[:, 1]
cls_id = prediction[:, 5]
prediction_yolo = np.stack([xc, yc, w, h, cls_id], axis=1)
pred_images.append(image)
pred_labels.append(prediction_yolo)
if index == 2:
break
@interact(index=(0, len(pred_images)-1))
def show_result(index=0):
result = visualize(pred_images[index], pred_labels[index][:, 0:4], pred_labels[index][:, 4])
plt.figure(figsize=(6, 6))
plt.imshow(result)
plt.show()
|
|
◼ 동영상으로 예측하기
video_path = './sample_video.mp4'
@torch.no_grad()
def model_predict(image, model):
tensor_image = transformer(image)
tensor_image = tensor_image.to(DEVICE)
prediction = model([tensor_image])
return prediction
video = cv2.VideoCapture(video_path)
while(video.isOpened()):
ret, frame = video.read()
if ret:
ori_h, ori_w = frame.shape[:2]
image = cv2.resize(frame, dsize=(IMAGE_SIZE, IMAGE_SIZE))
prediction = model_predict(image, model)
prediction = postprocess(prediction[0])
prediction[:, [0, 2]] *= (ori_w / IMAGE_SIZE)
prediction[:, [1, 3]] *= (ori_h / IMAGE_SIZE)
prediction[:, 2].clip(min=0, max=image.shape[1])
prediction[:, 3].clip(min=0, max=image.shape[0])
xc = (prediction[:, 0] + prediction[:, 2])/2
yc = (prediction[:, 1] + prediction[:, 3])/2
w = prediction[:, 2] - prediction[:, 0]
h = prediction[:, 3] - prediction[:, 1]
cls_id = prediction[:, 5]
prediction_yolo = np.stack([xc, yc, w, h, cls_id], axis=1)
canvas = visualize(frame, prediction_yolo[:, 0:4], prediction_yolo[:, 4])
cv2.imshow('camera', canvas)
key = cv2.waitKey(1)
if key == 27:
break
if key == ord('s'):
cv2.waitKey()
video.release()
|
'AI > 컴퓨터 비전' 카테고리의 다른 글
14. YOLO | 객체탐지 (0) | 2024.08.06 |
---|---|
@. 과제 (0) | 2024.08.05 |
12. VGG19 | 분류 (1) | 2024.07.24 |
11. OCR (1) | 2024.07.23 |
10. 레이블링 (2) | 2024.07.23 |