YMmim commited on
Commit
017c046
ยท
verified ยท
1 Parent(s): 8700c13

Object detection from scratch: Faster R-CNN + YOLO comparison

Browse files
Files changed (7) hide show
  1. box_utils.py +163 -0
  2. dataset.py +139 -0
  3. infer.py +149 -0
  4. losses.py +145 -0
  5. model.py +230 -0
  6. train.py +182 -0
  7. yolo_infer.py +88 -0
box_utils.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Faster R-CNN ๋ฐ‘๋ฐ”๋‹ฅ ๊ตฌํ˜„ โ€” [2/5] ๋ฐ•์Šค ์—ฐ์‚ฐ ์œ ํ‹ธ
3
+ ==================================================
4
+ ๊ฐ์ฒดํƒ์ง€์˜ ์ˆ˜ํ•™์  ํ•ต์‹ฌ์ด ๋ชจ๋‘ ์—ฌ๊ธฐ์— ์žˆ๋‹ค.
5
+
6
+ 1) ์•ต์ปค(anchor) ์ƒ์„ฑ : ๊ฒฉ์ž๋งˆ๋‹ค ์—ฌ๋Ÿฌ ํฌ๊ธฐยท๋น„์œจ์˜ ๊ธฐ์ค€ ๋ฐ•์Šค๋ฅผ ๊น๋‹ค
7
+ 2) IoU : ๋‘ ๋ฐ•์Šค๊ฐ€ ์–ผ๋งˆ๋‚˜ ๊ฒน์น˜๋Š”๊ฐ€
8
+ 3) ๋ฐ•์Šค ์ธ์ฝ”๋”ฉ/๋””์ฝ”๋”ฉ : (๋ฐ•์Šค โ†’ ํšŒ๊ท€ ํƒ€๊นƒ) / (์˜ˆ์ธก๊ฐ’ โ†’ ๋ฐ•์Šค)
9
+ 4) NMS : ๊ฒน์น˜๋Š” ์ค‘๋ณต ์˜ˆ์ธก์„ ์ œ๊ฑฐ
10
+
11
+ ์ด ํŒŒ์ผ๋งŒ ์ดํ•ดํ•˜๋ฉด Faster R-CNN์˜ ์ ˆ๋ฐ˜์„ ์ดํ•ดํ•œ ๊ฒƒ์ด๋‹ค.
12
+ """
13
+
14
+ import torch
15
+
16
+
17
+ # ---------------------------------------------------------------
18
+ # 1) ์•ต์ปค ์ƒ์„ฑ
19
+ # ---------------------------------------------------------------
20
+ def generate_anchors(base_size=16, ratios=(0.5, 1.0, 2.0),
21
+ scales=(8, 16, 32)):
22
+ """
23
+ ํ•œ ๊ฒฉ์ž์ (cell)์— ๋†“์„ ๊ธฐ์ค€ ์•ต์ปค๋“ค์„ ๋งŒ๋“ ๋‹ค.
24
+ ratios(๊ฐ€๋กœ์„ธ๋กœ๋น„) ร— scales(ํฌ๊ธฐ) ์กฐํ•ฉ โ†’ ๋ณดํ†ต 9๊ฐœ ์•ต์ปค.
25
+
26
+ ๋ฐ˜ํ™˜: [num_anchors, 4] ํ˜•ํƒœ์˜ (x1,y1,x2,y2), ์ค‘์‹ฌ์ด ์›์  ๊ธฐ์ค€.
27
+ """
28
+ anchors = []
29
+ for scale in scales:
30
+ area = (base_size * scale) ** 2
31
+ for ratio in ratios:
32
+ # ๋„“์ด๋Š” ์œ ์ง€ํ•˜๊ณ  ๊ฐ€๋กœ์„ธ๋กœ๋น„๋งŒ ๋ฐ”๊พผ๋‹ค
33
+ w = round((area / ratio) ** 0.5)
34
+ h = round(w * ratio)
35
+ anchors.append([-w / 2, -h / 2, w / 2, h / 2])
36
+ return torch.tensor(anchors, dtype=torch.float32)
37
+
38
+
39
+ def shift_anchors(base_anchors, feat_h, feat_w, stride):
40
+ """
41
+ ๊ธฐ์ค€ ์•ต์ปค๋ฅผ ํŠน์ง•๋งต ์ „์ฒด ๊ฒฉ์ž์— ๋ณต์ œยท์ด๋™์‹œ์ผœ
42
+ ๋ชจ๋“  ์œ„์น˜์˜ ์•ต์ปค๋ฅผ ๋งŒ๋“ ๋‹ค.
43
+
44
+ feat_h, feat_w : ํŠน์ง•๋งต ํฌ๊ธฐ
45
+ stride : ์›๋ณธ ์ด๋ฏธ์ง€ ๋Œ€๋น„ ํŠน์ง•๋งต ์ถ•์†Œ ๋ฐฐ์œจ(์˜ˆ: 16)
46
+ ๋ฐ˜ํ™˜: [feat_h*feat_w*num_anchors, 4] (์›๋ณธ ์ด๋ฏธ์ง€ ์ขŒํ‘œ๊ณ„)
47
+ """
48
+ # ๊ฐ ๊ฒฉ์ž์ ์˜ ์ด๋ฏธ์ง€์ƒ ์ค‘์‹ฌ ์ขŒํ‘œ
49
+ shift_x = (torch.arange(feat_w) + 0.5) * stride
50
+ shift_y = (torch.arange(feat_h) + 0.5) * stride
51
+ sy, sx = torch.meshgrid(shift_y, shift_x, indexing="ij")
52
+ shifts = torch.stack([sx.reshape(-1), sy.reshape(-1),
53
+ sx.reshape(-1), sy.reshape(-1)], dim=1) # [K,4]
54
+
55
+ # [K,1,4] + [1,A,4] โ†’ [K,A,4] โ†’ [K*A,4]
56
+ anchors = shifts[:, None, :] + base_anchors[None, :, :]
57
+ return anchors.reshape(-1, 4)
58
+
59
+
60
+ # ---------------------------------------------------------------
61
+ # 2) IoU (Intersection over Union)
62
+ # ---------------------------------------------------------------
63
+ def box_iou(boxes1, boxes2):
64
+ """
65
+ [N,4], [M,4] โ†’ [N,M] IoU ํ–‰๋ ฌ.
66
+ IoU = ๊ต์ง‘ํ•ฉ ๋„“์ด / ํ•ฉ์ง‘ํ•ฉ ๋„“์ด. 0(์•ˆ ๊ฒน์นจ)~1(์™„์ „ ์ผ์น˜).
67
+ """
68
+ area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
69
+ area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
70
+
71
+ lt = torch.max(boxes1[:, None, :2], boxes2[None, :, :2]) # ๊ต์ง‘ํ•ฉ ์ขŒ์ƒ๋‹จ
72
+ rb = torch.min(boxes1[:, None, 2:], boxes2[None, :, 2:]) # ๊ต์ง‘ํ•ฉ ์šฐํ•˜๋‹จ
73
+ wh = (rb - lt).clamp(min=0)
74
+ inter = wh[:, :, 0] * wh[:, :, 1]
75
+
76
+ union = area1[:, None] + area2[None, :] - inter
77
+ return inter / union.clamp(min=1e-6)
78
+
79
+
80
+ # ---------------------------------------------------------------
81
+ # 3) ๋ฐ•์Šค ์ธ์ฝ”๋”ฉ / ๋””์ฝ”๋”ฉ
82
+ # ---------------------------------------------------------------
83
+ def encode_boxes(gt, anchors):
84
+ """
85
+ ์ •๋‹ต ๋ฐ•์Šค(gt)๋ฅผ ์•ต์ปค ๊ธฐ์ค€ ํšŒ๊ท€ ํƒ€๊นƒ (dx,dy,dw,dh)์œผ๋กœ ๋ณ€ํ™˜.
86
+ ๋„คํŠธ์›Œํฌ๋Š” ์ ˆ๋Œ€ ์ขŒํ‘œ๊ฐ€ ์•„๋‹ˆ๋ผ "์•ต์ปค๋กœ๋ถ€ํ„ฐ์˜ ์ƒ๋Œ€ ๋ณ€ํ˜•"์„ ๋ฐฐ์šด๋‹ค.
87
+ """
88
+ aw = anchors[:, 2] - anchors[:, 0]
89
+ ah = anchors[:, 3] - anchors[:, 1]
90
+ ax = anchors[:, 0] + 0.5 * aw
91
+ ay = anchors[:, 1] + 0.5 * ah
92
+
93
+ gw = gt[:, 2] - gt[:, 0]
94
+ gh = gt[:, 3] - gt[:, 1]
95
+ gx = gt[:, 0] + 0.5 * gw
96
+ gy = gt[:, 1] + 0.5 * gh
97
+
98
+ dx = (gx - ax) / aw
99
+ dy = (gy - ay) / ah
100
+ dw = torch.log(gw / aw)
101
+ dh = torch.log(gh / ah)
102
+ return torch.stack([dx, dy, dw, dh], dim=1)
103
+
104
+
105
+ def decode_boxes(deltas, anchors):
106
+ """
107
+ ๋„คํŠธ์›Œํฌ๊ฐ€ ์˜ˆ์ธกํ•œ (dx,dy,dw,dh)๋ฅผ ์‹ค์ œ ๋ฐ•์Šค ์ขŒํ‘œ๋กœ ๋ณต์›.
108
+ encode_boxes์˜ ์—ญ์—ฐ์‚ฐ.
109
+ """
110
+ aw = anchors[:, 2] - anchors[:, 0]
111
+ ah = anchors[:, 3] - anchors[:, 1]
112
+ ax = anchors[:, 0] + 0.5 * aw
113
+ ay = anchors[:, 1] + 0.5 * ah
114
+
115
+ dx, dy, dw, dh = deltas[:, 0], deltas[:, 1], deltas[:, 2], deltas[:, 3]
116
+ # dw,dh ํญ์ฃผ ๋ฐฉ์ง€ ํด๋žจํ”„
117
+ dw = torch.clamp(dw, max=4.135)
118
+ dh = torch.clamp(dh, max=4.135)
119
+
120
+ px = dx * aw + ax
121
+ py = dy * ah + ay
122
+ pw = torch.exp(dw) * aw
123
+ ph = torch.exp(dh) * ah
124
+
125
+ x1 = px - 0.5 * pw
126
+ y1 = py - 0.5 * ph
127
+ x2 = px + 0.5 * pw
128
+ y2 = py + 0.5 * ph
129
+ return torch.stack([x1, y1, x2, y2], dim=1)
130
+
131
+
132
+ def clip_boxes(boxes, img_h, img_w):
133
+ """๋ฐ•์Šค๋ฅผ ์ด๋ฏธ์ง€ ๊ฒฝ๊ณ„ ์•ˆ์œผ๋กœ ์ž๋ฅธ๋‹ค."""
134
+ boxes[:, 0].clamp_(min=0, max=img_w)
135
+ boxes[:, 1].clamp_(min=0, max=img_h)
136
+ boxes[:, 2].clamp_(min=0, max=img_w)
137
+ boxes[:, 3].clamp_(min=0, max=img_h)
138
+ return boxes
139
+
140
+
141
+ # ---------------------------------------------------------------
142
+ # 4) NMS (Non-Maximum Suppression)
143
+ # ---------------------------------------------------------------
144
+ def nms(boxes, scores, iou_thresh=0.7):
145
+ """
146
+ ์ ์ˆ˜ ๋†’์€ ๋ฐ•์Šค๋ถ€ํ„ฐ ๋‚จ๊ธฐ๊ณ , ๊ทธ์™€ ๋งŽ์ด ๊ฒน์น˜๋Š” ๋ฐ•์Šค๋Š” ์ œ๊ฑฐ.
147
+ torchvision.ops.nms ๋ฅผ ์จ๋„ ๋˜์ง€๋งŒ, ์›๋ฆฌ ํ•™์Šต์šฉ์œผ๋กœ ์ง์ ‘ ๊ตฌํ˜„.
148
+ ๋ฐ˜ํ™˜: ๋‚จ๊ธธ ์ธ๋ฑ์Šค.
149
+ """
150
+ if boxes.numel() == 0:
151
+ return torch.empty((0,), dtype=torch.int64)
152
+
153
+ order = scores.argsort(descending=True)
154
+ keep = []
155
+ while order.numel() > 0:
156
+ i = order[0].item()
157
+ keep.append(i)
158
+ if order.numel() == 1:
159
+ break
160
+ ious = box_iou(boxes[i].unsqueeze(0), boxes[order[1:]]).squeeze(0)
161
+ # ์ž„๊ณ„๊ฐ’ ์ดํ•˜๋งŒ ๋‚จ๊ธด๋‹ค
162
+ order = order[1:][ious <= iou_thresh]
163
+ return torch.tensor(keep, dtype=torch.int64)
dataset.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Faster R-CNN ๋ฐ‘๋ฐ”๋‹ฅ ๊ตฌํ˜„ โ€” [1/5] ๋ฐ์ดํ„ฐ์…‹ (Pascal VOC)
3
+ =========================================================
4
+ Pascal VOC 2007/2012 ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด (์ด๋ฏธ์ง€, ๋ฐ•์Šค, ๋ผ๋ฒจ)์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
5
+
6
+ VOC ์–ด๋…ธํ…Œ์ด์…˜์€ XML ํ˜•์‹์ด๋ฉฐ, ๊ฐ ๊ฐ์ฒด๋งˆ๋‹ค ๋‹ค์Œ์„ ๋‹ด๋Š”๋‹ค:
7
+ - name : ํด๋ž˜์Šค ์ด๋ฆ„ (์˜ˆ: 'person', 'car')
8
+ - bndbox : xmin, ymin, xmax, ymax (์ขŒ์ƒ๋‹จยท์šฐํ•˜๋‹จ ํ”ฝ์…€ ์ขŒํ‘œ)
9
+ - difficult : ํŒ๋ณ„ ์–ด๋ ค์šด ๊ฐ์ฒด ํ‘œ์‹œ(ํ•™์Šต ์‹œ ๋ณดํ†ต ์ œ์™ธ)
10
+
11
+ ํ•ต์‹ฌ ๊ฐœ๋…:
12
+ - ๋ฐ•์Šค ์ขŒํ‘œ๋Š” [x1, y1, x2, y2] ์ ˆ๋Œ€ ํ”ฝ์…€ ์ขŒํ‘œ๋กœ ํ†ต์ผํ•œ๋‹ค.
13
+ - ์ด๋ฏธ์ง€๋ฅผ ๋ฆฌ์‚ฌ์ด์ฆˆํ•˜๋ฉด ๋ฐ•์Šค๋„ ๊ฐ™์€ ๋น„์œจ๋กœ ์Šค์ผ€์ผํ•ด์•ผ ํ•œ๋‹ค.
14
+ """
15
+
16
+ import os
17
+ import xml.etree.ElementTree as ET
18
+
19
+ import torch
20
+ from torch.utils.data import Dataset
21
+ from PIL import Image
22
+ import torchvision.transforms.functional as F
23
+
24
+
25
+ # Pascal VOC 20๊ฐœ ํด๋ž˜์Šค (์ธ๋ฑ์Šค 0์€ ๋ฐฐ๊ฒฝ์œผ๋กœ ์˜ˆ์•ฝ โ†’ ํด๋ž˜์Šค๋Š” 1๋ถ€ํ„ฐ)
26
+ VOC_CLASSES = [
27
+ "aeroplane", "bicycle", "bird", "boat", "bottle",
28
+ "bus", "car", "cat", "chair", "cow",
29
+ "diningtable", "dog", "horse", "motorbike", "person",
30
+ "pottedplant", "sheep", "sofa", "train", "tvmonitor",
31
+ ]
32
+ # ์ด๋ฆ„ โ†’ ์ธ๋ฑ์Šค (๋ฐฐ๊ฒฝ=0 ์ด๋ฏ€๋กœ +1)
33
+ CLASS_TO_IDX = {name: i + 1 for i, name in enumerate(VOC_CLASSES)}
34
+ NUM_CLASSES = len(VOC_CLASSES) + 1 # +1 = ๋ฐฐ๊ฒฝ(background)
35
+
36
+
37
+ class VOCDataset(Dataset):
38
+ """
39
+ Pascal VOC ๊ฐ์ฒดํƒ์ง€ ๋ฐ์ดํ„ฐ์…‹.
40
+
41
+ Args:
42
+ root: VOCdevkit/VOC2007 (๋˜๋Š” VOC2012) ๊ฒฝ๋กœ
43
+ split: 'train' | 'val' | 'trainval' | 'test'
44
+ min_size: ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ ์ด๋ฏธ์ง€ ์งง์€ ๋ณ€์˜ ๋ชฉํ‘œ ๊ธธ์ด
45
+ max_size: ๊ธด ๋ณ€์˜ ์ตœ๋Œ€ ๊ธธ์ด(๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์ƒํ•œ ์ ์šฉ)
46
+ keep_difficult: difficult=1 ๊ฐ์ฒด๋ฅผ ํฌํ•จํ• ์ง€ ์—ฌ๋ถ€(ํ•™์Šต ์‹œ False ๊ถŒ์žฅ)
47
+ """
48
+
49
+ def __init__(self, root, split="trainval", min_size=600, max_size=1000,
50
+ keep_difficult=False):
51
+ self.root = root
52
+ self.min_size = min_size
53
+ self.max_size = max_size
54
+ self.keep_difficult = keep_difficult
55
+
56
+ # ImageSets/Main/<split>.txt ์— ์ด๋ฏธ์ง€ ID ๋ชฉ๋ก์ด ์žˆ๋‹ค.
57
+ split_file = os.path.join(root, "ImageSets", "Main", f"{split}.txt")
58
+ with open(split_file) as f:
59
+ self.ids = [line.strip() for line in f if line.strip()]
60
+
61
+ def __len__(self):
62
+ return len(self.ids)
63
+
64
+ def _load_annotation(self, img_id):
65
+ """XML์„ ํŒŒ์‹ฑํ•ด ๋ฐ•์Šค์™€ ๋ผ๋ฒจ์„ ๋ฝ‘๋Š”๋‹ค."""
66
+ ann_path = os.path.join(self.root, "Annotations", f"{img_id}.xml")
67
+ tree = ET.parse(ann_path)
68
+ boxes, labels = [], []
69
+
70
+ for obj in tree.findall("object"):
71
+ difficult = int(obj.findtext("difficult", "0"))
72
+ if difficult and not self.keep_difficult:
73
+ continue
74
+
75
+ name = obj.findtext("name").strip().lower()
76
+ if name not in CLASS_TO_IDX:
77
+ continue
78
+
79
+ bnd = obj.find("bndbox")
80
+ # VOC ์ขŒํ‘œ๋Š” 1๋ถ€ํ„ฐ ์‹œ์ž‘ โ†’ 0-๊ธฐ๋ฐ˜์œผ๋กœ ๋ณด์ •(-1)
81
+ x1 = float(bnd.findtext("xmin")) - 1
82
+ y1 = float(bnd.findtext("ymin")) - 1
83
+ x2 = float(bnd.findtext("xmax")) - 1
84
+ y2 = float(bnd.findtext("ymax")) - 1
85
+ boxes.append([x1, y1, x2, y2])
86
+ labels.append(CLASS_TO_IDX[name])
87
+
88
+ boxes = torch.as_tensor(boxes, dtype=torch.float32).reshape(-1, 4)
89
+ labels = torch.as_tensor(labels, dtype=torch.int64)
90
+ return boxes, labels
91
+
92
+ def _resize(self, img, boxes):
93
+ """
94
+ ์งง์€ ๋ณ€์„ min_size๋กœ ๋งž์ถ”๋˜, ๊ธด ๋ณ€์ด max_size๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก ์Šค์ผ€์ผ.
95
+ ๋ฐ•์Šค๋„ ๊ฐ™์€ ๋น„์œจ๋กœ ์กฐ์ •ํ•œ๋‹ค. (Faster R-CNN ์›๋…ผ๋ฌธ ๋ฐฉ์‹)
96
+ """
97
+ w, h = img.size
98
+ short, long = min(w, h), max(w, h)
99
+ scale = self.min_size / short
100
+ if long * scale > self.max_size:
101
+ scale = self.max_size / long
102
+
103
+ new_w, new_h = int(round(w * scale)), int(round(h * scale))
104
+ img = img.resize((new_w, new_h), Image.BILINEAR)
105
+ if boxes.numel() > 0:
106
+ boxes = boxes * scale # ๋ฐ•์Šค๋„ ๋™์ผ ๋ฐฐ์œจ ์ ์šฉ
107
+ return img, boxes, scale
108
+
109
+ def __getitem__(self, idx):
110
+ img_id = self.ids[idx]
111
+ img_path = os.path.join(self.root, "JPEGImages", f"{img_id}.jpg")
112
+ img = Image.open(img_path).convert("RGB")
113
+
114
+ boxes, labels = self._load_annotation(img_id)
115
+ img, boxes, scale = self._resize(img, boxes)
116
+
117
+ # ํ…์„œ ๋ณ€ํ™˜ + ImageNet ์ •๊ทœํ™”(๋ฐฑ๋ณธ์ด ImageNet ์‚ฌ์ „ํ•™์Šต์ด๋ฏ€๋กœ)
118
+ img = F.to_tensor(img)
119
+ img = F.normalize(img,
120
+ mean=[0.485, 0.456, 0.406],
121
+ std=[0.229, 0.224, 0.225])
122
+
123
+ target = {
124
+ "boxes": boxes, # [N,4] ์ ˆ๋Œ€ ํ”ฝ์…€ (๋ฆฌ์‚ฌ์ด์ฆˆ ํ›„)
125
+ "labels": labels, # [N] 1..20 (0=๋ฐฐ๊ฒฝ)
126
+ "image_id": img_id,
127
+ "scale": scale, # ํ‰๊ฐ€ ์‹œ ์›๋ณธ ์ขŒํ‘œ๋กœ ๋˜๋Œ๋ฆด ๋•Œ ์‚ฌ์šฉ
128
+ }
129
+ return img, target
130
+
131
+
132
+ def collate_fn(batch):
133
+ """
134
+ ์ด๋ฏธ์ง€๋งˆ๋‹ค ํฌ๊ธฐ๊ฐ€ ๋‹ฌ๋ผ ๊ธฐ๋ณธ collate๋กœ ๋ชป ๋ฌถ๋Š”๋‹ค.
135
+ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ๊ทธ๋Œ€๋กœ ๋„˜๊ธฐ๊ณ , ๋ชจ๋ธ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค.
136
+ (๊ฐ„๋‹จํ™”๋ฅผ ์œ„ํ•ด batch_size=1 ์‚ฌ์šฉ์„ ๊ถŒ์žฅ)
137
+ """
138
+ imgs, targets = list(zip(*batch))
139
+ return list(imgs), list(targets)
infer.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Faster R-CNN ๋ฐ‘๋ฐ”๋‹ฅ ๊ตฌํ˜„ โ€” ์ถ”๋ก (inference) ์Šคํฌ๋ฆฝํŠธ
3
+ =====================================================
4
+ ํ•™์Šต๋œ frcnn.pth ๋กœ ์ž„์˜์˜ ์ด๋ฏธ์ง€์—์„œ ๊ฐ์ฒด๋ฅผ ํƒ์ง€ํ•˜๊ณ ,
5
+ ๋ฐ•์Šค + ํด๋ž˜์Šค๋ช… + ์ ์ˆ˜๋ฅผ ๊ทธ๋ ค์„œ ์ €์žฅํ•œ๋‹ค.
6
+
7
+ ์‹คํ–‰ ์˜ˆ:
8
+ # ์ด๋ฏธ์ง€ ํ•œ ์žฅ
9
+ python infer.py --ckpt frcnn.pth --image test.jpg
10
+
11
+ # ํด๋” ์•ˆ ๋ชจ๋“  ์ด๋ฏธ์ง€
12
+ python infer.py --ckpt frcnn.pth --image_dir ./samples --out_dir ./results
13
+
14
+ # ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’ ์กฐ์ •(๊ธฐ๋ณธ 0.5)
15
+ python infer.py --ckpt frcnn.pth --image test.jpg --score_thresh 0.7
16
+
17
+ ์ฃผ์˜:
18
+ - train.py, model.py ๋“ฑ๊ณผ ๊ฐ™์€ ํด๋”์—์„œ ์‹คํ–‰ํ•  ๊ฒƒ.
19
+ - ํ•™์Šต๊ณผ ๋™์ผํ•œ ๋ฆฌ์‚ฌ์ด์ฆˆ/์ •๊ทœํ™”๋ฅผ ์ ์šฉํ•ด์•ผ ๊ฒฐ๊ณผ๊ฐ€ ์ •์ƒ.
20
+ """
21
+
22
+ import os
23
+ import argparse
24
+
25
+ import torch
26
+ from PIL import Image, ImageDraw, ImageFont
27
+ import torchvision.transforms.functional as F
28
+
29
+ from model import FasterRCNN
30
+ from dataset import NUM_CLASSES, VOC_CLASSES
31
+
32
+
33
+ # ํด๋ž˜์Šค๋ณ„ ์ƒ‰์ƒ(20๊ฐœ) โ€” ์‹œ๊ฐ์ ์œผ๋กœ ๊ตฌ๋ถ„๋˜๋„๋ก HSV ๋ถ„ํ• 
34
+ def _class_colors():
35
+ import colorsys
36
+ colors = []
37
+ for i in range(len(VOC_CLASSES)):
38
+ h = i / len(VOC_CLASSES)
39
+ r, g, b = colorsys.hsv_to_rgb(h, 0.75, 0.95)
40
+ colors.append((int(r * 255), int(g * 255), int(b * 255)))
41
+ return colors
42
+
43
+ CLASS_COLORS = _class_colors()
44
+
45
+
46
+ def preprocess(pil_img, min_size=600, max_size=1000):
47
+ """ํ•™์Šต๊ณผ ๋™์ผํ•œ ๋ฆฌ์‚ฌ์ด์ฆˆ + ์ •๊ทœํ™”. ์›๋ณธ ๋ณต์›์šฉ scale๋„ ๋ฐ˜ํ™˜."""
48
+ w, h = pil_img.size
49
+ short, long = min(w, h), max(w, h)
50
+ scale = min_size / short
51
+ if long * scale > max_size:
52
+ scale = max_size / long
53
+ new_w, new_h = int(round(w * scale)), int(round(h * scale))
54
+
55
+ resized = pil_img.resize((new_w, new_h), Image.BILINEAR)
56
+ t = F.to_tensor(resized)
57
+ t = F.normalize(t, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
58
+ return t, scale
59
+
60
+
61
+ def draw_detections(pil_img, boxes, labels, scores, scale):
62
+ """ํƒ์ง€ ๊ฒฐ๊ณผ๋ฅผ ์›๋ณธ ์ด๋ฏธ์ง€ ์ขŒํ‘œ๋กœ ๋˜๋Œ๋ ค ๋ฐ•์Šค์™€ ๋ผ๋ฒจ์„ ๊ทธ๋ฆฐ๋‹ค."""
63
+ draw = ImageDraw.Draw(pil_img)
64
+ try:
65
+ font = ImageFont.truetype("arial.ttf", 16)
66
+ except Exception:
67
+ font = ImageFont.load_default()
68
+
69
+ for box, label, score in zip(boxes, labels, scores):
70
+ # ๋ชจ๋ธ์€ ๋ฆฌ์‚ฌ์ด์ฆˆ๋œ ์ขŒํ‘œ๋ฅผ ์ถœ๋ ฅ โ†’ ์›๋ณธ ํฌ๊ธฐ๋กœ ๋˜๋Œ๋ฆผ(รทscale)
71
+ x1, y1, x2, y2 = (box / scale).tolist()
72
+ cls_idx = int(label) - 1 # 0=๋ฐฐ๊ฒฝ ์ œ์™ธ
73
+ if cls_idx < 0 or cls_idx >= len(VOC_CLASSES):
74
+ continue
75
+ name = VOC_CLASSES[cls_idx]
76
+ color = CLASS_COLORS[cls_idx]
77
+
78
+ # ๋ฐ•์Šค
79
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
80
+
81
+ # ๋ผ๋ฒจ ๋ฐฐ๊ฒฝ + ํ…์ŠคํŠธ
82
+ text = f"{name} {score:.2f}"
83
+ tb = draw.textbbox((x1, y1), text, font=font)
84
+ draw.rectangle([tb[0], tb[1], tb[2], tb[3]], fill=color)
85
+ draw.text((x1, y1), text, fill="white", font=font)
86
+
87
+ return pil_img
88
+
89
+
90
+ @torch.no_grad()
91
+ def infer_image(model, img_path, device, score_thresh):
92
+ pil = Image.open(img_path).convert("RGB")
93
+ tensor, scale = preprocess(pil)
94
+ tensor = tensor.to(device).unsqueeze(0)
95
+
96
+ det = model(tensor) # eval ๋ชจ๋“œ โ†’ {boxes, labels, scores}
97
+ keep = det["scores"] >= score_thresh
98
+ boxes = det["boxes"][keep].cpu()
99
+ labels = det["labels"][keep].cpu()
100
+ scores = det["scores"][keep].cpu()
101
+
102
+ result = draw_detections(pil, boxes, labels, scores, scale)
103
+ return result, len(boxes)
104
+
105
+
106
+ def main():
107
+ ap = argparse.ArgumentParser()
108
+ ap.add_argument("--ckpt", required=True, help="ํ•™์Šต๋œ ๊ฐ€์ค‘์น˜ (frcnn.pth)")
109
+ ap.add_argument("--image", help="๋‹จ์ผ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ")
110
+ ap.add_argument("--image_dir", help="์ด๋ฏธ์ง€ ํด๋” ๊ฒฝ๋กœ")
111
+ ap.add_argument("--out_dir", default="./results", help="๊ฒฐ๊ณผ ์ €์žฅ ํด๋”")
112
+ ap.add_argument("--score_thresh", type=float, default=0.5,
113
+ help="์ด ์ ์ˆ˜ ์ด์ƒ๋งŒ ํ‘œ์‹œ")
114
+ args = ap.parse_args()
115
+
116
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
117
+ print("device:", device)
118
+
119
+ model = FasterRCNN(NUM_CLASSES).to(device)
120
+ model.load_state_dict(torch.load(args.ckpt, map_location=device))
121
+ model.eval()
122
+ print(f"๋ชจ๋ธ ๋กœ๋“œ ์™„๋ฃŒ: {args.ckpt}")
123
+
124
+ os.makedirs(args.out_dir, exist_ok=True)
125
+
126
+ # ์ฒ˜๋ฆฌํ•  ์ด๋ฏธ์ง€ ๋ชฉ๋ก ๊ตฌ์„ฑ
127
+ targets = []
128
+ if args.image:
129
+ targets.append(args.image)
130
+ if args.image_dir:
131
+ for fn in os.listdir(args.image_dir):
132
+ if fn.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")):
133
+ targets.append(os.path.join(args.image_dir, fn))
134
+
135
+ if not targets:
136
+ print("์ด๋ฏธ์ง€๋ฅผ ์ง€์ •ํ•˜์„ธ์š”: --image ๋˜๋Š” --image_dir")
137
+ return
138
+
139
+ for path in targets:
140
+ result, n = infer_image(model, path, device, args.score_thresh)
141
+ out_path = os.path.join(args.out_dir, "det_" + os.path.basename(path))
142
+ result.save(out_path)
143
+ print(f" {os.path.basename(path)}: {n}๊ฐœ ํƒ์ง€ โ†’ {out_path}")
144
+
145
+ print(f"์™„๋ฃŒ. ๊ฒฐ๊ณผ๋Š” {args.out_dir} ํด๋”์— ์ €์žฅ๋จ.")
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
losses.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Faster R-CNN ๋ฐ‘๋ฐ”๋‹ฅ ๊ตฌํ˜„ โ€” [4/5] ์†์‹ค ๊ณ„์‚ฐ
3
+ =============================================
4
+ Faster R-CNN์€ ๋‘ ๊ณณ์—์„œ ์†์‹ค์ด ๋ฐœ์ƒํ•œ๋‹ค.
5
+
6
+ RPN ์†์‹ค : ์•ต์ปค๊ฐ€ ๊ฐ์ฒด์ธ๊ฐ€(๋ถ„๋ฅ˜) + ์•ต์ปคโ†’์ •๋‹ต ๋ฐ•์Šค ๋ณด์ •(ํšŒ๊ท€)
7
+ RoI ์†์‹ค : ํ›„๋ณด์˜์—ญ์˜ ํด๋ž˜์Šค(๋ถ„๋ฅ˜) + ํด๋ž˜์Šค๋ณ„ ๋ฐ•์Šค ๋ณด์ •(ํšŒ๊ท€)
8
+
9
+ ํ•ต์‹ฌ์€ "ํƒ€๊นƒ ํ• ๋‹น(target assignment)":
10
+ - ๊ฐ ์•ต์ปค/ํ›„๋ณด์˜์—ญ์ด ์–ด๋–ค ์ •๋‹ต ๋ฐ•์Šค๋ฅผ ๋‹ด๋‹นํ•˜๋Š”์ง€ IoU๋กœ ์ •ํ•œ๋‹ค.
11
+ - IoU ๋†’์œผ๋ฉด positive(๊ฐ์ฒด), ๋‚ฎ์œผ๋ฉด negative(๋ฐฐ๊ฒฝ), ์• ๋งคํ•˜๋ฉด ๋ฌด์‹œ.
12
+ - ๊ทธ ๋‹ค์Œ positive์—๋งŒ ํšŒ๊ท€ ์†์‹ค, pos+neg์— ๋ถ„๋ฅ˜ ์†์‹ค์„ ๊ฑด๋‹ค.
13
+ """
14
+
15
+ import torch
16
+ import torch.nn.functional as F
17
+
18
+ from box_utils import box_iou, encode_boxes, decode_boxes, clip_boxes
19
+
20
+
21
+ def _sample(pos_idx, neg_idx, num, pos_frac):
22
+ """pos/neg๋ฅผ ์ •ํ•ด์ง„ ๊ฐœ์ˆ˜ยท๋น„์œจ๋กœ ๋ฌด์ž‘์œ„ ์ƒ˜ํ”Œ๋ง(ํด๋ž˜์Šค ๋ถˆ๊ท ํ˜• ๋ฐฉ์ง€)."""
23
+ num_pos = min(int(num * pos_frac), pos_idx.numel())
24
+ num_neg = min(num - num_pos, neg_idx.numel())
25
+ pos = pos_idx[torch.randperm(pos_idx.numel())[:num_pos]]
26
+ neg = neg_idx[torch.randperm(neg_idx.numel())[:num_neg]]
27
+ return pos, neg
28
+
29
+
30
+ # ---------------------------------------------------------------
31
+ # RPN ์†์‹ค
32
+ # ---------------------------------------------------------------
33
+ def rpn_loss(rpn_logits, rpn_deltas, anchors, gt_boxes, img_hw,
34
+ pos_iou=0.7, neg_iou=0.3, num_samples=256, pos_frac=0.5):
35
+ device = rpn_logits.device
36
+
37
+ # ์ด๋ฏธ์ง€ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ„ ์•ต์ปค๋Š” ํ•™์Šต์—์„œ ์ œ์™ธ
38
+ inside = ((anchors[:, 0] >= 0) & (anchors[:, 1] >= 0) &
39
+ (anchors[:, 2] <= img_hw[1]) & (anchors[:, 3] <= img_hw[0]))
40
+ idx_inside = torch.where(inside)[0]
41
+ anc = anchors[idx_inside]
42
+
43
+ labels = torch.full((anc.shape[0],), -1, dtype=torch.float32, device=device) # -1=๋ฌด์‹œ
44
+
45
+ if gt_boxes.numel() > 0:
46
+ ious = box_iou(anc, gt_boxes) # [A, G]
47
+ max_iou, argmax = ious.max(dim=1) # ๊ฐ ์•ต์ปค์˜ ์ตœ๊ณ  IoU ์ •๋‹ต
48
+ labels[max_iou < neg_iou] = 0 # ๋ฐฐ๊ฒฝ
49
+ labels[max_iou >= pos_iou] = 1 # ๊ฐ์ฒด
50
+ # ๊ฐ ์ •๋‹ต ๋ฐ•์Šค์— ๋Œ€ํ•ด IoU ์ตœ๋Œ€์ธ ์•ต์ปค๋„ ๊ฐ•์ œ๋กœ positive
51
+ gt_best = ious.argmax(dim=0)
52
+ labels[gt_best] = 1
53
+ matched_gt = gt_boxes[argmax]
54
+ else:
55
+ labels[:] = 0
56
+ matched_gt = torch.zeros_like(anc)
57
+
58
+ pos = torch.where(labels == 1)[0]
59
+ neg = torch.where(labels == 0)[0]
60
+ pos, neg = _sample(pos, neg, num_samples, pos_frac)
61
+ samp = torch.cat([pos, neg])
62
+
63
+ # --- ๋ถ„๋ฅ˜ ์†์‹ค (๊ฐ์ฒด/๋ฐฐ๊ฒฝ) ---
64
+ logits_inside = rpn_logits[idx_inside]
65
+ cls_loss = F.binary_cross_entropy_with_logits(
66
+ logits_inside[samp], labels[samp])
67
+
68
+ # --- ํšŒ๊ท€ ์†์‹ค (positive ์•ต์ปค๋งŒ) ---
69
+ if pos.numel() > 0:
70
+ deltas_inside = rpn_deltas[idx_inside]
71
+ reg_targets = encode_boxes(matched_gt[pos], anc[pos])
72
+ reg_loss = F.smooth_l1_loss(deltas_inside[pos], reg_targets,
73
+ beta=1.0 / 9.0)
74
+ else:
75
+ reg_loss = torch.tensor(0.0, device=device)
76
+
77
+ return cls_loss + reg_loss
78
+
79
+
80
+ # ---------------------------------------------------------------
81
+ # RoI ํƒ€๊นƒ ํ• ๋‹น (ํ›„๋ณด์˜์—ญ โ†’ ํ•™์Šต ์ƒ˜ํ”Œ)
82
+ # ---------------------------------------------------------------
83
+ def assign_roi_targets(proposals, gt_boxes, gt_labels,
84
+ pos_iou=0.5, neg_iou_hi=0.5, neg_iou_lo=0.0,
85
+ num_samples=128, pos_frac=0.25):
86
+ """ํ›„๋ณด์˜์—ญ์— ํด๋ž˜์Šค ๋ผ๋ฒจ๊ณผ ํšŒ๊ท€ ํƒ€๊นƒ์„ ๋ถ™์ด๊ณ  ์ƒ˜ํ”Œ๋ง."""
87
+ device = proposals.device
88
+ # ์ •๋‹ต ๋ฐ•์Šค๋„ ํ›„๋ณด์— ์ถ”๊ฐ€(ํ•™์Šต ์ดˆ๊ธฐ ์•ˆ์ •ํ™”)
89
+ if gt_boxes.numel() > 0:
90
+ proposals = torch.cat([proposals, gt_boxes], dim=0)
91
+
92
+ if gt_boxes.numel() == 0:
93
+ # ์ •๋‹ต์ด ์—†์œผ๋ฉด ์ „๋ถ€ ๋ฐฐ๊ฒฝ
94
+ n = min(num_samples, proposals.shape[0])
95
+ sel = proposals[:n]
96
+ labels = torch.zeros((n,), dtype=torch.int64, device=device)
97
+ reg_t = torch.zeros((n, 4), device=device)
98
+ return sel, labels, reg_t, torch.zeros((n,), dtype=torch.bool, device=device)
99
+
100
+ ious = box_iou(proposals, gt_boxes)
101
+ max_iou, argmax = ious.max(dim=1)
102
+ gt_for_prop = gt_labels[argmax]
103
+ matched_gt = gt_boxes[argmax]
104
+
105
+ labels = torch.zeros_like(gt_for_prop) # 0=๋ฐฐ๊ฒฝ ๊ธฐ๋ณธ
106
+ pos_mask = max_iou >= pos_iou
107
+ labels[pos_mask] = gt_for_prop[pos_mask] # ๊ฐ์ฒด ํด๋ž˜์Šค ๋ถ€์—ฌ
108
+
109
+ pos = torch.where(pos_mask)[0]
110
+ neg = torch.where((max_iou < neg_iou_hi) & (max_iou >= neg_iou_lo))[0]
111
+ pos, neg = _sample(pos, neg, num_samples, pos_frac)
112
+ samp = torch.cat([pos, neg])
113
+
114
+ sel_prop = proposals[samp]
115
+ sel_labels = labels[samp]
116
+ reg_targets = encode_boxes(matched_gt[samp], sel_prop)
117
+ is_pos = torch.zeros((samp.numel(),), dtype=torch.bool, device=device)
118
+ is_pos[:pos.numel()] = True
119
+ return sel_prop, sel_labels, reg_targets, is_pos
120
+
121
+
122
+ # ---------------------------------------------------------------
123
+ # RoI ์†์‹ค
124
+ # ---------------------------------------------------------------
125
+ def roi_loss(head, feat, stride, proposals, gt_boxes, gt_labels):
126
+ sel_prop, labels, reg_targets, is_pos = assign_roi_targets(
127
+ proposals, gt_boxes, gt_labels)
128
+
129
+ cls_logits, reg = head(feat, sel_prop, stride) # [S,C], [S,C*4]
130
+
131
+ # --- ๋ถ„๋ฅ˜ ์†์‹ค (์ „์ฒด ์ƒ˜ํ”Œ) ---
132
+ cls_loss = F.cross_entropy(cls_logits, labels)
133
+
134
+ # --- ํšŒ๊ท€ ์†์‹ค (positive๋งŒ, ํ•ด๋‹น ํด๋ž˜์Šค์˜ 4๊ฐœ ์ขŒํ‘œ๋งŒ) ---
135
+ if is_pos.sum() > 0:
136
+ S = reg.shape[0]
137
+ reg = reg.reshape(S, -1, 4)
138
+ pos_idx = torch.where(is_pos)[0]
139
+ pos_labels = labels[pos_idx]
140
+ reg_pos = reg[pos_idx, pos_labels] # ํ•ด๋‹น ํด๋ž˜์Šค ์ขŒํ‘œ
141
+ reg_loss = F.smooth_l1_loss(reg_pos, reg_targets[pos_idx], beta=1.0)
142
+ else:
143
+ reg_loss = torch.tensor(0.0, device=feat.device)
144
+
145
+ return cls_loss + reg_loss
model.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Faster R-CNN ๋ฐ‘๋ฐ”๋‹ฅ ๊ตฌํ˜„ โ€” [3/5] ๋ชจ๋ธ
3
+ ==========================================
4
+ ์ „์ฒด ๊ตฌ์กฐ:
5
+
6
+ ์ด๋ฏธ์ง€
7
+ โ”‚ [๋ฐฑ๋ณธ: ResNet50 (ImageNet ์‚ฌ์ „ํ•™์Šต)]
8
+ โ–ผ
9
+ ํŠน์ง•๋งต (stride 16)
10
+ โ”‚
11
+ โ”œโ”€โ–ถ [RPN] โ”€โ”€ ์•ต์ปค๋งˆ๋‹ค (๊ฐ์ฒด ์—ฌ๋ถ€, ๋ฐ•์Šค ๋ณด์ •) ์˜ˆ์ธก โ”€โ”€ ํ›„๋ณด์˜์—ญ(proposal) ์ƒ์„ฑ
12
+ โ”‚
13
+ โ–ผ
14
+ [RoI Align] โ”€โ”€ ๊ฐ ํ›„๋ณด์˜์—ญ์„ ๊ณ ์ • ํฌ๊ธฐ(7x7) ํŠน์ง•์œผ๋กœ ์ถ”์ถœ
15
+ โ”‚
16
+ โ–ผ
17
+ [RoI Head] โ”€โ”€ (ํด๋ž˜์Šค ๋ถ„๋ฅ˜, ํด๋ž˜์Šค๋ณ„ ๋ฐ•์Šค ๋ณด์ •) ์ตœ์ข… ์˜ˆ์ธก
18
+
19
+ 2-stage ํƒ์ง€๊ธฐ์˜ ์ •์„ ๊ตฌ์กฐ. RPN์ด "์–ด๋””์— ๋ญ”๊ฐ€ ์žˆ๋‹ค"๋ฅผ,
20
+ RoI Head๊ฐ€ "๊ทธ๊ฒŒ ๋ฌด์—‡์ด๊ณ  ์ •ํ™•ํ•œ ์œ„์น˜๋Š” ์–ด๋””"๋ฅผ ๋‹ด๋‹นํ•œ๋‹ค.
21
+ """
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.nn.functional as F
26
+ import torchvision
27
+ from torchvision.ops import roi_align
28
+
29
+ from box_utils import (generate_anchors, shift_anchors, box_iou,
30
+ encode_boxes, decode_boxes, clip_boxes, nms)
31
+
32
+
33
+ # ---------------------------------------------------------------
34
+ # ๋ฐฑ๋ณธ: ResNet50์˜ conv1~layer3๊นŒ์ง€ (stride 16 ํŠน์ง•๋งต)
35
+ # ---------------------------------------------------------------
36
+ class Backbone(nn.Module):
37
+ def __init__(self):
38
+ super().__init__()
39
+ resnet = torchvision.models.resnet50(weights="IMAGENET1K_V1")
40
+ # layer4๋Š” RoI Head์—์„œ ์“ฐ๊ณ , ์—ฌ๊ธฐ์„  layer3๊นŒ์ง€ โ†’ stride 16
41
+ self.body = nn.Sequential(
42
+ resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool,
43
+ resnet.layer1, resnet.layer2, resnet.layer3,
44
+ )
45
+ self.out_channels = 1024 # layer3 ์ถœ๋ ฅ ์ฑ„๋„
46
+ self.stride = 16
47
+
48
+ # ์ดˆ๊ธฐ ๋ ˆ์ด์–ด๋Š” ๋™๊ฒฐ(์ž‘์€ ๋ฐ์ดํ„ฐ์…‹ ๊ณผ์ ํ•ฉยท๋ถˆ์•ˆ์ • ๋ฐฉ์ง€)
49
+ for p in self.body[:5].parameters():
50
+ p.requires_grad = False
51
+
52
+ def forward(self, x):
53
+ return self.body(x)
54
+
55
+
56
+ # ---------------------------------------------------------------
57
+ # RPN (Region Proposal Network)
58
+ # ---------------------------------------------------------------
59
+ class RPN(nn.Module):
60
+ def __init__(self, in_channels=1024, num_anchors=9):
61
+ super().__init__()
62
+ self.conv = nn.Conv2d(in_channels, 512, 3, padding=1)
63
+ # ์•ต์ปค๋งˆ๋‹ค: ๊ฐ์ฒด/๋ฐฐ๊ฒฝ 2์ ์ˆ˜ โ†’ ์—ฌ๊ธฐ์„  objectness 1๊ฐœ(logit)๋กœ ๋‹จ์ˆœํ™”
64
+ self.cls = nn.Conv2d(512, num_anchors, 1) # objectness
65
+ self.reg = nn.Conv2d(512, num_anchors * 4, 1) # ๋ฐ•์Šค ๋ณด์ •
66
+ self.num_anchors = num_anchors
67
+
68
+ for layer in [self.conv, self.cls, self.reg]:
69
+ nn.init.normal_(layer.weight, std=0.01)
70
+ nn.init.constant_(layer.bias, 0)
71
+
72
+ def forward(self, feat):
73
+ t = F.relu(self.conv(feat))
74
+ logits = self.cls(t) # [B, A, H, W]
75
+ deltas = self.reg(t) # [B, A*4, H, W]
76
+
77
+ B, _, H, W = logits.shape
78
+ # [B, H*W*A] ํ˜•ํƒœ๋กœ ์ •๋ฆฌ
79
+ logits = logits.permute(0, 2, 3, 1).reshape(B, -1)
80
+ deltas = deltas.permute(0, 2, 3, 1).reshape(B, -1, 4)
81
+ return logits, deltas, (H, W)
82
+
83
+
84
+ # ---------------------------------------------------------------
85
+ # RoI Head (๋ถ„๋ฅ˜ + ๋ฐ•์Šค ํšŒ๊ท€)
86
+ # ---------------------------------------------------------------
87
+ class RoIHead(nn.Module):
88
+ def __init__(self, num_classes, in_channels=1024, roi_size=7):
89
+ super().__init__()
90
+ # layer4 ๋Œ€์‹  ๊ฐ„๋‹จํ•œ FC ํ—ค๋“œ๋กœ ๊ตฌ์„ฑ(ํ•™์Šต ๊ฐ€๋ฒผ์›€)
91
+ self.roi_size = roi_size
92
+ flat = in_channels * roi_size * roi_size
93
+ self.fc = nn.Sequential(
94
+ nn.Linear(flat, 1024), nn.ReLU(inplace=True),
95
+ nn.Linear(1024, 1024), nn.ReLU(inplace=True),
96
+ )
97
+ self.cls = nn.Linear(1024, num_classes) # ํด๋ž˜์Šค ๋ถ„๋ฅ˜
98
+ self.reg = nn.Linear(1024, num_classes * 4) # ํด๋ž˜์Šค๋ณ„ ๋ฐ•์Šค ๋ณด์ •
99
+ self.num_classes = num_classes
100
+
101
+ nn.init.normal_(self.cls.weight, std=0.01)
102
+ nn.init.normal_(self.reg.weight, std=0.001)
103
+ nn.init.constant_(self.cls.bias, 0)
104
+ nn.init.constant_(self.reg.bias, 0)
105
+
106
+ def forward(self, feat, rois, stride):
107
+ """
108
+ feat : ๋ฐฑ๋ณธ ํŠน์ง•๋งต [B,C,H,W]
109
+ rois : ํ›„๋ณด์˜์—ญ [N,4] (์ด๋ฏธ์ง€ ์ขŒํ‘œ)
110
+ """
111
+ # roi_align: ๊ฐ ํ›„๋ณด์˜์—ญ์„ 7x7 ๊ณ ์ • ํฌ๊ธฐ ํŠน์ง•์œผ๋กœ ์ถ”์ถœ
112
+ # batch index 0 (batch_size=1 ๊ฐ€์ •) ๋ฅผ ์•ž์— ๋ถ™์ธ๋‹ค
113
+ batch_idx = torch.zeros((rois.shape[0], 1), device=rois.device)
114
+ rois_b = torch.cat([batch_idx, rois], dim=1) # [N,5]
115
+ pooled = roi_align(feat, rois_b,
116
+ output_size=(self.roi_size, self.roi_size),
117
+ spatial_scale=1.0 / stride,
118
+ sampling_ratio=2)
119
+ x = pooled.flatten(1)
120
+ x = self.fc(x)
121
+ return self.cls(x), self.reg(x)
122
+
123
+
124
+ # ---------------------------------------------------------------
125
+ # ์ „์ฒด ๋ชจ๋ธ
126
+ # ---------------------------------------------------------------
127
+ class FasterRCNN(nn.Module):
128
+ def __init__(self, num_classes):
129
+ super().__init__()
130
+ self.backbone = Backbone()
131
+ self.rpn = RPN(self.backbone.out_channels)
132
+ self.head = RoIHead(num_classes, self.backbone.out_channels)
133
+ self.num_classes = num_classes
134
+
135
+ self.base_anchors = generate_anchors() # 9๊ฐœ
136
+ self.stride = self.backbone.stride
137
+
138
+ # ํ•™์Šต/์ถ”๋ก  ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ
139
+ self.rpn_pre_nms = 12000
140
+ self.rpn_post_nms_train = 2000
141
+ self.rpn_post_nms_test = 300
142
+ self.rpn_nms_thresh = 0.7
143
+
144
+ def _anchors_for(self, feat_h, feat_w, device):
145
+ a = shift_anchors(self.base_anchors, feat_h, feat_w, self.stride)
146
+ return a.to(device)
147
+
148
+ def _proposals(self, rpn_logits, rpn_deltas, anchors, img_hw, training):
149
+ """RPN ์ถœ๋ ฅ โ†’ NMS ๊ฑฐ์นœ ํ›„๋ณด์˜์—ญ(proposal) ์ƒ์„ฑ."""
150
+ scores = torch.sigmoid(rpn_logits[0]) # [K]
151
+ deltas = rpn_deltas[0] # [K,4]
152
+ proposals = decode_boxes(deltas, anchors)
153
+ proposals = clip_boxes(proposals, img_hw[0], img_hw[1])
154
+
155
+ # ์ ์ˆ˜ ์ƒ์œ„๋งŒ ์ถ”๋ฆฐ ๋’ค NMS
156
+ n_pre = min(self.rpn_pre_nms, scores.numel())
157
+ top = scores.topk(n_pre).indices
158
+ proposals, scores = proposals[top], scores[top]
159
+
160
+ keep = nms(proposals, scores, self.rpn_nms_thresh)
161
+ n_post = self.rpn_post_nms_train if training else self.rpn_post_nms_test
162
+ keep = keep[:n_post]
163
+ return proposals[keep], scores[keep]
164
+
165
+ def forward(self, image, target=None):
166
+ """
167
+ image : [1,3,H,W] ๋‹จ์ผ ์ด๋ฏธ์ง€(batch_size=1)
168
+ target : ํ•™์Šต ์‹œ {'boxes','labels'}, ์ถ”๋ก  ์‹œ None
169
+ """
170
+ device = image.device
171
+ img_h, img_w = image.shape[-2:]
172
+ feat = self.backbone(image)
173
+ _, _, fh, fw = feat.shape
174
+
175
+ anchors = self._anchors_for(fh, fw, device)
176
+ rpn_logits, rpn_deltas, _ = self.rpn(feat)
177
+
178
+ proposals, _ = self._proposals(
179
+ rpn_logits, rpn_deltas, anchors, (img_h, img_w),
180
+ training=self.training)
181
+
182
+ if self.training:
183
+ # ํ•™์Šต ๊ฒฝ๋กœ: ์†์‹ค ๊ณ„์‚ฐ (train.py ์˜ ํ—ฌํผ๊ฐ€ ๋‹ด๋‹น)
184
+ return {
185
+ "feat": feat, "anchors": anchors,
186
+ "rpn_logits": rpn_logits[0], "rpn_deltas": rpn_deltas[0],
187
+ "proposals": proposals, "stride": self.stride,
188
+ }
189
+ else:
190
+ # ์ถ”๋ก  ๊ฒฝ๋กœ: ์ตœ์ข… ํƒ์ง€ ๊ฒฐ๊ณผ
191
+ cls_logits, reg = self.head(feat, proposals, self.stride)
192
+ return self._postprocess(cls_logits, reg, proposals, (img_h, img_w))
193
+
194
+ @torch.no_grad()
195
+ def _postprocess(self, cls_logits, reg, proposals, img_hw,
196
+ score_thresh=0.05, nms_thresh=0.3, max_det=100):
197
+ """RoI Head ์ถœ๋ ฅ โ†’ ํด๋ž˜์Šค๋ณ„ NMS โ†’ ์ตœ์ข… ๋ฐ•์Šค/๋ผ๋ฒจ/์ ์ˆ˜."""
198
+ probs = F.softmax(cls_logits, dim=1) # [N, C]
199
+ N, C = probs.shape
200
+ reg = reg.reshape(N, C, 4)
201
+
202
+ all_boxes, all_scores, all_labels = [], [], []
203
+ for c in range(1, C): # 0=๋ฐฐ๊ฒฝ ์ œ์™ธ
204
+ scores_c = probs[:, c]
205
+ mask = scores_c > score_thresh
206
+ if mask.sum() == 0:
207
+ continue
208
+ boxes_c = decode_boxes(reg[mask, c], proposals[mask])
209
+ boxes_c = clip_boxes(boxes_c, img_hw[0], img_hw[1])
210
+ scores_c = scores_c[mask]
211
+
212
+ keep = nms(boxes_c, scores_c, nms_thresh)
213
+ all_boxes.append(boxes_c[keep])
214
+ all_scores.append(scores_c[keep])
215
+ all_labels.append(torch.full((keep.numel(),), c,
216
+ dtype=torch.int64, device=boxes_c.device))
217
+
218
+ if not all_boxes:
219
+ return {"boxes": torch.empty((0, 4)), "labels": torch.empty((0,), dtype=torch.int64),
220
+ "scores": torch.empty((0,))}
221
+
222
+ boxes = torch.cat(all_boxes)
223
+ scores = torch.cat(all_scores)
224
+ labels = torch.cat(all_labels)
225
+
226
+ # ์ „์ฒด์—์„œ ์ ์ˆ˜ ์ƒ์œ„ max_det๊ฐœ๋งŒ
227
+ if scores.numel() > max_det:
228
+ top = scores.topk(max_det).indices
229
+ boxes, scores, labels = boxes[top], scores[top], labels[top]
230
+ return {"boxes": boxes, "labels": labels, "scores": scores}
train.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Faster R-CNN ๋ฐ‘๋ฐ”๋‹ฅ ๊ตฌํ˜„ โ€” [5/5] ํ•™์Šต + ํ‰๊ฐ€
3
+ ================================================
4
+ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ:
5
+ ํ•™์Šต: ์ด๋ฏธ์ง€ โ†’ ๋ชจ๋ธ(train) โ†’ RPN ์†์‹ค + RoI ์†์‹ค โ†’ ์—ญ์ „ํŒŒ
6
+ ํ‰๊ฐ€: ์ด๋ฏธ์ง€ โ†’ ๋ชจ๋ธ(eval) โ†’ ํƒ์ง€ ๊ฒฐ๊ณผ โ†’ mAP@0.5 ๊ณ„์‚ฐ
7
+
8
+ ์‹คํ–‰ ์˜ˆ:
9
+ python train.py --voc_root /path/VOCdevkit/VOC2007 --epochs 12
10
+ python train.py --voc_root /path/VOCdevkit/VOC2007 --eval_only --ckpt frcnn.pth
11
+
12
+ ์ฃผ์˜:
13
+ - batch_size=1 ๋กœ ์„ค๊ณ„(์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ์ œ๊ฐ๊ฐ์ด๋ผ ๋‹จ์ˆœํ™”).
14
+ - GPU ๊ถŒ์žฅ. CPU๋กœ๋„ ๋Œ์ง€๋งŒ ๋งค์šฐ ๋А๋ฆฌ๋‹ค.
15
+ """
16
+
17
+ import argparse
18
+ import torch
19
+ from torch.utils.data import DataLoader
20
+
21
+ from dataset import VOCDataset, collate_fn, NUM_CLASSES, VOC_CLASSES
22
+ from model import FasterRCNN
23
+ from losses import rpn_loss, roi_loss
24
+ from box_utils import box_iou
25
+
26
+
27
+ # ---------------------------------------------------------------
28
+ # ํ•™์Šต ํ•œ ์—ํญ
29
+ # ---------------------------------------------------------------
30
+ def train_one_epoch(model, loader, optimizer, device, epoch):
31
+ model.train()
32
+ running = 0.0
33
+ for i, (imgs, targets) in enumerate(loader):
34
+ img = imgs[0].to(device).unsqueeze(0) # [1,3,H,W]
35
+ gt_boxes = targets[0]["boxes"].to(device)
36
+ gt_labels = targets[0]["labels"].to(device)
37
+ if gt_boxes.numel() == 0:
38
+ continue
39
+
40
+ out = model(img) # training=True โ†’ ์ค‘๊ฐ„ ์‚ฐ์ถœ๋ฌผ ๋ฐ˜ํ™˜
41
+ img_hw = img.shape[-2:]
42
+
43
+ l_rpn = rpn_loss(out["rpn_logits"], out["rpn_deltas"],
44
+ out["anchors"], gt_boxes, img_hw)
45
+ l_roi = roi_loss(model.head, out["feat"], out["stride"],
46
+ out["proposals"], gt_boxes, gt_labels)
47
+ loss = l_rpn + l_roi
48
+
49
+ optimizer.zero_grad()
50
+ loss.backward()
51
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 10.0) # ํญ์ฃผ ๋ฐฉ์ง€
52
+ optimizer.step()
53
+
54
+ running += loss.item()
55
+ if (i + 1) % 100 == 0:
56
+ print(f"[epoch {epoch}] iter {i+1}/{len(loader)} "
57
+ f"loss {running/(i+1):.4f} (rpn {l_rpn.item():.3f} roi {l_roi.item():.3f})")
58
+ return running / max(1, len(loader))
59
+
60
+
61
+ # ---------------------------------------------------------------
62
+ # ํ‰๊ฐ€: VOC ์Šคํƒ€์ผ mAP@0.5
63
+ # ---------------------------------------------------------------
64
+ @torch.no_grad()
65
+ def evaluate(model, loader, device, iou_thresh=0.5):
66
+ model.eval()
67
+ # ํด๋ž˜์Šค๋ณ„ (์ ์ˆ˜, ๋งž์Œ์—ฌ๋ถ€) ์ˆ˜์ง‘ + ์ •๋‹ต ๊ฐœ์ˆ˜
68
+ preds = {c: [] for c in range(1, NUM_CLASSES)}
69
+ n_gt = {c: 0 for c in range(1, NUM_CLASSES)}
70
+
71
+ for imgs, targets in loader:
72
+ img = imgs[0].to(device).unsqueeze(0)
73
+ det = model(img) # eval โ†’ {boxes, labels, scores}
74
+ gt_boxes = targets[0]["boxes"].to(device)
75
+ gt_labels = targets[0]["labels"].to(device)
76
+
77
+ for c in range(1, NUM_CLASSES):
78
+ gmask = gt_labels == c
79
+ gboxes = gt_boxes[gmask]
80
+ n_gt[c] += gboxes.shape[0]
81
+
82
+ pmask = det["labels"] == c
83
+ pboxes = det["boxes"][pmask]
84
+ pscores = det["scores"][pmask]
85
+ if pboxes.numel() == 0:
86
+ continue
87
+
88
+ order = pscores.argsort(descending=True)
89
+ pboxes, pscores = pboxes[order], pscores[order]
90
+
91
+ matched = torch.zeros(gboxes.shape[0], dtype=torch.bool)
92
+ for k in range(pboxes.shape[0]):
93
+ if gboxes.numel() == 0:
94
+ preds[c].append((pscores[k].item(), 0))
95
+ continue
96
+ ious = box_iou(pboxes[k:k+1], gboxes).squeeze(0)
97
+ best_iou, best_j = ious.max(0)
98
+ if best_iou >= iou_thresh and not matched[best_j]:
99
+ preds[c].append((pscores[k].item(), 1)) # TP
100
+ matched[best_j] = True
101
+ else:
102
+ preds[c].append((pscores[k].item(), 0)) # FP
103
+
104
+ # ํด๋ž˜์Šค๋ณ„ AP โ†’ mAP
105
+ aps = []
106
+ for c in range(1, NUM_CLASSES):
107
+ ap = _voc_ap(preds[c], n_gt[c])
108
+ aps.append(ap)
109
+ print(f" {VOC_CLASSES[c-1]:12s} AP = {ap:.4f}")
110
+ mAP = sum(aps) / len(aps)
111
+ print(f" {'mAP@0.5':12s} = {mAP:.4f}")
112
+ return mAP
113
+
114
+
115
+ def _voc_ap(pred_list, n_gt):
116
+ """(์ ์ˆ˜, TP์—ฌ๋ถ€) ๋ชฉ๋ก์œผ๋กœ precision-recall ๊ณก์„  ์•„๋ž˜ ๋„“์ด(AP) ๊ณ„์‚ฐ."""
117
+ if n_gt == 0 or len(pred_list) == 0:
118
+ return 0.0
119
+ pred_list.sort(key=lambda x: x[0], reverse=True)
120
+ tp = torch.tensor([p[1] for p in pred_list], dtype=torch.float32)
121
+ fp = 1 - tp
122
+ tp_cum = torch.cumsum(tp, 0)
123
+ fp_cum = torch.cumsum(fp, 0)
124
+ recall = tp_cum / n_gt
125
+ precision = tp_cum / (tp_cum + fp_cum).clamp(min=1e-6)
126
+
127
+ # 11-point ๋ณด๊ฐ„ (VOC2007 ๋ฐฉ์‹)
128
+ ap = 0.0
129
+ for t in torch.linspace(0, 1, 11):
130
+ mask = recall >= t
131
+ p = precision[mask].max().item() if mask.any() else 0.0
132
+ ap += p / 11.0
133
+ return ap
134
+
135
+
136
+ # ---------------------------------------------------------------
137
+ # ๋ฉ”์ธ
138
+ # ---------------------------------------------------------------
139
+ def main():
140
+ ap = argparse.ArgumentParser()
141
+ ap.add_argument("--voc_root", required=True, help="VOCdevkit/VOC2007 ๊ฒฝ๋กœ")
142
+ ap.add_argument("--epochs", type=int, default=12)
143
+ ap.add_argument("--lr", type=float, default=1e-3)
144
+ ap.add_argument("--ckpt", default="frcnn.pth")
145
+ ap.add_argument("--eval_only", action="store_true")
146
+ args = ap.parse_args()
147
+
148
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
149
+ print("device:", device)
150
+
151
+ model = FasterRCNN(NUM_CLASSES).to(device)
152
+
153
+ if args.eval_only:
154
+ model.load_state_dict(torch.load(args.ckpt, map_location=device))
155
+ test_ds = VOCDataset(args.voc_root, split="test")
156
+ test_loader = DataLoader(test_ds, batch_size=1, shuffle=False,
157
+ collate_fn=collate_fn, num_workers=4)
158
+ evaluate(model, test_loader, device)
159
+ return
160
+
161
+ # ํ•™์Šต
162
+ train_ds = VOCDataset(args.voc_root, split="trainval")
163
+ train_loader = DataLoader(train_ds, batch_size=1, shuffle=True,
164
+ collate_fn=collate_fn, num_workers=4)
165
+
166
+ params = [p for p in model.parameters() if p.requires_grad]
167
+ optimizer = torch.optim.SGD(params, lr=args.lr, momentum=0.9,
168
+ weight_decay=5e-4)
169
+ scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=8, gamma=0.1)
170
+
171
+ for epoch in range(1, args.epochs + 1):
172
+ avg = train_one_epoch(model, train_loader, optimizer, device, epoch)
173
+ scheduler.step()
174
+ print(f"[epoch {epoch}] avg loss = {avg:.4f}")
175
+ torch.save(model.state_dict(), args.ckpt)
176
+ print(f" checkpoint saved โ†’ {args.ckpt}")
177
+
178
+ print("ํ•™์Šต ์™„๋ฃŒ. ํ‰๊ฐ€ํ•˜๋ ค๋ฉด --eval_only ๋กœ ์‹คํ–‰ํ•˜์„ธ์š”.")
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
yolo_infer.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YOLOv8 ์ถ”๋ก  ์Šคํฌ๋ฆฝํŠธ (Ultralytics)
3
+ =====================================
4
+ COCO ์‚ฌ์ „ํ•™์Šต YOLOv8๋กœ ์ด๋ฏธ์ง€์—์„œ ๊ฐ์ฒด๋ฅผ ํƒ์ง€ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•œ๋‹ค.
5
+ ์•ž์„œ ๋งŒ๋“  ๋ฐ‘๋ฐ”๋‹ฅ Faster R-CNN(infer.py)๊ณผ ๊ฐ™์€ ์ด๋ฏธ์ง€๋กœ ๋น„๊ตํ•ด๋ณด๊ธฐ ์ข‹๋‹ค.
6
+
7
+ ์„ค์น˜:
8
+ pip install ultralytics
9
+
10
+ ์‹คํ–‰ ์˜ˆ:
11
+ # ์ด๋ฏธ์ง€ ํ•œ ์žฅ
12
+ python yolo_infer.py --image test.jpg
13
+
14
+ # ํด๋” ์ „์ฒด
15
+ python yolo_infer.py --image_dir ./samples --out_dir ./yolo_results
16
+
17
+ # ๋ชจ๋ธ ํฌ๊ธฐ ๋ณ€๊ฒฝ (n < s < m < l < x, ๋’ค๋กœ ๊ฐˆ์ˆ˜๋ก ์ •ํ™•ยท๋ฌด๊ฑฐ์›€)
18
+ python yolo_infer.py --image test.jpg --model yolov8s.pt
19
+
20
+ # ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’ ์กฐ์ • (๊ธฐ๋ณธ 0.25)
21
+ python yolo_infer.py --image test.jpg --conf 0.4
22
+
23
+ ๋ฉ”๋ชจ:
24
+ - ์ฒ˜์Œ ์‹คํ–‰ ์‹œ ๋ชจ๋ธ ๊ฐ€์ค‘์น˜(yolov8n.pt ๋“ฑ)๊ฐ€ ์ž๋™ ๋‹ค์šด๋กœ๋“œ๋œ๋‹ค.
25
+ - Faster R-CNN(20 VOC ํด๋ž˜์Šค)๊ณผ ๋‹ฌ๋ฆฌ YOLOv8์€ COCO 80 ํด๋ž˜์Šค๋ฅผ ํƒ์ง€ํ•œ๋‹ค.
26
+ - ๋ผ์ด์„ ์Šค: Ultralytics YOLO๋Š” AGPL-3.0. ์ƒ์—…์  ํ์‡„์†Œ์Šค ์‚ฌ์šฉ ์‹œ ์ƒ์šฉ ๋ผ์ด์„ ์Šค ํ•„์š”.
27
+ """
28
+
29
+ import os
30
+ import argparse
31
+ from ultralytics import YOLO
32
+
33
+
34
+ def main():
35
+ ap = argparse.ArgumentParser()
36
+ ap.add_argument("--model", default="yolov8n.pt",
37
+ help="yolov8n/s/m/l/x.pt (์—†์œผ๋ฉด ์ž๋™ ๋‹ค์šด๋กœ๋“œ)")
38
+ ap.add_argument("--image", help="๋‹จ์ผ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ")
39
+ ap.add_argument("--image_dir", help="์ด๋ฏธ์ง€ ํด๋” ๊ฒฝ๋กœ")
40
+ ap.add_argument("--out_dir", default="./yolo_results", help="๊ฒฐ๊ณผ ์ €์žฅ ํด๋”")
41
+ ap.add_argument("--conf", type=float, default=0.25,
42
+ help="์ด ์‹ ๋ขฐ๋„ ์ด์ƒ๋งŒ ํ‘œ์‹œ (๊ธฐ๋ณธ 0.25)")
43
+ args = ap.parse_args()
44
+
45
+ # ๋ชจ๋ธ ๋กœ๋“œ (COCO ์‚ฌ์ „ํ•™์Šต, ์ฒซ ์‹คํ–‰ ์‹œ ์ž๋™ ๋‹ค์šด๋กœ๋“œ)
46
+ print(f"๋ชจ๋ธ ๋กœ๋“œ: {args.model}")
47
+ model = YOLO(args.model)
48
+
49
+ # ์ฒ˜๋ฆฌํ•  ์ด๋ฏธ์ง€ ๋ชฉ๋ก ๊ตฌ์„ฑ
50
+ targets = []
51
+ if args.image:
52
+ targets.append(args.image)
53
+ if args.image_dir:
54
+ for fn in os.listdir(args.image_dir):
55
+ if fn.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")):
56
+ targets.append(os.path.join(args.image_dir, fn))
57
+
58
+ if not targets:
59
+ print("์ด๋ฏธ์ง€๋ฅผ ์ง€์ •ํ•˜์„ธ์š”: --image ๋˜๋Š” --image_dir")
60
+ return
61
+
62
+ os.makedirs(args.out_dir, exist_ok=True)
63
+
64
+ for path in targets:
65
+ # ์ถ”๋ก  ์‹คํ–‰
66
+ results = model(path, conf=args.conf, verbose=False)
67
+ r = results[0]
68
+
69
+ # ํƒ์ง€ ๊ฐœ์ˆ˜์™€ ํด๋ž˜์Šค๋ณ„ ์š”์•ฝ
70
+ n = len(r.boxes)
71
+ names = r.names # ํด๋ž˜์Šค ์ธ๋ฑ์Šค โ†’ ์ด๋ฆ„
72
+ detected = {}
73
+ for cls_id in r.boxes.cls.tolist():
74
+ name = names[int(cls_id)]
75
+ detected[name] = detected.get(name, 0) + 1
76
+
77
+ # ๋ฐ•์Šค ๊ทธ๋ ค์„œ ์ €์žฅ
78
+ out_path = os.path.join(args.out_dir, "yolo_" + os.path.basename(path))
79
+ r.save(filename=out_path)
80
+
81
+ summary = ", ".join(f"{k}ร—{v}" for k, v in detected.items()) or "์—†์Œ"
82
+ print(f" {os.path.basename(path)}: {n}๊ฐœ ํƒ์ง€ ({summary}) โ†’ {out_path}")
83
+
84
+ print(f"์™„๋ฃŒ. ๊ฒฐ๊ณผ๋Š” {args.out_dir} ํด๋”์— ์ €์žฅ๋จ.")
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()