PaddleDetection算法分析(5)

2021SC@SDUSC

接下来分析SOTA Anchor Free算法:

简要介绍:

PaddleDetection2.0更新后的招牌模型算法之一

PAFNet(Paddle Anchor Free)& PAFNet-Lite

相较于SSD、RCNN等系列各种Anchor-Based算法,Anchor-Free算法拥有更少的超参、更易配置、对多尺度目标检测效果更好等优点,但也存在检测结果不稳定、训练时间长等问题,是近些年科研领域的热点方向。飞桨当然一直紧跟全球科研动向,基于TTFNet进行多维度的优化,推出了在COCO数据集精度42.2,V100预测速度67FPS, 处于anchor free领域SOTA水平的PAFNet(Paddle Anchor Free)算法!同时提供移动端轻量级模型PAFNet-Lite,COCO数据集精度达到23.9,麒麟990芯片延时26ms。

一、Anchor-Based方法回顾

比如两阶段就以Fasrer RCNN为例,他就会在特征图的每一个点生成大小比例不一的anchor,然后通过RPN阶段对anchor进行筛选,训练阶段就是用anchor和真实框做一个编码得到训练目标,预测阶段就是通过anchor和预测出来的偏移量,然后解码得到一个预测框。单阶段就以yolo为例,yolo把原图分为若干个网格,然后通过聚类的方法,每个网格当中得到不同尺寸的anchor,然后再和真实框做一个IoU的比较,编码得到训练目标,或者说是监督信息;预测阶段,就是通过预测出来的偏移量,还有网格当中的anchor解码得到预测框。
以上就是对之前博文目标检测的一些回顾。

此时我们就会有一个问题“必须使用Anchor进行目标检测吗?”

基于Anchor的检测算法具有如下特点:
在同一像素点上生成多个不同大小和比例的候选框,并对其进行筛选,然后再进行分类和回归,一定程度上能够解决目标尺度不一和遮挡的问题,提高检测精度。
就比如有两个物体靠的很近,这样的话,他们的中心点有可能在特征图上就对准到来一个点,这个时候,要是你不考虑多个anchor的话,就很有可能丢失到其中一个物体的信息。

那么用anchor有哪些弊端呢?
该方法弊端有:
依赖过多的手动设计,也就是它的超参数是很难调的,比如说anchor box的数量、大小、宽高比呀等,而这往往需要根据项目的实际情况来设计,就比如说,在车牌的检测当中,就需要根据车牌的比例,比如说5:1、7:1等,这样的anchor box设计才比较合理,但又存在不同国家不同车牌比例不一样。但是人脸检测你就不能再用5:1的anchor了,这个时候就需要设置1:1了。一来这往往需要根据项目的实际情况和数据集来设计,二来基于anchor的方法,都需要在特征图上预设很多的anchor,这个过程就很耗时,同时为了增大召回率,有需要布局很多很密集的anchor,用这种方法来覆盖数据集中所有的真实框,这就会导致我们训练和预测过程的低效。另外一点,这么多的anchor,其实呢,只有很少一部分与真实框达到足够大的IoU来使其成为正样本,也就是说有绝大部分的anchor其实就是负样本,这就导致训练时,正负样本不平衡问题。

弊端总结为:

依赖过多的手动设计!
训练和预测过程低效!
正负样本不均问题!
去除anchor???

二、Anchor Free系列方法简介
1. Anchor Free系列算法历史

去除Anchor后如何表示检测框呢?
Anchor-based方法中通过Anchor和对应的编码信息来表示检测框。对应的编码信息换言之也就是偏移量。
Anchor Free目前主要分如下两类表示检测框的方法:

基于关键点的检测算法
先检测目标左上角和右下角点,再通过角点组合形成检测框。
基于中心的检测算法
直接检测物体的中心区域和边界信息,将分类和回归解耦为两个子网格。
如下图所示:

这两大算法都取得了很大效果。

2. Anchor free经典算法详解

anchor_te.py

import os
import cPickle
import numpy as np
import matplotlib.pyplot as plt

root_dir = 'data/caltech/train_3'
# root_dir = 'data/caltech/test'
all_img_path = os.path.join(root_dir, 'images')
all_anno_path = os.path.join(root_dir, 'annotations_new/')
res_path_gt = 'data/cache/caltech/train_gt'
res_path_nogt = 'data/cache/caltech/train_nogt'

