전력 변전소 가시광 이미지 결함 탐지 시스템 구축

Deep Learning Framework를 활용한 YOLOv8 기반의 전력 변전소 가시광 이미지 결함 탐지 시스템 개발

이 문서에서는 YOLOv8 모델을 사용하여 전력 변전소에서 발생할 수 있는 다양한 결함(계기 손상, 절연체 손상, 유출, 호흡기 이상 등)을 자동으로 탐지하는 방법을 다룹니다.

환경 설정

먼저 필요한 라이브러리를 설치합니다. 다음 명령어를 실행하세요:

pip install ultralytics lxml opencv-python-headless pandas scikit-learn

데이터 전처리

XML 형식의 레이블 파일을 YOLOv8에 맞는 TXT 형식으로 변환해야 합니다. 다음은 convert_xml_to_yolo.py 스크립트입니다:

import os
from xml.etree.ElementTree import parse
from pathlib import Path

base_dir = Path("datasets/substation_inspection")
xml_dir = base_dir / "Annotations"
image_dir = base_dir / "JPEGImages"
output_dir = base_dir / "labels"

os.makedirs(output_dir, exist_ok=True)

class_map = {
    "bjdsyc": 0,
    "bj_wkps": 1,
    "yw_nc": 2,
    "xmbhyc": 3,
    "kgg_ybh": 4,
    "gbps": 5,
    "yw_gkxfw": 6,
    "hxq_gjbs": 7,
    "bj_bpmh": 8,
    "jyz_pl": 9,
    "bj_bpps": 10,
    "sly_dmyw": 11,
    "wcaqm": 12,
    "wcgz": 13,
    "ywzt_yfyc": 14,
    "hxq_gitps": 15,
    "xy": 16,
}

def convert_bbox(image_width, image_height, bbox):
    x_min, y_min, x_max, y_max = map(float, bbox)
    center_x = ((x_min + x_max) / 2) / image_width
    center_y = ((y_min + y_max) / 2) / image_height
    width = (x_max - x_min) / image_width
    height = (y_max - y_min) / image_height
    return center_x, center_y, width, height

for xml_file in xml_dir.glob("*.xml"):
    tree = parse(xml_file)
    root = tree.getroot()

    image_width = int(root.find("size/width").text)
    image_height = int(root.find("size/height").text)

    with open(output_dir / f"{xml_file.stem}.txt", "w") as output:
        for obj in root.findall("object"):
            class_name = obj.find("name").text.replace(".", "_")
            if class_name not in class_map:
                continue
            class_id = class_map[class_name]
            bbox = obj.find("bndbox")
            bbox_data = [bbox.find(tag).text for tag in ["xmin", "ymin", "xmax", "ymax"]]
            yolo_format = convert_bbox(image_width, image_height, bbox_data)
            output.write(f"{class_id} {' '.join(map(str, yolo_format))}\n")

print("변환이 완료되었습니다.")

YAML 구성 파일 작성

데이터셋을 설명하는 substation_inspection.yaml 파일을 작성합니다:

train: ../datasets/substation_inspection/train/images
val: ../datasets/substation_inspection/val/images

nc: 17
names:
  - bjdsyc
  - bj_wkps
  - yw_nc
  - xmbhyc
  - kgg_ybh
  - gbps
  - yw_gkxfw
  - hxq_gjbs
  - bj_bpmh
  - jyz_pl
  - bj_bpps
  - sly_dmyw
  - wcaqm
  - wcgz
  - ywzt_yfyc
  - hxq_gitps
  - xy

데이터셋 분리

다음 스크립트를 사용하여 데이터셋을 훈련용, 검증용, 테스트용으로 나눕니다:

import os
import random
from sklearn.model_selection import train_test_split
from pathlib import Path

dataset_dir = Path("datasets/substation_inspection")
images_dir = dataset_dir / "JPEGImages"
annotations_dir = dataset_dir / "labels"

train_images_dir = dataset_dir / "train/images"
train_labels_dir = dataset_dir / "train/labels"
val_images_dir = dataset_dir / "val/images"
val_labels_dir = dataset_dir / "val/labels"
test_images_dir = dataset_dir / "test/images"
test_labels_dir = dataset_dir / "test/labels"

os.makedirs(train_images_dir, exist_ok=True)
os.makedirs(train_labels_dir, exist_ok=True)
os.makedirs(val_images_dir, exist_ok=True)
os.makedirs(val_labels_dir, exist_ok=True)
os.makedirs(test_images_dir, exist_ok=True)
os.makedirs(test_labels_dir, exist_ok=True)

