본문 바로가기
AI/컴퓨터 비전

13. Faster R-CNN | 객체 탐지

by 사라리24 2024. 7. 25.

 

1. 객체 탐지 (Object Detection)


- 컴퓨터 비전과 이미지 처리와 관련된 컴퓨터 비전 기술로써, 디지털 이미지와 비디오로 특정한 계열의 시맨틱 객체 인스턴스를 감지하는 일
- 얼굴 검출, 보행자 검출 등이 포함

 

  • 데이터확인
더보기

 

 

 

🟠.  컴퓨터 비전의 Task 비교

  • Image Classification: 이미지에 있는 객체 범주 목록 생성
  • Single-Object Localization: 이미지에 있는 개체 범주의 한 인스턴스의 위치와 배율을 나타내는 Bounding Box를 생성
  • Object Detection: 각 개체 범주의 모든 인스턴스의 위치와 배율을 나타내는 경계 상자와 함께 이미지에 이쓴 개체 목록을 생성
  • 참고 사이트 : https://oniss.tistory.com/39

 

 

  모듈 설치


       
      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, XMax, YMax)
  • 출력 좌표: (XMin, YMin, W, H)

  • unnorm_bboxes[:, 2:4]은 unnorm_bboxes 배열의 모든 행에 대해 3번째와 4번째 열(XMax와 YMax)을 선택합니다.
  • unnorm_bboxes[:, 0:2]은 unnorm_bboxes 배열의 모든 행에 대해 1번째와 2번째 열(XMin과 YMin)을 선택합니다.
  • unnorm_bboxes[:, 2:4] -= unnorm_bboxes[:, 0:2]는 XMax에서 XMin을 빼서 폭(W)을 계산하고, YMax에서 YMin을 빼서 높이(H)를 계산합니다. 이 연산은 unnorm_bboxes 배열을 직접 수정합니다.

 

 

경계 상자의 좌표를 변환


       
        # 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)로 변환

  • unnorm_bboxes[:, 0:2]:
    • unnorm_bboxes 배열의 모든 행에 대해 첫 번째와 두 번째 열 (XMin과 YMin)을 선택합니다.
  • unnorm_bboxes[:, 2:4]:
    • unnorm_bboxes 배열의 모든 행에 대해 세 번째와 네 번째 열 (W와 H, 즉 폭과 높이)을 선택합니다.
  • unnorm_bboxes[:, 2:4] / 2:
    • W와 H 값을 2로 나누어 각각의 중심을 계산합니다 (폭의 절반과 높이의 절반).
  • unnorm_bboxes[:, 0:2] += (unnorm_bboxes[:, 2:4] / 2):
    • 각 경계 상자의 (XMin, YMin) 좌표에 W/2와 H/2를 더합니다. 이렇게 하면 좌표가 왼쪽 상단 모서리에서 경계 상자의 중심 (CX, CY)으로 변환됩니다.

 



  이미지의 높이와 너비를 추출


      

    img_H, img_W, _ = image.shape
    img_H, img_W


(170, 256)

---------------------------------------------------------

  • image.shape:
    • image는 일반적으로 이미지 데이터를 저장하는 NumPy 배열입니다.
    • shape 속성은 배열의 차원(높이, 너비, 채널 수)을 반환합니다.
    • 예를 들어, 이미지가 3차원 배열로 저장되어 있다면 image.shape는 (높이, 너비, 채널 수) 형식의 튜플을 반환합니다.
  • img_H, img_W, _ = image.shape:
    • image.shape에서 반환된 튜플 값을 각각 img_H, img_W, _ 변수에 할당합니다.
    • img_H는 이미지의 높이(height)를 저장합니다.
    • img_W는 이미지의 너비(width)를 저장합니다.
    • _는 이미지의 채널 수(예: RGB 이미지의 경우 3)를 저장합니다. 여기서는 채널 수가 필요하지 않기 때문에 _를 사용하여 무시합니다.

 

 

상자의 좌표를 이미지 크기에 맞게 조정하는 작업


       
        # 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 ]])