rows, cols = 480, 640
image_data_gt, image_data_nogt = [], []

valid_count = 0
iggt_count = 0
box_count = 0
files = sorted(os.listdir(all_anno_path))
for l in range(len(files)):
	gtname = files[l]
	imgname = files[l].split('.')[0]+'.jpg'
	img_path = os.path.join(all_img_path, imgname)
	gt_path = os.path.join(all_anno_path, gtname)

	boxes = []
	ig_boxes = []
	with open(gt_path, 'rb') as fid:
		lines = fid.readlines()
	if len(lines)>1:
		for i in range(1, len(lines)):
			info = lines[i].strip().split(' ')
			label = info[0]
			occ, ignore = info[5], info[10]
			x1, y1 = max(int(float(info[1])), 0), max(int(float(info[2])), 0)
			w, h = min(int(float(info[3])), cols - x1 - 1), min(int(float(info[4])), rows - y1 - 1)
			box = np.array([int(x1), int(y1), int(x1) + int(w), int(y1) + int(h)])
			if int(ignore) == 0:
				boxes.append(box)
			else:
				ig_boxes.append(box)
	boxes = np.array(boxes)
	ig_boxes = np.array(ig_boxes)

	annotation = {}
	annotation['filepath'] = img_path
	box_count += len(boxes)
	iggt_count += len(ig_boxes)
	annotation['bboxes'] = boxes
	annotation['ignoreareas'] = ig_boxes
	if len(boxes) == 0:
		image_data_nogt.append(annotation)
	else:
		image_data_gt.append(annotation)
		valid_count += 1
print '{} images and {} valid images, {} valid gt and {} ignored gt'.format(len(files), valid_count, box_count, iggt_count)

if not os.path.exists(res_path_gt):
	with open(res_path_gt, 'wb') as fid:
		cPickle.dump(image_data_gt, fid, cPickle.HIGHEST_PROTOCOL)
if not os.path.exists(res_path_nogt):
	with open(res_path_nogt, 'wb') as fid:
		cPickle.dump(image_data_nogt, fid, cPickle.HIGHEST_PROTOCOL)

基于关键点:CornerNet、CornerNet-Lite
基于中心区域: FCOS, CenterNet — TTFNet

接下来我们详细解读这个经典的算法!

2.1. 基于关键点的Anchor Free检测算法

1. CornerNet

test.py

import os
import json
import torch
import pprint
import argparse
import importlib
import numpy as np

import matplotlib
matplotlib.use("Agg")

from config import system_configs
from nnet.py_factory import NetworkFactory
from db.datasets import datasets

torch.backends.cudnn.benchmark = False

def parse_args():
    parser = argparse.ArgumentParser(description="Test CornerNet")
    parser.add_argument("cfg_file", help="config file", type=str)
    parser.add_argument("--testiter", dest="testiter",
                        help="test at iteration i",
                        default=None, type=int)
    parser.add_argument("--split", dest="split",
                        help="which split to use",
                        default="validation", type=str)
    parser.add_argument("--suffix", dest="suffix", default=None, type=str)
    parser.add_argument("--debug", action="store_true")

    args = parser.parse_args()
    return args

def make_dirs(directories):
    for directory in directories:
        if not os.path.exists(directory):
            os.makedirs(directory)

def test(db, split, testiter, debug=False, suffix=None): 
    result_dir = system_configs.result_dir
    result_dir = os.path.join(result_dir, str(testiter), split)

    if suffix is not None:
        result_dir = os.path.join(result_dir, suffix)

    make_dirs([result_dir])

    test_iter = system_configs.max_iter if testiter is None else testiter
    print("loading parameters at iteration: {}".format(test_iter))

    print("building neural network...")
    nnet = NetworkFactory(db)
    print("loading parameters...")
    nnet.load_params(test_iter)

    test_file = "test.{}".format(db.data)
    testing = importlib.import_module(test_file).testing

    nnet.cuda()
    nnet.eval_mode()
    testing(db, nnet, result_dir, debug=debug)

