본문 바로가기
📁 [4] 개발자 정보 & 코드 노트/C#

C# Windows Forms 강의 81편: AI 기반 객체 탐지 및 추적 - YOLO 모델 활용

by wawManager 2025. 4. 26.

반응형

 

1. 강의 개요

이번 강의에서는 YOLO(You Only Look Once) 모델을 사용하여 객체 탐지 및 추적 애플리케이션을 제작합니다.
YOLO는 실시간 객체 탐지에 널리 사용되는 딥러닝 모델로,
이미지나 비디오 프레임에서 사람, 차량, 동물 등의 객체를 탐지할 수 있습니다.
이 강의에서는 YOLO 모델과 OpenCvSharp을 연동하여 실시간으로 객체를 탐지하고 화면에 표시하는 방법을 배웁니다.


2. 학습 목표

  • YOLO 모델을 사용한 객체 탐지 구현
  • 비디오 스트리밍 프레임에서 객체 탐지 및 경계 상자 표시
  • OpenCvSharp과 YOLO 모델 파일 연동
  • 실시간 성능 최적화

3. 기능 요구사항

필수 기능

1️⃣ YOLO 모델 로드:

  • YOLO 모델 가중치 파일과 설정 파일 로드

2️⃣ 객체 탐지:

  • 비디오 프레임에서 객체를 탐지하고 경계 상자(Bounding Box)를 표시

3️⃣ 실시간 스트리밍:

  • 웹캠 또는 비디오 파일에서 실시간 탐지

4️⃣ UI 구성 및 동작:

  • 탐지 결과를 PictureBox에 실시간으로 표시

4. 실습: YOLO 기반 객체 탐지 애플리케이션 제작

1️⃣ 사전 준비

  1. YOLO 모델 파일 다운로드:
    • YOLOv4 또는 YOLOv5 모델 파일 다운로드
    • 필요한 파일:
      • yolov4.weights (모델 가중치)
      • yolov4.cfg (모델 설정)
      • coco.names (객체 이름 파일)
    • YOLOv4 다운로드 링크
  2. NuGet 패키지 설치:
    • Visual Studio에서 OpenCvSharp4OpenCvSharp4.Windows 패키지를 설치합니다.

2️⃣ 폼 구성

  • 폼(Form) 이름: Form1
  • 컨트롤 배치:

컨트롤 타입 이름 위치 크기

Button btnStartStream 폼 상단 왼쪽 (100 x 30)
Button btnStopStream 폼 상단 중앙 (100 x 30)
PictureBox pictureBox 폼 하단 전체 (500 x 400)

📌 폼 디자인 예시:

--------------------------------------------------
| [Start Stream] [Stop Stream]                  |
--------------------------------------------------
|               [PictureBox - 객체 탐지 결과]    |
--------------------------------------------------

3️⃣ 코드 작성

(1) YOLO 모델 로드

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Dnn;

namespace WindowsFormsApp_YOLODetection
{
    public partial class Form1 : Form
    {
        private Net _net; // YOLO 네트워크 객체
        private VideoCapture _videoCapture;
        private CancellationTokenSource _cancellationTokenSource;

        private readonly string[] _classLabels; // 객체 이름
        private readonly Scalar[] _colors; // 클래스별 경계 상자 색상

        public Form1()
        {
            InitializeComponent();

            // 클래스 이름 파일 읽기
            _classLabels = File.ReadAllLines("coco.names");

            // 랜덤 색상 생성
            _colors = new Scalar[_classLabels.Length];
            var random = new Random();
            for (int i = 0; i < _colors.Length; i++)
            {
                _colors[i] = new Scalar(random.Next(0, 255), random.Next(0, 255), random.Next(0, 255));
            }

            // YOLO 모델 로드
            _net = CvDnn.ReadNetFromDarknet("yolov4.cfg", "yolov4.weights");
            _net.SetPreferableBackend(Backend.OPENCV);
            _net.SetPreferableTarget(Target.CPU);
        }
    }
}

