介绍
对这个项目的动机很简单。我们中的许多人转向了在线工作。随着在线工作量的增加,人们通常不得不通过电子邮件或其他方式呈现文档的数字化版本。换句话说,将任何文档转换为扫描文档。本文,将介绍如何使用 Python 从头开始创建文档扫描仪。准确地说,是用于图像/视频处理的 OpenCV 库。事不宜迟,让我们开始吧。文件扫描仪在进入编码部分之前,我们需要了解我们将要做什么。这是在开始这个项目之前问自己的一系列问题。我们想在这里建造什么?
— 文件扫描仪。好的。但它做什么或应该做什么?— 显然,要扫描文档。正确的。那么,扫描出来的文件应该是什么样子的呢?— 好问题,对吧?具体来看,扫描的文档应该有两个特点:看起来像扫描的文档,黑白 (B&W) 颜色;正确旋转(无随机角度)。让我们先从简单的功能开始,并根据需要增加其他功能。编码文件扫描仪首先让我们导入这个项目需要的所有库(我们可能会根据需要添加一些东西)import numpy as np
import cv2
from skimage.filters import threshold_local
import math
from scipy import ndimage
print("Imports are Done!")
I. 第一个属性:扫描(黑白)视图让我们从扫描仪的第一个属性开始——生成扫描图像!在这个例子中,我使用了一张照片,来自Yuval Noah Harari的书*“21 Lessons for the 21st Century*。
旁注:它是一本很棒的书。本系列的其他两本书(“Sapiens: A Brief History of Humankind”和“Homo Deus: A Brief History of Tomorrow”)都建议阅读!回到我们的文档扫描仪,我们希望通过更改配色方案使该图像看起来清晰明快。让我们将此操作称为 Scan_view()。为了使它成为一个完整的应用程序项目,让我们创建一个名为Scanner的类,其中Scan_view() 将是其方法。class Scanner:
def __init__(self, img):
self.img = img
def Scan_View(self):
print("Scanned View")
# read the original image, copy it,
# apply threshold to "scannify" it
image = cv2.imread(self.img)
orig = image.copy()
# convert our image to grayscale, apply threshold
# to create scanned view effect
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thr = threshold_local(image, 11, offset = 10, method = "gaussian")
image = (image > thr).astype("uint8") * 255
# show the original image and the edge detected image
#cv2.imshow("orig", orig)
#cv2.imshow("Scanned", image)
#cv2.waitKey(0)
#cv2.destroyAllWindows()
print(np.shape(orig), np.shape(image))
# Saving a B&W image itself
cv2.imwrite('Part_scan_view.png', image)
return image
代码的快速解释:创建一个scan具有图像作为其属性的对象。因此,self.img = img在__init__()方法中使用;另外,想要一种负责更改此属性(即图像/文档)的方法——更改配色方案、旋转、裁剪、调整大小等。因此Scan_View()对它的类属性(即,对它自己或self)执行操作。这个方法的实质隐藏在threshold_local操作中。这基本上是一种基于像素的局部邻域计算阈值掩码的操作。这也称为自适应阈值。阈值是像素的局部邻域的加权平均值减去常数。找到阈值掩码后,我们只需将前景像素值选择为image>threshold。我们可以保存新的清晰新鲜的图像并返回以备进一步处理。要运行代码,我们可以简单地创建一个scan对象并为其提供文档/照片作为其属性,如下所示:
if __name__=="__main__":
# Defining the image name
img = "21_Lesson_21th_Century.jpeg"
# Calling the scanner class
scan = Scanner(img)
# Scanning the image -> B&W scheme
scanned_im = scan.Scan_View()
结果,我们得到了这张图片:
上述文件的扫描版本。让我们继续进行项目的下一部分。II. 第二个属性:文档轮换让我们继续我们的扫描仪的第二个属性——文档旋转!假设,我们以随机角度拍摄了一本书的照片。自动旋转它以获得自上而下的正面视图不是很好吗?当然会!问题是怎么做?最初,我们正在考虑使用主成分分析 (PCA) 来确定文档方向。然而,对于这个项目来说,这似乎有点过分了。我们想要一些简单但有效的东西。会自动确定文本/边框线和水平线之间的旋转角度的东西。因此,想出了一个更简单的方法,它基本上利用了霍夫变换。简而言之,霍夫变换是一种用于检测各种形状的技术。在我们的例子中,这将是一组沿着文本行的行!好主意,对吧?但是为了使这种方法健壮,我们需要确保检测到正确的方向。一些线条可能沿着文本出现,但其他线条沿着书籍/文档边缘出现——我们不希望那样。所以,我们想要平均这些变化。换句话说,要找到所有线角的中值。因此,我们将扫描对象(Rotation())的新方法定义如下:class Scanner:
def __init__(self, img):
self.img = img
def Rotation(self):
print("Rotation")
# read the original image, copy it,
# rotate it
image = cv2.imread(self.img)
orig = image.copy()
image = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY)
img_edges = cv2.Canny(image, 100, 100, apertureSize=3)
lines = cv2.HoughLinesP(img_edges, rho=1, theta=np.pi / 180.0, threshold=160, minLineLength=100, maxLineGap=10)
# calculate all the angles:
angles = []
for [[x1, y1, x2, y2]] in lines:
angle = math.degrees(math.atan2(y2 - y1, x2 - x1))
angles.append(angle)
# average angles
median_angle = np.median(angles)
# actual rotation
image = ndimage.rotate(image, median_angle)
# Saving an image itself
cv2.imwrite('Part_rotation.png', image)
return image
如上所述,这部分应用程序的重点是找到正确的霍夫线(HoughLinesP()方法,其中P代表概率,请参阅参考资料以了解有关此方法的更多信息)我们从每条线获得角度的中值并将其用于文档旋转(ndimage.rotate()方法)。我们可以对清晰的黑白图像执行此操作,如下所示:if __name__=="__main__":
# Defining the image name
img = "Part_scan_view.png"
# Calling the scanner class
scan = Scanner(img)
# Performing Rotation
rotated_im = scan.Rotation()
结果,我们得到了一个旋转的图像:
精彩的扫描、清晰和旋转的图像!完整代码对于感兴趣的读者,这里是一个GitHub 存储库,其中包含每种方法的更多详细信息和文档。h概括在本文中,我们学习了如何使用著名的用于图像/视频处理的 Python 库 OpenCV 从头开始构建文档扫描仪的工作原型。未来发展将这个应用程序称为文档扫描仪 (v.1), 因为有一些可以进一步改进它的地方。1.例如,使用另一种(或改进的)算法进行文档轮换。由于其性质,该算法可能并不总是在 100% 的情况下提供完美的俯视图。一种替代方法可能是使用主成分分析(或 PCA)来确定文档的更精确方向。但这超出了本文的范围。2.关于旋转本身,我不喜欢旋转图像上的那些空黑块。这也可以解决以改进此应用程序。可能是使它们为空 ( NaN) 值。3.另一个可能需要仔细检查的操作是黑白颜色转换。我认为自适应阈值是一个不错的选择,但是,可能有更好的方法。