OpenCV中的文档扫描与DNN图像处理

一、摄像头读取与图像显示

文档扫描程序首先使用摄像头获取实时画面:

复制代码
cap = cv2.VideoCapture(0)

这里的0表示使用默认摄像头。

为了避免摄像头无法正常打开,需要进行判断:

复制代码
if not cap.isOpened():
    print("Cannot open camera")
    exit()

打开摄像头之后,通过循环不断读取画面:

复制代码
ret, image = cap.read()

其中ret用于判断当前帧是否读取成功,image就是当前获取到的图像。

程序还保存了一份原始图像:

复制代码
orig = image.copy()

这是因为后面会对图像进行各种处理,而透视变换时仍然需要使用最初读取到的原始图像。

在循环中可以通过:

复制代码
cv_show('image', image)

显示摄像头画面。

这里自己定义了一个显示函数:

复制代码
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(1)

这样在后面的代码中直接调用cv_show()即可,不需要重复编写imshow()waitKey()

二、文档检测的图像预处理

从摄像头获得彩色图像之后,首先需要将它转换为灰度图:

复制代码
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

文档检测主要关注的是文档的边缘,而不是具体的颜色,所以灰度图已经可以满足后续处理的需要。

接下来使用高斯模糊:

复制代码
gray = cv2.GaussianBlur(gray, (5, 5), 0)

高斯模糊可以降低图像中的一些噪声,使后面的边缘检测更加稳定。

然后使用Canny算法检测边缘:

复制代码
edged = cv2.Canny(gray, 30, 150)

经过Canny处理以后,图像中的物体边缘会被突出显示。

因此这一部分的处理流程可以简单表示为:

复制代码
摄像头原始图像
      ↓
    灰度化
      ↓
  高斯模糊
      ↓
 Canny边缘检测
      ↓
    边缘图

在文档扫描中,这一步非常重要,因为后面的轮廓检测就是建立在边缘图的基础上。

三、轮廓检测与文档筛选

得到边缘图之后,就可以进行轮廓检测:

复制代码
cnts = cv2.findContours(
    edged,
    cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE
)[-2]

这里使用RETR_EXTERNAL主要寻找最外层轮廓。

找到轮廓以后,再按照轮廓面积从大到小进行排序:

复制代码
cnts = sorted(
    cnts,
    key=cv2.contourArea,
    reverse=True
)

为什么要排序?

因为拍摄的画面中可能存在很多轮廓,而文档通常是画面中比较大的目标。因此优先检查面积较大的轮廓,可以更容易找到文档。

接下来遍历这些轮廓:

复制代码
for c in cnts:

首先计算轮廓周长:

复制代码
peri = cv2.arcLength(c, True)

然后使用approxPolyDP()进行轮廓近似:

复制代码
approx = cv2.approxPolyDP(
    c,
    0.02 * peri,
    True
)

轮廓近似的作用是将原本比较复杂的轮廓,用更少的点表示。

对于一张比较规则的纸张来说,它通常可以近似成一个四边形,因此程序判断:

复制代码
area = cv2.contourArea(approx)

if area > 5000 and len(approx) == 4:
    screenCnt = approx

这里设置了两个条件:

面积大于5000,并且轮廓近似后正好有4个点。

满足这两个条件后,就认为当前轮廓很可能是要检测的文档。

因此,文档检测的核心实际上可以概括为:

复制代码
寻找轮廓
   ↓
按照面积排序
   ↓
轮廓近似
   ↓
判断面积
   ↓
判断是否为4个点
   ↓
确定文档四个角点

这部分是整个文档扫描程序中非常关键的一步。

四、四个角点的排序

检测到文档以后,并不能直接进行透视变换。

原因是检测出来的4个点并没有固定的顺序,因此需要先将它们按照:

左上、右上、右下、左下

进行排序。

代码中定义了order_points()函数:

复制代码
def order_points(pts):
    rect = np.zeros((4, 2), dtype="float32")

首先创建一个4×2的数组,用于保存排序后的四个坐标。

通过:

复制代码
s = pts.sum(axis=1)

计算每个点的x+y

一般来说:

  • x+y最小 → 左上角;

  • x+y最大 → 右下角。