image_files = list(images_dir.glob("*.jpg"))
random.shuffle(image_files)

train_ratio = 0.7
val_ratio = 0.15
test_ratio = 0.15

num_images = len(image_files)
train_split = int(num_images * train_ratio)
val_split = int(num_images * (train_ratio + val_ratio))

train_set = image_files[:train_split]
val_set = image_files[train_split:val_split]
test_set = image_files[val_split:]

def copy_dataset(dataset, dest_image_dir, dest_label_dir):
    for img_path in dataset:
        label_path = annotations_dir / f"{img_path.stem}.txt"
        if label_path.exists():
            os.symlink(img_path, dest_image_dir / img_path.name)
            os.symlink(label_path, dest_label_dir / label_path.name)

copy_dataset(train_set, train_images_dir, train_labels_dir)
copy_dataset(val_set, val_images_dir, val_labels_dir)
copy_dataset(test_set, test_images_dir, test_labels_dir)

print("데이터셋 분리가 완료되었습니다.")

모델 훈련

다음 스크립트를 통해 YOLOv8 모델을 훈련합니다:

from ultralytics import YOLO

model = YOLO("yolov8n.pt")
results = model.train(
    data="../datasets/substation_inspection/substation_inspection.yaml",
    epochs=50,
    imgsz=128,
    batch=16,
    project="../runs/train",
    name="substation_inspection_detection"
)

metrics = model.val()
model.export(format="onnx")

평가 및 결과 시각화

훈련된 모델을 평가하고 성능을 확인합니다:

from ultralytics import YOLO

model = YOLO("../runs/train/substation_inspection_detection/weights/best.pt")
metrics = model.val(data="../datasets/substation_inspection/substation_inspection.yaml", conf=0.5, iou=0.45)
print(metrics)

GUI 개발

PyQt5를 사용하여 간단한 사용자 인터페이스를 생성합니다:

import sys
import cv2
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget, QFileDialog
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtCore import Qt, QTimer
from ultralytics import YOLO

model = YOLO("../runs/train/substation_inspection_detection/weights/best.pt")

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("전력 변전소 결함 탐지 시스템")
        self.setGeometry(100, 100, 800, 600)
        self.init_ui()

    def init_ui(self):
        central_widget = QWidget()
        layout = QVBoxLayout()

        self.image_label = QLabel(self)
        self.image_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.image_label)

        load_button = QPushButton("이미지 선택", self)
        load_button.clicked.connect(self.load_image)
        layout.addWidget(load_button)

        predict_button = QPushButton("결과 예측", self)
        predict_button.clicked.connect(self.predict_image)
        layout.addWidget(predict_button)

        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

        self.image_path = None

    def load_image(self):
        file_name, _ = QFileDialog.getOpenFileName(self, "이미지 선택", "", "Images (*.png *.jpg *.jpeg);;All Files (*)")
        if file_name:
            self.image_path = file_name
            pixmap = QPixmap(file_name)
            scaled_pixmap = pixmap.scaled(self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio)
            self.image_label.setPixmap(scaled_pixmap)

    def predict_image(self):
        if not self.image_path:
            return

        image = cv2.imread(self.image_path)
        results = model.predict(image, size=128, conf=0.5, iou=0.45)[0]

        for box in results.boxes.cpu().numpy():
            r = box.xyxy[0].astype(int)
            cls = int(box.cls[0])
            conf = box.conf[0]

            class_names = [
                "계기 손상", "절연체 손상", "유출", "호흡기 이상", "이물질",
                *[f"기타{i}" for i in range(12)]
            ]
            class_name = class_names[cls]

            cv2.rectangle(image, (r[0], r[1]), (r[2], r[3]), (0, 255, 0), 2)
            cv2.putText(
                image,
                f"{class_name} ({conf:.2f})",
                (r[0], r[1] - 10),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.9,
                (0, 255, 0),
                2
            )

        h, w, ch = image.shape
        bytes_per_line = ch * w
        qt_image = QImage(image.data, w, h, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
        pixmap = QPixmap.fromImage(qt_image)
        scaled_pixmap = pixmap.scaled(self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio)
        self.image_label.setPixmap(scaled_pixmap)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

위의 코드들을 순서대로 실행하면, 전력 변전소 가시광 이미지 결함 탐지 시스템을 완성할 수 있습니다.

태그: YOLOv8 deep-learning object-detection

5월 24일 12:36에 게시됨