找回密码
 立即注册
查看: 230|回复: 0

前处理letterbox 算法层优化

[复制链接]
发表于 2022-8-26 08:59 | 显示全部楼层 |阅读模式
1. 参考文档

2. 理论部分

letterbox的意义
以 yolo 举例:

  • 在训练时和推理时的原图尺寸不一定相同, 但是长宽比一定是严格相同的
  • 训练时图像若下采样2倍, 那么推理时的图像一定也是同样的下采样2倍, 这和图像的尺寸无关, 而是模型必须要进行的下采样操作
  • 那么如推理时的图像进行下采样后HW比例和yolo模型要求的比例不一致, 就需要进行padding
  • 粗暴的resize会使得图片失真,采用letterbox可以较好的解决这个问题。该方法可以保持图片的长宽比例,剩下的部分采用灰色填充


letterbox的算法概括


  • 先检查输入图像 是否需要进行一个padding
  • 如需进行padding, 那么先resisze
  • 然后调用cv2.copyMakeBorder() 进行边界填充
copyMakeBorder
cv.copyMakeBorder(src, top, bottom, left, right, borderType, None, value)

  • src: 即将被扩充边界的原始图像
  • top, bottom, left, right: 在图像上、下、左、右分别要扩充的行(列)数
  • borderType: 扩充的边界类型
  • cv.BORDER_CONSTANT: 用常数像素值填充扩充的边界(0或黑色)
  • cv.BORDER_REPLICATE: 原始边缘的行或列被复制到扩充的边界
  • value: 当使用cv.BORDER_CONSTANT时,设置的要填充的像素值
3. letterbox源码


  • yolo v5 3.1版本中的letterbox为例
def letterbox(img, new_shape=(416,416), color=(114,114,114),auto=True,scale_fill=False, scale_up=True):

# 1. 计算收缩比: Resize image to a 32-pixel-multiple rectangle https://github.com/ultralytics/yolov3/issues/232
        shape = img.shape[:2] # current shape [height, width]
       
        # new_shape是要padding成的尺寸,如果只有一个数,那就是正方形,自动给它变成二维的,current shape [height, width]
        if isinstance(new_shape,int):
                new_shape = (new_shape, new_shape)
       
        #收缩比取的是长宽方向上变化最大的 Scale ratio (new / old)
        r = min(new_shape[0]/shape[0],new_shape[1]/shape[1])
        if not scale_up: # only scale down, do not scale up (for better test mAP)
                r = min(r,1.0) #收缩比r一定是要小于1的
        ratio = r,r

# 2. 计算收缩后图片的长宽
        new_unpad = int(round(shape[1] - new_unpad[0]), int(round(shape[0]*r)))

# 3. 计算需要填充的像素
    #计算那个变化大的那一边需要填充的像素
        dw,dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
        # stride表示的即是模型下采样次数的2的次方,这个涉及感受野的问题,在YOLOV5中下采样次数为5,则stride为32
        if auto:
                dw, dh = np.mod(dw,32), np.mod(dh,32)
        elif scaleFill:
                dw, dh = 0.0,0.0
                new_unpad = (new_shape[1], new_shape[0])
                ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]
        # 除以2即最终每边填充的像素(四周padding,两边各padding一半)
        dw /= 2
        dh /= 2
# 4. 先resize图片
    if shape[::-1] != new_unpad:
            img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)

        # round(dw,dh - 0.1)直接让小于1的为0
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))

# 5.填充像素 添加灰边
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
    return img, ratio, (dw,dh)4. 自定义letterbox


  • talk is cheap, 理解后写一版优化的letterbox
def imgeCheck(self, image, max_stride=32, size_scle=2):

# 1. 计算收缩后图片的长宽       
        # size_scale: 根据下采样 stride 设定的, 图像在进行下采样时要缩减多少倍
        #floor: 向下取整数
        new_unpad = int(math.floor(image.shape[1] * (1.0 / size_scle))),
                                        int(math.floor*(image.shape[0] * (1.0 / size_scle)))