---------------------------------------------------------------------------

  • unnorm_bboxes[:, [0, 2]] *= img_W:
    • unnorm_bboxes 배열의 모든 행에서 첫 번째 열(XMin)과 세 번째 열(W)을 선택합니다.
    • 이 선택된 열들은 이미지 너비(img_W)에 곱해집니다.
    • 이 연산의 목적은 XMin 좌표와 경계 상자의 폭(W)을 이미지 너비에 맞게 조정하는 것입니다. 즉, 정규화된 좌표를 픽셀 단위로 변환합니다.
  • unnorm_bboxes[:, [1, 3]] *= img_H:
    • unnorm_bboxes 배열의 모든 행에서 두 번째 열(YMin)과 네 번째 열(H)을 선택합니다.
    • 이 선택된 열들은 이미지 높이(img_H)에 곱해집니다.
    • 이 연산의 목적은 YMin 좌표와 경계 상자의 높이(H)를 이미지 높이에 맞게 조정하는 것입니다. 즉, 정규화된 좌표를 픽셀 단위로 변환합니다.
  • unnorm_bboxes:
    • 변환된 unnorm_bboxes 배열을 출력하거나 반환하는 코드입니다. 이 배열은 이제 픽셀 단위의 좌표와 크기를 가지며, 이미지의 실제 크기와 맞아떨어집니다.

 

 

 

<<<<<  잠깐 파이참에서 작업 >>>>>