if __name__ == "__main__":
    args = parse_args()

    if args.suffix is None:
        cfg_file = os.path.join(system_configs.config_dir, args.cfg_file + ".json")
    else:
        cfg_file = os.path.join(system_configs.config_dir, args.cfg_file + "-{}.json".format(args.suffix))
    print("cfg_file: {}".format(cfg_file))

    with open(cfg_file, "r") as f:
        configs = json.load(f)
            
    configs["system"]["snapshot_name"] = args.cfg_file
    system_configs.update_config(configs["system"])

    train_split = system_configs.train_split
    val_split   = system_configs.val_split
    test_split  = system_configs.test_split

    split = {
        "training": train_split,
        "validation": val_split,
        "testing": test_split
    }[args.split]

    print("loading all datasets...")
    dataset = system_configs.dataset
    print("split: {}".format(split))
    testing_db = datasets[dataset](configs["db"], split)

    print("system config...")
    pprint.pprint(system_configs.full)

    print("db config...")
    pprint.pprint(testing_db.configs)

    test(testing_db, args.split, args.testiter, args.debug, args.suffix)

cornetNet可以算是近期anchor Free算法奠基之作,给目标检测算法开辟一条新的解决思路。cornerNet是借鉴了多人姿态估计的bottom Mask的思路,像人体姿态估计,分割这些思维任务来说,底层的逻辑就是要给点来打标签,然后再通过这些点组合起来形成上层的语意。授此启发,作者认为对于目标检测的框就等于检测目标框的左上角和右下角的点,然后将他们组合起来。之所以选择角点,是因为角点相比中心点更有利于训练的,比如说左上角的点,只会和两条边相关,而中心点要和四条边相关。

CornerNet – 核心思想

首先,它会利用bottom来提取特征,然后提取来的特征会分为两个检测器,分别去检测图的左上角点和右下角点,每一部分呢,又会分为heatmaps和Embeddings两个分支。Heatmaps负责去找到角点的位置,然后对角点进行分类,再通过Embeddings这个分支去将这两部分的点处于相同部分的点,进行一个组合,从而形成检测框。在这里,我们希望对于相同物体的角点,对应的embeddings具有较大的相似度,对于不同的物体的角点,对应的embeddings具有较大的距离。换言之,Heatmaps去负责找到点,Embeddings去负责找到匹配这两个点的方法。
接下来就详细介绍一下这两个部分!!!!
2. CornerNet – Heatmaps 解决如何找到corner的问题

第一步,我们要扩大一个学习区域,我们可以想一下,如果我们要找角点,我们就要告诉网络,哪些点是角点,那这样的话,一张图片只有一个物体的话,那么正样本只会有两个点,也就是左上角点是1,右下角点是1,其他点的位置都是0,是这样的一个信息,这样的话,它应该是一个二值的mask,但这样的话,所有的负样本的点就都被舍弃掉了,这个惩罚力度也就太大了,就不利于训练了,所以作者觉得需要将惩罚力度调小一点,就为这个角点设置了一个半径为R的区域,也就是图中圆圈部分。落在这个区域的点大部分是负样本,但还是有值得学习的价值的,因为在这个区域内所生成的定点,他们所组成的这个框,其实和真实框已经具备了很高的一个重叠度,就比如说上图中红色的框与绿色虚线的框,他们是非常接近的,你甚至不知道哪个才是真正的真实框了。这样的话,这个圆圈中的监督信息是通过高斯分布来产生的,也就是说越接近圆心的部分,它的目标值越接近1,越接近圆圈边界的位置,它的目标值越接近于0,用这种信息,用这种监督信息来替代刚刚说的二值mask,这样来训练,就能帮我们更好的找到角点了。
第二部分,通过我们很难通过直觉来判断找到目标框的角点的。

CornerNet – Corner pooling算法解读

max pooling它更注重的是将物体的信息集中在物体的中心区域,而Corner pooling期望物体的信息集中在角点,我们以检测左上角角点为例。首先我们要做的是top pooling和left pooling两步,这里我们就输入两张特征图,第一张特征图,按列从下往上扫描每一像素,去计算每一个像素点以及对应列下面的像素的最大值,也就是top pooling的过程;第二张特征图,同理,从右向左扫描每一像素,去计算每一个像素点以及对应行右边的像素的最大值,也就是left pooling的过程;然后将这两张图相加,最终得到左上角角点的输出。如上图右上方图所示。

