Python 从扫描图像中快速修剪空白带噪空间的方法

Python 从扫描图像中快速修剪空白带噪空间的方法,python,opencv,image-processing,optimization,Python,Opencv,Image Processing,Optimization,我正在处理扫描文件(身份证、驾照等)。当我对它们进行一些预处理时,我面临的问题是,文档只占据了图像的一小部分,其余的部分都是空白/有噪声的空间。出于这个原因,我想开发一个Python代码,自动地修剪不需要的区域,并且只保留文档所在的区域(而不必预先定义每个文档的分辨率)。使用OpenCV中的findContours(),这是可能的。然而,大多数文档(尤其是旧文档)轮廓不清晰,其末端不够清晰,无法检测。另外,在空白空间中的噪声也可以被检测为等高线,所以轮廓对于所有的情况都不起作用。 我的想法是:

我正在处理扫描文件(身份证、驾照等)。当我对它们进行一些预处理时,我面临的问题是,文档只占据了图像的一小部分,其余的部分都是空白/有噪声的空间。出于这个原因,我想开发一个Python代码,自动地修剪不需要的区域,并且只保留文档所在的区域(而不必预先定义每个文档的分辨率)。使用OpenCV中的
findContours()
,这是可能的。然而,大多数文档(尤其是旧文档)轮廓不清晰,其末端不够清晰,无法检测。另外,在空白空间中的噪声也可以被检测为等高线,所以轮廓对于所有的情况都不起作用。 我的想法是:

  • 读取图像并将其转换为灰度
  • 应用OpenCV中的
    按位\u not()
    函数来分隔 背景来自弗罗格伦
  • 应用自适应平均阈值以尽可能多地去除噪声(并最终使背景变白)
  • 在这个层次上,我的背景几乎是白色的,文档是黑色的,但包含一些白色的空白

  • 所以我用腐蚀来填补文档部分的空白
  • 读取图像的每一行,如果其中20%包含黑色,则 保留它,如果它是白色的,则删除它。并对图像的每一列执行相同的操作
  • 根据图像索引的最小值和最大值裁剪图像 黑色的线和列
  • 以下是我的代码和一些注释:

    import cv2
    import numpy as np
    
    def crop(filename):
        #Read the image
        img = cv2.imread(filename)
        #Convert to grayscale
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        #Separate the background from the foreground
        bit = cv2.bitwise_not(gray)
        #Apply adaptive mean thresholding
        amtImage = cv2.adaptiveThreshold(bit, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 35, 15)
        #Apply erosion to fill the gaps
        kernel = np.ones((15,15),np.uint8)
        erosion = cv2.erode(amtImage,kernel,iterations = 2)
        #Take the height and width of the image
        (height, width) = img.shape[0:2]
        #Ignore the limits/extremities of the document (sometimes are black, so they distract the algorithm)
        image = erosion[50:height - 50, 50: width - 50]
        (nheight, nwidth) = image.shape[0:2]
        #Create a list to save the indexes of lines containing more than 20% of black.
        index = []
        for x in range (0, nheight):
            line = []
    
            for y in range(0, nwidth):
                line2 = []
                if (image[x, y] < 150):
                    line.append(image[x, y])
            if (len(line) / nwidth > 0.2):  
                index.append(x)
        #Create a list to save the indexes of columns containing more than 15% of black.
        index2 = []
        for a in range(0, nwidth):
            line2 = []
            for b in range(0, nheight):
                if image[b, a] < 150:
                    line2.append(image[b, a])
            if (len(line2) / nheight > 0.15):
                index2.append(a)
    
        #Crop the original image according to the max and min of black lines and columns.
        img = img[min(index):max(index) + min(250, (height - max(index))* 10 // 11) , max(0, min(index2)): max(index2) + min(250, (width - max(index2)) * 10 // 11)]
        #Save the image
        cv2.imwrite('res_' + filename, img)
    
    导入cv2
    将numpy作为np导入
    def裁剪(文件名):
    #阅读图片
    img=cv2.imread(文件名)
    #转换为灰度
    灰色=cv2.CVT颜色(img,cv2.COLOR\U BGR2GRAY)
    #将背景与前景分开
    位=cv2。按位\u非(灰色)
    #应用自适应平均阈值
    amtImage=cv2.adaptiveThreshold(位,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_二进制,35,15)
    #应用侵蚀来填充间隙
    kernel=np.one((15,15),np.uint8)
    侵蚀=cv2。侵蚀(amtImage,内核,迭代次数=2)
    #获取图像的高度和宽度
    (高度、宽度)=图像形状[0:2]
    #忽略文档的限制/末端(有时是黑色的,因此会分散算法的注意力)
    图像=侵蚀[50:高度-50,50:宽度-50]
    (nheight,nwidth)=图像.形状[0:2]
    #创建一个列表以保存包含20%以上黑色的行的索引。
    索引=[]
    对于范围内的x(0,n高):
    行=[]
    对于范围(0,nwidth)内的y:
    第2行=[]
    如果(图像[x,y]<150):
    行。追加(图像[x,y])
    如果(长度(直线)/nwidth>0.2):
    index.append(x)
    #创建一个列表以保存包含15%以上黑色的列的索引。
    index2=[]
    对于范围内的(0,nwidth):
    第2行=[]
    对于范围(0,N)内的b:
    如果图像[b,a]<150:
    第2行追加(图像[b,a])
    如果(len(line2)/n高度>0.15):
    index2.append(a)
    #根据黑线和黑列的最大值和最小值裁剪原始图像。
    img=img[min(index):max(index)+min(250,(高度-max(index))*10//11),max(0,min(index2)):max(index2)+min(250,(宽度-max(index2))*10//11)]
    #保存图像
    cv2.imwrite('res_uu'+文件名,img)
    
    下面是一个例子:我使用了来自互联网的图像来避免任何保密问题
    这里需要注意的是,图像质量比我处理的示例要好得多(空白区域不包含噪声)。
    输入:1920x1080

    输出:801x623

    我用不同的文档测试了这段代码,效果很好。问题是,处理单个文档需要花费大量时间(因为循环和读取图像的每个像素两次:一次使用行,第二次使用列)。
    是否可以进行一些修改以优化代码并减少处理时间

    欢迎提出任何建议。
    多谢各位

    编辑:

    我忘了提到我已经把同一个问题发了进来,但是我没有得到答案。因此,我标记了这个问题,并要求主持人将其迁移到StakOverflow。因为我没有从主持人那里得到答案,所以我决定把它贴在这里,因为我认为它也是关于这个主题的。一旦我在其中一个网站上得到答案,我将删除另一个网站上的问题以避免重复。

    这是我的方法,请查看:

    import cv2
    import numpy as np
    
    img = cv2.imread("1.png")
    
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    #Separate the background from the foreground
    bit = cv2.bitwise_not(gray)
    
    nonzero = np.nonzero(bit)
    
    minx = min(nonzero[1])
    maxx = max(nonzero[1])
    
    miny = min(nonzero[0])
    maxy = max(nonzero[0])
    
    res = img[miny:maxy,minx:maxx].copy()
    
    cv2.rectangle(img,(minx,miny),(maxx,maxy),(0,0,255),2)
    
    cv2.imshow('img',img)
    cv2.imshow('bit',bit)
    
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    


    这是我的方法,请查看:

    import cv2
    import numpy as np
    
    img = cv2.imread("1.png")
    
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    #Separate the background from the foreground
    bit = cv2.bitwise_not(gray)
    
    nonzero = np.nonzero(bit)
    
    minx = min(nonzero[1])
    maxx = max(nonzero[1])
    
    miny = min(nonzero[0])
    maxy = max(nonzero[0])
    
    res = img[miny:maxy,minx:maxx].copy()
    
    cv2.rectangle(img,(minx,miny),(maxx,maxy),(0,0,255),2)
    
    cv2.imshow('img',img)
    cv2.imshow('bit',bit)
    
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    


    在与交换意见后,我最终找到了一个更优化的解决方案,按照他的建议使用了
    findContour
    。以下是我结束的代码:

    import cv2 
    import numpy as np
    def func(indir, filename, outdir):
        img = cv2.imread(indir + filename)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        bit = cv2.bitwise_not(gray)
        bit = bit[50:bit.shape[0] -50, 50:bit.shape[1] - 50]
        amtImage = cv2.adaptiveThreshold(bit, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 35, 15)
        kernel = np.ones((5,5),np.uint8)
        dilation = cv2.dilate(amtImage,kernel,iterations = 2)
        kernel = np.ones((25,25),np.uint8)
        erosion = cv2.erode(dilation, kernel, iterations = 10)
        bit = cv2.bitwise_not(erosion)
        _, contours, hierarchy = cv2.findContours(bit,  cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        if (contours != 0):
            c = max(contours, key = cv2.contourArea)
            x,y,w,h = cv2.boundingRect(c)
            print(x, y, w, h)
        final = img[max(0, (y - 50)):(y + h) + min(250, (img.shape[0] - (y + h)) * 10 // 11), max(0, (x - 50)):(x + w) + min(250, (img.shape[1] - (x + w)) * 10 // 11)]
        cv2.imwrite(outdir + filename, final)
    
    在这段代码中,我没有义务遍历图像的每个像素,也没有义务保留索引列表。所以速度要快得多
    我确信这段代码可以进行更多优化,这就是为什么我不接受我的答案。

    谢谢大家。

    在与交换意见后,我最终找到了一个更优化的解决方案,在该解决方案中,我按照他的建议使用了
    findContour
    。以下是我结束的代码:

    import cv2 
    import numpy as np
    def func(indir, filename, outdir):
        img = cv2.imread(indir + filename)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        bit = cv2.bitwise_not(gray)
        bit = bit[50:bit.shape[0] -50, 50:bit.shape[1] - 50]
        amtImage = cv2.adaptiveThreshold(bit, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 35, 15)
        kernel = np.ones((5,5),np.uint8)
        dilation = cv2.dilate(amtImage,kernel,iterations = 2)
        kernel = np.ones((25,25),np.uint8)
        erosion = cv2.erode(dilation, kernel, iterations = 10)
        bit = cv2.bitwise_not(erosion)
        _, contours, hierarchy = cv2.findContours(bit,  cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
        if (contours != 0):
            c = max(contours, key = cv2.contourArea)
            x,y,w,h = cv2.boundingRect(c)
            print(x, y, w, h)
        final = img[max(0, (y - 50)):(y + h) + min(250, (img.shape[0] - (y + h)) * 10 // 11), max(0, (x - 50)):(x + w) + min(250, (img.shape[1] - (x + w)) * 10 // 11)]
        cv2.imwrite(outdir + filename, final)
    
    在这段代码中,我没有义务遍历图像的每个像素,也没有义务保留索引列表。所以速度要快得多
    我确信这段代码可以进行更多优化,这就是为什么我不接受我的答案。

    谢谢大家。

    您可以只保留最小和最大索引,而不是保留索引列表。这会快得多。我会改变这一点,谢谢。这个问题在代码复查堆栈交换中得到了答案。通过,您可以只保留最小和最大索引,而不是保留索引列表。那会快得多。我会改的,谢谢。这个问题在代码复查堆栈交换中有答案