目标检测模型FCOS代码详细解析

目标检测模型FCOS代码详细解析

python -m torch.distributed.launch \ --nproc_per_node=1 \ --master_port=$((RANDOM + 10000)) \ D:\Project\FCOS-master\tools\train_net.py \ --config-file D:\Project\FCOS-master\configs\fcos\fcos_R_50_FPN_1x.yaml \ DATALOADER.NUM_WORKERS 1 \ OUTPUT_DIR D:\Project\FCOS-master\FCOS_imprv_R_50_FPN_1x.pth

train_net.py文件中

定义了main()函数,定义了train()函数,定义了test()函数

在main()函数中对传入的参数进行解析,对分布式训练进行判断。再判断是进行测试还是训练。

如果是测试,则进入tain()函数。

main()函数

main()函数中通过语句实现train()函数的调用。model = train(cfg, args.local_rank, args.distributed)

train()函数

train()函数中首先使用

**model = build_detection_model(cfg),**这句话是调用build_detection_model来根据cfg构建网络模型。

其中build_detection_model()函数调用自fcos_core.modeling.detector,

image-20240225123204134

依次追溯至GeneralizedRCNN类

image-20240225123216886

GeneralizedRCNN类

image-20240225123231158

其中GeneralizedRCNN类继承torch.nn.model类,有三个实例化变量

#  首先调用build_backbone函数构建主干网络,build_rpn函数构建区域生成网络,build_roi_heads构建区域兴趣头
    def __init__(self, cfg):
        super(GeneralizedRCNN, self).__init__()
        #  Backbone(主干网络):对输入的图像进行特征提取。
        self.backbone = build_backbone(cfg)
        #  RPN(区域生成网络):根据主干网络提取的特征生成候选框
        self.rpn = build_rpn(cfg, self.backbone.out_channels)
        #  RoI Heads(区域兴趣头):对候选框进行检测或者分割等任务。
        self.roi_heads = build_roi_heads(cfg, self.backbone.out_channels)

其中build_backbone()调用自fcos_core.modeling.backbone

build_rpn()函数调用自fcos_core.modeling.rpn.rpn

nuild_roi_heads()函数调用自fcos_score.modeling.roi_head.roi_heads

build_backbone()函数(建立网络backbone)

image-20240225123245755

找到

image-20240225123303102

registry调用自fcos_core.modeling,追索至fcos_core.utils.registry.Registry

Registry类的说明

是一个注册表类

# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.


def _register_generic(module_dict, module_name, module):
    assert module_name not in module_dict
    module_dict[module_name] = module

#  实现了一个注册表(Registry)类,用于管理不同模块(如backbone、ROI head、loss function等)的注册和访问
class Registry(dict):
    '''
    A helper class for managing registering modules, it extends a dictionary
    and provides a register functions.

    Eg. creeting a registry:
        some_registry = Registry({"default": default_module})

    There're two ways of registering new modules:
    1): normal way is just calling register function:
        def foo():
            ...
        some_registry.register("foo_module", foo)
    2): used as decorator when declaring the module:
        @some_registry.register("foo_module")
        @some_registry.register("foo_modeul_nickname")
        def foo():
            ...

    Access of module is just like using a dictionary, eg:
        f = some_registry["foo_modeul"]
    '''
    def __init__(self, *args, **kwargs):
        super(Registry, self).__init__(*args, **kwargs)
    #  该类继承了字典类,并提供了一个register函数,用于注册新的模块
    #  将register函数作为装饰器,用于在声明新模块时自动进行注册
    def register(self, module_name, module=None):
        # used as function call
        #  如果输入module不为空,将其直接加入到字典中
        if module is not None:
            _register_generic(self, module_name, module)
            return
        #  否则返回一个register_fn函数,用于将后续声明的函数作为装饰器进行注册
        # used as decorator
        def register_fn(fn):
            _register_generic(self, module_name, fn)
            return fn

        return register_fn

在build_backbone()函数上方

image-20240225123318181

#  使用了 Registry 类的注册功能,使用装饰器将函数 build_resnet_backbone 注册到 registry.BACKBONES 字典中,
#  并使用了四个不同的名称作为键。这意味着在其他地方可以使用build_resnet_backbone这个名称来访问 build_resnet_backbone这个函数
@registry.BACKBONES.register("R-50-C4")
@registry.BACKBONES.register("R-50-C5")
@registry.BACKBONES.register("R-101-C4")
@registry.BACKBONES.register("R-101-C5")
def build_resnet_backbone(cfg):
    #  build_resnet_backbone 函数接受一个配置文件 cfg 作为参数
    #  函数会构建 ResNet 的主干网络,并将其包装在一个 nn.Sequential 容器中
    body = resnet.ResNet(cfg)
    model = nn.Sequential(OrderedDict([("body", body)]))
    #  函数的最后一行设置了输出通道数,以便在后续的构建过程中使用。
    model.out_channels = cfg.MODEL.RESNETS.BACKBONE_OUT_CHANNELS
    return model

比如此处虽然使用的是“R-50-C4"这个名称,但是其实是访问Build_resnet_backbone这个函数

image-20240225123334502

因此build_backbone()->build_resnet_backbone()。因为在build_backbone函数中

image-20240225123342950

这里是使用注册表实现的,所以说在build_backbone函数时,进入到Build_resnet_backn=bone函数中

build_resnet_bockbone函数

在build_resnet_backbone函数的第一句中,调用resnet.py文件中的ResNet类实现ResNet网络的Body部分,resnet.ResNet

image-20240225123356181

image-20240225123406514

ResNet

首先看ResNet的方法__init()__

image-20240225123417069

stem_module

image-20240225123426237

->StemWithFixedBatchNorm(BaseStem)中

image-20240225123434190

->BaseStem类

image-20240225123443054

class BaseStem(nn.Module):
    def __init__(self, cfg, norm_func):
        #  数cfg和norm_func
        super(BaseStem, self).__init__()
        # 根据参数得到输出通道数
        out_channels = cfg.MODEL.RESNETS.STEM_OUT_CHANNELS
        #  定义了一个7x7的卷积层self.conv1
        self.conv1 = Conv2d(
            3, out_channels, kernel_size=7, stride=2, padding=3, bias=False
        )
        # 一个相应的归一化层self.bn1,归一化层类型是通过norm_func参数指定的
        self.bn1 = norm_func(out_channels)
        #  在初始化这些层的权重时,采用了Kaiming均匀初始化的方法。
        for l in [self.conv1,]:
            nn.init.kaiming_uniform_(l.weight, a=1)

    def forward(self, x):
        #  输入x首先经过卷积、归一化、relu
        x = self.conv1(x)
        x = self.bn1(x)
        x = F.relu_(x)
        #  3x3的最大池化层进行处理
        x = F.max_pool2d(x, kernel_size=3, stride=2, padding=1)
        return x