CornerNet – Embeddings解决如何组合corner的问题

就如上图,我们希望橙色点组成一个框,绿色点组成一个框,那怎么做呢?作者搜姿态估计任务的启发,就引入了Embeddings,进行角点的匹配。embeddings是什么呢?其实就是,它对输入特征图上的每个点进行编码,得到的编码向量即为embeddings,表示该点的位置信息。怎么理解呢?打个比方,我想知道今天来听课的同学都来至于哪里,这个可能不太好判断,但是如果老师能把大家都召集起来,听听各自家乡的方言,这个可能就很容易了,就比如北京的儿化音比较重,embedings就相当于给特征图加了方言,cornerNet使用一维embeddings特征,即通过卷积得到embeddings模块输出,输出维度为(N, 1, H, W),这样的话,我们就可以将左上角和右下角点的embeddings距离作为两个角点的相似度。

CornerNet – 网络结构

import os

import json
import torch
import numpy as np
import queue
import pprint
import random
import argparse
import importlib
import threading
import traceback

from tqdm import tqdm
from utils import stdout_to_tqdm
from config import system_configs
from nnet.py_factory import NetworkFactory
from torch.multiprocessing import Process, Queue, Pool
from db.datasets import datasets

torch.backends.cudnn.enabled   = True
torch.backends.cudnn.benchmark = True

def parse_args():
    parser = argparse.ArgumentParser(description="Train CornerNet")
    parser.add_argument("cfg_file", help="config file", type=str)
    parser.add_argument("--iter", dest="start_iter",
                        help="train at iteration i",
                        default=0, type=int)
    parser.add_argument("--threads", dest="threads", default=4, type=int)

    args = parser.parse_args()
    return args

def prefetch_data(db, queue, sample_data, data_aug):
    ind = 0
    print("start prefetching data...")
    np.random.seed(os.getpid())
    while True:
        try:
            data, ind = sample_data(db, ind, data_aug=data_aug)
            queue.put(data)
        except Exception as e:
            traceback.print_exc()
            raise e

def pin_memory(data_queue, pinned_data_queue, sema):
    while True:
        data = data_queue.get()

        data["xs"] = [x.pin_memory() for x in data["xs"]]
        data["ys"] = [y.pin_memory() for y in data["ys"]]

        pinned_data_queue.put(data)

        if sema.acquire(blocking=False):
            return

def init_parallel_jobs(dbs, queue, fn, data_aug):
    tasks = [Process(target=prefetch_data, args=(db, queue, fn, data_aug)) for db in dbs]
    for task in tasks:
        task.daemon = True
        task.start()
    return tasks

