YOLOv12 기반 옥수수 묘초 잡초 식별 시스템

1. 프로젝트 소개

본 시스템은 딥러닝 기반 YOLOv12 알고리즘을 활용하여 옥수수 묘초와 잡초를 정확히 식별하는 농업용 인공지능 솔루션입니다. 옥수수 밭에서의 정밀 제초 작업을 지원하기 위해 설계되었으며, 실시간으로 목표물을 탐지하고 분류하는 고성능 기능을 제공합니다. 시스템은 YOLOv12을 핵심 알고리즘으로 사용하며, 2개의 카테고리("잡초"와 "옥수수 묘초")로 구성된 전용 데이터셋을 기반으로 학습됩니다. 데이터셋은 학습용 2661장, 검증용 254장, 테스트용 127장의 이미지로 구성되어 모델의 일반화 능력을 보장합니다. Python 프레임워크를 기반으로 사용자 친화적인 UI 인터페이스와 다중 사용자 관리 기능을 통합하여 데이터 보안을 강화했습니다. 본 프로젝트는 완전한 소스 코드, 사전 학습된 모델, 데이터셋을 포함하며, 농업 분야의 자동화 제초 시스템 구축을 위한 실용적인 기반을 제공합니다.

2. 주요 기능

  • 사용자 인증 시스템: 안전한 로그인 및 회원가입 기능 제공
  • 다중 탐지 모드: 정적 이미지, 비디오 파일, 실시간 카메라 세 가지 탐지 방식 지원
  • 이중 화면 비교: 원본 영상과 탐지 결과를 동시에 표시
  • 데이터 시각화: 탐지된 객체의 종류, 신뢰도, 좌표 정보를 실시간 테이블로 제시
  • 동적 파라미터 조절: 신뢰도 슬라이더를 통한 탐지 정밀도 실시간 조정
  • 미래지향적 UI: 어두운 테마와 동적 조명 효과로 사용자 피로도 최소화
  • 멀티스레드 아키텍처: 독립적인 탐지 스레드로 원활한 시스템 작동 보장

2.1 사용자 관리 시스템

  • 안전한 로그인 및 회원가입 기능
  • 사용자명과 비밀번호 검증
  • 로컬 저장소 기반 계정 정보 관리 (accounts.json)
  • 최소 6자리 비밀번호 보안 정책

2.2 탐지 기능

  • 이미지 탐지: JPG/JPEG/PNG/BMP 형식 이미지 지원
  • 비디오 탐지: MP4/AVI/MOV 형식 비디오 프레임별 탐지
  • 실시간 탐지: 기본 카메라(0번)를 통한 실시간 영상 처리
  • 탐지 결과 자동 저장 ("results" 디렉토리)

2.3 결과 표시

  • 원본 이미지와 탐지 결과 이미지 동시 표시
  • 상세한 결과 테이블 제공:
  • 탐지된 객체 카테고리
  • 신뢰도 점수
  • 객체 위치 좌표(x,y)

2.4 파라미터 설정

  • 모델 선택 기능
  • 신뢰도 임계값 조절(0-1.0)
  • IoU(교차 합비율) 임계값 조절(0-1.0)
  • 슬라이더와 수치 입력창 동기화

2.5 기타 기능

  • 탐지 결과 저장 기능
  • 비디오 탐지 시 자동 결과 비디오 생성
  • 시스템 상태 및 최종 업데이트 시간 표시 상태바
  • 테두리 없는 창 디자인으로 드래그 및 크기 조절 가능

3. 기술적 특징

  • 멀티스레딩 기반 탐지 처리로 인터페이스 응답성 유지
  • 고급 UI 디자인과 미래지향적 시각 효과:
  • 발광 테두리 및 버튼
  • 마우스 오버 및 클릭 상태 효과
  • 커스텀 슬라이더, 테이블 및 드롭다운 스타일
  • 체계적인 탐지 결과 저장 메커니즘
  • 다양한 창 크기에 대응하는 반응형 레이아웃

4. 시스템 작업 흐름

  1. 사용자 로그인/회원가입
  2. 탐지 모드 선택(이미지/비디오/카메라)
  3. 탐지 파라미터 조정(선택 사항)
  4. 탐지 시작 및 결과 확인
  5. 탐지 결과 저장(선택 사항)
  6. 탐지 중지 또는 다른 모드 전환

5. 데이터셋 구성

본 시스템은 옥수수 묘초와 잡초 탐지 작업을 위해 특별히 구성된 데이터셋을 사용하며, 2개의 카테고리를 포함합니다: **"잡초"**와 "옥수수 묘초". 데이터셋은 엄격한 라벨링과 분할을 거쳐 모델 학습, 검증 및 테스트의 신뢰성을 확보했습니다.