이미지를 시각화하여 객체 감지 결과를 표시하는 두 가지 함수를 정의


      
        # 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(imagebboxclass_namecolor = 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(imagebboxescategory_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(imagebboxclass_namecolor = 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(imagebboxescategory_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__(selfdata_dirphasetransformer=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__(selfindex):
              """
              주어진 인덱스에 해당하는 이미지와 레이블을 반환
              :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(selfindex):
              """
              주어진 인덱스에 해당하는 이미지를 로드
              :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(selffilename):
              """
              주어진 파일 이름에 해당하는 레이블을 가져옴
              :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])
        ])


  1. IMAGE_SIZE = 448:
    • 이미지의 크기를 정의합니다. IMAGE_SIZE 변수는 448로 설정되어 있습니다. 이는 이미지의 가로 및 세로 크기를 448 픽셀로 조정하기 위함입니다.
  2. transforms.Compose([...]):
    • 여러 전처리 작업을 순차적으로 적용하기 위해 Compose를 사용합니다. Compose는 리스트 형태로 여러 변환(transform)을 받아서 차례대로 적용합니다.
  3. transforms.ToTensor():
    • 이미지를 PyTorch 텐서(tensor)로 변환합니다. 이미지 데이터는 일반적으로 PIL 이미지 또는 NumPy 배열로 불러오게 되는데, 이를 PyTorch에서 처리할 수 있는 텐서로 변환합니다.
    • 이미지를 [0, 255] 범위에서 [0.0, 1.0] 범위로 스케일링합니다.
  4. transforms.Resize(size = (IMAGE_SIZE, IMAGE_SIZE)):
    • 이미지를 지정된 크기인 (IMAGE_SIZE, IMAGE_SIZE), 즉 (448, 448)으로 리사이즈합니다. 이미지의 가로와 세로 크기를 448 픽셀로 조정하여 일관된 크기로 만듭니다.
  5. transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]):
    • 이미지를 정규화합니다. 각 채널(RGB)의 평균과 표준편차를 사용하여 정규화합니다.
      • [0.485, 0.456, 0.406]는 각 채널(R, G, B)의 평균값입니다.
      • [0.229, 0.224, 0.225]는 각 채널의 표준편차입니다.
    • 정규화 공식은 normalized = (image - mean) / std입니다. 이는 이미지의 픽셀 값이 평균 0과 표준편차 1을 갖도록 조정합니다. 일반적으로 사전 학습된 모델에 이미지를 입력할 때 사용됩니다.
이 전처리 과정을 통해, 이미지를 딥러닝 모델에 입력하기에 적합한 형식으로 변환할 수 있습니다. transformer 객체를 사용하여 이미지 데이터를 로드할 때 이 전처리 과정을 적용할 수 있습니다.

 

Detection_dataset을 이용하여 변환된 데이터셋을 생성하기


       
        data_dir='./Dataset/'
        transformed_dataset = Detection_dataset(data_dir=data_dir, phase='train', transformer=transformer)


  1. data_dir = './Dataset/':
    • data_dir 변수는 데이터셋이 저장된 디렉토리의 경로를 지정합니다. 이 경우, 데이터셋은 현재 작업 디렉토리 내의 ./Dataset/ 폴더에 저장되어 있습니다.
  2. transformed_dataset = Detection_dataset(data_dir=data_dir, phase='train', transformer=transformer):
    • Detection_dataset 클래스의 인스턴스를 생성합니다. 이는 transformed_dataset이라는 변수에 저장됩니다.
    • Detection_dataset 클래스는 사용자 정의 데이터셋 클래스로, 데이터를 로드하고 전처리하는 방법을 정의합니다.
    • 생성자 인자로 data_dir, phase, transformer를 전달합니다.
세부적으로 각 인자가 무엇을 의미하는지 살펴보겠습니다:
  • data_dir=data_dir: 데이터셋이 위치한 디렉토리 경로를 data_dir 인자로 전달합니다. 여기서는 './Dataset/' 경로가 전달됩니다.
  • phase='train': 데이터셋의 사용 목적을 지정하는 phase 인자를 전달합니다. 여기서는 'train'으로 설정되어 있으므로, 학습용 데이터셋임을 나타냅니다. 다른 예로 'test' 또는 'validation' 단계에 사용될 수도 있습니다.
  • transformer=transformer: 데이터셋을 로드할 때 적용할 전처리 변환을 지정하는 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

 
  • 리스트를 텐서로 변환:
    • image_list의 이미지들을 torch.stack을 사용하여 하나의 배치 텐서로 변환합니다.
    • target_list의 타겟들을 torch.tensor를 사용하여 하나의 배치 텐서로 변환합니다.
  • 파일 이름 리스트:
    • 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)


  • BATCH_SIZE = 8:
    • 배치 크기를 설정합니다. 이 값은 한 번에 데이터 로더가 반환할 데이터 샘플의 수를 정의합니다. 여기서는 8로 설정되어 있어, 한 배치에 8개의 샘플이 포함됩니다.
  • trainset = Detection_dataset(data_dir, phase='train', transformer=transformer):
    • Detection_dataset 클래스의 인스턴스를 생성합니다.
    • data_dir: 데이터셋이 저장된 디렉토리 경로를 지정합니다.
    • phase='train': 데이터셋의 사용 목적을 지정합니다. 'train'은 학습 데이터셋을 의미합니다.
    • transformer=transformer: 데이터셋에 적용할 전처리 변환을 지정합니다. 여기서는 이미지 전처리 변환(transformer)을 전달합니다.
  • tranloader = DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_fn):
    • DataLoader를 사용하여 trainset 데이터셋을 배치 단위로 로드합니다.
    • batch_size=BATCH_SIZE: 배치 크기를 8로 설정합니다. 즉, 각 배치에는 8개의 샘플이 포함됩니다.
    • shuffle=True: 데이터셋의 샘플을 매 에포크마다 무작위로 섞습니다. 이는 모델 학습을 더 효과적으로 만들기 위해 데이터를 랜덤하게 섞어 학습합니다.
    • collate_fn=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

 
  • transformer = transforms.Compose([...]):
    • 데이터 전처리를 정의합니다. transforms.Compose는 여러 이미지 변환을 연속적으로 적용합니다.
      • transforms.ToTensor(): 이미지를 PyTorch 텐서로 변환합니다.
      • transforms.Resize(size=(image_size, image_size)): 이미지를 지정된 크기로 리사이즈합니다.
      • transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]): 이미지 픽셀 값을 정규화합니다. 이는 일반적으로 ImageNet 데이터셋에서 학습된 모델에 대한 표준 정규화 값입니다.
  • dataloaders = {}:
    • 데이터 로더를 저장할 빈 딕셔너리를 생성합니다. 이 딕셔너리는 'train'과 'val'이라는 키를 사용하여 데이터 로더를 저장합니다.
  • train_dataset = Detection_dataset(data_dir=data_dir, phase='train', transformer=transformer):
    • 학습 데이터셋을 생성합니다. Detection_dataset 클래스를 사용하여 학습 데이터셋을 초기화합니다.
      • data_dir은 데이터셋의 경로를 지정합니다.
      • phase='train'은 학습 데이터셋을 의미합니다.
      • transformer는 이미지에 적용할 변환을 지정합니다.
  • dataloaders['train'] = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn):
    • 학습 데이터셋에 대한 DataLoader를 생성합니다.
      • batch_size=batch_size는 배치 크기를 설정합니다.
      • shuffle=True는 데이터를 매 에포크마다 무작위로 섞습니다.
      • collate_fn=collate_fn은 배치 단위로 데이터를 처리하는 사용자 정의 함수를 지정합니다.
  • val_dataset = Detection_dataset(data_dir=data_dir, phase='val', transformer=transformer):
    • 검증 데이터셋을 생성합니다. Detection_dataset 클래스를 사용하여 검증 데이터셋을 초기화합니다.
      • phase='val'은 검증 데이터셋을 의미합니다.
  • dataloaders['val'] = DataLoader(val_dataset, batch_size=1, shuffle=False, collate_fn=collate_fn):
    • 검증 데이터셋에 대한 DataLoader를 생성합니다.
      • batch_size=1은 각 배치에 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

 
  • dloaders = build_dataloader(data_dir):
    • build_dataloader 함수를 호출하여 학습과 검증 데이터로더를 생성합니다.
    • dloaders는 학습('train')과 검증('val') 데이터로더를 포함하는 딕셔너리입니다.
  • for phase in ['train', 'val']::
    • 데이터 로더의 두 가지 단계인 'train'과 'val'에 대해 반복합니다. 이 반복문을 통해 학습 데이터와 검증 데이터를 모두 처리합니다.
  • for index, batch in enumerate(dloaders[phase])::
    • 현재 단계(phase)에 대한 데이터로더에서 배치를 가져옵니다. enumerate를 사용하여 배치의 인덱스와 배치 데이터를 함께 가져옵니다.
    • dloaders[phase]는 현재 단계의 데이터로더를 참조합니다.
  • images = batch[0]: 현재 배치의 첫 번째 요소를 images 변수에 할당합니다. 이는 이미지 텐서입니다.
  • targets = batch[1]: 현재 배치의 두 번째 요소를 targets 변수에 할당합니다. 이는 타겟(레이블) 텐서입니다.
  • filenames = batch[2]: 현재 배치의 세 번째 요소를 filenames 변수에 할당합니다. 이는 파일 이름 리스트입니다.
  • print(filenames): 현재 배치의 파일 이름 리스트를 출력합니다.
  • if index == 0: 첫 번째 배치만 처리하도록 조건을 설정합니다. 첫 번째 배치가 처리된 후, 반복문을 종료합니다.
  • break: 첫 번째 배치 처리 후 반복문을 종료합니다. 이로 인해 각 단계(학습 및 검증)에서 첫 번째 배치만 출력됩니다.

 

 

2. Two-Stage모델


- 탐색 영역을 찾는 Region Proposal과 해당 영역을 분류하는 Detection 두 가지 과정이 순차적으로 수행되는 방법

1. 위치를 찾는 문제(Localization)
  * 하나의 이미지 안에서 물체가 있을 법한 위치를 찾아 나열하는 과정에 대한 정보를 제안


2. 분류 문제(Classification)
  * 각각의 위치에 대해 class를 분류
  * 이미지 내의 사물에 존재하는 bounding box를 예측하는 regression을 사용

 

 

 

1. R-CNN

  • Selective Search를 이용해서 2000개의 ROI를 추출
  • 각 ROI에 대하여 동일한 크기의 입력 이미지로 변경
  • 이미지를 CNN에 넣어서 백터 이미지를 추출
  • 해당 feature를 SVM에 넣어 class 분류 결과를 얻음
    • 입력 이미지에 대해 CPU 기반의 Selective Search를 진행하므로 많은 시간이 소요 

2. Fast R-CNN 

  • 이미지에 물체가 있을 법한 위치를 찾아 Featuer Map을 생성
    • 입력 이미지에 대해 CPU 기반의 Selective Search를 진행하므로 많은 시간이 소요 

3. Faster R-CNN

  • 속도가 느린 Region Proposal 작업을 GPU에서 수행함
  • RPN(Region Proposal Networks) 적용 
  • 슬라이딩 윈도우를 거쳐 각 위치에 대해 Regression과 classification을 수행 

 

 

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
 
      • model = models.detection.fasterrcnn_resnet50_fpn(pretrained=True):
        • Pretrained Faster R-CNN 모델을 ResNet-50 백본과 Feature Pyramid Network(FPN)을 사용하여 생성합니다.
        • pretrained=True로 설정하여 ImageNet으로 사전 학습된 가중치를 로드합니다.
      • in_features = model.roi_heads.box_predictor.cls_score.in_features:
        • 기존 모델의 roi_heads.box_predictor에서 cls_score의 입력 특성 수를 가져옵니다. 이 값은 분류기 입력의 피쳐 수입니다.
      • model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes):
        • 모델의 분류기를 새로운 FastRCNNPredictor로 교체합니다.
        • in_features는 입력 피쳐 수, num_classes는 클래스 수입니다.
      • return model: 수정된 모델을 반환합니다.1. build_model(num_classes)

 

 

 

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>)}

--------------------------------------------------------------------------------------------
  • 모델 학습 모드 설정
    • phase = 'train': 현재 작업 단계가 'train'임을 설정합니다. 이는 모델이 학습 모드로 설정되어야 함을 의미합니다.
    • model.train(): 모델을 학습 모드로 설정합니다. 이는 드롭아웃, 배치 정규화 등 학습 전용 레이어들이 학습 모드에서 작동하게 합니다.
  • 데이터 로더에서 배치 가져오기
    • for index, batch in enumerate(dloaders[phase]): 데이터 로더에서 배치를 가져옵니다. enumerate를 사용하여 배치의 인덱스와 데이터를 함께 가져옵니다. 여기서 dloaders[phase]는 'train' 단계의 데이터 로더입니다.
  • 배치 데이터 처리
    • images = batch[0]: 현재 배치에서 이미지 텐서를 가져옵니다.
    • targets = batch[1]: 현재 배치에서 타겟(정답) 데이터를 가져옵니다.
    • filenames = batch[2]: 현재 배치에서 파일 이름 리스트를 가져옵니다.
  • 이미지와 타겟 변환
    • image = list(image for image in images): 이미지 텐서를 리스트로 변환합니다. Faster R-CNN 모델은 이미지 리스트를 입력으로 받기 때문에 이렇게 변환합니다.
    • targets = [{k: v for k, v in t.items()} for t in targets]: 타겟 데이터를 사전 형태로 변환합니다. 각 타겟의 딕셔너리를 k: v 형태로 복사하여 모델이 예상하는 형식으로 변환합니다.
  • 모델에 입력하여 손실 계산
    • loss = model(images, targets): 모델에 이미지를 입력하고, 타겟을 사용하여 손실을 계산합니다. Faster R-CNN 모델은 입력 이미지와 타겟 데이터를 사용하여 손실 값을 반환합니다.
  • 첫 번째 배치 처리 후 반복문 종료
    • if index == 0:: 첫 번째 배치만 처리하도록 조건을 설정합니다. 이후 반복문을 종료합니다.
    • break: 반복문을 종료하여 더 이상 배치를 처리하지 않습니다.
  • 손실 출력
    • 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


 
 

 

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

 
  • 손실을 저장할 딕셔너리 생성
    • defaultdict(float): 손실 값을 저장할 딕셔너리를 생성합니다. 기본값은 0.0입니다.
  • 모델을 학습 모드로 설정
    • model.train(): 모델을 학습 모드로 설정합니다. 드롭아웃 및 배치 정규화가 학습 모드로 작동합니다.
  • 학습 및 검증 단계 루프
    • for phase in ['train', 'val']: 'train'과 'val' (검증) 단계에서 루프를 실행합니다.
  • 데이터 로딩 및 모델에 입력
    • dataloaders[phase]: 현재 단계에 맞는 데이터 로더에서 배치를 가져옵니다.
    • images = list(image for image in images): 이미지 텐서를 리스트로 변환합니다.
    • targets = [{k: v for k, v in t.items()} for t in targets]: 타겟 데이터를 사전 형태로 변환합니다.
  • 모델에 입력하여 손실 계산
    • torch.set_grad_enabled(phase == 'train'): 학습 단계에서는 기울기 계산을 활성화하고, 검증 단계에서는 비활성화합니다.
    • loss = model(images, targets): 모델에 이미지를 입력하고, 타겟을 사용하여 손실을 계산합니다.
  • 총 손실 계산
    • total_loss: 손실 딕셔너리의 모든 손실 값을 합산하여 총 손실을 계산합니다.
  • 학습 단계의 경우
    • optimizer.zero_grad(): 기울기를 초기화합니다.
    • total_loss.backward(): 총 손실에 대한 기울기를 계산합니다.
    • optimizer.step(): 기울기를 사용하여 모델의 매개변수를 업데이트합니다.
    • if (index > 0) and (index % VERBOSE_FREQ == 0):: 주기적으로 학습 상태를 출력합니다.
    • train_loss[k] += v.item(): 각 손실 항목을 학습 손실에 추가합니다.
    • train_loss['total_loss'] += total_loss.item(): 총 손실을 학습 손실에 추가합니다.
  • 검증 단계의 경우
    • for k, v in loss.items():: 각 손실 항목을 검증 손실에 추가합니다.
    • val_loss['total_loss'] += total_loss.item(): 총 손실을 검증 손실에 추가합니다.
  • 평균 손실 계산
    • 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(dataloadersmodeloptimizerdevice):
              # 학습 손실과 검증 손실을 저장할 딕셔너리 초기화
              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)


  • 데이터 디렉토리와 CUDA 설정
    • data_dir: 데이터가 저장된 디렉토리의 경로를 지정합니다.
    • is_cuda: CUDA 사용 여부를 설정하는 플래그입니다. 현재는 False로 설정되어 있으며, 이를 통해 GPU가 사용되지 않도록 설정되어 있습니다.
  • 하이퍼파라미터 설정
    • NUM_CLASSES: 모델이 감지할 클래스의 수를 설정합니다. 현재는 2개로 설정되어 있습니다.
    • IMAGE_SIZE: 입력 이미지의 크기를 설정합니다. 모든 이미지를 448x448 픽셀로 리사이즈합니다.
    • BATCH_SIZE: 학습 및 검증 시 사용하는 배치 크기를 설정합니다.
    • VERBOSE_FREQ: 학습 상태를 출력할 주기를 설정합니다. 이 값에 따라 특정 배치마다 학습 정보를 출력합니다.
  • 디바이스 설정
    • torch.device('cuda' if torch.cuda.is_available() and is_cuda else 'cpu'): CUDA가 사용 가능하고 is_cuda가 True인 경우 GPU를 사용하도록 설정합니다. 그렇지 않으면 CPU를 사용합니다.
    • torch.cuda.is_available(): 시스템에서 CUDA가 사용 가능한지 여부를 확인하는 함수입니다. 현재 코드에서는 is_cuda가 False로 설정되어 있어 GPU를 사용하지 않도록 되어 있습니다.
  • 데이터 로더 생성
    • build_dataloader: 앞서 정의한 함수를 호출하여 데이터 로더를 생성합니다. 이 함수는 학습 및 검증 데이터 로더를 생성합니다.
  • 모델 생성 및 디바이스에 배치
    • build_model: 모델을 생성하고, NUM_CLASSES에 맞게 조정합니다.
    • model.to(DEVICE): 모델을 선택된 디바이스(CPU 또는 GPU)로 이동시킵니다.
  • 최적화기 설정
    • torch.optim.SGD: 확률적 경사 하강법(SGD) 최적화기를 설정합니다.
      • model.parameters(): 모델의 모든 파라미터를 최적화 대상에 포함시킵니다.
      • lr=0.001: 학습률을 0.001로 설정합니다.
      • momentum=0.9: 모멘텀을 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