基于边缘检测和透视变换的文档图像校正

实验任务与要求:

对发生透视变换的文档图像进行几何校正处理,得到规范的文档图像。
几何校正的目的是把发生了透视变换的目标变换为具有真实比例和角度的目标,如下图所示:左图中的地板砖经过透视变换之后,不再是正方形,经过校正之后,得到右侧图像,其中的地板砖被校正为正方形:
基于边缘检测和透视变换的文档图像校正
基于边缘检测和透视变换的文档图像校正
本次实验提供了6张图像,每张图像中含有一个平面文档,你要对这6张图像中的文档进行校正,并把校正结果插入到本实验报告中。欢迎大家自己拍摄文档/海报等的图像,看看使用自己开发的程序能否正确的校正。
可以要求手工输入文档的真实长宽比例,或者真实长宽参数。
本次实验的6个文档的物理尺寸(cm)分别是:(长,宽)
1.jpg: (24,17); 2.jpg: (18.2,11.2); 3.jpg:(20,14); 4.jpg: (9.5,6.5); 5.jpg: (12,8.5); 6.jpg: (26,21)
参考第四讲、第五讲和第六讲的内容,通过实验探索:
1) 如何自动提取文档图像的四个角点或四条直边;
2) 如何抑制各种噪声干扰;

问题分析

本实验采用计算机视觉中边缘检测和透视变换等技术,对发生透视变换的文档图像进行几何校正处理,得到规范的文档图像。
通过分析本实验需要处理的图像,可以发现有些图像的边缘与背景很接近,会给边缘检测带来较大的挑战,对于提取图片中存在的直线段产生较大的干扰。
通过分析样例,目前明确问题处理的基本流程,通过canny算法提取边缘图,通过霍夫直线检测得到图片中的直线段,通过对直线的处理得到图片中文档的边界线,通过四条边界线求交点,得到图片中文档的边界点,进而再由文档的真实尺寸实现透视变换,进而达到文档矫正的目的。
通过分析样本数据可以发现,会出现较多的噪声的干扰,对于直线的检测,与边界点的求解产生较大的干扰。因此需要考虑采取一系列的方法来降低噪声的影响。

方法流程的设计

通过分析文档矫正的场景,首先尝试采用最直接的方法,通过单通道来得到灰度图,再通过阈值化得到二值图,有利于对图中的直线的检测,通过canny算法提取边缘,通过霍夫直线检测得到直线段,然后我尝试将提取到的直线段进行合并,进而尽可能地得到较长的线段,通过对将直线段转换成向量,通过向量之间的夹角的余弦值,作为直线是否平行的判断依据,若平行再判断直线的两端点所构成的向量是否仍平行,如果平行则可认为两直线段是可以进行合并,通过将直线段投影到x轴,来确定文档的内部边界,来去除文档内部边界内的直线,消除其的影响,然后获取最长的四条直线,就会是文档的边界,通过求解直线之间的交点,得到文档的边界点,进而通过透视转换得到矫正后的文档图片。
通过对实验结果的分析,发现单通道GRB的选择对于图中文档边缘的检测有影响,通过分析发现如果文档的颜色偏红色,则在选择RGB通道时候,不选择R通道,会使得边缘检测结果更好,基于这样的发现,我在进行选择单通道时候,通过计算每个通道的颜色的平均值,选择通道平均值最小的通道,作为此图片的单通道,结果发现有较好的边缘检测效果,可以解决有些边缘检测不到的问题。

实验配置与结果

实验配置主要是通过canny算法进行边缘检测,其中canny边缘检测中设置两个阈值,通过实验发现,低阈值为70,高阈值为200,实验效果较好。
在利用霍夫直线检测提取直线段时,通过采用不同尺寸的图像,采用不同的参数,进而达到较好的效果,通过对小尺寸的图片,采取较小的最短直线段距离(40,10),可以检测到较多的直线段。
尺寸较大的图片采取(100,30)的参数设置。
在进行直线段的合并时,通过设置阈值0.0001,若两向量之间夹角的余弦值小于此阈值,就认为两向量平行。