def train(training_dbs, validation_db, start_iter=0):
    learning_rate    = system_configs.learning_rate
    max_iteration    = system_configs.max_iter
    pretrained_model = system_configs.pretrain
    snapshot         = system_configs.snapshot
    val_iter         = system_configs.val_iter
    display          = system_configs.display
    decay_rate       = system_configs.decay_rate
    stepsize         = system_configs.stepsize

    # getting the size of each database
    training_size   = len(training_dbs[0].db_inds)
    validation_size = len(validation_db.db_inds)

    # queues storing data for training
    training_queue   = Queue(system_configs.prefetch_size)
    validation_queue = Queue(5)

    # queues storing pinned data for training
    pinned_training_queue   = queue.Queue(system_configs.prefetch_size)
    pinned_validation_queue = queue.Queue(5)

    # load data sampling function
    data_file   = "sample.{}".format(training_dbs[0].data)
    sample_data = importlib.import_module(data_file).sample_data

    # allocating resources for parallel reading
    training_tasks   = init_parallel_jobs(training_dbs, training_queue, sample_data, True)
    if val_iter:
        validation_tasks = init_parallel_jobs([validation_db], validation_queue, sample_data, False)

    training_pin_semaphore   = threading.Semaphore()
    validation_pin_semaphore = threading.Semaphore()
    training_pin_semaphore.acquire()
    validation_pin_semaphore.acquire()

    training_pin_args   = (training_queue, pinned_training_queue, training_pin_semaphore)
    training_pin_thread = threading.Thread(target=pin_memory, args=training_pin_args)
    training_pin_thread.daemon = True
    training_pin_thread.start()

    validation_pin_args   = (validation_queue, pinned_validation_queue, validation_pin_semaphore)
    validation_pin_thread = threading.Thread(target=pin_memory, args=validation_pin_args)
    validation_pin_thread.daemon = True
    validation_pin_thread.start()

    print("building model...")
    nnet = NetworkFactory(training_dbs[0])

    if pretrained_model is not None:
        if not os.path.exists(pretrained_model):
            raise ValueError("pretrained model does not exist")
        print("loading from pretrained model")
        nnet.load_pretrained_params(pretrained_model)

    if start_iter:
        learning_rate /= (decay_rate ** (start_iter // stepsize))

        nnet.load_params(start_iter)
        nnet.set_lr(learning_rate)
        print("training starts from iteration {} with learning_rate {}".format(start_iter + 1, learning_rate))
    else:
        nnet.set_lr(learning_rate)

    print("training start...")
    nnet.cuda()
    nnet.train_mode()
    with stdout_to_tqdm() as save_stdout:
        for iteration in tqdm(range(start_iter + 1, max_iteration + 1), file=save_stdout, ncols=80):
            training = pinned_training_queue.get(block=True)
            training_loss = nnet.train(**training)

            if display and iteration % display == 0:
                print("training loss at iteration {}: {}".format(iteration, training_loss.item()))
            del training_loss

            if val_iter and validation_db.db_inds.size and iteration % val_iter == 0:
                nnet.eval_mode()
                validation = pinned_validation_queue.get(block=True)
                validation_loss = nnet.validate(**validation)
                print("validation loss at iteration {}: {}".format(iteration, validation_loss.item()))
                nnet.train_mode()

            if iteration % snapshot == 0:
                nnet.save_params(iteration)

            if iteration % stepsize == 0:
                learning_rate /= decay_rate
                nnet.set_lr(learning_rate)

    # sending signal to kill the thread
    training_pin_semaphore.release()
    validation_pin_semaphore.release()

    # terminating data fetching processes
    for training_task in training_tasks:
        training_task.terminate()
    for validation_task in validation_tasks:
        validation_task.terminate()

if __name__ == "__main__":
    args = parse_args()

    cfg_file = os.path.join(system_configs.config_dir, args.cfg_file + ".json")
    with open(cfg_file, "r") as f:
        configs = json.load(f)
            
    configs["system"]["snapshot_name"] = args.cfg_file
    system_configs.update_config(configs["system"])

    train_split = system_configs.train_split
    val_split   = system_configs.val_split

    print("loading all datasets...")
    dataset = system_configs.dataset
    # threads = max(torch.cuda.device_count() * 2, 4)
    threads = args.threads
    print("using {} threads".format(threads))
    training_dbs  = [datasets[dataset](configs["db"], train_split) for _ in range(threads)]
    validation_db = datasets[dataset](configs["db"], val_split)

    print("system config...")
    pprint.pprint(system_configs.full)

    print("db config...")
    pprint.pprint(training_dbs[0].configs)

    print("len of db: {}".format(len(training_dbs[0].db_inds)))
    train(training_dbs, validation_db, args.start_iter)

offset,在卷积的过程中是存在下采样的过程的,这就不可避免的是一些点的坐标产生一些偏移,这样就从原始图的输入到最后特征图的输出过程,这些误差就会累计,对于小目标的误差就不可接受了,所以就引入了偏移量来修正它,「N, 2, H, W」,表示的是x和y方向的误差。
下面看一下loss。
cornerNet loss,要是外表小的话,loss就会比较大了,就说明如果这个点离真实的点比较远的时候,就认为它是一个困难的样本,就需要更多的去学习。
embeddings部分,表示每一个点的特征向量,这里会计算两个loss分量,分别是同一目标角点的距离,不同目标的距离。首先来看这个pull分支,这个分支的目的是希望训练完之后,我就知道每一个北京人说话都有儿化音,儿化音就可以作为ek了,其次是push,就表示不同物体角点的平均值,通俗来讲,北京方言和上海方言的特点。
offset:smooth loss

CornerNet – 效果检测

问题来了:同类别不同目标的角点容易错误地组合起来形成误检框。比如上图右边。

CornerNet – 进化版

cornerNet squeeze可以作为一个实时的目标检测器。从上图来看,无论从速度还是精度都超过了原版的yolov3。这里就着重来介绍以下cornerNet Squeeze。

CornerNet Squeeze – 优化方法
怎么做的呢?
看上面的那张图,它受squeeze的启发,CornerNet Squeeze将resenet模块替换了fire module。将33的卷积替换成11conv和11conv+33conv的卷积作为输出,然后又受mobileNet的启发,将33conv替换成了33的深度可分离卷积,来实现进一步的加速。
代码如下:

CornerNet Squeeze – 实验结果

*CornerNet-Squeeze-dcn-mixup-cosine是基于原版CornerNet-Squeeze优化效果最好的模型,在ResNet的骨干网络基础上增加了mixup预处理和使用cosine_decay的学习率策略。

CornerNet – 算法小结

核心思想:通过一对角点进行目标检测.

Heatmaps解决如何找到corner的问题

Reduce penalty
corner Pooling
Embeddings解决如何组合corner的问题

同一目标的一对角点的相似度高
不同目标的一对角点的距离大
网络结构和损失函数

cornerNet进化版–CornerNet Squeeze

减少每个像素点的处理过程

2. 2 基于中心的Anchor Free检测算法
1. FCOS

这个算法也算是基于中心的经典算法之一了。它就是在特征图上直接去预测特征点的类别,以及这个像素点到四边的距离,这个算法又通过spn的结构去分层,不同层去预测不同尺度的物体,这样就避免了同一个特征图上,多个物体框重叠的情况。那么我么来看一下。

FCOS – 核心思想

FCOS就使用全卷积网络直接对特征图的每个位置到原图四边的距离进行一个回归,换言之,FCOS直接把每一个点也就每一个位置都作为训练样本,这一点就跟FCN用于语义分割的原理是相同的,而且也取得了跟anchorbased目标检测方法相近的效果。
FCOS – 网络结构

首先它采用了一个FPN的结构,为什么这么做呢?物体的重叠有可能在训练的过程中产生一些歧义,就比如,有一只大象,下面是一个老鼠,这样的话就很有可能有一个点,这个点即属于大象,又属于老鼠,这样的话。网络就不好把两个目标区分开了,这个模糊性就会使基于FCN的检测效果下降。在FCOS中使用FCN多层预测多尺度的物体,可以有效解决这个模糊性的问题。换言之,大象和老鼠就可以不用一张图去预测了,就可以把大象放在一个深的特征图上,老鼠放在一个浅的特征图上去预测了。
然后看一下输出部分,输出又引出3个部分:分类,中心点,回归。这里与anchor based相类似的地方是都有一个分类分支和回归分支。我们先看一下分类和回归分支。
FCOS – 分类+回归分支

1)分类分支:以像素点为训练样本,每个像素被标注上类别或者背景标签进行分类分支的训练。在anchor based算法上,它是将anchor与真实框的IOU的阈值大于一定值,作为正样本,将这个IOU最大的真实框的类别作为这个anchor的类别。但在FCOS当中呢,如果某一个位置落到了任何一个真实框的话,那它就是一个正样本,类别就标记为这个真实框的类别。当然,这个时候还是有可能一个点落在了两个重叠的真实框内,我们就会把这个点分配给那个面积比较小的真实框了。这里在计算loss时,我们就使用一个focal loss。
2)回归分支:因为没有anchor了,我们就不能再用RCNN和yolo中的偏移量作为回归目标了,在FCOS当中呢,以真实框中每个像素点到真实框四条边的距离作为回归目标值(监督值)进行边框回归分支的训练。根据上面的计算公式,我们就能知道中心点到四边的距离,我们计算loss的使用IOU Loss。
这个时候又有一个问题,因为多尺度结构的存在,就需要确定这个真实框box分配给哪个层级呢?传统的FPN做法是会根据这个box的面积进行分配,而FCOS,就会根据Box中点到四边的最大距离来分配,也就是上面公式的四个分量,怎么分配呢?我们会给每一个层级分配阈值范围,然后看四个分类的最大值落在哪个区间就分配给哪个层级。

