상세 컨텐츠

본문 제목

자동차 추적, 탐지 프로젝트

자율주행/컴퓨터 비젼 프로젝트(python)

by nosungmin 2023. 2. 15. 21:26

본문

import cv2
import IPython
import numpy as np
import time
import math
from google.colab.patches import cv2_imshow
 
min_confidence = 0.5
weight_file = 'yolov3.weights'
cfg_file = 'cfg/yolov3.cfg'
name_file = 'data/coco.names'

file_name = 'cabc30fc-e7726578.mp4'
 
이 파일들은 이번 프로젝트에 쓰일 가중치 파일과 딥러닝 모델, 데이터셋, 비디오 영상이다.
 
# Load Yolo
net = cv2.dnn.readNet(weight_file, cfg_file)
classes = []
with open(name_file, 'r'as f:
    classes = [line.strip() for line in f.readlines()]
print(classes)
 
이 코드는 name_file 경로에서 클래스 이름이 포함된 파일을 열어서 클래스 이름을 리스트로 읽어들이는 것입니다. with문을 사용하여 파일을 열고, readlines() 함수를 사용하여 파일의 모든 라인을 읽은 후에 각 라인의 공백이나 개행 문자를 제거하여 클래스 이름만 추출하고 리스트 classes에 할당합니다. 마지막으로 print() 함수를 사용하여 추출된 클래스 이름이 잘 저장되었는지 출력합니다. 이 코드는 YOLOv3 모델의 클래스 이름이 포함된 파일을 읽어들이는 데 사용될 수 있습니다.
 
layer_names = net.getLayerNames()
output_layers = [layer_names[i[0] - 1for i in net.getUnconnectedOutLayers()]
 
이 코드는 net 객체에서 레이어 이름 리스트를 가져와서 출력 레이어의 이름을 가져오는 것입니다. getLayerNames() 함수를 사용하여 net 객체의 모든 레이어의 이름을 가져옵니다. 그런 다음, getUnconnectedOutLayers() 함수를 사용하여 출력 레이어의 인덱스를 가져옵니다. 이 인덱스는 1부터 시작하므로 1을 뺀 다음 해당 인덱스의 레이어 이름을 가져와 output_layers 리스트에 추가합니다. 이 코드는 YOLOv3 모델의 출력 레이어의 이름을 가져오는 데 사용될 수 있습니다.
 
def writeFrame(img):
    # use global variable, writer
    global writer
    height, width = img.shape[:2]
    if writer is None and output_name is not None:
        fourcc = cv2.VideoWriter_fourcc(*'MJPG')
        writer = cv2.VideoWriter(output_name, fourcc, 24, (width, height), True)
    if writer is not None:
        writer.write(img)
 

이 코드는 OpenCV를 사용하여 비디오 파일을 작성하는 함수인 writeFrame(img)을 정의하는 코드입니다.

함수는 이미지 (img)를 인자로 받습니다. 먼저, img의 높이와 너비를 추출합니다. 그런 다음, 전역 변수인 writer를 사용하여 비디오 라이터 객체를 만들고 초기화합니다. 이를 위해, writer가 None이고 output_name이 지정된 경우, MJPG 코덱을 사용하고 프레임 속도는 24fps로 설정하여 비디오 라이터를 만듭니다.

비디오 라이터가 초기화되면 writer 객체를 사용하여 img를 쓰고 비디오 파일을 작성합니다. 쓰기 작업이 성공적으로 수행되면 True가 반환됩니다.

즉, 이 함수는 이미지를 비디오 파일에 쓰기 위한 OpenCV 비디오 라이터 객체를 생성하고 초기화한 다음, 전달받은 이미지를 비디오 파일에 씁니다.

 

 
frame_count = 0
# initialize the video writer 
writer = None
output_name = 'output_car_tracking.avi'

detected = False
frame_mode = 'Tracking'
elapsed_time = 0
margin = 70
tracker = cv2.TrackerKCF_create()
trackers = cv2.MultiTracker_create()
 

이 코드는 OpenCV 라이브러리를 사용하여 이미지 추적(tracking)을 수행하는 데 필요한 변수와 객체를 정의하는 것입니다. 구체적으로:

  • detected = False: 물체가 이미지에서 감지되었는지 여부를 나타내는 불리언 변수입니다. 이 값은 추적 알고리즘이 시작될 때 False로 초기화됩니다.
  • frame_mode = 'Tracking': 현재 프레임의 모드를 나타내는 문자열 변수입니다. 이 변수는 프로그램의 다른 부분에서 사용되며, 현재는 'Tracking'으로 설정되어 있습니다.
  • elapsed_time = 0: 추적이 시작된 이후 경과한 시간(초)을 나타내는 변수입니다. 이 변수는 추적이 끝날 때까지 업데이트됩니다.
  • margin = 70: 추적할 물체 주위에 추가로 포함할 여분의 여백(pixel) 크기입니다. 이 값을 적절히 설정하면 물체의 일부를 잃어버리는 문제를 방지할 수 있습니다.
  • tracker = cv2.TrackerKCF_create(): KCF(Kernelized Correlation Filters) 알고리즘을 사용하여 단일 물체 추적기(tracker) 객체를 만듭니다. 이 추적기는 영상 내에서 한 개의 물체를 추적할 때 사용됩니다.
  • trackers = cv2.MultiTracker_create(): 여러 개의 추적기를 사용하여 동시에 여러 물체를 추적할 수 있는 MultiTracker 객체를 생성합니다. 이 객체는 프로그램의 다른 부분에서 사용됩니다.

위 코드에서 사용되는 cv2는 OpenCV 라이브러리를 Python에서 사용할 수 있도록 하는 모듈이며, cv2.TrackerKCF_create()와 cv2.MultiTracker_create()는 OpenCV에서 제공하는 추적 알고리즘을 쉽게 사용할 수 있도록 하는 함수입니다.


detected_width = 0
vs = cv2.VideoCapture(file_name)

이 코드는 OpenCV 라이브러리를 사용하여 동영상 파일을 읽어와 프레임별로 처리할 수 있는 VideoCapture 객체를 만드는 것입니다. 구체적으로:

  • detected_width = 0: 추적 중인 물체의 폭(너비)를 저장하는 변수입니다. 추적이 시작되기 전에는 0으로 초기화됩니다.
  • vs = cv2.VideoCapture(file_name): file_name으로 지정된 동영상 파일을 열어서 읽을 수 있는 VideoCapture 객체(vs)를 생성합니다. 이 객체를 사용하여 동영상 파일에서 프레임을 읽고, 처리할 수 있습니다.

while True:
    start_time = time.time()
    frame_count += 1
    ret, frame = vs.read()
    if frame is None:
        print('### No more frame ###')
        break
    IPython.display.clear_output(wait=True)
    height, width, channedls = frame.shape
 

이 코드는 VideoCapture 객체를 사용하여 동영상 파일에서 프레임을 읽어오고, 해당 프레임의 속성을 추출하는 것입니다. 구체적으로:

  • start_time = time.time(): 현재 프레임이 처리되기 시작한 시간을 기록하는 변수입니다. 추적 및 분석의 소요 시간을 계산하는 데 사용됩니다.
  • frame_count += 1: 읽어온 프레임 수를 증가시키는 변수입니다. 추후 프레임 속도(프레임 속도가 낮은 경우) 및 추적 성능(높은 프레임 속도에서 손실되는 프레임 수)를 추정하는 데 사용됩니다.
  • ret, frame = vs.read(): VideoCapture 객체에서 다음 프레임을 읽어옵니다. ret 변수에는 프레임이 성공적으로 읽혔는지 여부가 저장되고, frame 변수에는 실제 프레임 데이터가 저장됩니다.
  • if frame is None: ...: 만약 읽어온 프레임이 None이면, 즉 동영상 파일의 끝까지 모든 프레임을 읽었거나 다른 문제가 발생한 경우, 반복문을 탈출합니다.
  • IPython.display.clear_output(wait=True): Jupyter Notebook에서 출력되는 이미지나 동영상 프레임의 중복을 방지하기 위해, 현재 출력된 셀의 내용을 지웁니다.
  • height, width, channedls = frame.shape: 읽어온 프레임의 속성을 추출합니다. frame.shape는 프레임의 크기와 채널 수를 반환하며, height, width, channels 변수에 각각 할당됩니다. 여기서 channels는 프레임의 채널 수(일반적으로 3 또는 4)를 나타냅니다.

    class_ids = []
    confidences = []
    boxes = []
    # Region of Interest
    roi_left = int(0.3 * width)
    roi_right = int(0.6 * width)

    if detected:
        frame_mode = 'Tracking'
        (success, tracking_boxes) = trackers.update(frame)
        tracking_box = tracking_boxes[0]
        tx = int(tracking_box[0])
        ty = int(tracking_box[1])
        tw = int(tracking_box[2])
        th = int(tracking_box[3])
        roi = frame[ty-margin:ty+th+margin, tx-margin:tx+tw+margin]
        roi_width, roi_height = roi.shape[:2]
        blob = cv2.dnn.blobFromImage(roi, 0.00392, (416416), (000), True, crop=False)

        net.setInput(blob)
        outs = net.forward(output_layers)
 

이 코드는 객체를 감지한 경우, 추적 및 분석 프로세스를 실행하는 부분입니다. 구체적으로:

  • if detected: detected 변수가 True이면, 즉 객체를 감지한 경우 다음 작업을 수행합니다.
  • frame_mode = 'Tracking': frame_mode 변수를 'Tracking'으로 설정하여 추적 모드로 변경합니다.
  • (success, tracking_boxes) = trackers.update(frame): MultiTracker 객체를 사용하여 이전 프레임에서 추적 중인 모든 객체를 새 프레임으로 업데이트합니다. success 변수는 모든 추적이 성공적으로 수행되었는지 여부를 나타내며, tracking_boxes 변수는 추적 결과를 반환합니다.
  • tracking_box = tracking_boxes[0]: 추적 상자 중 첫 번째 상자(객체)를 선택합니다. 다중 객체 추적의 경우 이 부분을 수정하여 다른 객체를 선택할 수 있습니다.
  • tx, ty, tw, th: 추적 상자의 x, y 좌표, 너비 및 높이 값을 추출합니다.
  • roi = frame[ty-margin:ty+th+margin, tx-margin:tx+tw+margin]: 추적 상자의 주변 부분을 추출합니다. margin 변수는 추적 상자 주변에 추가적으로 추출할 픽셀의 수를 나타내며, 이를 통해 추적 상자보다 큰 영역에서 객체를 분석할 수 있습니다.
  • roi_width, roi_height = roi.shape[:2]: 추출한 주변 영역의 너비와 높이 값을 추출합니다.
  • blob = cv2.dnn.blobFromImage(roi, 0.00392, (416, 416), (0, 0, 0), True, crop=False): 추출한 주변 영역에서 YOLO 모델에 입력할 blob 이미지를 생성합니다. 이를 위해 추출한 주변 영역을 전처리하고, blobFromImage() 함수를 사용하여 blob 이미지를 생성합니다.
  • net.setInput(blob): YOLO 모델에 blob 이미지를 입력합니다.
  • outs = net.forward(output_layers): YOLO 모델의 출력층에서 예측된 값들을 반환합니다. 이 값을 이용하여 객체를 분류하고, 위치와 크기를 추정할 수 있습니다.
        for out in outs:
            for detection in out:
                scores = detection[5:]
                class_id = np.argmax(scores)
                confidence = scores[class_id]
                if (confidence > min_confidence) and (class_id == 2):
                    # Object detected
                    center_x = int(detection[0] * roi_width)
                    center_y = int(detection[1] * roi_height)
                    w = int(detection[2] * roi_width)
                    h = int(detection[3] * roi_height)

                    # Rectangle coordinates
                    x = int(center_x - w / 2)
                    y = int(center_y - h / 2)
                    boxes.append([x, y, w, h])
 

이 코드는 YOLO 모델의 출력층에서 예측된 값들을 분석하여 객체를 분류하고, 위치와 크기를 추정합니다. 구체적으로:

  • for out in outs: ...: YOLO 모델의 출력층에서 반환된 값들을 반복하여 분석합니다.
  • for detection in out: ...: 출력층에서 반환된 각각의 예측값을 반복하여 분석합니다.
  • scores = detection[5:]: 예측값에서 클래스 확률값을 추출합니다.
  • class_id = np.argmax(scores): 클래스 확률값 중 가장 높은 값을 가지는 인덱스(즉, 클래스 ID)를 추출합니다.
  • confidence = scores[class_id]: 추출한 클래스 ID에 대한 클래스 확률값을 추출합니다.
  • if (confidence > min_confidence) and (class_id == 2): ...: 추출한 클래스 ID와 클래스 확률값이 일정 조건을 만족하는 경우 다음 작업을 수행합니다. 여기서는 클래스 ID가 2인 경우(즉, 자동차를 나타내는 클래스)를 선택하고, 클래스 확률값이 min_confidence 변수보다 큰 경우를 선택합니다.
  • center_x = int(detection[0] * roi_width): 예측된 객체 중심의 x 좌표 값을 계산합니다.
  • center_y = int(detection[1] * roi_height): 예측된 객체 중심의 y 좌표 값을 계산합니다.
  • w = int(detection[2] * roi_width): 예측된 객체의 너비 값을 계산합니다.
  • h = int(detection[3] * roi_height): 예측된 객체의 높이 값을 계산합니다.
  • x = int(center_x - w / 2): 예측된 객체의 좌상단 x 좌표 값을 계산합니다.
  • y = int(center_y - h / 2): 예측된 객체의 좌상단 y 좌표 값을 계산합니다.
  • boxes.append([x, y, w, h]): 예측된 객체의 좌표와 크기 값을 리스트에 추가합니다. 이 리스트는 이후 Non-Maximum Suppression(NMS) 알고리즘에서 중복되는 객체를 제거하는 데 사용됩니다.
 
        if len(boxes):
            boxes.sort(key=lambda x: x[2], reverse=True)
            box = boxes[0]
            x = box[0]
            y = box[1]
            w = box[2]
            h = box[3]
            roi_x = tx-margin + x
            roi_y = ty-margin + y
            distance_width = w - detected_width
            cv2.rectangle(frame, (roi_x, roi_y), (roi_x + w, roi_y + h), (0255255), 1)
            label = 'Initial Width : ' + str(detected_width) + ', Current Width : ' + str(w) + ', Distance : ' + str(w-detected_width)
            print(box, label)
 

해당 코드는 영상에서 감지된 물체의 경계 상자(boxes) 중에서 너비(width)가 가장 큰 경계 상자를 찾아서 그 상자의 위치와 크기를 이용하여 물체 주변에 사각형(rectangle)을 그리고, 해당 물체의 초기 너비(detected_width)와 현재 너비(w), 두 값의 차이(distance)를 표시하는 레이블(label)을 생성하는 코드입니다.

여기서 boxes는 감지된 물체의 경계 상자들을 담은 리스트이며, sort 메서드를 사용하여 해당 리스트를 너비(width)를 기준으로 내림차순으로 정렬합니다. 따라서 boxes 리스트의 첫 번째 요소(box)가 가장 큰 너비를 가진 상자가 됩니다.

그리고 해당 상자의 좌표(x, y)와 너비(w), 높이(h)를 변수에 저장합니다. 이후, 상자의 위치와 크기를 이용하여 물체 주변에 노란색(0, 255, 255)의 사각형(rectangle)을 그립니다.

마지막으로, 레이블(label) 문자열을 생성하여 해당 문자열을 화면에 출력합니다. 이 문자열은 초기에 감지된 물체의 너비(detected_width)와 현재 상자의 너비(w) 그리고 두 값의 차이(distance)를 문자열로 변환하여 표시합니다.

 
            if abs(distance_width) > 5:
                if distance_width < 0:
                    cv2.putText(frame, 'Speed up', (30100), cv2.FONT_HERSHEY_COMPLEX, 2, (25500), 5)
                elif distance_width < 30:
                    cv2.putText(frame, 'Slow down', (30100), cv2.FONT_HERSHEY_COMPLEX, 2, (0128255), 5)
                else:
                    cv2.putText(frame, 'Be Careful', (30100), cv2.FONT_HERSHEY_COMPLEX, 2, (00255), 5)
            cv2.putText(frame, label, (3030), cv2.FONT_HERSHEY_COMPLEX, 0.5, (02550), 1
        cv2.rectangle(frame, (tx, ty), (tx+tw, ty+th), (2552550), 1)
        cv2.rectangle(frame, (tx-margin, ty-margin), (tx+tw+margin, ty+th+margin), (25500), 1)
 

해당 코드는 감지된 물체의 너비를 기준으로 물체와의 거리가 일정 기준에 미치지 않을 경우에 대한 경고 메시지를 출력하는 코드입니다.

코드의 첫 번째 if문에서는 현재 물체의 너비와 초기에 감지된 물체의 너비 간의 차이(distance_width)가 5보다 큰 경우를 처리합니다. 이때, 물체의 크기가 감소한 경우(distance_width < 0)는 "Speed up" 메시지를, 물체의 크기가 일정 범위 내로 유지되는 경우(distance_width < 30)는 "Slow down" 메시지를, 그렇지 않은 경우는 "Be Careful" 메시지를 화면에 출력합니다.

또한, 코드의 마지막 부분에서는 감지된 물체를 기준으로 화면에 검출된 물체 경계 상자(rectangle)를 두 개 그립니다. 하나는 물체가 위치한 곳을 표시하기 위한 녹색(rectangle), 다른 하나는 물체 주변에 마진(margin)을 더한 경계 상자를 표시하기 위한 파란색(rectangle)입니다.

마지막으로, 위에서 생성한 레이블(label) 문자열을 이용하여 물체의 너비 정보와 거리 경고 메시지를 표시합니다. 이때, 레이블은 물체 경계 상자 위에 위치하며, 텍스트의 크기는 작게 설정됩니다.

         

    else:
        frame_mode = 'Detection'
        # Detecting objects
        blob = cv2.dnn.blobFromImage(frame, 0.00392, (416416), (000), True, crop=False)

        net.setInput(blob)
        outs = net.forward(output_layers)
 

해당 코드는 프레임을 입력으로 받아서 객체를 검출하는 작업을 수행하는 코드입니다.

우선, 코드의 첫 부분에서는 현재 프레임의 모드를 'Detection'으로 설정합니다. 그리고 나서, OpenCV의 딥러닝 모듈(dnn)을 사용하여 프레임으로부터 객체를 검출합니다. 이를 위해 먼저 입력 이미지(frame)를 전처리 과정을 거쳐 416x416 크기의 이미지(blob)로 변환합니다. 이때, blobFromImage() 함수는 입력 이미지를 신경망 모델에 적합한 형태로 전처리하기 위한 함수입니다.

그 다음, blob을 모델의 입력으로 설정하고 forward() 함수를 호출하여 모델이 각 레이어(layer)에서 출력하는 결과(outs)를 계산합니다. 이때, outs는 넘파이 배열(ndarray) 형태로 반환됩니다. 이러한 결과는 신경망 모델을 통해 입력 이미지에서 감지된 객체의 위치, 크기, 클래스 등에 대한 정보를 담고 있습니다.

따라서, 이후의 코드에서는 이 정보를 이용하여 객체의 경계 상자(bounding box)를 그리고, 객체의 클래스 이름 등을 표시할 수 있습니다. 이러한 처리는 일반적으로 detect.py, yolov3-tiny.py 등과 같은 파일에서 수행됩니다.


        for out in outs:
            for detection in out:
                scores = detection[5:]
                class_id = np.argmax(scores)
                confidence = scores[class_id]
                if (confidence > min_confidence) and (class_id == 2):
                    # Object detected
                    center_x = int(detection[0] * width)
                    center_y = int(detection[1] * height)
                    w = int(detection[2] * width)
                    h = int(detection[3] * height)

                    # Rectangle coordinates
                    x = int(center_x - w / 2)
                    y = int(center_y - h / 2)

                    boxes.append([x, y, w, h])
                    confidences.append(float(confidence))
                    class_ids.append(class_id)
 

해당 코드는 객체 검출 결과(outs)에서 클래스 ID가 2(자동차)인 객체에 대해 경계 상자(bounding box)를 계산하여 boxes, confidences, class_ids 리스트에 추가하는 작업을 수행하는 코드입니다.

먼저, outs에서 각각의 detection에 대해서 클래스 ID와 신뢰도(confidence)를 계산합니다. 이를 위해 detection의 6번째 인덱스부터 클래스별로 해당 detection이 속할 확률(score)을 scores에 저장하고, 이 중 가장 높은 값을 가진 클래스 ID를 class_id로 지정합니다.

그 다음, confidence가 최소 신뢰도(min_confidence)보다 크고, class_id가 2(자동차)일 때, 해당 객체의 중심 좌표(center_x, center_y)와 너비(w), 높이(h)를 계산합니다. 이를 이용하여 bounding box를 지정하고, boxes, confidences, class_ids 리스트에 추가합니다.

이러한 과정은 각 detection에 대해서 수행되며, 최종적으로 boxes, confidences, class_ids 리스트에는 프레임에서 감지된 모든 자동차 객체의 bounding box 정보와 신뢰도, 클래스 ID가 저장됩니다. 이후의 코드에서는 이러한 정보를 이용하여 자동차 객체의 크기, 속도 등을 계산할 수 있습니다.


        indexes = cv2.dnn.NMSBoxes(boxes, confidences, min_confidence, 0.4)
        font = cv2.FONT_HERSHEY_COMPLEX
        for i in range(len(boxes)):
            if i in indexes:
                x, y, w, h = boxes[i]
                # Eliminate Small object(<50)
                if (w > 50and (x > roi_left) and (x < roi_right):
                    selected = boxes[i]
                    detected_width = w
                    cv2.rectangle(frame, (x, y), (x + w, y + h), (0255255), 5)    
        trackers.add(tracker, frame, tuple(selected))
        detected = True 
 

해당 코드는 검출된 자동차 객체의 bounding box 정보(boxes)를 이용하여, 크기가 일정 이상인(50보다 큰) 객체 중 ROI(Region of Interest) 영역 내에 위치한 객체를 선택합니다. 이후, 선택된 객체의 bounding box 정보를 이용하여 객체 추적(track)를 수행하고, detected 변수를 True로 설정하여 객체가 감지되었음을 알립니다.

for 루프에서는 boxes 리스트의 모든 객체에 대해서, 만약 해당 객체가 non-max suppression 과정에서 선택된 객체(indexes 리스트에 해당 객체의 인덱스가 있을 경우), 그리고 해당 객체의 크기(w)가 50보다 크고, ROI 영역 내에 위치할 경우, 해당 객체를 선택(selected)하고, detected_width 변수에 객체의 너비(w)를 저장합니다.

그 다음, trackers.add() 함수를 이용하여 선택된 객체에 대한 tracker를 생성하고, 해당 객체를 추적합니다. 이 때, add() 함수는 현재 프레임, 선택된 객체의 위치(selected)와 함께 호출됩니다. 마지막으로, detected 변수를 True로 설정하여 객체가 감지되었음을 알리고, 이후의 코드에서는 이러한 정보를 이용하여 객체의 이동 거리, 속도 등을 계산합니다.


    cv2_imshow(frame)
    writeFrame(frame)
    frame_time = time.time() - start_time
    elapsed_time += frame_time
    print("[{}] Frame {} time {}".format(frame_mode, frame_count, frame_time))

print("Elapsed time {}".format(elapsed_time))
vs.release()         

이 코드는 OpenCV 라이브러리를 사용하여 비디오를 처리하는 Python 스크립트의 일부분입니다. 이 코드의 목적은 다음과 같습니다.

  1. cv2_imshow(frame) 함수는 현재 프레임을 OpenCV 윈도우에 보여줍니다.
  2. writeFrame(frame) 함수는 현재 프레임을 비디오 파일에 쓰기 위해 사용됩니다.
  3. frame_time 변수는 현재 프레임의 처리 시간을 저장합니다.
  4. elapsed_time 변수는 이전까지 처리된 모든 프레임의 총 처리 시간을 저장합니다.
  5. print("[{}] Frame {} time {}".format(frame_mode, frame_count, frame_time)) 함수는 현재 프레임의 처리 시간과 처리된 프레임 수, 그리고 프레임 처리 모드를 출력합니다.
  6. print("Elapsed time {}".format(elapsed_time)) 함수는 모든 프레임의 처리 시간의 합계를 출력합니다.
  7. vs.release() 함수는 비디오 파일 또는 웹캠과 같은 입력 소스를 해제합니다.

이 코드는 OpenCV를 사용하여 비디오 처리를 수행하는 데 사용되며, 처리된 각 프레임의 처리 시간을 측정하고 출력하고 총 처리 시간을 계산합니다. 이러한 정보를 사용하여 비디오 처리 성능을 평가하고 최적화할 수 있습니다.

 

관련글 더보기