(2) 실시간 스트리밍 및 객체 탐지

        // 실시간 스트리밍 시작
        private async void btnStartStream_Click(object sender, EventArgs e)
        {
            _videoCapture = new VideoCapture(0); // 0번 웹캠
            if (!_videoCapture.IsOpened())
            {
                MessageBox.Show("웹캠을 열 수 없습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            _cancellationTokenSource = new CancellationTokenSource();
            CancellationToken token = _cancellationTokenSource.Token;

            await Task.Run(() =>
            {
                using (Mat frame = new Mat())
                {
                    while (!_cancellationTokenSource.Token.IsCancellationRequested)
                    {
                        _videoCapture.Read(frame);
                        if (frame.Empty()) continue;

                        var detectedFrame = DetectObjects(frame);
                        DisplayFrame(detectedFrame);

                        Cv2.WaitKey(1);
                    }
                }
            });
        }

        // YOLO를 사용한 객체 탐지
        private Mat DetectObjects(Mat frame)
        {
            Mat blob = CvDnn.BlobFromImage(frame, 1 / 255.0, new OpenCvSharp.Size(416, 416), new Scalar(0, 0, 0), true, false);
            _net.SetInput(blob);

            var outputLayers = GetOutputLayers();
            var outputs = new List<Mat>();

            _net.Forward(outputs, outputLayers);

            PostProcess(frame, outputs);

            return frame;
        }

        // YOLO의 출력 계층 이름 가져오기
        private IEnumerable<string> GetOutputLayers()
        {
            var names = _net.GetUnconnectedOutLayersNames();
            return names;
        }

        // 탐지된 객체를 프레임에 표시
        private void PostProcess(Mat frame, List<Mat> outputs)
        {
            const float confidenceThreshold = 0.5f; // 신뢰도 임계값
            const float nmsThreshold = 0.4f; // NMS(Non-Maximum Suppression) 임계값

            var boxes = new List<Rect>();
            var classIds = new List<int>();
            var confidences = new List<float>();

            for (int i = 0; i < outputs.Count; i++)
            {
                var data = outputs[i].ToArray<float>();
                for (int j = 0; j < data.Length / 85; j++) // COCO 데이터셋 기준 85 요소
                {
                    float confidence = data[j * 85 + 4];
                    if (confidence > confidenceThreshold)
                    {
                        var scores = data.Skip(j * 85 + 5).Take(80).ToArray();
                        int classId = Array.IndexOf(scores, scores.Max());
                        float score = scores[classId];

                        if (score > confidenceThreshold)
                        {
                            int centerX = (int)(data[j * 85 + 0] * frame.Width);
                            int centerY = (int)(data[j * 85 + 1] * frame.Height);
                            int width = (int)(data[j * 85 + 2] * frame.Width);
                            int height = (int)(data[j * 85 + 3] * frame.Height);
                            int left = centerX - width / 2;
                            int top = centerY - height / 2;

                            boxes.Add(new Rect(left, top, width, height));
                            classIds.Add(classId);
                            confidences.Add(score);
                        }
                    }
                }
            }

            // NMS로 겹치는 상자 제거
            int[] indices = CvDnn.NMSBoxes(boxes, confidences, confidenceThreshold, nmsThreshold);

            foreach (int index in indices)
            {
                var box = boxes[index];
                Cv2.Rectangle(frame, box, _colors[classIds[index]], 2);
                Cv2.PutText(frame, $"{_classLabels[classIds[index]]} {confidences[index]:0.00}", 
                    new Point(box.X, box.Y - 5), HersheyFonts.HersheySimplex, 0.5, _colors[classIds[index]]);
            }
        }

        // 프레임 표시
        private void DisplayFrame(Mat frame)
        {
            if (frame.Empty()) return;

            var bitmap = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame);
            pictureBox.Invoke(new Action(() =>
            {
                pictureBox.Image?.Dispose();
                pictureBox.Image = bitmap;
            }));
        }

        // 스트리밍 종료
        private void btnStopStream_Click(object sender, EventArgs e)
        {
            _cancellationTokenSource?.Cancel();
            _videoCapture?.Release();
            pictureBox.Image = null;
        }

4️⃣ 실행 결과

1️⃣ 실시간 웹캠 스트리밍

  • "Start Stream" 클릭 → 웹캠에서 실시간 비디오 스트리밍 시작

2️⃣ 객체 탐지 및 경계 상자 표시

  • 탐지된 객체 주위에 경계 상자와 이름 표시

3️⃣ 스트리밍 종료

  • "Stop Stream" 클릭 → 스트리밍 종료

5. 주요 개념 요약

  • YOLO 모델: 실시간 객체 탐지를 위한 딥러닝 모델
  • CvDnn: OpenCV의 DNN(Deep Neural Network) 모듈
  • BlobFromImage: 입력 이미지를 YOLO에 맞는 포맷으로 변환
  • NMSBoxes: Non-Maximum Suppression으로 겹치는 경계 상자 제거

 

 

📌 #CSharp #WindowsForms #YOLO #객체탐지 #OpenCvSharp #DNN

반응형