因此:

复制代码
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]

再计算:

复制代码
diff = np.diff(pts, axis=1)

利用x-y的关系确定另外两个点:

  • x-y最小 → 左下角;

  • x-y最大 → 右上角。

最后得到固定顺序:

复制代码
rect[0] → 左上
rect[1] → 右上
rect[2] → 右下
rect[3] → 左下

这种点排序方法是透视变换中比较常见的处理方式。

五、透视变换

获得正确顺序的四个角点以后,就可以进行透视变换。

首先计算文档的宽度。

代码分别计算上下两条边的长度:

复制代码
widthA = np.sqrt(
    ((br[0] - bl[0]) ** 2) +
    ((br[1] - bl[1]) ** 2)
)

widthB = np.sqrt(
    ((tr[0] - tl[0]) ** 2) +
    ((tr[1] - tl[1]) ** 2)
)

然后取两者较大的值作为新的图像宽度:

复制代码
maxWidth = max(
    int(widthA),
    int(widthB)
)

高度的计算也是类似的:

复制代码
heightA = np.sqrt(
    ((tr[0] - br[0]) ** 2) +
    ((tr[1] - br[1]) ** 2)
)

heightB = np.sqrt(
    ((tl[0] - bl[0]) ** 2) +
    ((tl[1] - bl[1]) ** 2)
)

最终得到:

复制代码
maxHeight = max(
    int(heightA),
    int(heightB)
)

接下来定义目标图像的四个角点:

复制代码
dst = np.array([
    [0, 0],
    [maxWidth - 1, 0],
    [maxWidth - 1, maxHeight - 1],
    [0, maxHeight - 1]
], dtype="float32")

原始图像中的四个角点和目标图像中的四个角点一一对应。

然后计算透视变换矩阵:

复制代码
M = cv2.getPerspectiveTransform(
    rect,
    dst
)

最后使用:

复制代码
warped = cv2.warpPerspective(
    image,
    M,
    (maxWidth, maxHeight)
)

完成透视变换。

这样原本可能存在倾斜、变形的文档,就可以被转换成比较规整的矩形图像。

可以把这个过程理解成:

复制代码
摄像头拍摄的倾斜文档
          ↓
       找到四角
          ↓
      排序四个角点
          ↓
    计算透视变换矩阵
          ↓
      重新映射像素
          ↓
      得到规整文档

代码中的four_point_transform()函数就是将这些步骤组合起来完成透视变换。

六、透视后的图像二值化

完成透视变换之后,还可以进一步对文档进行二值化。

首先转换成灰度图:

复制代码
warped = cv2.cvtColor(
    warper,
    cv2.COLOR_BGR2GRAY
)

然后使用阈值处理:

复制代码
ref = cv2.threshold(
    warped,
    200,
    255,
    cv2.THRESH_BINARY
)[1]

这里设置阈值为200

像素值高于阈值的区域会被处理成白色,否则会被处理成黑色。

经过二值化之后,可以让文档中的文字和背景之间产生更加明显的区别。

所以这个文档扫描程序最终实际上完成了:

复制代码
原始图像
 ↓
灰度化
 ↓
高斯模糊
 ↓
Canny边缘检测
 ↓
轮廓检测
 ↓
轮廓近似
 ↓
寻找四个角点
 ↓
透视变换
 ↓
灰度化
 ↓
二值化
 ↓
扫描后的文档

七、使用DNN进行图像预处理

第二个程序开始接触OpenCV中的DNN模块。

首先读取一张图像:

复制代码
image = cv2.imread('pinjie.jpg')

然后获取图像尺寸:

复制代码
(h, w) = image.shape[:2]

在普通OpenCV处理中,图像通常以H×W×C的形式存在,但是神经网络模型通常有自己的输入数据格式,因此需要进行预处理。

这里使用:

复制代码
blob = cv2.dnn.blobFromImage(
    image,
    1,
    (w, h),
    (0, 0, 0),
    swapRB=False,
    crop=False
)

blobFromImage()可以将普通图像转换成适合神经网络输入的Blob数据。

其中几个参数比较重要:

scalefactor表示像素值缩放比例。

