Object detection from scratch: Faster R-CNN + YOLO comparison
Browse files- box_utils.py +163 -0
- dataset.py +139 -0
- infer.py +149 -0
- losses.py +145 -0
- model.py +230 -0
- train.py +182 -0
- 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()
|