YMmim's picture
Object detection from scratch: Faster R-CNN + YOLO comparison
017c046 verified
Raw
History Blame Contribute Delete
9.16 kB
"""
Faster R-CNN ๋ฐ‘๋ฐ”๋‹ฅ ๊ตฌํ˜„ โ€” [3/5] ๋ชจ๋ธ
==========================================
์ „์ฒด ๊ตฌ์กฐ:
์ด๋ฏธ์ง€
โ”‚ [๋ฐฑ๋ณธ: ResNet50 (ImageNet ์‚ฌ์ „ํ•™์Šต)]
โ–ผ
ํŠน์ง•๋งต (stride 16)
โ”‚
โ”œโ”€โ–ถ [RPN] โ”€โ”€ ์•ต์ปค๋งˆ๋‹ค (๊ฐ์ฒด ์—ฌ๋ถ€, ๋ฐ•์Šค ๋ณด์ •) ์˜ˆ์ธก โ”€โ”€ ํ›„๋ณด์˜์—ญ(proposal) ์ƒ์„ฑ
โ”‚
โ–ผ
[RoI Align] โ”€โ”€ ๊ฐ ํ›„๋ณด์˜์—ญ์„ ๊ณ ์ • ํฌ๊ธฐ(7x7) ํŠน์ง•์œผ๋กœ ์ถ”์ถœ
โ”‚
โ–ผ
[RoI Head] โ”€โ”€ (ํด๋ž˜์Šค ๋ถ„๋ฅ˜, ํด๋ž˜์Šค๋ณ„ ๋ฐ•์Šค ๋ณด์ •) ์ตœ์ข… ์˜ˆ์ธก
2-stage ํƒ์ง€๊ธฐ์˜ ์ •์„ ๊ตฌ์กฐ. RPN์ด "์–ด๋””์— ๋ญ”๊ฐ€ ์žˆ๋‹ค"๋ฅผ,
RoI Head๊ฐ€ "๊ทธ๊ฒŒ ๋ฌด์—‡์ด๊ณ  ์ •ํ™•ํ•œ ์œ„์น˜๋Š” ์–ด๋””"๋ฅผ ๋‹ด๋‹นํ•œ๋‹ค.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torchvision.ops import roi_align
from box_utils import (generate_anchors, shift_anchors, box_iou,
encode_boxes, decode_boxes, clip_boxes, nms)
# ---------------------------------------------------------------
# ๋ฐฑ๋ณธ: ResNet50์˜ conv1~layer3๊นŒ์ง€ (stride 16 ํŠน์ง•๋งต)
# ---------------------------------------------------------------
class Backbone(nn.Module):
def __init__(self):
super().__init__()
resnet = torchvision.models.resnet50(weights="IMAGENET1K_V1")
# layer4๋Š” RoI Head์—์„œ ์“ฐ๊ณ , ์—ฌ๊ธฐ์„  layer3๊นŒ์ง€ โ†’ stride 16
self.body = nn.Sequential(
resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool,
resnet.layer1, resnet.layer2, resnet.layer3,
)
self.out_channels = 1024 # layer3 ์ถœ๋ ฅ ์ฑ„๋„
self.stride = 16
# ์ดˆ๊ธฐ ๋ ˆ์ด์–ด๋Š” ๋™๊ฒฐ(์ž‘์€ ๋ฐ์ดํ„ฐ์…‹ ๊ณผ์ ํ•ฉยท๋ถˆ์•ˆ์ • ๋ฐฉ์ง€)
for p in self.body[:5].parameters():
p.requires_grad = False
def forward(self, x):
return self.body(x)
# ---------------------------------------------------------------
# RPN (Region Proposal Network)
# ---------------------------------------------------------------
class RPN(nn.Module):
def __init__(self, in_channels=1024, num_anchors=9):
super().__init__()
self.conv = nn.Conv2d(in_channels, 512, 3, padding=1)
# ์•ต์ปค๋งˆ๋‹ค: ๊ฐ์ฒด/๋ฐฐ๊ฒฝ 2์ ์ˆ˜ โ†’ ์—ฌ๊ธฐ์„  objectness 1๊ฐœ(logit)๋กœ ๋‹จ์ˆœํ™”
self.cls = nn.Conv2d(512, num_anchors, 1) # objectness
self.reg = nn.Conv2d(512, num_anchors * 4, 1) # ๋ฐ•์Šค ๋ณด์ •
self.num_anchors = num_anchors
for layer in [self.conv, self.cls, self.reg]:
nn.init.normal_(layer.weight, std=0.01)
nn.init.constant_(layer.bias, 0)
def forward(self, feat):
t = F.relu(self.conv(feat))
logits = self.cls(t) # [B, A, H, W]
deltas = self.reg(t) # [B, A*4, H, W]
B, _, H, W = logits.shape
# [B, H*W*A] ํ˜•ํƒœ๋กœ ์ •๋ฆฌ
logits = logits.permute(0, 2, 3, 1).reshape(B, -1)
deltas = deltas.permute(0, 2, 3, 1).reshape(B, -1, 4)
return logits, deltas, (H, W)
# ---------------------------------------------------------------
# RoI Head (๋ถ„๋ฅ˜ + ๋ฐ•์Šค ํšŒ๊ท€)
# ---------------------------------------------------------------
class RoIHead(nn.Module):
def __init__(self, num_classes, in_channels=1024, roi_size=7):
super().__init__()
# layer4 ๋Œ€์‹  ๊ฐ„๋‹จํ•œ FC ํ—ค๋“œ๋กœ ๊ตฌ์„ฑ(ํ•™์Šต ๊ฐ€๋ฒผ์›€)
self.roi_size = roi_size
flat = in_channels * roi_size * roi_size
self.fc = nn.Sequential(
nn.Linear(flat, 1024), nn.ReLU(inplace=True),
nn.Linear(1024, 1024), nn.ReLU(inplace=True),
)
self.cls = nn.Linear(1024, num_classes) # ํด๋ž˜์Šค ๋ถ„๋ฅ˜
self.reg = nn.Linear(1024, num_classes * 4) # ํด๋ž˜์Šค๋ณ„ ๋ฐ•์Šค ๋ณด์ •
self.num_classes = num_classes
nn.init.normal_(self.cls.weight, std=0.01)
nn.init.normal_(self.reg.weight, std=0.001)
nn.init.constant_(self.cls.bias, 0)
nn.init.constant_(self.reg.bias, 0)
def forward(self, feat, rois, stride):
"""
feat : ๋ฐฑ๋ณธ ํŠน์ง•๋งต [B,C,H,W]
rois : ํ›„๋ณด์˜์—ญ [N,4] (์ด๋ฏธ์ง€ ์ขŒํ‘œ)
"""
# roi_align: ๊ฐ ํ›„๋ณด์˜์—ญ์„ 7x7 ๊ณ ์ • ํฌ๊ธฐ ํŠน์ง•์œผ๋กœ ์ถ”์ถœ
# batch index 0 (batch_size=1 ๊ฐ€์ •) ๋ฅผ ์•ž์— ๋ถ™์ธ๋‹ค
batch_idx = torch.zeros((rois.shape[0], 1), device=rois.device)
rois_b = torch.cat([batch_idx, rois], dim=1) # [N,5]
pooled = roi_align(feat, rois_b,
output_size=(self.roi_size, self.roi_size),
spatial_scale=1.0 / stride,
sampling_ratio=2)
x = pooled.flatten(1)
x = self.fc(x)
return self.cls(x), self.reg(x)
# ---------------------------------------------------------------
# ์ „์ฒด ๋ชจ๋ธ
# ---------------------------------------------------------------
class FasterRCNN(nn.Module):
def __init__(self, num_classes):
super().__init__()
self.backbone = Backbone()
self.rpn = RPN(self.backbone.out_channels)
self.head = RoIHead(num_classes, self.backbone.out_channels)
self.num_classes = num_classes
self.base_anchors = generate_anchors() # 9๊ฐœ
self.stride = self.backbone.stride
# ํ•™์Šต/์ถ”๋ก  ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ
self.rpn_pre_nms = 12000
self.rpn_post_nms_train = 2000
self.rpn_post_nms_test = 300
self.rpn_nms_thresh = 0.7
def _anchors_for(self, feat_h, feat_w, device):
a = shift_anchors(self.base_anchors, feat_h, feat_w, self.stride)
return a.to(device)
def _proposals(self, rpn_logits, rpn_deltas, anchors, img_hw, training):
"""RPN ์ถœ๋ ฅ โ†’ NMS ๊ฑฐ์นœ ํ›„๋ณด์˜์—ญ(proposal) ์ƒ์„ฑ."""
scores = torch.sigmoid(rpn_logits[0]) # [K]
deltas = rpn_deltas[0] # [K,4]
proposals = decode_boxes(deltas, anchors)
proposals = clip_boxes(proposals, img_hw[0], img_hw[1])
# ์ ์ˆ˜ ์ƒ์œ„๋งŒ ์ถ”๋ฆฐ ๋’ค NMS
n_pre = min(self.rpn_pre_nms, scores.numel())
top = scores.topk(n_pre).indices
proposals, scores = proposals[top], scores[top]
keep = nms(proposals, scores, self.rpn_nms_thresh)
n_post = self.rpn_post_nms_train if training else self.rpn_post_nms_test
keep = keep[:n_post]
return proposals[keep], scores[keep]
def forward(self, image, target=None):
"""
image : [1,3,H,W] ๋‹จ์ผ ์ด๋ฏธ์ง€(batch_size=1)
target : ํ•™์Šต ์‹œ {'boxes','labels'}, ์ถ”๋ก  ์‹œ None
"""
device = image.device
img_h, img_w = image.shape[-2:]
feat = self.backbone(image)
_, _, fh, fw = feat.shape
anchors = self._anchors_for(fh, fw, device)
rpn_logits, rpn_deltas, _ = self.rpn(feat)
proposals, _ = self._proposals(
rpn_logits, rpn_deltas, anchors, (img_h, img_w),
training=self.training)
if self.training:
# ํ•™์Šต ๊ฒฝ๋กœ: ์†์‹ค ๊ณ„์‚ฐ (train.py ์˜ ํ—ฌํผ๊ฐ€ ๋‹ด๋‹น)
return {
"feat": feat, "anchors": anchors,
"rpn_logits": rpn_logits[0], "rpn_deltas": rpn_deltas[0],
"proposals": proposals, "stride": self.stride,
}
else:
# ์ถ”๋ก  ๊ฒฝ๋กœ: ์ตœ์ข… ํƒ์ง€ ๊ฒฐ๊ณผ
cls_logits, reg = self.head(feat, proposals, self.stride)
return self._postprocess(cls_logits, reg, proposals, (img_h, img_w))
@torch.no_grad()
def _postprocess(self, cls_logits, reg, proposals, img_hw,
score_thresh=0.05, nms_thresh=0.3, max_det=100):
"""RoI Head ์ถœ๋ ฅ โ†’ ํด๋ž˜์Šค๋ณ„ NMS โ†’ ์ตœ์ข… ๋ฐ•์Šค/๋ผ๋ฒจ/์ ์ˆ˜."""
probs = F.softmax(cls_logits, dim=1) # [N, C]
N, C = probs.shape
reg = reg.reshape(N, C, 4)
all_boxes, all_scores, all_labels = [], [], []
for c in range(1, C): # 0=๋ฐฐ๊ฒฝ ์ œ์™ธ
scores_c = probs[:, c]
mask = scores_c > score_thresh
if mask.sum() == 0:
continue
boxes_c = decode_boxes(reg[mask, c], proposals[mask])
boxes_c = clip_boxes(boxes_c, img_hw[0], img_hw[1])
scores_c = scores_c[mask]
keep = nms(boxes_c, scores_c, nms_thresh)
all_boxes.append(boxes_c[keep])
all_scores.append(scores_c[keep])
all_labels.append(torch.full((keep.numel(),), c,
dtype=torch.int64, device=boxes_c.device))
if not all_boxes:
return {"boxes": torch.empty((0, 4)), "labels": torch.empty((0,), dtype=torch.int64),
"scores": torch.empty((0,))}
boxes = torch.cat(all_boxes)
scores = torch.cat(all_scores)
labels = torch.cat(all_labels)
# ์ „์ฒด์—์„œ ์ ์ˆ˜ ์ƒ์œ„ max_det๊ฐœ๋งŒ
if scores.numel() > max_det:
top = scores.topk(max_det).indices
boxes, scores, labels = boxes[top], scores[top], labels[top]
return {"boxes": boxes, "labels": labels, "scores": scores}