size表示输入图像的尺寸。

mean表示均值减法。

swapRB用于交换R、B通道。

crop用于控制调整大小之后是否进行中心裁剪。

OpenCV默认使用BGR通道顺序,而很多深度学习模型使用RGB,因此在使用模型时,需要特别注意swapRB参数。

八、加载神经网络模型并进行推理

完成图像预处理以后,需要加载训练好的模型。

代码中使用:

复制代码
net = cv2.dnn.readNetFromTorch(
    r'model\starry_night.t7'
)

这里使用的是readNetFromTorch(),说明加载的是PyTorch模型。

OpenCV的DNN模块可以支持多种深度学习模型格式,例如代码注释中提到的:

  • Caffe

  • TensorFlow

  • Torch

  • Darknet

  • ONNX

  • OpenVINO

加载模型之后,需要将前面得到的Blob作为模型输入:

复制代码
net.setInput(blob)

然后执行前向传播:

复制代码
out = net.forward()

forward()就是让输入数据经过神经网络计算,最终得到模型输出。

整个过程可以理解为:

复制代码
原始图像
    ↓
blobFromImage()
    ↓
Blob数据
    ↓
setInput()
    ↓
输入神经网络
    ↓
forward()
    ↓
模型输出

这里实际上已经开始接触深度学习模型推理的基本流程了。

九、理解神经网络输出的BCHW格式

模型输出out并不是普通的图片格式,而是一个四维数组。

代码中将它理解为:

复制代码
B × C × H × W

其中:

  • B:Batch,一次处理的图像数量;

  • C:Channel,通道数;

  • H:Height,高度;

  • W:Width,宽度。

代码通过:

复制代码
out_new = out.reshape(
    out.shape[1],
    out.shape[2],
    out.shape[3]
)

去掉第一维Batch,将四维数据转换成三维数据。

也就是说:

复制代码
B × C × H × W
       ↓
   C × H × W

但是OpenCV显示图像时通常需要:

复制代码
H × W × C

因此还需要进行维度转置:

复制代码
result = out_new.transpose(1, 2, 0)

也就是:

复制代码
C × H × W
       ↓
H × W × C

这个过程对于理解深度学习模型的输入输出格式非常重要。

十、输出归一化与结果显示

模型输出之后,还需要进行归一化:

复制代码
cv2.normalize(
    out_new,
    out_new,
    norm_type=cv2.NORM_MINMAX
)

归一化可以将数据调整到指定范围,使输出结果更加适合后续处理和显示。

完成归一化和维度转换后:

复制代码
result = out_new.transpose(1, 2, 0)

最后就可以使用OpenCV显示:

复制代码
cv2.imshow('result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()

因此,DNN部分的完整流程可以总结为:

复制代码
读取图像
   ↓
获取图像尺寸
   ↓
blobFromImage预处理
   ↓
加载PyTorch模型
   ↓
setInput设置输入
   ↓
forward前向传播
   ↓
得到四维输出
   ↓
reshape调整维度
   ↓
normalize归一化
   ↓
transpose调整通道顺序
   ↓
显示结果
相关推荐
沉默王二17 分钟前
豆包工作Agent正式发布,直接给到夯。
人工智能·面试·agent
建模小刘18 分钟前
2026数学建模国赛准备----AI提示词!!!!!
人工智能·数学建模·2026数学建模国赛
FPC_小西19 分钟前
PCB化学沉金的作用有哪些?
大数据·人工智能·ai·pcb工艺·排线
满怀冰雪3 小时前
24-PaddleClas 模型评估、导出与推理部署入门
大数据·人工智能·python·深度学习·paddle
Mid_search4 小时前
随机排列与Fisher-Yates算法
人工智能·深度学习·强化学习·随机排列·fisher-yates
ZGIAI9 小时前
ZGI Workflow:条件分支走错时先查哪一层
人工智能·架构
X54先生(人文科技)9 小时前
《元创力》纪实录 · 桥段 《窑变纪元:一份来自星历2227年的深空考古笔记》
人工智能·开源·ai写作·零知识证明
ZGIAI9 小时前
ZGI 文件产物:生成报告后怎样交付
人工智能·架构