5.1 데이터셋 구성
  • 학습 세트(Training Set): 2661장의 이미지로 YOLOv12 모델이 옥수수 묘초와 잡초의 특징을 학습하도록 함
  • 검증 세트(Validation Set): 254장의 이미지로 학습 과정 중 하이퍼파라미터 조정 및 과적합 방지
  • 테스트 세트(Test Set): 127장의 이미지로 모델의 일반화 능력 최종 평가

데이터셋 구성 파일

데이터셋은 표준 YOLO 형식으로 구성되어 있습니다:

train: F:\옥수수_묘초_잡초_데이터셋\train  
val: F:\옥수수_묘초_잡초_데이터셋\valid  
test:  F:\옥수수_묘초_잡초_데이터셋\test

# 클래스 수
nc: 2

# 클래스 이름
names: ["잡초", "옥수수_묘초"]

6. 개발 환경 설정

가상 환경 생성

먼저 Anaconda 환경을 새로 생성합니다. 각 프로젝트별로 독립적인 환경을 사용하면 의존성 패키지 간 충돌을 방지할 수 있습니다.

터미널에 입력:

conda create -n corn_weed_detection python==3.9

가상 환경 활성화

conda activate corn_weed_detection

CPU 버전 PyTorch 설치

pip install torch torchvision torchaudio

필요한 라이브러리 설치

pip install -r requirements.txt

PyCharm에서 Anaconda 환경 설정

7. 모델 학습

학습 코드

from ultralytics import YOLO

model_path = 'yolov12s.pt'
data_path = 'data.yaml'

if __name__ == '__main__':
    model = YOLO(model_path)
    results = model.train(data=data_path,
                          epochs=100,
                          batch=8,
                          device='0',
                          workers=0,
                          project='runs',
                          name='experiment',
                          )

모델 선택 가이드:

  • yolov12n.yaml (나노): 경량화 모델, 임베디드 장치에 적합, 속도 빠름 but 정밀도 약간 낮음

  • yolov12s.yaml (스몰): 소형 모델, 실시간 작업에 적합

  • yolov12m.yaml (미디엄): 중간 크기 모델, 속도와 정밀도의 균형

  • yolov12b.yaml (베이스): 기본 모델, 대부분의 응용 시나리오에 적합

  • yolov12l.yaml (라지): 대형 모델, 높은 정밀도 요구 작업에 적합

  • --batch 8: 배치당 8장의 이미지

  • --epochs 100: 100 에포크 학습

  • --datasets/data.yaml: 데이터셋 구성 파일

  • --weights yolov12s.pt: 사전 학습된 경량 YOLO 모델 가중치

학습 결과

8. 핵심 코드 구현

import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import QApplication, QMessageBox, QFileDialog
from PyQt5.QtCore import QThread, pyqtSignal
from ultralytics import YOLO
from UiMain import UiMainWindow
import time
import os
from PyQt5.QtWidgets import QDialog
from LoginWindow import LoginWindow

class DetectionThread(QThread):
    frame_received = pyqtSignal(np.ndarray, np.ndarray, list)  # 원본 프레임, 탐지 프레임, 탐지 결과
    finished_signal = pyqtSignal()  # 스레드 완료 신호

    def __init__(self, model, source, conf, iou, parent=None):
        super().__init__(parent)
        self.model = model
        self.source = source
        self.conf = conf
        self.iou = iou
        self.running = True

    def run(self):
        try:
            if isinstance(self.source, int) or self.source.endswith(('.mp4', '.avi', '.mov')):  # 비디오 또는 카메라
                cap = cv2.VideoCapture(self.source)
                while self.running and cap.isOpened():
                    ret, frame = cap.read()
                    if not ret:
                        break

                    # 원본 프레임 저장
                    original_frame = frame.copy()

                    # 탐지 수행
                    results = self.model(frame, conf=self.conf, iou=self.iou)
                    annotated_frame = results[0].plot()

                    # 탐지 결과 추출
                    detections = []
                    for result in results:
                        for box in result.boxes:
                            class_id = int(box.cls)
                            class_name = self.model.names[class_id]
                            confidence = float(box.conf)
                            x, y, w, h = box.xywh[0].tolist()
                            detections.append((class_name, confidence, x, y))

                    # 신호 발송
                    self.frame_received.emit(
                        cv2.cvtColor(original_frame, cv2.COLOR_BGR2RGB),
                        cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB),
                        detections
                    )

                    # 프레임률 제어
                    time.sleep(0.03)  # 약 30fps

                cap.release()
            else:  # 이미지
                frame = cv2.imread(self.source)
                if frame is not None:
                    original_frame = frame.copy()
                    results = self.model(frame, conf=self.conf, iou=self.iou)
                    annotated_frame = results[0].plot()

                    # 탐지 결과 추출
                    detections = []
                    for result in results:
                        for box in result.boxes:
                            class_id = int(box.cls)
                            class_name = self.model.names[class_id]
                            confidence = float(box.conf)
                            x, y, w, h = box.xywh[0].tolist()
                            detections.append((class_name, confidence, x, y))

                    self.frame_received.emit(
                        cv2.cvtColor(original_frame, cv2.COLOR_BGR2RGB),
                        cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB),
                        detections
                    )

        except Exception as e:
            print(f"탐지 오류: {e}")
        finally:
            self.finished_signal.emit()

    def stop(self):
        self.running = False