实验结果分析

实验结果如下所示:
基于边缘检测和透视变换的文档图像校正
基于边缘检测和透视变换的文档图像校正
基于边缘检测和透视变换的文档图像校正
基于边缘检测和透视变换的文档图像校正
注:具体实验结果(包含每一步的执行结果)可在jupyter notebook文件中看到
通过此实验,可以发现通过不同的颜色主图的图片采取不同的颜色单通道,有利于边缘的检测,(在图片3中如果采取其他颜色通道,会出现检测不到最左边的边界的情况)其中图片2的内部线存在较大的干扰,如果不进行内部线段的去除操作,会导致把较长的内部线作为边界线的情况发生。
图片5是最难处理的,由于其边缘与图片背景一致,导致提取不出来边界,因此只能通过将霍夫直线检测的最小距离参数调小,再通过合并,去除内部线,由于内部线距离边界较近,可以发现,对于内部线的去除操作效果不理想。导致边界求解出现问题,仍需要改进优化。其余文档图片的矫正效果较理想。

源代码

# 将直线的两端点转化为向量
def points2vector(lines):
    vector_dict = {}
    for line in lines:
        for points in line:
            vector = (points[0] - points[2], points[1] - points[3])
            vector_dict[(points[0], points[1])] = vector
    return vector_dict

# 用直线的首端点和向量方向转化为直线的两点(首尾)表示
def vector2points(dict_vector):
    lines_new_tmp = []
    for points, vector in dict_vector.items():
        tmp_x = points[0] - vector[0]
        tmp_y = points[1] - vector[1]
        tmp = (points[0], points[1], tmp_x, tmp_y)
        lines_new_tmp.append(tmp)
    return lines_new_tmp


# 求两向量的夹角的余弦值的绝对值
def angle_cos_of_vector(v1, v2):
    inner = v1[0] * v2[0] + v1[1] * v2[1]
    tmp = inner / (np.math.sqrt(v1[0] ** 2 + v1[1] ** 2) * np.math.sqrt(v2[0] ** 2 + v2[1] ** 2))
    return abs(tmp)


def get_set_lines(lines):
    set_of_lines = []
    for line in lines:
        for points in line:
            set_of_lines.append(tuple(points))
    return set_of_lines


def get_endpoints(p1, p2, p3, p4):
    points = {p1[0]: p1[1], p2[0]: p2[1], p3[0]: p3[1], p4[0]: p4[1]}
    tmp = [p1[0], p2[0], p3[0], p4[0]]
    tmp.sort()
    return (tmp[0], points[tmp[0]]), (tmp[3], points[tmp[3]])


def distance_of_line(temp):
    return np.math.sqrt((temp[2] - temp[0]) ** 2 + (temp[3] - temp[1]) ** 2)

def takeFirst(elem):
    return elem[0]


# 获得图片的四个边界点
def get_four_node(lines):
    cos_edge = []
    temp = lines[0][1]
    v1 = (temp[0] - temp[2], temp[1] - temp[3])
    lines_list = []
    for i in range(1, len(lines)):
        v2 = (lines[i][1][0] - lines[i][1][2], lines[i][1][1] - lines[i][1][3])
        cos_edge.append(angle_cos_of_vector(v1, v2))
        lines_list.append((lines[i][1]))
    max_index = cos_edge.index(max(cos_edge))
    edge_of_side = [temp, lines[max_index + 1][1]]
    lines_list.remove(lines[max_index + 1][1])
    four_points = []

    for line in edge_of_side:
        for each in lines_list:
            x, y = get_intersection_point_lines(line, each)
            four_points.append([x, y])

    # 对获得的端点按照顺时针排序
    x_list = []
    for each in four_points:
        x_list.append(each[0])
    x_temp = sorted(x_list)
    min_index1 = x_list.index(x_temp[0])
    min_index2 = x_list.index(x_temp[1])
    if four_points[min_index1][1] > four_points[min_index2][1]:
        order_points = [four_points[min_index1], four_points[min_index2]]
    else:
        order_points = [four_points[min_index2], four_points[min_index1]]
    index = [x for x in range(4) if x != min_index2 and x != min_index1]
    if four_points[index[0]][1] < four_points[index[1]][1]:
        order_points.append(four_points[index[0]])
        order_points.append(four_points[index[1]])
    else:
        order_points.append(four_points[index[1]])
        order_points.append(four_points[index[0]])

    print("获取到的四个端点", order_points)
    return order_points


