File size: 15,751 Bytes
5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc 0dc0a9b 5f462cc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | """
Raw Custom Object Detection Neural Network
Built from mathematical first principles in pure PyTorch (torch.nn)
Zero third-party model libraries or frameworks.
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class ConvBNAct(nn.Module):
"""
Mathematical Convolutional Block:
Z = W * X + b
Z_norm = gamma * (Z - mu) / sqrt(var + eps) + beta
Out = SiLU(Z_norm) = Z_norm * sigmoid(Z_norm)
"""
def __init__(self, in_c, out_c, kernel_size=3, stride=1, padding=1):
super().__init__()
self.conv = nn.Conv2d(in_c, out_c, kernel_size, stride, padding, bias=False)
self.bn = nn.BatchNorm2d(out_c)
self.act = nn.SiLU(inplace=True)
def forward(self, x):
return self.act(self.bn(self.conv(x)))
class SqueezeExcitation(nn.Module):
"""
Mathematical Squeeze-and-Excitation Channel Attention:
Z_se = Sigmoid( W2 * SiLU( W1 * AdaptiveAvgPool2d(X) ) )
Out = X * Z_se
Dynamically recalibrates channel-wise feature responses to amplify defect-specific
textural frequencies (corrosion roughness, unbolted joint cavities) while suppressing
clean structural background steelwork.
"""
def __init__(self, channels, reduction=16):
super().__init__()
reduced = max(8, channels // reduction)
self.fc = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Conv2d(channels, reduced, kernel_size=1, bias=True),
nn.SiLU(inplace=True),
nn.Conv2d(reduced, channels, kernel_size=1, bias=True),
nn.Sigmoid()
)
def forward(self, x):
return x * self.fc(x)
class ResidualBlock(nn.Module):
"""
Residual Skip Connection with Squeeze-and-Excitation Channel Attention:
y = F(x, {W_i}) * SE(F(x)) + x
Mitigates vanishing gradient and enables deep feature representations.
"""
def __init__(self, channels, use_se=True):
super().__init__()
reduced = max(16, channels // 2)
self.c1 = ConvBNAct(channels, reduced, kernel_size=1, stride=1, padding=0)
self.c2 = ConvBNAct(reduced, channels, kernel_size=3, stride=1, padding=1)
self.se = SqueezeExcitation(channels) if use_se else nn.Identity()
def forward(self, x):
return x + self.se(self.c2(self.c1(x)))
class SPPFLayer(nn.Module):
"""
Spatial Pyramid Pooling - Fast (SPPF):
Fuses multi-scale receptive field context via sequential MaxPool2d(5, stride=1, padding=2),
yielding equivalent pooling at 5x5, 9x9, and 13x13 receptive fields.
Dramatically enhances semantic context for large defects (vegetation canopy,
extended corrosion fields) with minimal computational latency.
"""
def __init__(self, in_c, out_c):
super().__init__()
c_mid = in_c // 2
self.c1 = ConvBNAct(in_c, c_mid, kernel_size=1, stride=1, padding=0)
self.c2 = ConvBNAct(c_mid * 4, out_c, kernel_size=1, stride=1, padding=0)
self.pool = nn.MaxPool2d(kernel_size=5, stride=1, padding=2)
def forward(self, x):
x = self.c1(x)
p1 = self.pool(x)
p2 = self.pool(p1)
p3 = self.pool(p2)
return self.c2(torch.cat([x, p1, p2, p3], dim=1))
class CustomBackbone(nn.Module):
"""
Deep Convolutional Feature Extractor with Multi-Scale Pyramid Hierarchy:
Input: (B, 3, H, W)
Stem: (B, 32, H/2, W/2) - Stride 2
Stage1: (B, 64, H/4, W/4) - Stride 4
Stage2: (B, 128, H/8, W/8) - Stride 8 -> Feature Map P3 (high resolution, small objects: bolts)
Stage3: (B, 256, H/16, W/16) - Stride 16 -> Feature Map P4 (medium objects: nests, insulators)
Stage4: (B, 512, H/32, W/32) - Stride 32 -> Feature Map P5 (global context & large defects: rust/veg)
"""
def __init__(self, in_channels=3):
super().__init__()
# Stem
self.stem = ConvBNAct(in_channels, 32, kernel_size=3, stride=2, padding=1)
# Stage 1: Stride 4
self.stage1 = nn.Sequential(
ConvBNAct(32, 64, kernel_size=3, stride=2, padding=1),
ResidualBlock(64),
)
# Stage 2: Stride 8 (P3)
self.stage2 = nn.Sequential(
ConvBNAct(64, 128, kernel_size=3, stride=2, padding=1),
ResidualBlock(128),
ResidualBlock(128),
)
# Stage 3: Stride 16 (P4)
self.stage3 = nn.Sequential(
ConvBNAct(128, 256, kernel_size=3, stride=2, padding=1),
ResidualBlock(256),
ResidualBlock(256),
)
# Stage 4: Stride 32 (P5) with SPPF Receptive Field Aggregation
self.stage4 = nn.Sequential(
ConvBNAct(256, 512, kernel_size=3, stride=2, padding=1),
ResidualBlock(512),
ResidualBlock(512),
SPPFLayer(512, 512),
)
def forward(self, x):
x = self.stem(x)
x = self.stage1(x)
p3 = self.stage2(x)
p4 = self.stage3(p3)
p5 = self.stage4(p4)
return p3, p4, p5
class FeaturePyramidNeck(nn.Module):
"""
3-Scale Multi-Scale Feature Aggregator (FPN + PAN dual-path fusion):
Fuses high-level semantic abstractions from P5 and P4 into fine-grained spatial details in P3,
then propagates refined spatial patterns back up to P4 and P5.
"""
def __init__(self, c3=128, c4=256, c5=512, out_c=128):
super().__init__()
# Lateral 1x1 convolutions to project all stages to uniform neck channel dimension
self.lat_p5 = ConvBNAct(c5, out_c, kernel_size=1, stride=1, padding=0)
self.lat_p4 = ConvBNAct(c4, out_c, kernel_size=1, stride=1, padding=0)
self.lat_p3 = ConvBNAct(c3, out_c, kernel_size=1, stride=1, padding=0)
# Top-down FPN smooth convolutions
self.smooth_p4_td = ConvBNAct(out_c * 2, out_c, kernel_size=3, stride=1, padding=1)
self.smooth_p3_td = ConvBNAct(out_c * 2, out_c, kernel_size=3, stride=1, padding=1)
# Bottom-up PAN downsampling and smooth convolutions
self.down_p3 = ConvBNAct(out_c, out_c, kernel_size=3, stride=2, padding=1)
self.smooth_p4_pan = ConvBNAct(out_c * 2, out_c, kernel_size=3, stride=1, padding=1)
self.down_p4 = ConvBNAct(out_c, out_c, kernel_size=3, stride=2, padding=1)
self.smooth_p5_pan = ConvBNAct(out_c * 2, out_c, kernel_size=3, stride=1, padding=1)
def forward(self, p3, p4, p5):
# 1. Lateral projections
p5_lat = self.lat_p5(p5)
p4_lat = self.lat_p4(p4)
p3_lat = self.lat_p3(p3)
# 2. Top-down pathway (FPN)
p5_up = F.interpolate(p5_lat, size=p4.shape[2:], mode="nearest")
p4_td = self.smooth_p4_td(torch.cat([p4_lat, p5_up], dim=1))
p4_up = F.interpolate(p4_td, size=p3.shape[2:], mode="nearest")
p3_out = self.smooth_p3_td(torch.cat([p3_lat, p4_up], dim=1))
# 3. Bottom-up pathway (PAN)
p3_down = self.down_p3(p3_out)
p4_out = self.smooth_p4_pan(torch.cat([p4_td, p3_down], dim=1))
p4_down = self.down_p4(p4_out)
p5_out = self.smooth_p5_pan(torch.cat([p5_lat, p4_down], dim=1))
return p3_out, p4_out, p5_out
class DecoupledDetectionHead(nn.Module):
"""
Mathematical Decoupled Prediction Head:
Separates Box Regression [tx, ty, tw, th] from Objectness [obj] and Class Probabilities [cls_1..cls_C]
"""
def __init__(self, in_c, num_classes):
super().__init__()
self.num_classes = num_classes
# Coordinate branch (tx, ty, tw, th)
self.box_conv = nn.Sequential(
ConvBNAct(in_c, in_c, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_c, 4, kernel_size=1)
)
# Objectness branch
self.obj_conv = nn.Sequential(
ConvBNAct(in_c, in_c // 2, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_c // 2, 1, kernel_size=1)
)
# Classification branch
self.cls_conv = nn.Sequential(
ConvBNAct(in_c, in_c, kernel_size=3, stride=1, padding=1),
nn.Conv2d(in_c, num_classes, kernel_size=1)
)
# Mathematical initialization: initialize objectness bias with negative prior
# sigmoid(-4.59) ~ 0.01 to prevent initial gradient explosion on mostly empty backgrounds
nn.init.constant_(self.obj_conv[-1].bias, -4.59)
def forward(self, x):
# x: (B, C, H_g, W_g)
box = self.box_conv(x).permute(0, 2, 3, 1) # (B, H_g, W_g, 4)
obj = self.obj_conv(x).permute(0, 2, 3, 1) # (B, H_g, W_g, 1)
cls_logits = self.cls_conv(x).permute(0, 2, 3, 1) # (B, H_g, W_g, num_classes)
# Concatenate predictions: [tx, ty, tw, th, obj, c1, ..., cK]
return torch.cat([box, obj, cls_logits], dim=-1)
class RawCustomDetector(nn.Module):
"""
End-to-end Raw Custom Object Detector
Combines:
- Custom Deep Residual Backbone with Squeeze-and-Excitation & SPPF
- 3-Scale Multi-scale Feature Pyramid Neck (FPN + PAN)
- Triple-scale Decoupled Detection Heads:
* Stride 8 (P3): High resolution for tiny defects (missing_bolt, small rust spots)
* Stride 16 (P4): Medium receptive field (bird_nest, damaged_insulator)
* Stride 32 (P5): Global receptive field (extensive rust zones, vegetation canopy)
"""
def __init__(self, num_classes=5, in_channels=3):
super().__init__()
self.num_classes = num_classes
self.strides = [8, 16, 32]
self.backbone = CustomBackbone(in_channels=in_channels)
self.neck = FeaturePyramidNeck(c3=128, c4=256, c5=512, out_c=128)
self.head_p3 = DecoupledDetectionHead(in_c=128, num_classes=num_classes)
self.head_p4 = DecoupledDetectionHead(in_c=128, num_classes=num_classes)
self.head_p5 = DecoupledDetectionHead(in_c=128, num_classes=num_classes)
def forward(self, x):
p3, p4, p5 = self.backbone(x)
p3_fused, p4_fused, p5_fused = self.neck(p3, p4, p5)
pred_p3 = self.head_p3(p3_fused) # (B, H/8, W/8, 5 + num_classes)
pred_p4 = self.head_p4(p4_fused) # (B, H/16, W/16, 5 + num_classes)
pred_p5 = self.head_p5(p5_fused) # (B, H/32, W/32, 5 + num_classes)
return [pred_p3, pred_p4, pred_p5]
def decode_predictions(pred_maps, strides=None, conf_thresh=0.25):
"""
Mathematical coordinate decoding from raw model logits to bounding boxes [x1, y1, x2, y2, score, class_id].
bx = (grid_x + sigmoid(tx)) * stride
by = (grid_y + sigmoid(ty)) * stride
bw = exp(tw) * stride
bh = exp(th) * stride
"""
if strides is None:
if len(pred_maps) == 3:
strides = [8, 16, 32]
else:
strides = [8, 16]
batch_detections = []
batch_size = pred_maps[0].shape[0]
for b in range(batch_size):
all_boxes = []
all_scores = []
all_classes = []
for pred_map, stride in zip(pred_maps, strides):
pred = pred_map[b] # (H_g, W_g, 5 + C)
h_g, w_g, _ = pred.shape
device = pred.device
tx = pred[..., 0]
ty = pred[..., 1]
tw = pred[..., 2]
th = pred[..., 3]
obj_logit = pred[..., 4]
cls_logits = pred[..., 5:]
# Grid mesh
gy, gx = torch.meshgrid(
torch.arange(h_g, device=device, dtype=torch.float32),
torch.arange(w_g, device=device, dtype=torch.float32),
indexing="ij"
)
# Mathematical center & size transformation
bx = (gx + torch.sigmoid(tx)) * stride
by = (gy + torch.sigmoid(ty)) * stride
bw = torch.exp(torch.clamp(tw, max=5.0)) * stride
bh = torch.exp(torch.clamp(th, max=5.0)) * stride
# Convert to [x1, y1, x2, y2]
x1 = bx - bw / 2.0
y1 = by - bh / 2.0
x2 = bx + bw / 2.0
y2 = by + bh / 2.0
# Probabilities
obj_prob = torch.sigmoid(obj_logit)
cls_prob = torch.softmax(cls_logits, dim=-1)
scores = obj_prob.unsqueeze(-1) * cls_prob # (H_g, W_g, num_classes)
# Flatten [x1, y1, x2, y2, bx, by] (bounding box + exact location point)
boxes = torch.stack([x1, y1, x2, y2, bx, by], dim=-1).view(-1, 6)
scores = scores.view(-1, cls_logits.shape[-1])
max_scores, class_ids = torch.max(scores, dim=-1)
mask = max_scores >= conf_thresh
if mask.sum() > 0:
all_boxes.append(boxes[mask])
all_scores.append(max_scores[mask])
all_classes.append(class_ids[mask])
if len(all_boxes) > 0:
batch_boxes = torch.cat(all_boxes, dim=0)
batch_scores = torch.cat(all_scores, dim=0)
batch_classes = torch.cat(all_classes, dim=0)
batch_detections.append((batch_boxes, batch_scores, batch_classes))
else:
batch_detections.append((
torch.empty((0, 6), device=pred_maps[0].device),
torch.empty((0,), device=pred_maps[0].device),
torch.empty((0,), dtype=torch.long, device=pred_maps[0].device)
))
return batch_detections
def custom_nms(boxes, scores, classes, iou_thresh=0.45):
"""
Mathematical Non-Maximum Suppression (NMS)
Removes redundant overlapping bounding boxes using Intersection over Union (IoU).
Preserves exact location points [x1, y1, x2, y2, bx, by, score, class_id].
"""
if boxes.shape[0] == 0:
return torch.empty((0, 8), device=boxes.device)
keep_indices = []
# Separate NMS per class to avoid suppressing adjacent different defects
unique_classes = torch.unique(classes)
for c in unique_classes:
c_mask = (classes == c)
c_indices = torch.nonzero(c_mask).squeeze(-1)
c_boxes = boxes[c_indices]
c_scores = scores[c_indices]
# Sort descending by score
order = torch.argsort(c_scores, descending=True)
keep = []
while order.numel() > 0:
curr = order[0].item()
keep.append(c_indices[curr].item())
if order.numel() == 1:
break
curr_box = c_boxes[order[0], :4].unsqueeze(0)
rest_boxes = c_boxes[order[1:], :4]
# Mathematical IoU
x1 = torch.max(curr_box[:, 0], rest_boxes[:, 0])
y1 = torch.max(curr_box[:, 1], rest_boxes[:, 1])
x2 = torch.min(curr_box[:, 2], rest_boxes[:, 2])
y2 = torch.min(curr_box[:, 3], rest_boxes[:, 3])
inter_w = torch.clamp(x2 - x1, min=0)
inter_h = torch.clamp(y2 - y1, min=0)
inter_area = inter_w * inter_h
area_curr = (curr_box[:, 2] - curr_box[:, 0]) * (curr_box[:, 3] - curr_box[:, 1])
area_rest = (rest_boxes[:, 2] - rest_boxes[:, 0]) * (rest_boxes[:, 3] - rest_boxes[:, 1])
union_area = area_curr + area_rest - inter_area + 1e-7
iou = inter_area / union_area
remaining = torch.nonzero(iou <= iou_thresh).squeeze(-1)
if remaining.numel() == 0:
break
order = order[remaining + 1]
keep_indices.extend(keep)
if len(keep_indices) == 0:
return torch.empty((0, 8), device=boxes.device)
keep_idx = torch.tensor(keep_indices, device=boxes.device, dtype=torch.long)
final_boxes = boxes[keep_idx] # (N, 6): [x1, y1, x2, y2, bx, by]
final_scores = scores[keep_idx].unsqueeze(-1)
final_classes = classes[keep_idx].unsqueeze(-1).float()
return torch.cat([final_boxes, final_scores, final_classes], dim=-1)
|