Scale Match方法的代码阅读

“SM”(Scale Match):这是一种将相同的标注标准应用于不同尺度图像样本的标注技术。具体而言,“SM” 技术通过在不同尺度的图像中使用相同的边界框坐标和标签来表示目标物体的位置和属性,从而在不同尺度的图像样本中实现一致性的标注。这样,在进行目标检测算法训练或测试时,可以使用相同的标注信息来匹配不同尺度的图像样本,从而保持目标物体在不同尺度下的标注一致性。

项目的源代码是基于mmdetection框架进行实现的。我们在框架的源码中可以寻找到配置文件TOV_mmdetection/configs2/TinyPerson/scale_match/faster_rcnn_r50_fpn_1x_coco_sm_tinyperson.py

在这里插入图片描述
其中,我们发现ScaleMatchResize定义了数据处理的方法

dict(type='ScaleMatchResize', scale_match_type="ScaleMatch",
         anno_file="data/tiny_set/mini_annotations/tiny_set_train_all_erase.json",
         bins=100, default_scale=0.25, scale_range=(0.1, 1))
         
其中'bins': 键,值为整数 100,表示直方图的 bin 数量,用于计算尺度分布。
'default_scale': 键,值为浮点数 0.25,表示默认的尺度因子,用于缩放图像。
'scale_range': 键,值为元组 (0.1, 1),表示尺度范围的最小值和最大值,用于限制尺度因子的取值范围。

来看这个类的具体定义,TOV_mmdetection/mmdet/datasets/pipelines/scale_match.py

在这里插入图片描述
首先看初始化函数

def __init__(self, distribute=None, sizes=None,  # param group 1
                 anno_file=None, bins=100, except_rate=-1.,  # param group 2
                 scale_range=(0., 2.), default_scale=1.0, max_sample_try=5, out_scale_deal='clip', use_log_bins=False,
                 mode='bilinear', debug_no_image_resize=False, debug_close_record=True):

其中的参数:
distribute: 默认值为 None,用于传入尺度分布的数组。
sizes: 默认值为 None,用于传入原始尺度的数组。
anno_file: 默认值为 None,用于传入注释文件的路径,用于加载数据集的注释信息。
bins: 默认值为 100,表示直方图的 bin 数量,用于计算尺度分布。
except_rate: 默认值为 -1.,表示异常值剔除的比例,用于在计算尺度分布时剔除异常值。
scale_range: 默认值为 (0., 2.),表示尺度范围的最小值和最大值,用于限制尺度因子的取值范围。
default_scale: 默认值为 1.0,表示默认的尺度因子,用于缩放图像。
max_sample_try: 默认值为 5,表示最大尝试采样次数,用于控制尺度匹配的最大尝试次数。
out_scale_deal: 默认值为 'clip',表示超出尺度范围的处理方式,可选值为 'clip' 和 'use_default_scale'。
use_log_bins: 默认值为 False,表示是否使用对数尺度的直方图 bin。
mode: 默认值为 'bilinear',表示图像缩放的插值方式,可选值为 'bilinear'、'nearest'、'bicubic'、'lanczos' 等。
debug_no_image_resize: 默认值为 False,表示是否在调试模式下不对图像进行缩放。
debug_close_record: 默认值为 True,表示是否在调试模式下关闭记录。

函数开始先判断anno_file和distribute是否存在,如果distribute为空那么我们使用anno_file来创建尺度分布的数组,具体函数ScaleMatch._get_distribute(json.load(open(anno_file))['annotations'], bins,except_rate, use_log_bins)

在这里插入图片描述
按照默认设置,函数中的参数除了except_rate会发生变化其余均保持默认。

函数先进行了一些预处理:

  • 代码将注释信息(annotations)中不包含’iscrowd’字段的注释筛选出来,存储在annos列表中。
  • 将annos列表中不包含’ignore’字段的注释筛选出来,重新赋值给annos变量
  • 从annos列表中的每个注释中提取出bbox(边界框)的宽度和高度,并计算其面积,然后将面积值存储在numpy数组中。这里使用了平方根运算,可能是为了将面积转换为边界框的尺寸。
  • 将sizes数组中大于0的元素提取出来,重新赋值给sizes变量。这可能是为了过滤掉面积为0的样本,以避免除以0的错误

接着主要对样本的尺寸进行归一化,并根据给定的mu和sigma进行缩放,并对尺寸进行截断。但因为这里的

