feat: 초기 프로젝트 구조 추가
3
ai/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# ai 패키지 — Detector, Trainer 노출
|
||||
from .detector import Detector
|
||||
from .trainer import Trainer
|
||||
9
ai/dataset/data.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
names:
|
||||
- 스크래치
|
||||
- 이물
|
||||
- 흑점
|
||||
- 변형
|
||||
nc: 4
|
||||
path: e:\ANT\ai\dataset
|
||||
train: images/train
|
||||
val: images/val
|
||||
BIN
ai/dataset/images/train/스크린샷 2026-04-27 175539.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
ai/dataset/images/train/스크린샷 2026-05-07 100837.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
ai/dataset/images/val/스크린샷 2026-04-27 175231.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
ai/dataset/labels/train.cache
Normal file
1
ai/dataset/labels/train/스크린샷 2026-04-27 175539.txt
Normal file
@@ -0,0 +1 @@
|
||||
0 0.233974 0.409524 0.339744 0.590476
|
||||
1
ai/dataset/labels/train/스크린샷 2026-05-07 100837.txt
Normal file
@@ -0,0 +1 @@
|
||||
0 0.318598 0.542781 0.472561 0.754011
|
||||
BIN
ai/dataset/labels/val.cache
Normal file
2
ai/dataset/labels/val/스크린샷 2026-04-27 175231.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
0 0.227074 0.195219 0.401747 0.215139
|
||||
1 0.853712 0.252988 0.257642 0.330677
|
||||
59
ai/detector.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# AI 추론 — YOLOv8 기반 불량(스크래치/이물/흑점/변형) 검출
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
from utils.path_helper import BASE_PATH
|
||||
|
||||
|
||||
class Detector:
|
||||
class_names = ["스크래치", "이물", "흑점", "변형"]
|
||||
|
||||
def __init__(self):
|
||||
self._model = None
|
||||
self.model_path = None
|
||||
|
||||
def load_model(self, model_path: str) -> bool:
|
||||
if model_path and not os.path.isabs(model_path):
|
||||
model_path = os.path.join(BASE_PATH, model_path)
|
||||
try:
|
||||
from ultralytics import YOLO # 지연 로딩 — 앱 시작 시 torch DLL 오류 방지
|
||||
self._model = YOLO(model_path)
|
||||
self.model_path = model_path
|
||||
print(f"[AI] 모델 로드 성공: {model_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[AI] 모델 로드 실패: {e}")
|
||||
self._model = None
|
||||
return False
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self._model is not None
|
||||
|
||||
def detect(self, image: np.ndarray) -> list:
|
||||
"""
|
||||
image: numpy array (BGR)
|
||||
반환: [{"class_id": int, "class_name": str,
|
||||
"confidence": float, "bbox": [x1,y1,x2,y2]}, ...]
|
||||
"""
|
||||
if self._model is None:
|
||||
return []
|
||||
try:
|
||||
results = self._model(image, verbose=False)
|
||||
detections = []
|
||||
for result in results:
|
||||
for box in result.boxes:
|
||||
class_id = int(box.cls[0])
|
||||
detections.append({
|
||||
"class_id": class_id,
|
||||
"class_name": (
|
||||
self.class_names[class_id]
|
||||
if class_id < len(self.class_names)
|
||||
else str(class_id)
|
||||
),
|
||||
"confidence": float(box.conf[0]),
|
||||
"bbox": box.xyxy[0].tolist(),
|
||||
})
|
||||
return detections
|
||||
except Exception as e:
|
||||
print(f"[AI] 추론 오류: {e}")
|
||||
return []
|
||||
BIN
ai/models/best.pt
Normal file
BIN
ai/runs/train/BoxF1_curve.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
ai/runs/train/BoxPR_curve.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
ai/runs/train/BoxP_curve.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
ai/runs/train/BoxR_curve.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
110
ai/runs/train/args.yaml
Normal file
@@ -0,0 +1,110 @@
|
||||
task: detect
|
||||
mode: train
|
||||
model: yolov8n.pt
|
||||
data: e:\ANT\ai\dataset\data.yaml
|
||||
epochs: 100
|
||||
time: null
|
||||
patience: 100
|
||||
batch: 16
|
||||
imgsz: 640
|
||||
save: true
|
||||
save_period: -1
|
||||
cache: false
|
||||
device: null
|
||||
workers: 0
|
||||
project: e:\ANT\ai\runs
|
||||
name: train
|
||||
exist_ok: true
|
||||
pretrained: true
|
||||
optimizer: auto
|
||||
verbose: true
|
||||
seed: 0
|
||||
deterministic: true
|
||||
single_cls: false
|
||||
rect: false
|
||||
cos_lr: false
|
||||
close_mosaic: 10
|
||||
resume: false
|
||||
amp: false
|
||||
fraction: 1.0
|
||||
profile: false
|
||||
freeze: null
|
||||
multi_scale: 0.0
|
||||
compile: false
|
||||
overlap_mask: true
|
||||
mask_ratio: 4
|
||||
dropout: 0.0
|
||||
val: true
|
||||
split: val
|
||||
save_json: false
|
||||
conf: null
|
||||
iou: 0.7
|
||||
max_det: 300
|
||||
half: false
|
||||
dnn: false
|
||||
plots: false
|
||||
end2end: null
|
||||
source: null
|
||||
vid_stride: 1
|
||||
stream_buffer: false
|
||||
visualize: false
|
||||
augment: false
|
||||
agnostic_nms: false
|
||||
classes: null
|
||||
retina_masks: false
|
||||
embed: null
|
||||
show: false
|
||||
save_frames: false
|
||||
save_txt: false
|
||||
save_conf: false
|
||||
save_crop: false
|
||||
show_labels: true
|
||||
show_conf: true
|
||||
show_boxes: true
|
||||
line_width: null
|
||||
format: torchscript
|
||||
keras: false
|
||||
optimize: false
|
||||
int8: false
|
||||
dynamic: false
|
||||
simplify: true
|
||||
opset: null
|
||||
workspace: null
|
||||
nms: false
|
||||
lr0: 0.01
|
||||
lrf: 0.01
|
||||
momentum: 0.937
|
||||
weight_decay: 0.0005
|
||||
warmup_epochs: 3.0
|
||||
warmup_momentum: 0.8
|
||||
warmup_bias_lr: 0.1
|
||||
box: 7.5
|
||||
cls: 0.5
|
||||
cls_pw: 0.0
|
||||
dfl: 1.5
|
||||
pose: 12.0
|
||||
kobj: 1.0
|
||||
rle: 1.0
|
||||
angle: 1.0
|
||||
nbs: 64
|
||||
hsv_h: 0.015
|
||||
hsv_s: 0.7
|
||||
hsv_v: 0.4
|
||||
degrees: 0.0
|
||||
translate: 0.1
|
||||
scale: 0.5
|
||||
shear: 0.0
|
||||
perspective: 0.0
|
||||
flipud: 0.0
|
||||
fliplr: 0.5
|
||||
bgr: 0.0
|
||||
mosaic: 1.0
|
||||
mixup: 0.0
|
||||
cutmix: 0.0
|
||||
copy_paste: 0.0
|
||||
copy_paste_mode: flip
|
||||
auto_augment: randaugment
|
||||
erasing: 0.4
|
||||
cfg: null
|
||||
tracker: botsort.yaml
|
||||
save_dir: E:\ANT\ai\runs\train
|
||||
BIN
ai/runs/train/confusion_matrix.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
ai/runs/train/confusion_matrix_normalized.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
101
ai/runs/train/results.csv
Normal file
@@ -0,0 +1,101 @@
|
||||
epoch,time,train/box_loss,train/cls_loss,train/dfl_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/dfl_loss,lr/pg0,lr/pg1,lr/pg2
|
||||
1,0.744601,2.4784,4.2576,2.37292,0.00581,0.5,0.00599,0.00178,2.08106,4.5092,2.3941,0,0,0
|
||||
2,1.17594,3.54636,4.89413,2.76008,0.00588,0.5,0.00622,0.00124,2.10159,4.51392,2.40401,1.23763e-05,1.23763e-05,1.23763e-05
|
||||
3,1.4434,3.2212,4.47996,3.91873,0.01514,1,0.01568,0.0022,2.24221,4.37976,2.4629,2.4505e-05,2.4505e-05,2.4505e-05
|
||||
4,1.81097,3.23832,4.18544,3.38558,0.01459,1,0.01534,0.0037,2.32948,4.47892,2.46147,3.63862e-05,3.63862e-05,3.63862e-05
|
||||
5,2.17724,2.76973,4.43589,2.74067,0.00556,0.5,0.00711,0.00195,2.33679,4.46132,2.45867,4.802e-05,4.802e-05,4.802e-05
|
||||
6,2.47758,2.93916,4.25681,2.81574,0.00562,0.5,0.00711,0.00197,2.23356,4.4439,2.55513,5.94062e-05,5.94062e-05,5.94062e-05
|
||||
7,2.77856,2.95798,3.80506,2.41629,0.01433,1,0.0167,0.00167,2.13047,4.47684,2.46296,7.0545e-05,7.0545e-05,7.0545e-05
|
||||
8,3.08538,2.97272,3.8706,2.66583,0.00847,0.5,0.00939,0.00094,1.93983,4.4964,2.44096,8.14362e-05,8.14362e-05,8.14362e-05
|
||||
9,3.38628,1.96121,4.27233,2.04591,0,0,0,0,1.67434,4.64554,2.3051,9.208e-05,9.208e-05,9.208e-05
|
||||
10,3.68733,2.62766,3.67184,2.50334,0,0,0,0,1.44683,4.71852,2.19216,0.000102476,0.000102476,0.000102476
|
||||
11,3.97817,2.60241,4.1424,2.70991,0.00556,0.5,0.01605,0.0016,1.42181,4.70011,2.16441,0.000112625,0.000112625,0.000112625
|
||||
12,4.28608,3.28551,4.52089,2.67545,0.00588,0.5,0.01345,0.00269,1.52292,4.67835,2.13383,0.000122526,0.000122526,0.000122526
|
||||
13,4.58546,2.2033,4.2669,2.49512,0.00562,0.5,0.01157,0.00231,1.68387,4.61194,2.17338,0.00013218,0.00013218,0.00013218
|
||||
14,4.88485,2.87394,4.40826,2.70916,0.00568,0.5,0.00975,0.00195,1.89075,4.58147,2.30027,0.000141586,0.000141586,0.000141586
|
||||
15,5.19968,2.71982,4.27341,2.72914,0,0,0,0,1.96866,4.44855,2.33624,0.000150745,0.000150745,0.000150745
|
||||
16,5.49215,1.68605,4.24646,1.87531,0,0,0,0,2.00553,4.48707,2.29285,0.000159656,0.000159656,0.000159656
|
||||
17,5.79325,2.00138,3.94773,2.26758,0,0,0,0,2.09648,4.63246,2.4007,0.00016832,0.00016832,0.00016832
|
||||
18,6.09155,3.05671,5.53908,2.66707,0,0,0,0,2.09648,4.63246,2.4007,0.000176736,0.000176736,0.000176736
|
||||
19,6.39052,2.20901,3.52372,2.04393,0.00806,0.5,0.02369,0.00237,2.20865,4.54601,2.56285,0.000184905,0.000184905,0.000184905
|
||||
20,6.66892,2.4483,3.70065,2.23467,0.00806,0.5,0.02369,0.00237,2.20865,4.54601,2.56285,0.000192826,0.000192826,0.000192826
|
||||
21,6.9332,2.19448,3.86943,2.10117,0.00463,0.5,0.03317,0.00663,2.32559,4.48125,2.62548,0.0002005,0.0002005,0.0002005
|
||||
22,7.30519,2.9479,4.3599,2.76934,0.00463,0.5,0.03317,0.00663,2.32559,4.48125,2.62548,0.000207926,0.000207926,0.000207926
|
||||
23,7.68986,1.39714,3.40426,1.6151,0.01578,1,0.08837,0.01146,2.44041,4.6826,2.72339,0.000215105,0.000215105,0.000215105
|
||||
24,7.99967,2.46841,3.48691,2.13383,0.01578,1,0.08837,0.01146,2.44041,4.6826,2.72339,0.000222036,0.000222036,0.000222036
|
||||
25,8.31396,2.56835,4.10281,2.20972,0.00481,0.5,0.01777,0.00355,2.61255,4.61058,2.86581,0.00022872,0.00022872,0.00022872
|
||||
26,8.55453,1.65391,3.70866,1.88456,0.00481,0.5,0.01777,0.00355,2.61255,4.61058,2.86581,0.000235156,0.000235156,0.000235156
|
||||
27,8.80235,2.00119,3.45089,1.89926,0.01604,1,0.05212,0.00521,2.74287,4.6437,2.91424,0.000241345,0.000241345,0.000241345
|
||||
28,9.04528,1.89956,3.68555,2.16165,0.01604,1,0.05212,0.00521,2.74287,4.6437,2.91424,0.000247286,0.000247286,0.000247286
|
||||
29,9.29273,1.42472,2.92227,1.75125,0.01111,0.5,0.12437,0.01244,3.06977,4.5736,3.06093,0.00025298,0.00025298,0.00025298
|
||||
30,9.60359,1.35767,2.62078,1.55004,0.01111,0.5,0.12437,0.01244,3.06977,4.5736,3.06093,0.000258426,0.000258426,0.000258426
|
||||
31,9.94422,2.37035,4.66417,2.27559,0.01316,0.5,0.16583,0.01658,3.36893,4.24859,3.23425,0.000263625,0.000263625,0.000263625
|
||||
32,10.2594,1.61322,2.86179,1.53237,0.01316,0.5,0.16583,0.01658,3.36893,4.24859,3.23425,0.000268576,0.000268576,0.000268576
|
||||
33,10.6412,1.85856,3.15064,1.88045,0.02083,0.5,0.16583,0.01658,3.44544,4.47775,3.34489,0.00027328,0.00027328,0.00027328
|
||||
34,10.9661,1.62452,2.88937,1.72429,0.02083,0.5,0.16583,0.01658,3.44544,4.47775,3.34489,0.000277736,0.000277736,0.000277736
|
||||
35,11.2888,1.21431,3.51628,1.62717,0,0,0,0,3.47481,4.57555,3.33539,0.000281945,0.000281945,0.000281945
|
||||
36,11.5318,1.39337,2.94668,1.47319,0,0,0,0,3.47481,4.57555,3.33539,0.000285906,0.000285906,0.000285906
|
||||
37,11.7887,2.14196,3.0238,1.80533,0,0,0,0,3.7452,4.84862,3.44227,0.00028962,0.00028962,0.00028962
|
||||
38,12.0408,1.81846,2.33607,1.79617,0,0,0,0,3.7452,4.84862,3.44227,0.000293086,0.000293086,0.000293086
|
||||
39,12.2951,1.17175,1.92086,1.3652,0,0,0,0,4.06912,4.91101,3.62994,0.000296305,0.000296305,0.000296305
|
||||
40,12.5315,1.62593,2.48208,1.50529,0,0,0,0,4.06912,4.91101,3.62994,0.000299276,0.000299276,0.000299276
|
||||
41,12.8727,2.11165,2.67321,1.7814,0,0,0,0,4.39582,4.54093,3.81935,0.000302,0.000302,0.000302
|
||||
42,13.1287,1.58216,2.68408,1.63978,0,0,0,0,4.39582,4.54093,3.81935,0.000304476,0.000304476,0.000304476
|
||||
43,13.4027,1.4236,2.75829,1.527,0.01351,0.5,0.02163,0.00216,4.3829,4.34724,3.73475,0.000306705,0.000306705,0.000306705
|
||||
44,13.6611,1.58086,3.04931,1.97095,0.01351,0.5,0.02163,0.00216,4.3829,4.34724,3.73475,0.000308686,0.000308686,0.000308686
|
||||
45,13.94,1.70552,2.1028,1.7847,0.0119,0.5,0.03109,0.00311,4.02893,4.68709,3.46274,0.00031042,0.00031042,0.00031042
|
||||
46,14.2063,1.17287,2.30212,1.21687,0.0119,0.5,0.03109,0.00311,4.02893,4.68709,3.46274,0.000311906,0.000311906,0.000311906
|
||||
47,14.4787,0.75953,1.79745,0.99198,0.01111,0.5,0.04523,0.00452,4.16401,4.4644,3.55846,0.000313145,0.000313145,0.000313145
|
||||
48,14.7356,2.12559,4.16293,2.21951,0.01111,0.5,0.04523,0.00452,4.16401,4.4644,3.55846,0.000314136,0.000314136,0.000314136
|
||||
49,15.0753,0.92121,2.05049,1.10199,0.00962,0.5,0.04146,0.00829,3.90365,4.64333,3.48688,0.00031488,0.00031488,0.00031488
|
||||
50,15.3345,1.19831,1.99876,1.28056,0.00962,0.5,0.04146,0.00829,3.90365,4.64333,3.48688,0.000315376,0.000315376,0.000315376
|
||||
51,15.6062,2.135,2.40625,1.81904,0.01042,0.5,0.05528,0.0229,3.18088,5.77238,3.2612,0.000315625,0.000315625,0.000315625
|
||||
52,15.9429,1.72253,3.24847,1.71661,0.01042,0.5,0.05528,0.0229,3.18088,5.77238,3.2612,0.000315626,0.000315626,0.000315626
|
||||
53,16.2756,0.98827,2.52014,1.26921,0.01042,0.5,0.05528,0.0229,3.18088,5.77238,3.2612,0.00031538,0.00031538,0.00031538
|
||||
54,16.6349,1.8328,2.53368,1.66716,0.01316,0.5,0.12437,0.01244,3.8633,4.65938,3.23911,0.000314886,0.000314886,0.000314886
|
||||
55,16.8944,1.1818,1.78296,1.15008,0.01316,0.5,0.12437,0.01244,3.8633,4.65938,3.23911,0.000314145,0.000314145,0.000314145
|
||||
56,17.2351,1.26713,2.38825,1.40594,0.01316,0.5,0.12437,0.01244,3.8633,4.65938,3.23911,0.000313156,0.000313156,0.000313156
|
||||
57,17.5174,1.32827,1.94378,1.44297,0.01562,0.5,0.02073,0.00207,3.86529,4.77191,3.13775,0.00031192,0.00031192,0.00031192
|
||||
58,17.7998,0.93866,1.97455,1.19817,0.01562,0.5,0.02073,0.00207,3.86529,4.77191,3.13775,0.000310436,0.000310436,0.000310436
|
||||
59,18.0565,1.34164,4.69334,1.52493,0.01562,0.5,0.02073,0.00207,3.86529,4.77191,3.13775,0.000308705,0.000308705,0.000308705
|
||||
60,18.3262,1.70827,3.61104,1.67179,0,0,0,0,3.93656,4.99738,3.11248,0.000306726,0.000306726,0.000306726
|
||||
61,18.6133,0.82569,1.96032,1.1105,0,0,0,0,3.93656,4.99738,3.11248,0.0003045,0.0003045,0.0003045
|
||||
62,18.8669,1.33918,2.16787,1.37237,0,0,0,0,3.93656,4.99738,3.11248,0.000302026,0.000302026,0.000302026
|
||||
63,19.133,0.9404,2.00419,1.09295,0,0,0,0,3.54929,5.50927,3.04358,0.000299305,0.000299305,0.000299305
|
||||
64,19.4517,0.83957,1.71834,1.04536,0,0,0,0,3.54929,5.50927,3.04358,0.000296336,0.000296336,0.000296336
|
||||
65,19.7046,1.05975,1.98836,1.25573,0,0,0,0,3.54929,5.50927,3.04358,0.00029312,0.00029312,0.00029312
|
||||
66,19.966,0.79495,1.71511,1.07986,0.01111,0.5,0.02073,0.00415,3.48677,6.07562,2.94905,0.000289656,0.000289656,0.000289656
|
||||
67,20.2315,0.89674,1.76647,1.12805,0.01111,0.5,0.02073,0.00415,3.48677,6.07562,2.94905,0.000285945,0.000285945,0.000285945
|
||||
68,20.4951,1.27186,2.05522,1.24621,0.01111,0.5,0.02073,0.00415,3.48677,6.07562,2.94905,0.000281986,0.000281986,0.000281986
|
||||
69,20.763,0.72455,1.86789,1.0569,0,0,0,0,4.16796,5.98914,3.4865,0.00027778,0.00027778,0.00027778
|
||||
70,21.0206,0.94838,1.5672,1.15049,0,0,0,0,4.16796,5.98914,3.4865,0.000273326,0.000273326,0.000273326
|
||||
71,21.2798,1.62956,2.8345,1.75908,0,0,0,0,4.16796,5.98914,3.4865,0.000268625,0.000268625,0.000268625
|
||||
72,21.6185,0.82159,1.62683,0.94171,0,0,0,0,4.70051,5.39397,3.87655,0.000263676,0.000263676,0.000263676
|
||||
73,21.8885,0.84998,2.02611,1.30797,0,0,0,0,4.70051,5.39397,3.87655,0.00025848,0.00025848,0.00025848
|
||||
74,22.1454,0.85087,1.42472,1.15593,0,0,0,0,4.70051,5.39397,3.87655,0.000253036,0.000253036,0.000253036
|
||||
75,22.4095,1.1786,1.83474,1.21092,0,0,0,0,4.86689,6.24371,3.95955,0.000247345,0.000247345,0.000247345
|
||||
76,22.6694,0.78564,1.66187,0.97539,0,0,0,0,4.86689,6.24371,3.95955,0.000241406,0.000241406,0.000241406
|
||||
77,22.9286,0.79013,1.96926,1.26206,0,0,0,0,4.86689,6.24371,3.95955,0.00023522,0.00023522,0.00023522
|
||||
78,23.1943,0.8152,1.59465,1.07906,0,0,0,0,4.81344,5.97521,4.01022,0.000228786,0.000228786,0.000228786
|
||||
79,23.4546,0.61829,1.29337,0.98868,0,0,0,0,4.81344,5.97521,4.01022,0.000222105,0.000222105,0.000222105
|
||||
80,23.7685,0.95007,1.79471,1.1416,0,0,0,0,4.81344,5.97521,4.01022,0.000215176,0.000215176,0.000215176
|
||||
81,24.1034,0.93272,2.22583,1.20421,0.00617,0.5,0.00905,0.0009,4.60774,5.39816,3.81369,0.000208,0.000208,0.000208
|
||||
82,24.3629,0.63241,1.21512,1.08097,0.00617,0.5,0.00905,0.0009,4.60774,5.39816,3.81369,0.000200576,0.000200576,0.000200576
|
||||
83,24.6222,1.39649,2.39204,1.52958,0.00617,0.5,0.00905,0.0009,4.60774,5.39816,3.81369,0.000192905,0.000192905,0.000192905
|
||||
84,24.9101,0.63485,1.24139,0.96726,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.000184986,0.000184986,0.000184986
|
||||
85,25.1903,0.88995,1.76109,1.04753,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.00017682,0.00017682,0.00017682
|
||||
86,25.4597,1.16455,2.35159,1.15176,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.000168406,0.000168406,0.000168406
|
||||
87,25.761,0.86574,1.60094,1.1271,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.000159745,0.000159745,0.000159745
|
||||
88,26.1232,0.57931,1.17361,0.95885,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.000150836,0.000150836,0.000150836
|
||||
89,26.4022,0.88857,1.97356,1.07497,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.00014168,0.00014168,0.00014168
|
||||
90,26.7071,0.71015,1.22162,1.07789,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.000132276,0.000132276,0.000132276
|
||||
91,27.0615,0.80611,2.15097,0.95926,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.000122625,0.000122625,0.000122625
|
||||
92,27.3798,0.75395,1.9817,1.18844,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,0.000112726,0.000112726,0.000112726
|
||||
93,27.6734,0.52999,2.16673,0.94249,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,0.00010258,0.00010258,0.00010258
|
||||
94,27.9692,0.29519,1.34975,0.94508,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,9.21862e-05,9.21862e-05,9.21862e-05
|
||||
95,28.2525,0.533,1.80971,0.96369,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,8.1545e-05,8.1545e-05,8.1545e-05
|
||||
96,28.5584,0.37619,1.67301,0.82287,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,7.06563e-05,7.06563e-05,7.06563e-05
|
||||
97,28.8522,0.58993,1.73032,0.93823,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,5.952e-05,5.952e-05,5.952e-05
|
||||
98,29.2014,1.19689,2.95317,1.27242,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,4.81363e-05,4.81363e-05,4.81363e-05
|
||||
99,29.5194,0.44641,1.50801,0.93925,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,3.6505e-05,3.6505e-05,3.6505e-05
|
||||
100,29.8906,0.52149,1.81476,0.89577,0.0061,0.5,0.01157,0.00231,3.45621,5.51821,2.93443,2.46263e-05,2.46263e-05,2.46263e-05
|
||||
|
BIN
ai/runs/train/results.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
ai/runs/train/train_batch0.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ai/runs/train/train_batch1.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ai/runs/train/train_batch2.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
ai/runs/train/train_batch90.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
ai/runs/train/train_batch91.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
ai/runs/train/train_batch92.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
ai/runs/train/val_batch0_labels.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
ai/runs/train/val_batch0_pred.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ai/runs/train/weights/best.pt
Normal file
BIN
ai/runs/train/weights/last.pt
Normal file
269
ai/trainer.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# AI 학습 — YOLOv8 재학습 및 모델 저장
|
||||
import multiprocessing
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
|
||||
from utils.path_helper import get_path
|
||||
|
||||
import yaml
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
|
||||
|
||||
class Trainer:
|
||||
def __init__(self):
|
||||
self.model = None
|
||||
self.is_training = False
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def prepare_dataset(self, image_folder: str) -> str:
|
||||
dataset_dir = get_path("ai", "dataset")
|
||||
if os.path.exists(dataset_dir):
|
||||
shutil.rmtree(dataset_dir)
|
||||
|
||||
for split in ("train", "val"):
|
||||
os.makedirs(os.path.join(dataset_dir, "images", split), exist_ok=True)
|
||||
os.makedirs(os.path.join(dataset_dir, "labels", split), exist_ok=True)
|
||||
|
||||
pairs = []
|
||||
for f in os.listdir(image_folder):
|
||||
if f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")):
|
||||
img_path = os.path.join(image_folder, f)
|
||||
txt_path = os.path.join(
|
||||
image_folder, os.path.splitext(f)[0] + ".txt"
|
||||
)
|
||||
if os.path.exists(txt_path):
|
||||
pairs.append((img_path, txt_path))
|
||||
|
||||
random.shuffle(pairs)
|
||||
split_idx = int(len(pairs) * 0.8)
|
||||
train_pairs = pairs[:split_idx]
|
||||
val_pairs = pairs[split_idx:]
|
||||
|
||||
for img, lbl in train_pairs:
|
||||
shutil.copy(img, os.path.join(dataset_dir, "images", "train"))
|
||||
shutil.copy(lbl, os.path.join(dataset_dir, "labels", "train"))
|
||||
for img, lbl in val_pairs:
|
||||
shutil.copy(img, os.path.join(dataset_dir, "images", "val"))
|
||||
shutil.copy(lbl, os.path.join(dataset_dir, "labels", "val"))
|
||||
|
||||
yaml_content = {
|
||||
"path": dataset_dir,
|
||||
"train": "images/train",
|
||||
"val": "images/val",
|
||||
"nc": 4,
|
||||
"names": ["스크래치", "이물", "흑점", "변형"],
|
||||
}
|
||||
yaml_path = os.path.join(dataset_dir, "data.yaml")
|
||||
with open(yaml_path, "w", encoding="utf-8") as fh:
|
||||
yaml.dump(yaml_content, fh, allow_unicode=True)
|
||||
|
||||
return yaml_path
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def train(
|
||||
self,
|
||||
image_folder: str,
|
||||
epochs: int,
|
||||
batch: int,
|
||||
save_path: str,
|
||||
log_callback=None,
|
||||
progress_callback=None,
|
||||
):
|
||||
self.is_training = True
|
||||
|
||||
try:
|
||||
if log_callback:
|
||||
log_callback("데이터셋 준비 중...")
|
||||
yaml_path = self.prepare_dataset(image_folder)
|
||||
if log_callback:
|
||||
log_callback(f"데이터셋 준비 완료: {yaml_path}")
|
||||
|
||||
if log_callback:
|
||||
log_callback("YOLOv8 모델 로드 중...")
|
||||
from ultralytics import YOLO # 지연 로딩 — 앱 시작 시 torch DLL 오류 방지
|
||||
self.model = YOLO("yolov8n.pt")
|
||||
|
||||
if log_callback:
|
||||
log_callback(f"학습 시작 (epoch={epochs}, batch={batch})")
|
||||
|
||||
def _on_epoch_end(trainer):
|
||||
ep = trainer.epoch + 1
|
||||
try:
|
||||
loss_val = float(trainer.loss)
|
||||
loss_str = f"{loss_val:.4f}"
|
||||
except Exception:
|
||||
loss_str = "?"
|
||||
if log_callback:
|
||||
log_callback(f"Epoch {ep}/{epochs} loss={loss_str}")
|
||||
if progress_callback:
|
||||
progress_callback(int(ep / epochs * 100))
|
||||
|
||||
self.model.add_callback("on_train_epoch_end", _on_epoch_end)
|
||||
|
||||
self.model.train(
|
||||
data=yaml_path,
|
||||
epochs=epochs,
|
||||
batch=batch,
|
||||
imgsz=640,
|
||||
project=get_path("ai", "runs"),
|
||||
name="train",
|
||||
exist_ok=True,
|
||||
verbose=True,
|
||||
workers=0, # disable DataLoader multiprocessing inside subprocess
|
||||
amp=False, # AMP check also spawns a subprocess on Windows
|
||||
plots=False, # matplotlib can interfere with Qt event loop
|
||||
)
|
||||
|
||||
# best.pt 복사
|
||||
best_pt = get_path("ai", "runs", "train", "weights", "best.pt")
|
||||
if os.path.exists(best_pt):
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
shutil.copy(best_pt, save_path)
|
||||
if log_callback:
|
||||
log_callback(f"모델 저장 완료: {save_path}")
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(100)
|
||||
if log_callback:
|
||||
log_callback("학습 완료!")
|
||||
|
||||
except BaseException as e:
|
||||
import traceback
|
||||
if log_callback:
|
||||
try:
|
||||
log_callback(f"학습 오류: {e}")
|
||||
log_callback(traceback.format_exc())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
finally:
|
||||
self.is_training = False
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def stop(self):
|
||||
self.is_training = False
|
||||
|
||||
|
||||
# ====================================================================== #
|
||||
# Subprocess entry point — defined at module level so it is picklable.
|
||||
# ultralytics training can call sys.exit() internally; running it in a
|
||||
# separate process completely isolates the Qt application from that.
|
||||
# ====================================================================== #
|
||||
|
||||
def _train_subprocess_main(queue, image_folder, epochs, batch, save_path):
|
||||
"""Entry point for the isolated training subprocess."""
|
||||
try:
|
||||
trainer = Trainer()
|
||||
|
||||
def _log(msg):
|
||||
try:
|
||||
queue.put(("log", msg))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _progress(pct):
|
||||
try:
|
||||
queue.put(("progress", int(pct)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
trainer.train(
|
||||
image_folder=image_folder,
|
||||
epochs=epochs,
|
||||
batch=batch,
|
||||
save_path=save_path,
|
||||
log_callback=_log,
|
||||
progress_callback=_progress,
|
||||
)
|
||||
queue.put(("done", True))
|
||||
|
||||
except BaseException as e:
|
||||
import traceback
|
||||
try:
|
||||
queue.put(("log", f"학습 오류: {e}"))
|
||||
queue.put(("log", traceback.format_exc()))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
queue.put(("done", False))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ====================================================================== #
|
||||
|
||||
|
||||
class TrainWorker(QThread):
|
||||
log_signal = pyqtSignal(str)
|
||||
progress_signal = pyqtSignal(int)
|
||||
finished_signal = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, trainer, image_folder, epochs, batch, save_path):
|
||||
super().__init__()
|
||||
self.trainer = trainer
|
||||
self.image_folder = image_folder
|
||||
self.epochs = epochs
|
||||
self.batch = batch
|
||||
self.save_path = save_path
|
||||
self._proc = None # training subprocess handle
|
||||
|
||||
def run(self):
|
||||
# Spawn an isolated subprocess so that any sys.exit() call inside
|
||||
# ultralytics does not reach PyQt5's QThread handler and trigger
|
||||
# QApplication.exit() in the main process.
|
||||
ctx = multiprocessing.get_context("spawn")
|
||||
q = ctx.Queue()
|
||||
|
||||
self._proc = ctx.Process(
|
||||
target=_train_subprocess_main,
|
||||
args=(q, self.image_folder, self.epochs, self.batch, self.save_path),
|
||||
daemon=True,
|
||||
)
|
||||
self._proc.start()
|
||||
|
||||
success = False
|
||||
while True:
|
||||
proc_alive = self._proc.is_alive()
|
||||
try:
|
||||
msg_type, msg_data = q.get(timeout=0.3)
|
||||
except Exception:
|
||||
# queue.Empty — check if subprocess died unexpectedly
|
||||
if not proc_alive:
|
||||
# Give one last chance to read remaining messages
|
||||
while True:
|
||||
try:
|
||||
msg_type, msg_data = q.get_nowait()
|
||||
except Exception:
|
||||
break
|
||||
if msg_type == "log":
|
||||
self.log_signal.emit(str(msg_data))
|
||||
elif msg_type == "progress":
|
||||
self.progress_signal.emit(int(msg_data))
|
||||
elif msg_type == "done":
|
||||
success = bool(msg_data)
|
||||
break
|
||||
continue
|
||||
|
||||
if msg_type == "log":
|
||||
self.log_signal.emit(str(msg_data))
|
||||
elif msg_type == "progress":
|
||||
self.progress_signal.emit(int(msg_data))
|
||||
elif msg_type == "done":
|
||||
success = bool(msg_data)
|
||||
break
|
||||
|
||||
self._proc.join(timeout=30)
|
||||
if self._proc.is_alive():
|
||||
self._proc.terminate()
|
||||
self._proc.join(timeout=5)
|
||||
|
||||
self.finished_signal.emit(success)
|
||||
|
||||
def stop_subprocess(self):
|
||||
"""Call from the main thread to forcefully stop training."""
|
||||
if self._proc and self._proc.is_alive():
|
||||
self._proc.terminate()
|
||||