• 文本检测网络EAST学习(二)


     EAST是旷视科技在2017年论文East: An Efficient and Accurate Scene Text Detector中提出,能检测任意角度的文字,速度和准确度都很有优势。

     East算是一篇很有特色的文章,还是从网络设计,GroundTruth生成,loss函数和Locality-Aware NMS(后处理)四部分来学习下。

    1.网络设计

      East论文中网络结构如下图所示,采用PVANet提取特征,将不同层的特征进行上采样合并,随后预测最后的score和box。关于box的表示方式,论文中提出了两种方法,即RBOX和QUAD,若box数据采用RBOX形式标注,模型最后预测1个chanel的score_map和4个channel的box_map; 若box数据采用QUAD的形式标注,模型最后预测1个chanel的score_map和8个channel的box_map.

      实际工作中,我主要用到Resnet作为backbone的East网络,并使用RBOX形式的标注框,下面是具体的网络结构如下图所示,训练过程中网络的数据流总结如下:

    • 尺寸为1x3x512x512的图片输入Resnet50提取特征,将layer1, layer2, layer3, layer4层输出的特征f4(1x256x128x128), f3(1x512x64x64), f2(1x1024x32x32), f1(1x2048x16x16)送入特征合并层

    • f1上采样后和f2进行concat,随后经过1x1,3x3的卷积得到h2(1x128x32x32), 同样的h2上采样,和f3进行concat,卷积得到h3(1x64x64x64), 最后h3上采样,和f4进行concat,卷积得到h4(1x32x128x128)

    • 上述h4经过一个3x3的卷积后,分别进入三个卷积预测分支,预测得到score_map(1x1x128x128), geo_map(1x4x128x128), angle_map(1x1x128x128)。需要注意的时预测的map都经过了sigmoid,值在0-1之间,对于geo_map和angle_map还需进行如下后处理:

    geo_map = self.sigmoid2(geo_map) * 512        (输入图片尺寸为512,变化到像素值)
    angle_map = (angle_map - 0.5) * math.pi / 2   (变化到[-Π/2, Π/2]之间)

    2. GroundTruth生成

    2.1 GroundTruth含义理解

      上述提到了box的标注有两种形式RBOX和QUAD,其GroundTruth也不一样

    RBOX

      RBOX的GroundTruth包括score_map,geo_map和angle_map。score map文字框区域的像素值为1,其他非文本框区域值为0,如下图中(b)所示。geo_map的文本区域中每个像素点都包含4个值,即像素点到文本框上,下,左,右的距离,如下面示意图中,图(d)中深蓝/黄/红/绿分别表示这个像素点到上,下,左,右的距离。图(e)是angle_map ,表示文本框的旋转角度angle。特别注意,这里考虑文本区域中每个像素点到文本框的距离,其他非文本框区域的像素点的这5个值置为0,最后得到的是WxHx4大小的geo_map和WxHx1的angle_map),W和H分别表示原始图片的宽和高。(注意的是,这里的文本框都是实际文本框的缩小版)

    QUAD

      QUAD的GroundTruth包括score_map和geo_map,其score_map和RBOX一样,box标记出文本所在框的四个角点坐标 [公式] ,这个无需做额外处理,geo_ma的文本区域中每个像素点包含8个值,为四个角点坐标的集合。

    2.2 GroundTruth相关代码理解

      在产生geo_map和angle_map时,有很多代码不是很好理解,值得说明下。

    polygon_area()函数

      主要是用来验证box四个坐标是否按顺时针排序,若按逆时针排序,需要转换为顺时针排序,其原理是利用了鞋带定理。鞋带定理(Shoelace Theorem)能根据多边形的顶点坐标,计算任意多边形的面积,坐标顺时针排列时为负数,逆时针排列时为正数。(鞋带定理:https://zhuanlan.zhihu.com/p/110025234)

    def polygon_area(poly):
        '''
        compute area of a polygon
        :param poly:
        :return:
        '''
        edge = [
            (poly[1][0] - poly[0][0]) * (poly[1][1] + poly[0][1]),
            (poly[2][0] - poly[1][0]) * (poly[2][1] + poly[1][1]),
            (poly[3][0] - poly[2][0]) * (poly[3][1] + poly[2][1]),
            (poly[0][0] - poly[3][0]) * (poly[0][1] + poly[3][1])
        ]
        return np.sum(edge)/2.
    
    
    def check_and_validate_polys(polys, tags, size):
        '''
        check so that the text poly is in the same direction,
        and also filter some invalid polygons
        :param polys:
        :param tags:
        :return:
        '''
        (h, w) = size
        if polys.shape[0] == 0:
            return polys
        polys[:, :, 0] = np.clip(polys[:, :, 0], 0, w-1)
        polys[:, :, 1] = np.clip(polys[:, :, 1], 0, h-1)
    
        validated_polys = []
        validated_tags = []
        for poly, tag in zip(polys, tags):
            p_area = polygon_area(poly)
            if abs(p_area) < 1:
                # print poly
                print('invalid poly')
                continue
            if p_area > 0:
                print('poly in wrong direction')
                poly = poly[(0, 3, 2, 1), :]
            validated_polys.append(poly)
            validated_tags.append(tag)
        return np.array(validated_polys), np.array(validated_tags)
    polygon_area()

      判断多边形排序应用:

    #鞋带定理(Shoelace Theorem)能根据多边形的顶点坐标,计算任意多边形的面积,坐标顺时针排列时为负数,逆时针排列时为正数
    
    def validate_clockwise_points(points):  #顺时针排序时报错
        """
        Validates that the points that the 4 points that dlimite a polygon are in counter_clockwise order.
        """
    
        if len(points) != 8:
            raise Exception("Points list not valid." + str(len(points)))
    
        point = [
            [int(points[0]), int(points[1])],
            [int(points[2]), int(points[3])],
            [int(points[4]), int(points[5])],
            [int(points[6]), int(points[7])]
        ]
        edge = [
            (point[1][0] - point[0][0]) * (point[1][1] + point[0][1]),
            (point[2][0] - point[1][0]) * (point[2][1] + point[1][1]),
            (point[3][0] - point[2][0]) * (point[3][1] + point[2][1]),
            (point[0][0] - point[3][0]) * (point[0][1] + point[3][1])
        ]
    
        summatory = edge[0] + edge[1] + edge[2] + edge[3]
        if summatory < 0:
            raise Exception("Points are not counter_clockwise.")
    多边形顶点排序

    point_dist_to_line()函数

      np.cross表示向量的叉积,而向量的叉积表示这两个向量形成的平行四边形的面积,面积除以底边得到高,即p3到p1p2边的距离

      (向量叉积参考:https://zhuanlan.zhihu.com/p/148780358?utm_source=cn.wps.moffice_eng)

    def point_dist_to_line(p1, p2, p3):
        # compute the distance from p3 to p1-p2
        return np.linalg.norm(np.cross(p2 - p1, p1 - p3)) / np.linalg.norm(p2 - p1)

    generate_rbox()函数

      这个函数最复杂,其中计算包围box最小矩形的代码比较难理解,大致流程就是从每个顶点出发,找到对应的平行四边形及其矩形,然后比较所有矩形的面积,取面积最小的矩形,如下图所示:

      generate_rbox的代码如下:

    def generate_rbox(im_size, polys, tags):
        h, w = im_size
        poly_mask = np.zeros((h, w), dtype=np.uint8)
        score_map = np.zeros((h, w), dtype=np.uint8)
        geo_map = np.zeros((h, w, 5), dtype=np.float32)
        # mask used during traning, to ignore some hard areas
        training_mask = np.ones((h, w), dtype=np.uint8)
        for poly_idx, poly_tag in enumerate(zip(polys, tags)):
            poly = poly_tag[0]
            tag = poly_tag[1]
    
            r = [None, None, None, None]
            for i in range(4):
                r[i] = min(np.linalg.norm(poly[i] - poly[(i + 1) % 4]),
                           np.linalg.norm(poly[i] - poly[(i - 1) % 4]))
            # score map
            shrinked_poly = shrink_poly(poly.copy(), r).astype(np.int32)[np.newaxis, :, :]
            cv2.fillPoly(score_map, shrinked_poly, 1)
            cv2.fillPoly(poly_mask, shrinked_poly, poly_idx + 1)
            # if the poly is too small, then ignore it during training
            poly_h = min(np.linalg.norm(poly[0] - poly[3]), np.linalg.norm(poly[1] - poly[2]))
            poly_w = min(np.linalg.norm(poly[0] - poly[1]), np.linalg.norm(poly[2] - poly[3]))
            if min(poly_h, poly_w) < FLAGS.min_text_size:
                cv2.fillPoly(training_mask, poly.astype(np.int32)[np.newaxis, :, :], 0)
            if tag:
                cv2.fillPoly(training_mask, poly.astype(np.int32)[np.newaxis, :, :], 0)
    
            xy_in_poly = np.argwhere(poly_mask == (poly_idx + 1))
            # if geometry == 'RBOX':
            # 对任意两个顶点的组合生成一个平行四边形 - generate a parallelogram for any combination of two vertices
            fitted_parallelograms = []
            for i in range(4):
                p0 = poly[i]
                p1 = poly[(i + 1) % 4]
                p2 = poly[(i + 2) % 4]
                p3 = poly[(i + 3) % 4]
                edge = fit_line([p0[0], p1[0]], [p0[1], p1[1]])                 #直线p0p1
                backward_edge = fit_line([p0[0], p3[0]], [p0[1], p3[1]])         #直线p0p3
                forward_edge = fit_line([p1[0], p2[0]], [p1[1], p2[1]])        #直线p1p2
                if point_dist_to_line(p0, p1, p2) > point_dist_to_line(p0, p1, p3):    #p2到直线p0p1的距离大于p3到p0p1的距离
                    # 平行线经过p2 - parallel lines through p2
                    if edge[1] == 0:                                #经过p2平行于p0p1的直线
                        edge_opposite = [1, 0, -p2[0]]
                    else:
                        edge_opposite = [edge[0], -1, p2[1] - edge[0] * p2[0]]
                else:
                    # 经过p3 - after p3
                    if edge[1] == 0:                             #经过p3平行于p0p1的直线
                        edge_opposite = [1, 0, -p3[0]]
                    else:
                        edge_opposite = [edge[0], -1, p3[1] - edge[0] * p3[0]]
                # move forward edge
                new_p0 = p0
                new_p1 = p1
                new_p2 = p2
                new_p3 = p3
                new_p2 = line_cross_point(forward_edge, edge_opposite)          #直线forward_edge和直线edge_opposite的交点
                if point_dist_to_line(p1, new_p2, p0) > point_dist_to_line(p1, new_p2, p3):
                    # across p0
                    if forward_edge[1] == 0:                                  #经过p0,平行于forward_edge的直线
                        forward_opposite = [1, 0, -p0[0]]
                    else:
                        forward_opposite = [forward_edge[0], -1, p0[1] - forward_edge[0] * p0[0]]
                else:
                    # across p3
                    if forward_edge[1] == 0:                         #经过p3,平行于forward_edge的直线
                        forward_opposite = [1, 0, -p3[0]]
                    else:
                        forward_opposite = [forward_edge[0], -1, p3[1] - forward_edge[0] * p3[0]]
                new_p0 = line_cross_point(forward_opposite, edge)                 #直线forward_opposite和直线edge的交点
                new_p3 = line_cross_point(forward_opposite, edge_opposite)          #直线forward_opposite和直线edge_opposite的交点
                fitted_parallelograms.append([new_p0, new_p1, new_p2, new_p3, new_p0])
                # or move backward edge
                new_p0 = p0
                new_p1 = p1
                new_p2 = p2
                new_p3 = p3
                new_p3 = line_cross_point(backward_edge, edge_opposite)
                if point_dist_to_line(p0, p3, p1) > point_dist_to_line(p0, p3, p2):
                    # across p1
                    if backward_edge[1] == 0:
                        backward_opposite = [1, 0, -p1[0]]
                    else:
                        backward_opposite = [backward_edge[0], -1, p1[1] - backward_edge[0] * p1[0]]
                else:
                    # across p2
                    if backward_edge[1] == 0:
                        backward_opposite = [1, 0, -p2[0]]
                    else:
                        backward_opposite = [backward_edge[0], -1, p2[1] - backward_edge[0] * p2[0]]
                new_p1 = line_cross_point(backward_opposite, edge)
                new_p2 = line_cross_point(backward_opposite, edge_opposite)
                fitted_parallelograms.append([new_p0, new_p1, new_p2, new_p3, new_p0])
            areas = [Polygon(t).area for t in fitted_parallelograms]
            parallelogram = np.array(fitted_parallelograms[np.argmin(areas)][:-1], dtype=np.float32)
            # sort thie polygon
            parallelogram_coord_sum = np.sum(parallelogram, axis=1)
            min_coord_idx = np.argmin(parallelogram_coord_sum)
            parallelogram = parallelogram[
                [min_coord_idx, (min_coord_idx + 1) % 4, (min_coord_idx + 2) % 4, (min_coord_idx + 3) % 4]]
    
            rectange = rectangle_from_parallelogram(parallelogram)
            rectange, rotate_angle = sort_rectangle(rectange)
    
            p0_rect, p1_rect, p2_rect, p3_rect = rectange
            for y, x in xy_in_poly:
                point = np.array([x, y], dtype=np.float32)
                # top
                geo_map[y, x, 0] = point_dist_to_line(p0_rect, p1_rect, point)
                # right
                geo_map[y, x, 1] = point_dist_to_line(p1_rect, p2_rect, point)
                # down
                geo_map[y, x, 2] = point_dist_to_line(p2_rect, p3_rect, point)
                # left
                geo_map[y, x, 3] = point_dist_to_line(p3_rect, p0_rect, point)
                # angle
                geo_map[y, x, 4] = rotate_angle
        return score_map, geo_map, training_mask
    generate_rbox

    3. loss函数

      损失函数包括两部分,score_map的的分类任务损失和geo_map,angle_map的回归损失,论文中总损失计算如下:

    分类损失

      score_map中文本所在区域的像素点值为1,背景区域的像素点值为0,是一个二分类问题,由于类别平衡,论文中使用类平衡的交叉熵损失(class-balanced cross-entropy)

      很多实现代码中都使用dice loss代替了类平衡损失,dice loss的实现代码如下: 

    def dice_coefficient(y_true_cls, y_pred_cls,
                         training_mask):
        '''
        dice loss
        :param y_true_cls:
        :param y_pred_cls:
        :param training_mask:
        :return:
        '''
        eps = 1e-5
        intersection =torch.sum(y_true_cls * y_pred_cls * training_mask)
        union = torch.sum(y_true_cls * training_mask) + torch.sum(y_pred_cls * training_mask) + eps
        loss = 1. - (2 * intersection / union)

      dice loss参考:https://zhuanlan.zhihu.com/p/86704421https://zhuanlan.zhihu.com/p/269592183

    回归损失

      RBOX损失的计算,包括box位置geo_map损失和box角度angle_map的损失,box位置采用了比较有特色的IOU Loss, 即gt框和预测框的交并比,如下面等式

       box的角度损失采用了余弦角度差损失,如下面等式

     

       总的RBOX损失值如下

       总的loss函数的实现代码如下:

    import torch
    import torch.nn as nn
    
    def dice_coefficient(y_true_cls, y_pred_cls,
                         training_mask):
        '''
        dice loss
        :param y_true_cls:
        :param y_pred_cls:
        :param training_mask:
        :return:
        '''
        eps = 1e-5
        intersection =torch.sum(y_true_cls * y_pred_cls * training_mask)
        union = torch.sum(y_true_cls * training_mask) + torch.sum(y_pred_cls * training_mask) + eps
        loss = 1. - (2 * intersection / union)
    
        return loss
    
    class LossFunc(nn.Module):
        def __init__(self):
            super(LossFunc, self).__init__()
            return 
        
        def forward(self, y_true_cls, y_pred_cls, y_true_geo, y_pred_geo, training_mask):
            classification_loss = dice_coefficient(y_true_cls, y_pred_cls, training_mask)
            # scale classification loss to match the iou loss part
            classification_loss *= 0.01
    
            # d1 -> top, d2->right, d3->bottom, d4->left
        #     d1_gt, d2_gt, d3_gt, d4_gt, theta_gt = tf.split(value=y_true_geo, num_or_size_splits=5, axis=3)
            d1_gt, d2_gt, d3_gt, d4_gt, theta_gt = torch.split(y_true_geo, 1, 1)
        #     d1_pred, d2_pred, d3_pred, d4_pred, theta_pred = tf.split(value=y_pred_geo, num_or_size_splits=5, axis=3)
            d1_pred, d2_pred, d3_pred, d4_pred, theta_pred = torch.split(y_pred_geo, 1, 1)
            area_gt = (d1_gt + d3_gt) * (d2_gt + d4_gt)
            area_pred = (d1_pred + d3_pred) * (d2_pred + d4_pred)
            w_union = torch.min(d2_gt, d2_pred) + torch.min(d4_gt, d4_pred)
            h_union = torch.min(d1_gt, d1_pred) + torch.min(d3_gt, d3_pred)
            area_intersect = w_union * h_union
            area_union = area_gt + area_pred - area_intersect
            L_AABB = -torch.log((area_intersect + 1.0)/(area_union + 1.0))
            L_theta = 1 - torch.cos(theta_pred - theta_gt)
            L_g = L_AABB + 20 * L_theta
    
            return torch.mean(L_g * y_true_cls * training_mask) + classification_loss
    EastLoss

    4. Locality-Aware NMS(后处理)

    在测试阶段,需要根据score_map和geo_map得到最后的检测框box,流程如下:

    • 选取score_map中预测分数大于score_map_thresh的区域,作为可能的文本检测区域

    • 根据筛选后的score_map和geo_map, 将RBOXA,A,B,B,angle)的文本框表示形式转成QUAD的形式

    • 所有坐标点按照y坐标,对于y坐标相邻两个box进行weighted_merge(以分数为权重进行合并)

    • 根据score排序,并做NMS,过滤多余文本框。

     将RBOX形式转换为QUAD的逻辑,代码中采用函数restore_rectangle_rbox()实现,其逻辑是:对于文本区域中的每一个像素点,先旋转矩阵计算得到旋转后的坐标,再平移到该像素点即可,如下图所示:

      restore_rectangle_rbox()代码如下:

    def restore_rectangle_rbox(origin, geometry):
        # origin:是所有文本区域点的坐标,(x, y)形式
        # geometry:是origin中每个点对应四边的距离和角度[A, A, B, B, angle]
        d = geometry[:, :4]      # 四边距离[A, A, B, B]
        angle = geometry[:, 4]   # 角度angle
        # for angle > 0
        origin_0 = origin[angle >= 0]
        d_0 = d[angle >= 0]
        angle_0 = angle[angle >= 0]
        if origin_0.shape[0] > 0:
            p = np.array([np.zeros(d_0.shape[0]), -d_0[:, 0] - d_0[:, 2],
                          d_0[:, 1] + d_0[:, 3], -d_0[:, 0] - d_0[:, 2],
                          d_0[:, 1] + d_0[:, 3], np.zeros(d_0.shape[0]),
                          np.zeros(d_0.shape[0]), np.zeros(d_0.shape[0]),
                          d_0[:, 3], -d_0[:, 2]])
            p = p.transpose((1, 0)).reshape((-1, 5, 2))  # N*5*2
    
            rotate_matrix_x = np.array([np.cos(angle_0), np.sin(angle_0)]).transpose((1, 0))
            rotate_matrix_x = np.repeat(rotate_matrix_x, 5, axis=1).reshape(-1, 2, 5).transpose((0, 2, 1))  # N*5*2
    
            rotate_matrix_y = np.array([-np.sin(angle_0), np.cos(angle_0)]).transpose((1, 0))
            rotate_matrix_y = np.repeat(rotate_matrix_y, 5, axis=1).reshape(-1, 2, 5).transpose((0, 2, 1))
    
            p_rotate_x = np.sum(rotate_matrix_x * p, axis=2)[:, :, np.newaxis]  # N*5*1
            p_rotate_y = np.sum(rotate_matrix_y * p, axis=2)[:, :, np.newaxis]  # N*5*1
    
            p_rotate = np.concatenate([p_rotate_x, p_rotate_y], axis=2)  # N*5*2
    
            p3_in_origin = origin_0 - p_rotate[:, 4, :]
            new_p0 = p_rotate[:, 0, :] + p3_in_origin  # N*2
            new_p1 = p_rotate[:, 1, :] + p3_in_origin
            new_p2 = p_rotate[:, 2, :] + p3_in_origin
            new_p3 = p_rotate[:, 3, :] + p3_in_origin
    
            new_p_0 = np.concatenate([new_p0[:, np.newaxis, :], new_p1[:, np.newaxis, :],
                                      new_p2[:, np.newaxis, :], new_p3[:, np.newaxis, :]], axis=1)  # N*4*2
        else:
            new_p_0 = np.zeros((0, 4, 2))
        # for angle < 0
        origin_1 = origin[angle < 0]
        d_1 = d[angle < 0]
        angle_1 = angle[angle < 0]
        if origin_1.shape[0] > 0:
            p = np.array([-d_1[:, 1] - d_1[:, 3], -d_1[:, 0] - d_1[:, 2],
                          np.zeros(d_1.shape[0]), -d_1[:, 0] - d_1[:, 2],
                          np.zeros(d_1.shape[0]), np.zeros(d_1.shape[0]),
                          -d_1[:, 1] - d_1[:, 3], np.zeros(d_1.shape[0]),
                          -d_1[:, 1], -d_1[:, 2]])
            p = p.transpose((1, 0)).reshape((-1, 5, 2))  # N*5*2
    
            rotate_matrix_x = np.array([np.cos(-angle_1), -np.sin(-angle_1)]).transpose((1, 0))
            rotate_matrix_x = np.repeat(rotate_matrix_x, 5, axis=1).reshape(-1, 2, 5).transpose((0, 2, 1))  # N*5*2
    
            rotate_matrix_y = np.array([np.sin(-angle_1), np.cos(-angle_1)]).transpose((1, 0))
            rotate_matrix_y = np.repeat(rotate_matrix_y, 5, axis=1).reshape(-1, 2, 5).transpose((0, 2, 1))
    
            p_rotate_x = np.sum(rotate_matrix_x * p, axis=2)[:, :, np.newaxis]  # N*5*1
            p_rotate_y = np.sum(rotate_matrix_y * p, axis=2)[:, :, np.newaxis]  # N*5*1
    
            p_rotate = np.concatenate([p_rotate_x, p_rotate_y], axis=2)  # N*5*2
    
            p3_in_origin = origin_1 - p_rotate[:, 4, :]
            new_p0 = p_rotate[:, 0, :] + p3_in_origin  # N*2
            new_p1 = p_rotate[:, 1, :] + p3_in_origin
            new_p2 = p_rotate[:, 2, :] + p3_in_origin
            new_p3 = p_rotate[:, 3, :] + p3_in_origin
    
            new_p_1 = np.concatenate([new_p0[:, np.newaxis, :], new_p1[:, np.newaxis, :],
                                      new_p2[:, np.newaxis, :], new_p3[:, np.newaxis, :]], axis=1)  # N*4*2
        else:
            new_p_1 = np.zeros((0, 4, 2))
        return np.concatenate([new_p_0, new_p_1])
    restore_rectangle_rbox

      locality-aware NMS就是在NMS之前,对于y坐标相邻很近的box先进行一次合并,然后再进行NMS,其中合并采用了weigthed_merge方法,需要注意下,python示例代码如下:

    import numpy as np
    from shapely.geometry import Polygon
    
    
    def intersection(g, p):
        g = Polygon(g[:8].reshape((4, 2)))
        p = Polygon(p[:8].reshape((4, 2)))
        if not g.is_valid or not p.is_valid:
            return 0
        inter = Polygon(g).intersection(Polygon(p)).area
        union = g.area + p.area - inter
        if union == 0:
            return 0
        else:
            return inter/union
    
    
    def weighted_merge(g, p):
        g[:8] = (g[8] * g[:8] + p[8] * p[:8])/(g[8] + p[8])
        g[8] = (g[8] + p[8])
        return g
    
    
    def standard_nms(S, thres):
        order = np.argsort(S[:, 8])[::-1]
        keep = []
        while order.size > 0:
            i = order[0]
            keep.append(i)
            ovr = np.array([intersection(S[i], S[t]) for t in order[1:]])
    
            inds = np.where(ovr <= thres)[0]
            order = order[inds+1]
    
        return S[keep]
    
    
    def nms_locality(polys, thres=0.3):
        '''
        locality aware nms of EAST
        :param polys: a N*9 numpy array. first 8 coordinates, then prob
        :return: boxes after nms
        '''
        S = []
        p = None
        for g in polys:
            if p is not None and intersection(g, p) > thres:
                p = weighted_merge(g, p)
            else:
                if p is not None:
                    S.append(p)
                p = g
        if p is not None:
            S.append(p)
    
        if len(S) == 0:
            return np.array([])
        return standard_nms(np.array(S), thres)
    
    
    if __name__ == '__main__':
        # 343,350,448,135,474,143,369,359
        print(Polygon(np.array([[343, 350], [448, 135],
                                [474, 143], [369, 359]])).area)
    locality_aware_nms

    参考文章:

      https://www.cnblogs.com/lillylin/p/9954981.html

      https://zhuanlan.zhihu.com/p/71182747

      https://blog.csdn.net/sxlsxl119/article/details/103934957

  • 相关阅读:
    返回表对象的方法之一--bulk collect into
    coolite 获取新的页面链接到当前页面指定位置Panel的运用
    oracle 当前年到指定年的年度范围求取
    JAVA WEB 过滤器
    Java复习笔记(二):数据类型以及逻辑结构
    Java复习笔记(一):概念解释和运行步骤
    装饰器理解
    Flask大型项目框架结构理解
    JSP内置对象(一)
    Java Web第一个应用搭建
  • 原文地址:https://www.cnblogs.com/silence-cho/p/14053286.html
Copyright © 2020-2023  润新知