FCOS内,我们会把目标框内所有的点平等的作为正样本,这样就会导致产生一个离目标框中心距离远的低质量的目标框。于是,在FCOS中就提出了中心度的概念,也就是center-ness分支,来抑制这些低质量的目标框。

FCOS – 中心度center-ness分支

center-ness作用于网络中,和分类分支是并行的,是用来预测每个点的中心度。训练的时候,使用二值交叉熵loss,测试的时候,就会把中心度的得分和分类分支的得分做一个相乘,作为某一个点的最终得分。这样每一个位置就有了权重得分了。真正远离中心点的点就会被削弱。后面就可以用NMS的方法将这些点过滤掉了。

FCOS – 进化版

FCOS – 实验结果

FCOS – 算法小结

核心思想:检测点和点到真实框四边的距离。
分类+回归分支
真实框内的点均为正样本
通过点到四边距离的最大值决定层级
中心度分支
去除离中心点较远的低质量的预测框
像素点离中心点越远,中心度越小
FCOS进化版
FCOS没有很明确确定中心点,它是依赖于标注信息的。

2. CenterNet

CenterNet – 核心思想

CebterNet – 预测目标中心点的位置

每个物体仅有一个正样本点,就不再需要NMS,我们就可以通过输出的Heatmap去提取局部的最大值,这样我就知道这个区域我要用哪一个点产生的预测框,也就替代了NMS这个相对费时的检测的后处理。
在cornet中只把中心点作为正样本,但是中心点附近的点还是有一些学习价值的,因此也会通过圆形的高斯分布来生成一个监督信息,也就是说越靠近中心点,这个监督信息越接近于1。