class MainWindow(UiMainWindow):
    def __init__(self):
        super().__init__()

        # 모델 초기화
        self.model = None
        self.detection_thread = None
        self.current_image = None
        self.current_result = None
        self.video_writer = None
        self.is_camera_running = False
        self.is_video_running = False
        self.last_detection_result = None  # 마지막 탐지 결과 저장

        # 버튼 신호 연결
        self.image_btn.clicked.connect(self.detect_image)
        self.video_btn.clicked.connect(self.detect_video)
        self.camera_btn.clicked.connect(self.detect_camera)
        self.stop_btn.clicked.connect(self.stop_detection)
        self.save_btn.clicked.connect(self.save_result)

        # 모델 로드
        self.load_model()

    def load_model(self):
        try:
            model_name = self.model_combo.currentText()
            self.model = YOLO(f"{model_name}.pt")  # 자동 다운로드 또는 로컬 모델 로드
            self.update_status(f"모델 {model_name} 로드 성공")
        except Exception as e:
            QMessageBox.critical(self, "오류", f"모델 로드 실패: {str(e)}")
            self.update_status("모델 로드 실패")

    def detect_image(self):
        if self.detection_thread and self.detection_thread.isRunning():
            QMessageBox.warning(self, "경고", "현재 진행 중인 탐지 작업을 먼저 중지해주세요")
            return

        file_path, _ = QFileDialog.getOpenFileName(
            self, "이미지 선택", "", "이미지 파일 (*.jpg *.jpeg *.png *.bmp)")

        if file_path:
            self.clear_results()
            self.current_image = cv2.imread(file_path)
            self.current_image = cv2.cvtColor(self.current_image, cv2.COLOR_BGR2RGB)
            self.display_image(self.original_image_label, self.current_image)

            # 탐지 스레드 생성
            conf = self.confidence_spinbox.value()
            iou = self.iou_spinbox.value()
            self.detection_thread = DetectionThread(self.model, file_path, conf, iou)
            self.detection_thread.frame_received.connect(self.on_frame_received)
            self.detection_thread.finished_signal.connect(self.on_detection_finished)
            self.detection_thread.start()

            self.update_status(f"이미지 탐지 중: {os.path.basename(file_path)}")

    def detect_video(self):
        if self.detection_thread and self.detection_thread.isRunning():
            QMessageBox.warning(self, "경고", "현재 진행 중인 탐지 작업을 먼저 중지해주세요")
            return

        file_path, _ = QFileDialog.getOpenFileName(
            self, "비디오 선택", "", "비디오 파일 (*.mp4 *.avi *.mov)")

        if file_path:
            self.clear_results()
            self.is_video_running = True

            # 비디오 라이터 초기화
            cap = cv2.VideoCapture(file_path)
            frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
            frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
            fps = cap.get(cv2.CAP_PROP_FPS)
            cap.release()

            # 저장 경로 생성
            save_dir = "results"
            os.makedirs(save_dir, exist_ok=True)
            timestamp = time.strftime("%Y%m%d_%H%M%S")
            save_path = os.path.join(save_dir, f"result_{timestamp}.mp4")

            fourcc = cv2.VideoWriter_fourcc(*'mp4v')
            self.video_writer = cv2.VideoWriter(save_path, fourcc, fps, (frame_width, frame_height))

            # 탐지 스레드 생성
            conf = self.confidence_spinbox.value()
            iou = self.iou_spinbox.value()
            self.detection_thread = DetectionThread(self.model, file_path, conf, iou)
            self.detection_thread.frame_received.connect(self.on_frame_received)
            self.detection_thread.finished_signal.connect(self.on_detection_finished)
            self.detection_thread.start()

            self.update_status(f"비디오 탐지 중: {os.path.basename(file_path)}")

    def detect_camera(self):
        if self.detection_thread and self.detection_thread.isRunning():
            QMessageBox.warning(self, "경고", "현재 진행 중인 탐지 작업을 먼저 중지해주세요")
            return

        self.clear_results()
        self.is_camera_running = True

        # 탐지 스레드 생성 (기본 카메라 0번 사용)
        conf = self.confidence_spinbox.value()
        iou = self.iou_spinbox.value()
        self.detection_thread = DetectionThread(self.model, 0, conf, iou)
        self.detection_thread.frame_received.connect(self.on_frame_received)
        self.detection_thread.finished_signal.connect(self.on_detection_finished)
        self.detection_thread.start()

        self.update_status("카메라를 통한 실시간 탐지 중...")