if mu_sigma[0] > 0 and mu_sigma[1] > 0:
    print('distribute(mu, sigma): ', np.mean(sizes), np.std(sizes), end='->')
    sizes = (sizes - np.mean(sizes)) / np.std(sizes)
    sizes = sizes * mu_sigma[1] + mu_sigma[0]
    print(np.mean(sizes), np.std(sizes))
    sizes = sizes.clip(1)

如果参数 use_log_bins 为 True,那么将对尺寸进行对数变换,即对尺寸取对数。这可能是为了将尺寸分布在较大尺寸范围内变得更加均匀,从而在生成缩放尺寸时能够更好地匹配各种尺寸的样本。但是因为默认参数是False所以这一段代码也不会执行

 if use_log_bins:
     sizes = np.log(sizes)

接下来,对尺寸进行排序,并根据给定的 except_rate 参数来裁剪尺寸分布的上下端,以剔除尺寸分布中的极端值。except_rate 表示裁剪掉的尺寸分布的比例,一般取值在 [0, 1] 之间。

sizes = np.sort(sizes)
N = len(sizes)
hist_sizes = sizes[int(N * except_rate / 2): int(N * (1 - except_rate / 2))]

然后是关键的一步

if except_rate > 0:
    c, s = np.histogram(hist_sizes, bins=bins - 2)
    c = np.array([int(N * except_rate / 2)] + c.tolist() + [N - int(N * (1 - except_rate / 2))])
    s = [sizes[0]] + s.tolist() + [sizes[-1]]
    s = np.array(s)
else:
    c, s = np.histogram(hist_sizes, bins=bins)

将使用 np.histogram 函数对裁剪后的尺寸分布 hist_sizes 进行直方图统计,并指定 bins 参数为 bins - 2。这里的 -2 是为了给裁剪后的尺寸分布留出两个位置,用于添加裁剪掉的上下端的边界值。

np.histogram 是 NumPy 库中的一个函数,用于计算一维数组的直方图。直方图是一种图形表示方式,用于展示数据的分布情况,通常用于统计学和数据分析中。
np.histogram 函数接受一个一维数组作为输入,返回两个值:直方图的计数值(也称为频数,表示每个 bin 中的数据数量)和 bin 的边界值(即直方图的横轴,表示数据的范围)

接下来,将直方图统计的结果 c 和对应的尺寸值 s 分别添加裁剪掉的上下端的边界值,并转换为 numpy 数组。这里使用 int(N * except_rate / 2) 和 N - int(N * (1 - except_rate / 2)) 分别表示裁剪掉的上下端的边界值在直方图中的位置索引,其中 N 表示尺寸分布的样本数量。
如果 except_rate 小于等于 0,则说明不需要裁剪尺寸分布的上下端,此时直接使用 np.histogram 函数对尺寸分布 hist_sizes 进行直方图统计,并指定 bins 参数为 bins。
最后,将直方图统计结果 c 进行归一化,除以尺寸样本数量 len(sizes),并根据 use_log_bins 参数决定是否需要将尺寸值 s 进行指数变换,从而得到最终的尺寸分布 c 和 s。

c:是一个表示尺寸分布频次的 numpy 数组。通过对尺寸分布进行直方图统计后得到,其中 c[i] 表示尺寸值在区间 [s[i], s[i+1]) 内的频次,s 是尺寸值的边界值数组。
s:是一个表示尺寸分布的尺寸值的 numpy 数组。通过对尺寸分布进行直方图统计后得到,其中 s 是尺寸值的边界值数组,表示直方图的分 bin 的边界值,用于表示尺寸值的区间范围。例如,s[i] 和 s[i+1] 之间的区间表示尺寸分布在 [s[i], s[i+1]) 范围内的频次。

至此,_get_distribute执行完成。

返回初始化函数,继续往下看。self.distri_cumsum:是一个 numpy 数组,表示尺寸分布频次的累积和

self.distri_cumsum = np.cumsum(distribute)
self.sizes = sizes
self.mode = PIL_RESIZE_MODE[mode]

至此,初始化函数的运行已经完成,接下来我们来看call执行函数


call函数的定义如下

在这里插入图片描述
首先来看第一行的get_new_size函数,它传入的参数是输入的图像和输入图像中的目标边界框

在这里插入图片描述

target = BoxList(deepcopy(bbox), image_hw[::-1], mode)
if len(target.bbox) == 0:
    return self.default_scale_deal(image_hw)

# record old target info
old_mode = target.mode