# 2. 进行resize
        image2 = cv2.resize(image, new_unpad, interpolation=cv2.INTER_LINEAR)

# 3. 计算需要填充的像素
        right_1 = max_stride - np.mod(new_unpad[0], max_stride)
        bottom_1 = max_stride - np.mod(new_unpad[1], max_stride)
        top = bottom_1 // 2 #计算图像上边界需要填充的像素
        bottom = bottom_1 - top ##计算图像下边界需要填充的像素
        left = right_1 // 2  ##计算图像左边界需要填充的像素
        right = right_1 - left ##计算图像右边界需要填充的像素

# 4. 计算下采样时图像长宽的缩小比例用于返回
        scale_h = size_scle
        scale_w = size_scle

# 5. 边界值填充
        new_img = cv2.copyMakeBorder(image2, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114,114,114))

        return new_img,scale_h,scale_w5.scale_coords


  • 图像经过letterbox后, 目标检测框的坐标等信息也需要进行更新
def scale_coords(self, img1_shape, coords, img0_shape, size_scle=1):
        gain = size_scle
        pad = (img1_shape[1] - img0_shape[1] / gain) // 2
        coords[:,[0,2]] -= pad[0]
        coords[:,[1,3]] -= pad[1]
        coords[:, :4] *= gain
        clip_coords(coords, img0_shape)
        return coords

  • 源码注释版
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
    """
    将预测的坐标信息coords(相对img1_shape)转换回相对原图尺度(img0_shape)
    :param img1_shape: 缩放后的图像大小  [H, W]=[384, 512]
    :param coords: 预测的box信息 [7,4]  [anchor_nums, x1y1x2y2] 这个预测信息是相对缩放后的图像尺寸(img1_shape)的
    :param img0_shape: 原图的大小  [H, W, C]=[375, 500, 3]
    :param ratio_pad: 缩放过程中的缩放比例以及pad  一般不传入
    :return: coords: 相对原图尺寸(img0_shape)的预测信息
    """
    # Rescale coords (xyxy) from img1_shape to img0_shape
    if ratio_pad is None:  # calculate from img0_shape
        # gain = old/new = 1.024  max(img1_shape): 求img1的较长边  这一步对应的是之前的letterbox步骤
        gain = max(img1_shape) / max(img0_shape)
        # wh padding 这一步起不起作用,完全取决于letterbox的方式
        # 当letterbox为letter_pad_img时,pad=(0.0, 64.0); 当letterbox为leeter_img时,pad=(0.0, 0.0)
        pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2
    else:
        gain = ratio_pad[0][0]
        pad = ratio_pad[1]

    # 将相对img1的预测信息缩放得到相对原图img0的预测信息
    coords[:, [0, 2]] -= pad[0]  # x padding
    coords[:, [1, 3]] -= pad[1]  # y padding
    coords[:, :4] /= gain        # 缩放
    # 缩放到原图的预测结果,并对预测值进行了一定的约束,防止预测结果超出图像的尺寸
    clip_coords(coords, img0_shape)
    return coords

def clip_coords(boxes, img_shape):
    """
    Clip bounding xyxy bounding boxes to image shape (height, width)
    c.clamp_(a, b): 将矩阵c中所有的元素约束在[a, b]中间
                    如果某个元素小于a,就将这个元素变为a;如果元素大于b,就将这个元素变为b
    这里将预测得到的xyxy做个约束,是因为当物体处于图片边缘的时候,预测值是有可能超过图片大小的
    :param boxes: 函数开始=>缩放到原图的预测结果[7, 4]
                  函数结束=>缩放到原图的预测结果,并对预测值进行了一定的约束,防止预测结果超出图像的尺寸
    :param img_shape: 原图的shape [H, W, C]=[375, 500, 3]
    """
    boxes[:, 0].clamp_(0, img_shape[1])  # x1
    boxes[:, 1].clamp_(0, img_shape[0])  # y1
    boxes[:, 2].clamp_(0, img_shape[1])  # x2
    boxes[:, 3].clamp_(0, img_shape[0])  # y2

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 02:37 , Processed in 0.099025 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表