feat(models): 纯卷积缺陷分类定位模型

面向实时设备部署,全程只用 convolution,不含 attention / transformer。

- backbones: 自研 tiny_conv(深度可分离,默认)与 torchvision 封装(resnet/mobilenet/shufflenet)
- necks: nearest 上采样 + 3x3 卷积,把特征恢复到 stride 4,兼顾小缺陷定位与速度
- heads: anchor-free 中心点定位头(heatmap + wh + offset)与图像级分类头;
  分类头输出维度随 label_mode 切换(多类多热 / 单类互斥含背景位)
- postprocess: 3x3 max-pool 峰值抑制替代 NMS 循环,无循环、可导出 TensorRT / OpenVINO

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-02 17:54:55 +08:00
co-authored by Claude Opus 5
parent f4eff8beee
commit 3a36aad6c3
14 changed files with 504 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
from models.backbones.resnet import TorchvisionBackbone
from models.backbones.simple_conv import TinyConvNet
__all__ = ["TinyConvNet", "TorchvisionBackbone"]
+73
View File
@@ -0,0 +1,73 @@
"""torchvision 卷积骨架封装(resnet / mobilenet 等纯卷积模型)。"""
from __future__ import annotations
import torch
from torch import nn
from models.registry import BACKBONES
# 仅允许纯卷积骨架;ViT/Swin 等不在部署范围内。
_SUPPORTED = {
"resnet18": 512,
"resnet34": 512,
"resnet50": 2048,
"mobilenet_v3_small": 576,
"mobilenet_v3_large": 960,
"shufflenet_v2_x1_0": 1024,
}
@BACKBONES.register("torchvision")
class TorchvisionBackbone(nn.Module):
"""输出 stride=32 的单尺度特征。
Args:
arch: `_SUPPORTED` 中的模型名。
pretrained: 是否加载 ImageNet 权重(需要联网或本地缓存)。
in_channels: 输入通道,非 3 时会把第一层卷积权重按通道求均值复制。
frozen: 是否冻结骨架参数。
"""
def __init__(
self,
arch: str = "resnet18",
pretrained: bool = True,
in_channels: int = 3,
frozen: bool = False,
) -> None:
super().__init__()
if arch not in _SUPPORTED:
raise ValueError(f"不支持的骨架 {arch},可选 {sorted(_SUPPORTED)}")
import torchvision
weights = "DEFAULT" if pretrained else None
net = getattr(torchvision.models, arch)(weights=weights)
if arch.startswith("resnet"):
self.features = nn.Sequential(*list(net.children())[:-2])
else:
self.features = net.features
if in_channels != 3:
self._patch_first_conv(in_channels)
self.out_channels = _SUPPORTED[arch]
self.out_stride = 32
if frozen:
for param in self.parameters():
param.requires_grad_(False)
def _patch_first_conv(self, in_channels: int) -> None:
for module in self.features.modules():
if isinstance(module, nn.Conv2d):
weight = module.weight.data.mean(dim=1, keepdim=True).repeat(1, in_channels, 1, 1)
module.in_channels = in_channels
module.weight = nn.Parameter(weight * (3.0 / in_channels))
return
raise RuntimeError("骨架中未找到 Conv2d,无法调整输入通道")
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.features(x)
+62
View File
@@ -0,0 +1,62 @@
"""自研轻量卷积骨架,面向实时推理。"""
from __future__ import annotations
from collections.abc import Sequence
import torch
from torch import nn
from models.layers import ConvBNAct, DepthwiseSeparableConv, ResidualBlock
from models.registry import BACKBONES
_BLOCKS = {"dw": DepthwiseSeparableConv, "res": ResidualBlock}
@BACKBONES.register("tiny_conv")
class TinyConvNet(nn.Module):
"""4 个下采样 stage,输出 stride=16 的单尺度特征。
Args:
in_channels: 输入通道(薄膜灰度图为 1,RGB 为 3)。
widths: 每个 stage 的输出通道。
depths: 每个 stage 的 block 数量。
block: "dw"(深度可分离,更快) 或 "res"(残差,更强)。
"""
def __init__(
self,
in_channels: int = 3,
widths: Sequence[int] = (32, 64, 128, 256),
depths: Sequence[int] = (1, 2, 2, 2),
block: str = "dw",
) -> None:
super().__init__()
if len(widths) != len(depths):
raise ValueError("widths 与 depths 长度必须一致")
if block not in _BLOCKS:
raise ValueError(f"未知 block 类型: {block},可选 {sorted(_BLOCKS)}")
block_cls = _BLOCKS[block]
self.stem = ConvBNAct(in_channels, widths[0], kernel_size=3, stride=2)
stages: list[nn.Module] = []
prev = widths[0]
for stage_idx, (width, depth) in enumerate(zip(widths, depths, strict=True)):
layers: list[nn.Module] = []
for block_idx in range(depth):
stride = 2 if (block_idx == 0 and stage_idx > 0) else 1
layers.append(block_cls(prev, width, stride))
prev = width
stages.append(nn.Sequential(*layers))
self.stages = nn.ModuleList(stages)
self.out_channels = prev
# stem 下采样 2 倍,其后每个 stage(除第一个)各下采样 2 倍。
self.out_stride = 2 * 2 ** (len(widths) - 1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.stem(x)
for stage in self.stages:
x = stage(x)
return x