로그인 및 인증 시스템

LoginWindow.py 파일 내용:

# 계정 검증 핵심 로직
def handle_login(self):
    username = self.username_input.text().strip()
    password = self.password_input.text().strip()
    
    if not username or not password:
        QMessageBox.warning(self, "경고", "사용자명과 비밀번호를 입력해주세요!")
        return
    
    if username in self.accounts and self.accounts[username] == password:
        self.accept()  # 검증 성공
    else:
        QMessageBox.warning(self, "오류", "사용자명 또는 비밀번호가 올바르지 않습니다!")

# 비밀번호 강도 확인 (가입 시)
def handle_register(self):
    if len(password) < 6:  # 비밀번호 길이 6자 이상
        QMessageBox.warning(self, "경고", "비밀번호는 최소 6자 이상이어야 합니다!")

다중 탐지 모드

main.py 파일 내용:

이미지 탐지

def detect_image(self):
    file_path, _ = QFileDialog.getOpenFileName(
        self, "이미지 선택", "", "이미지 파일 (*.jpg *.jpeg *.png *.bmp)")
    if file_path:
        self.detection_thread = DetectionThread(self.model, file_path, conf, iou)
        self.detection_thread.start()  # 탐지 스레드 시작

비디오 탐지

def detect_video(self):
    file_path, _ = QFileDialog.getOpenFileName(
        self, "비디오 선택", "", "비디오 파일 (*.mp4 *.avi *.mov)")
    if file_path:
        self.video_writer = cv2.VideoWriter()  # 비디오 라이터 초기화
        self.detection_thread = DetectionThread(self.model, file_path, conf, iou)

실시간 카메라

def detect_camera(self):
    self.detection_thread = DetectionThread(self.model, 0, conf, iou)  # 카메라 장치 번호 0
    self.detection_thread.start()

시각화 인터페이스

UiMain.py 파일 내용:

이중 화면 표시

def display_image(self, label, image):
    q_img = QImage(image.data, w, h, bytes_per_line, QImage.Format_RGB888)
    pixmap = QPixmap.fromImage(q_img)
    label.setPixmap(pixmap.scaled(label.size(), Qt.KeepAspectRatio))  # 비율 유지 자동 조절

결과 테이블

def add_detection_result(self, class_name, confidence, x, y):
    self.results_table.insertRow(row)
    items = [
        QTableWidgetItem(class_name),  # 카테고리 열
        QTableWidgetItem(f"{confidence:.2f}"),  # 신뢰도
        QTableWidgetItem(f"{x:.1f}"),  # X 좌표
        QTableWidgetItem(f"{y:.1f}")   # Y 좌표
    ]

파라미터 제어 시스템

UiMain.py 파일 내용:

이중 임계값 연동 제어

# 신뢰도 임계값 동기화
def update_confidence(self, value):
    confidence = value / 100.0
    self.confidence_spinbox.setValue(confidence)  # 슬라이더 → 수치 입력창
    self.confidence_label.setText(f"신뢰도 임계값: {confidence:.2f}")

# IoU 임계값 동기화  
def update_iou(self, value):
    iou = value / 100.0
    self.iou_spinbox.setValue(iou)

UI 미학적 디자인

UiMain.py 파일 내용:

미래지향적 스타일 버튼

def create_button(self, text, color):
    return f"""
    QPushButton {{
        border: 1px solid {color};
        color: {color};
        border-radius: 6px;
    }}
    QPushButton:hover {{
        background-color: {self.lighten_color(color, 10)};
        box-shadow: 0 0 10px {color};  # 호버 시 발광 효과
    }}
    """

동적 상태바

def update_status(self, message):
    self.status_bar.showMessage(
        f"상태: {message} | 마지막 업데이트: {time.strftime('%H:%M:%S')}"  # 실시간 타임스탬프
    )

지능형 작업 흐름

main.py 파일 내용:

스레드 관리

class DetectionThread(QThread):
    frame_received = pyqtSignal(np.ndarray, np.ndarray, list)  # 신호량 통신
    
    def run(self):
        while self.running:  # 멀티스레드 탐지 루프
            results = self.model(frame, conf=self.conf, iou=self.iou)
            self.frame_received.emit(original_frame, result_frame, detections)

태그: YOLOv12 컴퓨터비전 딥러닝 옥수수 잡초탐지

9월 20일 00:38에 게시됨