CenterNet – 预测不同任务中的不同属性

检测任务中直接预测目标框大小
目标框大小的监督信息仅在目标框中心点处产生并使用L1损失函数计算loss
同样可以预测其他视觉任务中的额外属性,例如3D目标检测,姿态估计等待
CenterNet – 实验结果

CenterNet – 算法小结

核心思想:直接检测物体中心点和尺寸
优势:
模型结构设计简单
对于其他视觉任务有较好的拓展性
去除NMS后处理利于加速模型预测
劣势:
训练时间长,COCO数据集需要训练140epoch
回归监督信息仅通过中心点位置产生
由于上面的劣势,就有新的改进算法 TTFNet算法。

3. TTFNet – CenterNet的改进版

TTFNet – 核心思想

保持性能的基础上,提高模型收敛速度,以往经验:
1)提高学习率,对应也需要增大batchSize;但是在CenterNet当中,并没有提高模型的收敛速度,在这个时候,TTFNet提出了,如果增加真实框产生的监督信息是可以像“增大学习率和BS”一样的效果的。举例子:打篮球要想发的更快更准,就得多练,比如要发500次球才可以,那么我们就可以一天发100个,发5天,或者一天发50个,发10,一样得。如果有一个好的教练告诉你好的技巧。这就是TTFNet提到得,需要增加真实框产生得监督信息。
2)减少数据预处理,会提高训练速度,但是在CenterNet当中数据预处理去掉,检测精度会大大下降,刚刚提到了,CenterNet的监督信息只在中心点处产生,而回归分支的监督信息只在中心点处产生,因此,我们就希望进一步提高这个监督信息的质量。

TTFNet – 改进方案

TTFNet – 网络结构

TTFNet – 效果总结

1)讨论并验证了batch size和真实框产生的编码信息的关联
2)提出通过高斯核产生中心点定位核尺寸回归监督信息的新的方法
3)在保持高性能的同时,极大的降低了检测器训练时间
4) 模型结构有利于开展搜素实验

3. AnchorFree系列算法小结

三、目标检测总结

 

 

版权声明:本文为CSDN博主「魏振川」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/tegddhdrhdd/article/details/121606690

魏振川

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

OTA:目标检测中的最优传输分配

1 引言该论文主要是关于目标检测中的标签分配问题,作者创新性地从全局的角度重新审视了该问题,并提出将标签分配问题看成是一个最优运输问题。要知道最优传输问题是当前最优化理论和GAN理论研究领域中的一个很火的研究课题。论