上述BaseStem类对应resNet的这一部分

image-20240225123453179

self.conv1 = Conv2d(
3, out_channels, kernel_size=7, stride=2, padding=3, bias=False
)

其中conv2d追溯到

conv2d

image-20240225123503635

参数中的FrozenBatchNorm2d追溯到

FrozenBatchNorm2d类

image-20240225123517141

class FrozenBatchNorm2d(nn.Module):
    """
    BatchNorm2d where the batch statistics and the affine parameters
    are fixed
    """
    #  使用register_buffer方法来注册四个buffer变量,分别是权重weight、偏置bias、运行时均值running_mean和方差running_var
    def __init__(self, n):
        super(FrozenBatchNorm2d, self).__init__()
        self.register_buffer("weight", torch.ones(n))
        self.register_buffer("bias", torch.zeros(n))
        self.register_buffer("running_mean", torch.zeros(n))
        self.register_buffer("running_var", torch.ones(n))

    def forward(self, x):
        #  通过这四个buffer变量来计算每个通道上的缩放系数scale和偏置bias
        scale = self.weight * self.running_var.rsqrt()
        bias = self.bias - self.running_mean * scale
        scale = scale.reshape(1, -1, 1, 1)
        bias = bias.reshape(1, -1, 1, 1)
        #  将其应用于输入张量x,最终返回归一化的结果。
        #  由于冻结了batch的统计量和仿射参数,因此在训练过程中这个层的参数是不会被更新的。
        return x * scale + bias

回到ResNet类中

stage_specs

->

image-20240225123531727

image-20240225123543329

image-20240225123550821

transformation_module

->image-20240225123602769

->BottleneckWithFixedBatchNorm类

这个类其实就是继承父类,只是传入的参数对父类实现一个继承

image-20240225123611163