def main(filename):
    # 读取并处理图片
    im0 = cv2.imread(filename, cv2.IMREAD_COLOR)
    if im0 is None:
        print('read image failed')
        exit()

    #对不同颜色分布的图片采用不同的颜色单通道,有利于后面边缘提取
    temp = []
    for i in range(3):
        im1 = im0[:, :, i]
        temp.append(np.mean(im1))
    #通过实验发现选取RGB中平均值最小的通道有利于边缘的提取
    index = temp.index(min(temp))
    im1 = im0[:, :, index]
    
    #通过阈值化和canny函数来提取边缘
    _, im2 = cv2.threshold(im1, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    edges = cv2.Canny(im2, 75, 200)
    #显示边缘图
    plt.imshow(edges,cmap = 'gray')
    plt.title('grayscale')
    plt.show()

    im = im0.copy()
    

    p_tuple = set_parameter(filename)
    
    lines = cv2.HoughLinesP(edges, 1, np.pi / 180, p_tuple[0], minLineLength=p_tuple[0], maxLineGap=p_tuple[1])

    im1 = im0.copy()
    for i in range(lines.shape[0]):
        x1 = lines[i][0][0]
        y1 = lines[i][0][1]    
        x2 = lines[i][0][2]
        y2 = lines[i][0][3]    
        cv2.line(im1,(x1,y1),(x2,y2),(0,255,0),2)

    #显示提取到的直线段
    plt.imshow(cv2.cvtColor(im1, cv2.COLOR_BGR2RGB))
    plt.title('Find the line')
    plt.show()

    dict_of_vector = points2vector(lines)
    vector_of_dict = merge_vector(dict_of_vector, threshold=0.001, max_iter=10)
    lines_new = vector2points(vector_of_dict)

    im2 = draw_lines_of_list(im0.copy(),lines_new)
    
    #显示合并过直线段的图片
    plt.title('Merge the lines')
    plt.imshow(cv2.cvtColor(im2, cv2.COLOR_BGR2RGB))
    plt.show()

    lines_new = remove_inner_lines(lines=lines_new)

    im3 = draw_lines_of_list(im0.copy(),lines_new)
    
    #显示去除图像内部干扰直线段的图片
    plt.title('remove inner lines')
    plt.imshow(cv2.cvtColor(im3, cv2.COLOR_BGR2RGB))
    plt.show()

    edge_lines = find_borderline(lines=lines_new)

    im4 = draw_edge_lines(im0.copy(),edge_lines)
    
    #显示找到的最长的四条边界
    plt.title('Find the longest lines')
    plt.imshow(cv2.cvtColor(im4, cv2.COLOR_BGR2RGB))
    plt.show()

    # 求四个边界交点
    four_edge_points = get_four_node(edge_lines)
    pts_img = np.array(four_edge_points, dtype=np.float32) - 1
    pts_obj = get_real_size(filename)
    obj_width, obj_height = pts_obj[2, 0], pts_obj[2, 1]

    for j in range(4):
        cv2.circle(im, (int(pts_img[j, 0]), int(pts_img[j, 1])), 6, (0, 0, 255), 4)

    M = cv2.getPerspectiveTransform(pts_img, pts_obj)
    warped = cv2.warpPerspective(im0, M, (int(obj_width), int(obj_height)))

    plt.title('Conversion image')
    plt.imshow(cv2.cvtColor(warped, cv2.COLOR_BGR2RGB))s
    plt.show()
上一篇:Minimum Height Trees 解答


下一篇:Python读取txt文件查找对应内容并生成新文件