创建了一个新的BoxList对象,并传入了三个参数:deepcopy(bbox)表示目标边界框的深拷贝,图像的高度和宽度的逆序,目标边界框的模式
boxes = target.convert('xywh').bbox.cpu().numpy()
sizes = np.sqrt(boxes[:, 2] * boxes[:, 3])
sizes = sizes[sizes > 0]
src_size = np.exp(np.log(sizes).mean())

将目标边界框的坐标格式转换为'xywh'格式,其中'xywh'表示左上角点的坐标(x, y)和边界框的宽度和高度(width, height)
将目标边界框的坐标数据从GPU内存转移到CPU内存,并将其转换为NumPy数组
计算目标边界框的面积大小
将尺寸数组取对数,计算对数的平均值,再取指数,得到目标框尺寸的平均值,其中src_size 是一个浮点数类型的变量,sizes 是一个 NumPy 数组
scale = self.default_scale     获取默认的缩放比例
for try_i in range(self.max_sample_try):
   dst_size = self._sample_by_distribute()        从某种分布中采样一个值
   _scale = dst_size / src_size                          计算目标边界框大小的采样值与原始目标边界框大小的比例
   if self.scale_range[1] > _scale > self.scale_range[0]:    判断采样得到的比例值是否在self.scale_range指定的范围内
       scale = _scale       								将合法的比例值赋值给scale变量
       break  # last time forget

接下来·看看·_sample_by_distribute函数

在这里插入图片描述
首先生成一个在[0, 1)之间的均匀分布的随机数r。过比较r和self.distri_cumsum数组中的元素,找到第一个满足条件r <= self.distri_cumsum + 1e-6的元素索引。根据索引idx和idx + 1从self.sizes数组中取出对应的两个元素,分别表示某个区间的最小值mins和最大值maxs。然后再次调用了random.uniform()函数,生成一个在[0, 1)之间的均匀分布的随机数ir,将随机数ir乘以区间长度(maxs - mins)并加上最小值mins,从而得到一个在区间[mins, maxs]内的随机数作为采样结果。

回到原函数

self.debug_record(_scale)
if self.out_scale_deal == 'clip':        当超出设定的比例尺范围时如何处理,可选值为'clip'和'use_default_scale',默认值是clip
    if _scale >= self.scale_range[1]:
        scale = self.scale_range[1]  # note 1
    elif _scale <= self.scale_range[0]:
        scale = self.scale_range[0]  # note 1
size = int(round(scale * image_hw[0])), int(round(scale * image_hw[1]))   计算新的目标框尺寸,将原始图像的高和宽分别乘以比例尺scale,并取整
target = target.convert(old_mode)                                 将目标框的格式转换回原始的模式old_mode,例如从xywh转换回xyxy格式
target = target.resize((size[1], size[0]))      将目标框的尺寸调整为新的尺寸(size[1], size[0]),其中size[1]表示调整后的宽度,size[0]表示调整后的高度
对调整后的目标框进行过滤,保留宽度和高度都大于等于2的目标框,并处理调整失败的情况

if len(target.bbox) > 0:
target = target[(target.bbox[:, 2] - target.bbox[:, 0] + 1) >= 2]
target = target[(target.bbox[:, 3] - target.bbox[:, 1] + 1) >= 2]

if len(target.bbox) == 0:
self.fail_time += 1
if self.fail_time % 1 == 0:
    warnings.warn("Scale Matching failed for {} times, you may need to change the mean to min. "
                  "dst_size is {}, src_size is {}, sizes".format(self.fail_time, dst_size, src_size, sizes))
return self.default_scale_deal(image_hw)

函数返回缩放比例。

回到call函数中

target = target.resize((size[1], size[0]))
if len(target.bbox) > 0:
    target = target[(target.bbox[:, 2] - target.bbox[:, 0] + 1) >= 2]
    target = target[(target.bbox[:, 3] - target.bbox[:, 1] + 1) >= 2]
if not self.debug_no_image_resize:
    image = F.resize(image, size, self.mode)
return image, target

进行了尺寸的调整,将其调整为 (size[1], size[0]) 的大小
进一步对 target 进行了裁剪操作,将不符合条件的 bbox 进行了过滤
image 通过 F.resize(image, size, self.mode) 进行了图像的 resize 操作,将其调整为 size 的大小,并使用了 self.mode 参数指定的插值方式

最后,将调整好的图像和bbox框返回。至此Scale Match对于图像的操作就做好了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值