->继承自bottleneck
class Bottleneck(nn.Module):
    #  该模块的参数设置
    def __init__(
        self,
        in_channels,
        bottleneck_channels,
        out_channels,
        num_groups,
        stride_in_1x1,
        stride,
        dilation,
        norm_func,
        dcn_config
    ):
        super(Bottleneck, self).__init__()

        self.downsample = None
        #  如果输入和输出通道数不同,该模块会使用一个 1x1 卷积层作为下采样的方式
        if in_channels != out_channels:
            down_stride = stride if dilation == 1 else 1
            #  下采样通过 self.downsample 层来实现
            self.downsample = nn.Sequential(
                #  Conv2d() 函数表示这个卷积层的定义,使用了 kernel_size=1、stride=down_stride、bias=False 等参数
                Conv2d(
                    in_channels, out_channels,
                    kernel_size=1, stride=down_stride, bias=False
                ),
                #   norm_func(out_channels) 表示使用了归一化函数来对卷积层的输出进行标准化处理
                norm_func(out_channels),
            )
            # 对于 self.downsample 里面的 Conv2d 层进行权重初始化
            for modules in [self.downsample,]:
                for l in modules.modules():
                    if isinstance(l, Conv2d):
                        #  来对卷积层的权重进行初始化,保证了神经网络的训练效果。
                        nn.init.kaiming_uniform_(l.weight, a=1)
        #  如果 dilation 大于 1,说明当前模块使用了空洞卷积(dilated convolution),
        #  在这种情况下,如果继续使用原本的 stride,会导致输出 feature map 大小与输入 feature map 大小不匹配。
        #  因此需要将 stride 重置为 1。
        if dilation > 1:
            stride = 1 # reset to be 1

        # The original MSRA ResNet models have stride in the first 1x1 conv
        # The subsequent fb.torch.resnet and Caffe2 ResNe[X]t implementations have
        # stride in the 3x3 conv
        #  stride_in_1x1 为 True,表示在第一个1x1卷积层上使用步长stride,在第二个 3x3 卷积层上使用步长 1,否则反过来
        stride_1x1, stride_3x3 = (stride, 1) if stride_in_1x1 else (1, stride)

        self.conv1 = Conv2d(
            in_channels,
            bottleneck_channels,
            kernel_size=1,
            stride=stride_1x1,
            bias=False,
        )
        self.bn1 = norm_func(bottleneck_channels)
        # TODO: specify init for the above
        with_dcn = dcn_config.get("stage_with_dcn", False)
        #  根据是否使用可变卷积deformable convolutional networks模块来选择不同的卷积层
        #  如果使用可变形卷积模块,则使用 DFConv2d 类,否则使用普通的 Conv2d 类
        if with_dcn:
            deformable_groups = dcn_config.get("deformable_groups", 1)
            with_modulated_dcn = dcn_config.get("with_modulated_dcn", False)
            self.conv2 = DFConv2d(
                bottleneck_channels,
                bottleneck_channels,
                with_modulated_dcn=with_modulated_dcn,
                kernel_size=3,
                stride=stride_3x3,
                groups=num_groups,
                dilation=dilation,
                deformable_groups=deformable_groups,
                bias=False
            )
        else:
            self.conv2 = Conv2d(
                bottleneck_channels,
                bottleneck_channels,
                kernel_size=3,
                stride=stride_3x3,
                padding=dilation,
                bias=False,
                groups=num_groups,
                dilation=dilation
            )
            nn.init.kaiming_uniform_(self.conv2.weight, a=1)
        #  定义bn层
        self.bn2 = norm_func(bottleneck_channels)
        #  定义第三个卷积层,用于将特征图的通道数从 bottleneck_channels 降到 out_channels。
        self.conv3 = Conv2d(
            bottleneck_channels, out_channels, kernel_size=1, bias=False
        )
        #  定义Bn
        self.bn3 = norm_func(out_channels)
        #  对卷积层的权重进行初始化
        for l in [self.conv1, self.conv3,]:
            nn.init.kaiming_uniform_(l.weight, a=1)

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = F.relu_(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = F.relu_(out)

        out0 = self.conv3(out)
        out = self.bn3(out0)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = F.relu_(out)

        return out

上述过程是基本的Residual Block模块,ResNet是由多个这样的Residual Block组成的深度卷积神经网络,其中包含多个层级,包括卷积层、池化层、全连接层等。每个Residual Block中包含多个卷积层、批量归一化层和激活函数层,其中还包括一条捷径(shortcut)连接,用于跳过某些层级,以避免深度网络中的梯度消失问题。整个ResNet的结构是由多个这样的Residual Block组成,其中深度可以通过增加Residual Block的数量来控制。

#  其中stem_module是通过配置文件cfg中的MODEL.RESNETS.STEM_FUNC来决定使用哪种类型的stem模块。
        self.stem = stem_module(cfg)

        #  从配置文件中获取ResNet中的卷积层组数(num_groups)
        # Constuct the specified ResNet stages
        num_groups = cfg.MODEL.RESNETS.NUM_GROUPS
        #  获得每个卷积层组内卷积核数量的基数,这个值可以控制模型的宽度,即卷积核的数量
        width_per_group = cfg.MODEL.RESNETS.WIDTH_PER_GROUP
        #  STEM_OUT_CHANNELS是一个整数,表示ResNet的stem阶段的输出通道数,即特征图的深度
        in_channels = cfg.MODEL.RESNETS.STEM_OUT_CHANNELS
        #  计算 Stage 2 的 Bottleneck 块的输出通道数
        #   num_groups 和 width_per_group 两个参数决定的。num_groups 是指 ResNet 中的组数,
        #   即 Group Convolution 的数量,而 width_per_group 则表示每个组中的卷积核数量
        stage2_bottleneck_channels = num_groups * width_per_group
        #  为了从配置文件中读取ResNet第二个阶段的输出通道数(即stage2的输出通道数),
        #  并将其存储在一个变量stage2_out_channels中,以便后续的构建网络。
        stage2_out_channels = cfg.MODEL.RESNETS.RES2_OUT_CHANNELS

        #  for循环中用于构造ResNet的各个stage阶段,并将它们添加到模型中
        self.stages = [] #  定义一个空列表,用于保存所有的stage
        self.return_features = {} #  定义一个空字典,用于保存每个stage的返回特征
        #  使用stage_specs中的信息(即定义ResNet时的stage数量,block数量等信息),来循环构建每个stage模块
        for stage_spec in stage_specs:
            #  为每个stage起个名字,根据stage的index来定义名字
            name = "layer" + str(stage_spec.index)
            #  stage_spec.index 是一个整数,表示当前阶段的索引,例如第一个阶段的索引是 1,第二个阶段的索引是 2
            #  2 ** (stage_spec.index - 1) 表示将 2 的 (stage_spec.index - 1) 次方作为 stage2_relative_factor 的值
            #  即当前阶段的相对比例因子,这个因子会用来计算当前阶段的bottleneck通道数和输出通道数。
            stage2_relative_factor = 2 ** (stage_spec.index - 1) #  计算Bottleneck的通道数和通道数的缩放因子
            #  当前stage(即每个layer)中 bottleneck block 中的输出通道数
            #  随着 stage 的深入,每个 bottleneck block 的输出通道数都逐步增加
            bottleneck_channels = stage2_bottleneck_channels * stage2_relative_factor #  根据缩放因子计算当前bottleneck的通道数
            out_channels = stage2_out_channels * stage2_relative_factor  #  根据缩放因子计算当前stage中的输出通道数
            stage_with_dcn = cfg.MODEL.RESNETS.STAGE_WITH_DCN[stage_spec.index - 1] #  判断当前stage是否使用DCN
            #  使用_make_stage函数构造具有所需卷积层和块的stage

stage_with_dcn = cfg.MODEL.RESNETS.STAGE_WITH_DCN[stage_spec.index - 1]

image-20240225123708431

dcn指Deformable convlutions可变性卷积,且配置为关闭,因此看后面的代码可暂且不管dcn有关的部分。

image-20240225123724327

在resnet.py文件中,找到def _make_stage()函数

def _make_stage()函数

def _make_stage()是用于生成一个由多个卷积块组成的 stage(阶段)的函数

def _make_stage(
    transformation_module,
    in_channels,#  in_channels: 当前 stage 的输入通道数
    bottleneck_channels,#  bottleneck 层的通道数,如果使用的是 BasicBlock,则此参数无效。
    out_channels, #  当前 stage 的输出通道数
    block_count,#   当前 stage 中卷积块的数量
    num_groups,#  在 bottleneck 层中使用的分组卷积的组数
    stride_in_1x1,#   在 bottleneck 层中使用的 1x1 卷积的步幅
    first_stride,#  第一个卷积块的步幅
    dilation=1,#  卷积块中卷积核的膨胀率
    dcn_config=None #  是否使用 Deformable Convolutional Networks(DCN)
):
    #  函数接收的参数有:transformation_module: 一个转换模块的类,用于生成每个卷积块。
    #  这个模块通常是基于 ResNet 的 BasicBlock 或 Bottleneck
    blocks = []
    stride = first_stride
    #  进行for循环
    for _ in range(block_count):
        #  将每个block_count添加到blocks列表中
        #  # 当前卷积块加入 blocks 列表中,
        blocks.append(
            transformation_module(
                in_channels,
                bottleneck_channels,
                out_channels,
                num_groups,
                stride_in_1x1,
                stride,
                dilation=dilation,
                dcn_config=dcn_config
            )
        )
        #  根据是否为第一个卷积块,设置当前卷积块的步幅
        stride = 1
        #  并将输入通道数更新为输出通道数,以便下一个卷积块使用
        in_channels = out_channels
    #  函数将所有卷积块连接成一个 nn.Sequential,作为整个 stage 的输出
    return nn.Sequential(*blocks)

故上段代码定义每一阶段模型,每个阶段的block_count参数指定模型中串接的transformation_module个数

image-20240225123735955

其中image-20240225123746208

ResNet的init()方法结束

ResNet的方法_freeze_backbone()开始

  #  用于冻结ResNet模型的某些层的参数,使得这些层的参数在训练过程中不会被更新
    def _freeze_backbone(self, freeze_at):
        #  输入参数 freeze_at 指定了要冻结到哪一层(从0开始计数,0表示冻结所有层)。
        #  在函数内部,它会遍历模型的所有层,并将指定层之前的所有参数的 requires_grad 属性设为 False。
        if freeze_at < 0:
            return
        for stage_index in range(freeze_at):
            if stage_index == 0:
                m = self.stem  # stage 0 is the stem
            else:
                m = getattr(self, "layer" + str(stage_index))
            for p in m.parameters():
                p.requires_grad = False

根据注释,作者定义stage()是stem,即stem_module。而stage1、2、3分别是3、4、6个transformation_module串接成的模型。stage0、1的requires_grad设定为False

ResNet的forward()方法

    def forward(self, x):
        outputs = []
        #  先将x输入到stem中
        x = self.stem(x)
        #  依次将数据输入到每个 ResNet 阶段self.stages 中进行处理,处理完成后,将需要返回的阶段的输出数据存储在 outputs 列表中并返回
        for stage_name in self.stages:
            x = getattr(self, stage_name)(x)
            if self.return_features[stage_name]:
                outputs.append(x)
        return outputs

stem_module结构

in->conv1->bn1->relu->max_pool2d->out

transformation_module结构

in->conv1->bn1->relu->conv2->bn2->relu->conv3->bn3

in->downsample(conv)若输入输出通道数不同时有此跳跃连接层

bn3+downsample->relu->out

ResNet结构

stage0:stem_module

stage1:transformation_module->t_m->t_m

stage2:

transformation_module->t_m->t_m->t_m

stages3:

transformation_module->t_m->t_m->t_m->t_m->t_m

image-20240225123802227

ResNet总体结构

整个ResNet结构被分为几部分

init()模块:用于构建ResNet的各个模块,并将它们组装在一起,以构建完整的ResNet。

stem模块:数据输入后经过的第一次池化和卷积,用于构建ResNet的stem部分,也就是ResNet的前几层。

各个stage模块:通过循环构建各个stage模块,并将它们添加到模型中,最终组成完整的ResNet模型。每个stage模块包含若干个bottleneck block(残差块),通过循环构建若干个bottleneck block,并将它们添加到stage模块中来构建整个stage模块。bottleneck block包含三个卷积层:

1x1卷积层、3x3卷积层和1x1卷积层,其中1x1卷积层用于减少通道数,3x3卷积层用于进行卷积操作,另一个1x1卷积层用于增加通道数。bottleneck block的作用是在增加网络深度的同时减少计算量,同时提高网络的精度和效果。

image-20240225123900287

至此ResNet分析结束

build_rpn()

build_rpn调用自

fcos_core.modeling.rpn.rpn

#  用于构建rpn模块的代码
def build_rpn(cfg, in_channels):
    """
    This gives the gist of it. Not super important because it doesn't change as much
    """
    #  根据cfg.MODEL.FCOS_ON 和 cfg.MODEL.RETINANET_ON 的值,决定使用哪种模型进行构建
    #   如果cfg.MODEL.FCOS_ON 为 True,则会调用 build_fcos 函数进行 FCOS 模型的构建
    if cfg.MODEL.FCOS_ON:
        return build_fcos(cfg, in_channels)
    #  如果 cfg.MODEL.RETINANET_ON 为 True,则会调用 build_retinanet 函数进行 RetinaNet 模型的构建
    if cfg.MODEL.RETINANET_ON:
        return build_retinanet(cfg, in_channels)
    #  如果 cfg.MODEL.FCOS_ON 和 cfg.MODEL.RETINANET_ON 都为 False,则会使用 RPNModule 类构建传统的基于 anchor 的 RPN 模块。
    return RPNModule(cfg, in_channels)

image-20240225123914915

那么会调用build_fcos。其中build_fcos调用自fcod_core.modeling.rpn.fcos.fcos

image-20240225123923667

故builf_rpn()->build_fcos->FCOSModule

FCOSModule类

image-20240225123934525

类FCOSModule有4个实例变量

head:FCOSHead在上方定义

box_selector_test:

make_fcos_postprocessor调用自fcos_core.modeling.rpn.fcos.inference

loss_evaluation:make_fcos_loss_evaluator()

调用自fcos_core.modeling.rpn.fcos.loss(损失)

fpn_strides:在focs_core.config.defaults中找到

image-20240225123945485

head=FCOSHead(cfg,in_channels)
init()方法

image-20240225123953000

_C.MODEL.RETINANET.NUM_CLASSES = 81
_C.MODEL.FCOS.FPN_STRIDES = [8, 16, 32, 64, 128]
_C.MODEL.FCOS.NORM_REG_TARGETS = False
_C.MODEL.FCOS.CENTERNESS_ON_REG = False
_C.MODEL.FCOS.USE_DCN_IN_TOWER = False

定义子模型cls_tower、bbox_tower

  #  定义两个空列表
        cls_tower = []
        bbox_tower = []
        #  对cls_tower和bbox_tower循环 cfg.MODEL.FCOS.NUM_CONVS 次
        for i in range(cfg.MODEL.FCOS.NUM_CONVS): # 4次
            #  self.use_dcn_in_tower 为真,且当前是最后一次循环的话
            #  则使用特殊的 DFConv2d 类型的卷积层,否则使用标准的 nn.Conv2d 类型的卷积层。
            if self.use_dcn_in_tower and \
                    i == cfg.MODEL.FCOS.NUM_CONVS - 1:

                conv_func = DFConv2d
            else:
                conv_func = nn.Conv2d

            cls_tower.append(
                #  每次循环时,先向 cls_tower 和 bbox_tower 分别添加一个卷积层,该卷积层的输入通道数和输出通道数都是 in_channels
                conv_func(
                    in_channels,
                    in_channels,
                    kernel_size=3,
                    stride=1,
                    padding=1,
                    bias=True
                )
            )
            #  然后向 cls_tower 和 bbox_tower 分别添加一个 GroupNorm 层,通道数为 in_channels,再加上一个 ReLU 激活函数。
            cls_tower.append(nn.GroupNorm(32, in_channels))
            cls_tower.append(nn.ReLU())

            bbox_tower.append(
                conv_func(
                    in_channels,
                    in_channels,
                    kernel_size=3,
                    stride=1,
                    padding=1,
                    bias=True
                )
            )
            bbox_tower.append(nn.GroupNorm(32, in_channels))
            bbox_tower.append(nn.ReLU())
        #  循环执行这个过程,添加 cfg.MODEL.FCOS.NUM_CONVS 个卷积层到两个 Tower 中
        #  “也就是说对分类和回归进行的都是一样的操作”
        #  对分类、回归和中心度预测分支的网络结构进行定义
        #  首先通过nn.Sequential将之前定义的分类和回归特征提取网络结构cls_tower和bbox_tower封装成序列化的网络结构,
        #  并将其作为一个子模块添加到当前模型中。
        self.add_module('cls_tower', nn.Sequential(*cls_tower))
        self.add_module('bbox_tower', nn.Sequential(*bbox_tower))

        #  最终定义了三个卷积层cls_logits、bbox_pred和centerness,分别用于预测类别概率、边界框坐标和中心度
        #  三个卷积层的输入通道数都是in_channels,表示它们都会在之前定义的特征提取网络的基础上进行预测。

        #  cls_logits的输出通道数是分类数num_classes
        self.cls_logits = nn.Conv2d(
            in_channels, num_classes, kernel_size=3, stride=1,
            padding=1
        )
        #  bbox_pred的输出通道数是4
        self.bbox_pred = nn.Conv2d(
            in_channels, 4, kernel_size=3, stride=1,
            padding=1
        )
        # centerness的输出通道数是1
        self.centerness = nn.Conv2d(
            in_channels, 1, kernel_size=3, stride=1,
            padding=1
        )
        #  对网络的权重参数进行初始化
        #  在目标检测中。对于卷积层的初始化常常使用均值为0,方差为0.01的正态分布随机初始化方法。而对于偏置,通常初始化为0
        # initialization
        for modules in [self.cls_tower, self.bbox_tower,
                        self.cls_logits, self.bbox_pred,
                        self.centerness]:
            for l in modules.modules():
                if isinstance(l, nn.Conv2d):
                    torch.nn.init.normal_(l.weight, std=0.01)
                    torch.nn.init.constant_(l.bias, 0)

        #  FCOS模型中的分类分支设置偏置,通过调整可以使模型收敛的更快
        #  initialize the bias for focal loss
        #  prior_prob是一个超参数,代表了在训练集中目标出现的概率
        prior_prob = cfg.MODEL.FCOS.PRIOR_PROB
        #  bias_value是根据prior_prob计算出的偏置值,可以被看作是一种先验知识,用于让模型更好地适应训练集中的目标出现概率
        bias_value = -math.log((1 - prior_prob) / prior_prob)
        torch.nn.init.constant_(self.cls_logits.bias, bias_value)
        #  创建了一个包含5个Scale实例的nn.ModuleList对象,scale类如下所示

        self.scales = nn.ModuleList([Scale(init_value=1.0) for _ in range(5)])
    #class Scale(nn.Module):
    # def __init__(self, init_value=1.0):
    #     super(Scale, self).__init__()
    #     self.scale = nn.Parameter(torch.FloatTensor([init_value]))
    #

其中scales调用自

class Scale(nn.Module):
    def __init__(self, init_value=1.0):
        super(Scale, self).__init__()
        self.scale = nn.Parameter(torch.FloatTensor([init_value]))
    def forward(self, input):
        return input * self.scale
#  实现了一个可学习的缩放因子,可以用来对输入数据进行缩放
class Scale(nn.Module):
    def __init__(self, init_value=1.0):
        super(Scale, self).__init__()
        #  nn.Parameter 创建一个可学习的参数 self.scale,并将其初始化为指定的值 init_value
        self.scale = nn.Parameter(torch.FloatTensor([init_value]))

    def forward(self, input):
        #  在 forward 函数中,将输入数据 input 乘以缩放因子 self.scale,得到缩放后的输出
        return input * self.scale

论文中提到we shared the heads between dufferent feature levels

regress different size range->exp(SiX)

五层特征共享head,但其回归范围不同,用缩放因子scale对回归结果进行缩放

方法init()结束

接着看FCOSHead的方法forward()

forward()函数
     #  接收输入张量x,通过卷积和激活等操作输出模型预测结果。
    def forward(self, x):
        logits = []
        bbox_reg = []
        centerness = []
        #  首先遍历输入张量x的每一层预测结果
        for l, feature in enumerate(x):
            #  将feature map通过分类头的卷积层cls_tower和回归头的卷积层bbox_tower,分别得到特征图cls_tower和box_tower。
            cls_tower = self.cls_tower(feature)  #  cls_tower在init函数中已经初始化完成了
            box_tower = self.bbox_tower(feature) #  同理box_tower也是
            #  对cls_tower通过分类头的卷积层cls_logits进行分类预测,得到预测类别概率logits
            logits.append(self.cls_logits(cls_tower))
            #  如果设置了centerness_on_reg,对box_tower通过回归头的卷积层centerness,得到中心度预测结果centerness
            #  否则,对cls_tower后得到的特征图进行centerness
            if self.centerness_on_reg:#  centerness_on_reg = False
                centerness.append(self.centerness(box_tower))
            else:
                centerness.append(self.centerness(cls_tower))
            #  首先通过self.bbox_pred(box_tower)得到预测框的初步偏移量,
            #  而self.scales[l]则是一个用来缩放偏移量的因子,这个因子会随着层级的不同而有所变化。
            #  通过将这两部分相乘,可以得到最终的预测框偏移量,即 bbox_pred。
            bbox_pred = self.scales[l](self.bbox_pred(box_tower))
            #  对预测的边界框坐标进行处理和归一化
            if self.norm_reg_targets:#  为True
                #  对边界框坐标进行relu激活函数,通过Relu将坐标限制为非负,防止模型输出无限制的坐标值
                bbox_pred = F.relu(bbox_pred)
                if self.training:
                    bbox_reg.append(bbox_pred)
                else:
                    bbox_reg.append(bbox_pred * self.fpn_strides[l])
            else:
            # 指数操作将预测的坐标转化为绝对坐标,乘以特征图步长后则得到相对于原始图像的偏移量,从而可以直接应用于计算预测框的位置
                bbox_reg.append(torch.exp(bbox_pred))
        #  得到预测类别分数,边界框坐标和中心度预测值
        return logits, bbox_reg, centerness

目前为止:分类分数有了、边界框坐标和中心度预测值也有了

cls_tower结构

in->conv1->gn1->relu->conv2->gn2->relu->conv3->gn3->relu->conv4->gn4->relu->out

根据self.rpn=build_rpn(cfg,self.backbone.out_channels,各通道数都为4*256

box_tower结构

in->conv1->gn1->relu->conv2->gn2->relu->conv3->gn3->relu->conv4->gn4->relu->out

FCOSHead结构

in->cls_tower->cls_logits(conv)->out

in->box_tower

box_tower->bbox_pred(conv)->scales->exp->out

box_tower->centerness(conv)->out

image-20240225124015465

FCOS的training部分关键在于调用train_net.py,故解析FCOS源码从该文件开始。

train_net.py->train()开头->build_detection_model()->GeneralizedRCNN->build_backbone(),build_rpn(),build_roi_heads()

build_backbone()->build_resnet_backbone()->ResNet

Head结束,Head中生成了所有预测的预测框、类别和中心度

Head部分完成,现在需要选取生成的预测盒子了,我们已经生成了预测盒子,但是不是所有的都要,需要做一个选择

box_selector_test

image-20240225124024379

由上述截图可知make_fcos_postprocessor()调用自fcos_core.modeling.rpn.fcos.inference

make_fcos_postprocessor()

用来创建FCOS后处理器对象的。FCOS后处理器(FCOSPostProcessor)是一个用于从模型输出中提取预测边界框的工具,它将FCOS模型的输出转换为一组边界框,这些边界框可以用于后续的非极大值抑制(NMS)步骤以及可选的水平翻转和尺度变换。

image-20240225124037171

def make_fcos_postprocessor(config):
    #  接收配置对象config作为参数,并使用该对象中的配置信息来创建FCOSPostProcessor对象
    pre_nms_thresh = config.MODEL.FCOS.INFERENCE_TH #  pre_nms_thresh指定了用于保留预测边界框的分数阈值
    pre_nms_top_n = config.MODEL.FCOS.PRE_NMS_TOP_N #  pre_nms_top_n指定了在应用该分数阈值之前要保留的最大预测框数
    nms_thresh = config.MODEL.FCOS.NMS_TH #  nms_thresh指定了用于进行NMS的IoU阈值
    fpn_post_nms_top_n = config.TEST.DETECTIONS_PER_IMG  #  fpn_post_nms_top_n指定了每张图片在NMS之后最多保留的预测框数
    bbox_aug_enabled = config.TEST.BBOX_AUG.ENABLED #  bbox_aug_enabled指定了是否启用bbox_augmentation

    box_selector = FCOSPostProcessor(
        pre_nms_thresh=pre_nms_thresh,
        pre_nms_top_n=pre_nms_top_n,
        nms_thresh=nms_thresh,
        fpn_post_nms_top_n=fpn_post_nms_top_n,
        min_size=0, #  min_size指定了要保留的预测框的最小边长
        num_classes=config.MODEL.FCOS.NUM_CLASSES, #  num_classes指定了模型要预测的类别数
        bbox_aug_enabled=bbox_aug_enabled #  bbox_aug_enabled指定了是否启用bbox_augmentation
    )
    #  返回创建的FCOSPostProcessor对象
    return box_selector

其中

_C.MODEL.RETINANET.INFERENCE_TH = 0.05
_C.MODEL.FCOS.PRE_NMS_TOP_N = 1000
_C.MODEL.FCOS.NMS_TH = 0.6
_C.TEST.DETECTIONS_PER_IMG = 100
_C.TEST.BBOX_AUG.ENABLED = False

后续通过box_selector = FCOSPostProcessor,找到FCOSPostProcessor类,也就是说预测框的选择是通过FCOSPostProcessor类实现的

FCOSPostProcessor类

image-20240225124048455

image-20240225124056701

FCOSPostProcessor类中的forward函数

forward函数
   def forward(self, locations, box_cls, box_regression, centerness, image_sizes):
        """
        Arguments:
            anchors: list[list[BoxList]]
            box_cls: list[tensor]:类别得分(box_cls)
            box_regression: list[tensor]:边框回归(box_regression)
            image_sizes: list[(h, w)]
        Returns:
            boxlists (list[BoxList]): the post-processed anchors, after
                applying box decoding and NMS
        """
        #  locations:特征图上每个点的位置信息(locations)
        #  centerness:中心度得分(centerness)
        #  输入图片的大小(image_sizes)
        sampled_boxes = []
        #  遍历每个FPN级别,并对每个级别调用forward_for_single_feature_map函数,
        for _, (l, o, b, c) in enumerate(zip(locations, box_cls, box_regression, centerness)):
            sampled_boxes.append(
                #  通过forward_for_single_feature_map函数得到了每个 FPN 级别的采样检测框,然后将其添加在sample_boxes中
                self.forward_for_single_feature_map(
                    #  函数接收一组输入,其中l是当前FPN层的位置信息,o:类别概率。b:边界框回归量,c:中心度。image_size:输入图像尺寸
                    l, o, b, c, image_sizes
                )
            )
            #  最终sampled_boxes包含了所有特征级别上的预测框信息
        #  通过将 sampled_boxes 列表转置并将其每个元素连接起来,可以得到采样检测框的列表,这些检测框是从所有 FPN 级别获得的
        boxlists = list(zip(*sampled_boxes))
        boxlists = [cat_boxlist(boxlist) for boxlist in boxlists]
        #  如果bbox_aug_enabled 标志被禁用,则还需要对它们进行NMS,以进一步过滤掉冗余的检测框
        if not self.bbox_aug_enabled:
            boxlists = self.select_over_all_levels(boxlists)
        #  输出是一个list,包含多个Boxlist对象,每个BoxList代表检测结果中的一个物体框
        #  返回过滤掉的检测框列表
        return boxlists

image-20240225124133486

image-20240225124148530

如论文3.1中Inference提到的,由神经网络得到的分类分数(the classification scores),该值大于0.05则作为正采样并得到预测边界框。

.clamp():即将正采样个数限制在设定的pre_nms_top_n以内。

image-20240225124158280

中心度用于给最终分数加上权重,过滤远离目标中心位置生成的低质量边界框。

image-20240225124206959

image-20240225124214131

image-20240225124221073

image-20240225124228272

其中BoxList调用自fcos_core.structures.bounding_box

BoxList类

image-20240225124236263

class BoxList(object):
    """
    This class represents a set of bounding boxes.
    The bounding boxes are represented as a Nx4 Tensor.
    In order to uniquely determine the bounding boxes with respect
    to an image, we also store the corresponding image dimensions.
    They can contain extra information that is specific to each bounding box, such as
    labels.
    """

    def __init__(self, bbox, image_size, mode="xyxy"):
        #  bbox是一个边界框张量
        #  image_size表示图像大小的一个元祖
        #  mode表示边界框的坐标格式,可以是xyxy或xywh


        #  根据传入的 bbox 参数判断其设备类型是否为 torch.Tensor,如果是则使用 bbox.device 作为设备,否则使用 CPU 作为设备
        device = bbox.device if isinstance(bbox, torch.Tensor) else torch.device("cpu")
        #  将传入的 bbox 参数转换为 PyTorch 张量,并指定数据类型为 torch.float32
        bbox = torch.as_tensor(bbox, dtype=torch.float32, device=device)
        #  检查 bbox 张量是否为二维张量,如果不是则抛出异常。
        if bbox.ndimension() != 2:
            raise ValueError(
                "bbox should have 2 dimensions, got {}".format(bbox.ndimension())
            )
        # 检查 bbox 张量的最后一个维度是否为 4,如果不是则抛出异常。
        if bbox.size(-1) != 4:
            raise ValueError(
                "last dimension of bbox should have a "
                "size of 4, got {}".format(bbox.size(-1))
            )
        if mode not in ("xyxy", "xywh"):
            raise ValueError("mode should be 'xyxy' or 'xywh'")
        #  将bbox、size、mode赋值给类实例的属性
        self.bbox = bbox
        self.size = image_size  # (image_width, image_height)
        self.mode = mode
        # 创建一个空的字典 self.extra_fields,用于存储额外的字段
        self.extra_fields = {}

    #  将一个新的字段和相应的数据添加到对象的 extra_fields 字典中
    #  field是要添加的字段的名称,field_data是要添加的字段的数据
    def add_field(self, field, field_data):
        self.extra_fields[field] = field_data

    #  用于获取BoxList对象中的额外字段数据
    # get_field()方法可以根据给定的字段名称获取相应的数据。
    def get_field(self, field):
        return self.extra_fields[field]

    #  用于判断当前BoxList对象是否包含给定的field
    #  只需要检查field是否在self.extra_fields字典中即可。
    def has_field(self, field):
        return field in self.extra_fields

    #  这个函数返回一个列表,列表中包含了该物体实例的所有额外字段的键
    def fields(self):
        return list(self.extra_fields.keys())

    #  实现了将bbox对象的额外字段复制到当前的bbox对象中
    def _copy_extra_fields(self, bbox):
        for k, v in bbox.extra_fields.items():
            self.extra_fields[k] = v

    #  将 BoxList 对象的边界框格式从当前格式转换为指定的格式
    def convert(self, mode):
        if mode not in ("xyxy", "xywh"):
            raise ValueError("mode should be 'xyxy' or 'xywh'")
        if mode == self.mode:
            return self
        # we only have two modes, so don't need to check
        # self.mode
        # 将BoxList中的bbox按照当前的mode解析成xmin, ymin, xmax, ymax的形式。如果当前mode是"xyxy",
        # 那么这四个值分别对应bbox中的左上角和右下角坐标;如果当前mode是"xywh",那么xmin和ymin对应bbox的中心点坐标减去宽和高的一半,
        # xmax和ymax则对应中心点坐标加上宽和高的一半
        xmin, ymin, xmax, ymax = self._split_into_xyxy()
        #  如果转换的格式是 "xyxy",则直接将 xmin、ymin、xmax、ymax 拼接在一起构造新的 bbox
        if mode == "xyxy":
            bbox = torch.cat((xmin, ymin, xmax, ymax), dim=-1)
            bbox = BoxList(bbox, self.size, mode=mode)
        else:
            #  将计算宽度和高度并添加一个常量值(TO_REMOVE)以构造新的 bbox
            TO_REMOVE = 1
            bbox = torch.cat(
                (xmin, ymin, xmax - xmin + TO_REMOVE, ymax - ymin + TO_REMOVE), dim=-1
            )
            bbox = BoxList(bbox, self.size, mode=mode)
        bbox._copy_extra_fields(self)
        return bbox

image-20240225124248232

由上述可知,类BoxList的实例化变量self.bbox即边界框左上右下坐标/左上坐标+高宽

image-20240225124258065

若box[:,3]=box[:,1]或box[:,2]=box[:,0]。TO_REMOVE将使keep为False,从而删去空边界框

remove_small_boxes调用自fcos_core.structures.boxlist_ops

remove_small_boxes()函数

image-20240225124357584

image-20240225124404370

forward()函数中调用的函数

image-20240225124411460

cat_boxlist调用自fcos_core.structures.boxlist_ops

cat_boxlist()

image-20240225124417463

上述分析了FCOSModule类中的head、box_selector_test部分,现在分析loss_evaluator部分

loss_evaluator

fcos.fcos.FCOSModule

loss_evaluator->make_fcos_loss_evaluator()

调用自fcos_core.modeling.rpn.fcos.loss

image-20240225124427691

image-20240225124436970

因此,loss_evaluator->make_fcos_loss_evaluator()->FCOSLossComputation

image-20240225124444195

image-20240225124450731

FCOSLossComputation

image-20240225124456774

image-20240225124505262

SigmoidFocalLoss类用于计算分类损失

image-20240225124520427

p是模型输出的概率值,image-20240225124531416是权重,image-20240225124539486是可调参数。

image-20240225124547525

以sigmoid_focal_loss_cpu为例

sigmoid_focal_loss_cpu

def sigmoid_focal_loss_cpu(logits, targets, gamma, alpha):
    #  用来生成类别标签的范围 class_range,从1到num_classes,以便对每个类别单独计算sigmoid focal loss
    num_classes = logits.shape[1] #  用来获取模型输出 logits 的通道数,也就是模型预测的类别数
    #  只需要使用单个值。因此,我们通过这两行代码来提取列表中的第一个值。
    gamma = gamma[0]
    alpha = alpha[0]

    dtype = targets.dtype#  获取targets张量的类型
    device = targets.device # 是获取 targets 张量所在的设备(device)
    #  创建一个包含从1到num_classes的序列的张量,并将其移动到目标设备(device)上,并将数据类型设置为dtype
    #  将张量的形状调整为(1,num_classes)
    #  使用unsqueeze(0)将1维插入到第一维,因为后面的代码需要对这个张量进行广播
    class_range = torch.arange(1, num_classes+1, dtype=dtype, device=device).unsqueeze(0)
    #  将t进行维度扩充,为了后面进行和 logits 张量的计算
    t = targets.unsqueeze(1)#  将targets张量增加一个维度,从而将其从 shape 为 (N,) 的一维张量转换为 shape 为 (N, 1) 的二维张量
    p = torch.sigmoid(logits) #  将分类得分进行一个sigmoid计算,将其压缩到0-1之间
    term1 = (1 - p) ** gamma * torch.log(p)
    term2 = p ** gamma * torch.log(1 - p)
    #  带入公式
    return -(t == class_range).float() * term1 * alpha - ((t != class_range) * (t >= 0)).float() * term2 * (1 - alpha)

分类损失到此结束

继续看FCOSLossComputation的方法

centerness_loss

image-20240225124607322nn.BCEWithLogitsLoss(reduction=“sum”)->sigmoid+BCELoss( Binary Cross Entropy Loss)

BCEWithLogitsLoss是一个集合sigmoid和BCE损失的函数。

image-20240225124614550

IOULoss

image-20240225124623518

image-20240225124630692

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

执行这句以后进入FCOSLossComputation中,执行__call__()函数

FCOSLossComputation中的prepare_targets()方法

prepare_targets()

image-20230422200721890

image-20240225124706790

compute_targets_for_locations()
    def compute_targets_for_locations(self, locations, targets, object_sizes_of_interest):
        #  locations:anchor点的位置,形状为[N,2],其中N是anchor点的个数
        #   targets经过转换后的目标框,包含每个图像中的所有目标,每个目标包含bbox和labels的信息
        #   object_sizes_of_interest:列表,其中每个元素代表一个尺度范围
        labels = []
        reg_targets = []
        xs, ys = locations[:, 0], locations[:, 1]
        #  对每张图片进行循环处理,获取当前图像中的目标框及其类别信息
        for im_i in range(len(targets)):
            #  获取一个图片的ground truth目标信息

            #  targets是一个长度为N的列表,其中每个元素对应一个输入图片的目标信息,im_i表示当前处理的输入图片的索引
            targets_per_im = targets[im_i]#  获取当前输入图片的目标信息
            assert targets_per_im.mode == "xyxy" #  确保目标框的格式为左上角和右下角的坐标。
            bboxes = targets_per_im.bbox #  获取当前输入图片中所有目标框的坐标信息
            labels_per_im = targets_per_im.get_field("labels") # 获取当前输入图片中所有目标的类别信息
            area = targets_per_im.area() #  获取当前输入图片中所有目标框的面积信息

            #  计算当前 feature map 中每个位置与目标框的相对坐标偏移量
            l = xs[:, None] - bboxes[:, 0][None] #  当前 feature map 中每个位置到目标框左边界的距离
            t = ys[:, None] - bboxes[:, 1][None] #  当前 feature map 中每个位置到目标框上边界的距离
            r = bboxes[:, 2][None] - xs[:, None] #  当前 feature map 中每个位置到目标框右边界的距离
            b = bboxes[:, 3][None] - ys[:, None] #  当前 feature map 中每个位置到目标框下边界的距离
            reg_targets_per_im = torch.stack([l, t, r, b], dim=2)
            #  由于一张图片中会有多个目标框,所以需要使用循环来实现

            #  根据中心采样半径对当前位置进行采样,或者选择所有位于ground-truth bounding box中的位置
            #  self.center_sampling_radius大于0,则调用self.get_sample_region函数得到位于中心区域内的位置
            if self.center_sampling_radius > 0:
                is_in_boxes = self.get_sample_region(
                    bboxes,
                    self.fpn_strides,
                    self.num_points_per_level,
                    xs, ys,
                    radius=self.center_sampling_radius
                )
                #  如果self.center_sampling_radius等于0,则选择所有在ground-truth bounding box中的位置。
            else:
                # no center sampling, it will use all the locations within a ground-truth box
                is_in_boxes = reg_targets_per_im.min(dim=2)[0] > 0
                #    is_in_boxes 表示每个点是否在目标框内
            #  计算每个anchor对应的ground-truth boxes中最大的偏移量值
            #  max(dim=2)表示在第三维上取最大值,[0]表示返回最大值的值部分
            max_reg_targets_per_im = reg_targets_per_im.max(dim=2)[0]
            # limit the regression range for each location
            #  用来判断每个位置上的anchor与目标框的大小是否在当前FPN层的兴趣大小区间内,从而决定是否对该anchor进行回归预测
            is_cared_in_the_level = \
                (max_reg_targets_per_im >= object_sizes_of_interest[:, [0]]) & \
                (max_reg_targets_per_im <= object_sizes_of_interest[:, [1]])

            #  计算了每个预测位置和每个目标框之间的IoU
            #  使用repeat方法将目标框的面积area扩展到与预测位置相同的尺寸,以便后面的操作可以将每个预测位置与每个目标框进行比较
            locations_to_gt_area = area[None].repeat(len(locations), 1)
            #  表示该预测位置不在目标框内,对应的位置会被赋予INF
            locations_to_gt_area[is_in_boxes == 0] = INF
            #  表示该预测位置不关心目标框大小范围,也会被赋予INF
            locations_to_gt_area[is_cared_in_the_level == 0] = INF


            # if there are still more than one objects for a location,
            # we choose the one with minimal area
            #  找出每个预测点所对应的最小的ground-truth区域面积和对应的ground-truth索引的
            locations_to_min_area, locations_to_gt_inds = locations_to_gt_area.min(dim=1)
            #  得到每个位置最匹配的目标框的回归目标和类别标签
            reg_targets_per_im = reg_targets_per_im[range(len(locations)), locations_to_gt_inds]
            labels_per_im = labels_per_im[locations_to_gt_inds]
            #  如果某个位置没有任何目标框匹配,则将其标签置为0
            labels_per_im[locations_to_min_area == INF] = 0

            labels.append(labels_per_im)
            reg_targets.append(reg_targets_per_im)

        return labels, reg_targets

reg_targets_per_im:

dim0->各层各像素点

dim1->每个目标边界框

dim3->l.t.r.b

方法compute_targets_for_locations()将对应每张图的目标边界框转换为对应各层特征图各像素点为单位的labels、reg_targets

image-20240225124721898

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
目标检测模型中,速度快的模型一般是指能够实时或接近实时地进行目标检测模型。以下是一些速度较快的目标检测模型: 1. YOLO (You Only Look Once): YOLO是一种非常快速的目标检测算法,通过将目标检测问题转化为一个回归问题,并使用单个神经网络来实现端到端的目标检测。YOLO的实时性能较好,可以在实时视频流中进行目标检测。 2. SSD (Single Shot MultiBox Detector): SSD也是一种快速的目标检测算法,它通过在不同尺度的特征图上进行预测,从而检测出多个不同尺度的目标。SSD具有较高的准确率和较快的速度,适用于对速度要求较高的应用场景。 3. EfficientDet: EfficientDet是一系列高效的目标检测模型,它基于EfficientNet作为骨干网络,并使用了一种叫做BiFPN(Bi-directional Feature Pyramid Network)的特征金字塔网络结构。EfficientDet在速度和准确率上取得了很好的平衡,能够提供较快的目标检测速度。 4. FCOS (Fully Convolutional One-Stage Object Detection): FCOS是一种基于全卷积网络的单阶段目标检测算法。它通过在整个特征图上进行密集的预测,避免了锚框的使用,从而提高了检测速度。 需要注意的是,速度与准确率之间存在一定的权衡。通常情况下,速度更快的模型可能会在一定程度上牺牲准确率。因此,在选择模型时,需要根据具体应用需求和场景来平衡速度和准确率的需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值