这是「Python 计算机视觉三部曲」的第一篇。 如果你是零基础小白,这篇文章会从"一张图片到底是什么"讲起,带你用 Python 和 OpenCV 亲手操作每一个像素,最终理解图像处理的核心逻辑。
你可以把它当作后续深度学习与目标检测的地基。读完后你会明确三件事:
图像在计算机里到底如何表示与变换
经典图像处理算子如何解决真实问题
如何把分散知识串成可落地的小项目流程
阅读导航
零基础首次阅读 :按 第 1 → 6 章 顺序先建立像素、色彩空间与几何变换直觉
想直接上手项目 :重点看 第 7~11 章(滤波、边缘、阈值、特征、文档扫描器)
准备进入深度学习 :至少掌握 第 8~10 章 的边缘与特征部分
系列导航
第一篇 :计算机视觉与图像处理入门 — 从像素到特征(本文)
第二篇 :深度学习与计算机视觉 — 从 CNN 到实战
第三篇 :目标检测实战 — 从 YOLO 到部署
1. 什么是计算机视觉?
一个简单的思想实验:你看到一个苹果,你的大脑用了多久判断出"这是苹果"?大概 0.1 秒。但如果你要让计算机做同样的事,你需要告诉它:
眼睛接收的是什么信号?(像素阵列)
如何从信号中提取有意义的信息?(特征)
如何根据特征做出判断?(分类/检测/分割)
计算机视觉(Computer Vision, CV) 就是教计算机"看"和"理解"的学科。它的核心任务包括:
任务
输入
输出
例子
图像分类
一张图片
一个类别标签
“这是一只猫”
目标检测
一张图片
类别 + 位置框
“猫在 (x₁,y₁,x₂,y₂)”
语义分割
一张图片
逐像素类别
每个像素属于猫/背景
实例分割
一张图片
逐像素实例
区分"猫 A"和"猫 B"
而图像处理(Image Processing) 是计算机视觉的基础——它关注的是对图像本身的操作:增强、滤波、变换、特征提取。没有图像处理,就没有计算机视觉。
一句话区分: 图像处理 = 图片→图片;计算机视觉 = 图片→语义。
2. 图像的本质:从像素开始
2.1 数字图像 = 像素矩阵
在计算机眼中,一张图片就是一个多维数组 (numpy array)。
一张 1920×1080 的彩色图片:
宽度 :1920 个像素
高度 :1080 个像素
通道 :3(R、G、B)
所以在 NumPy 中,它的形状是 (1080, 1920, 3)。
⚠️ 易错点 :NumPy 的维度顺序是 (行, 列, 通道) = (高度, 宽度, 通道),而我们在说"1920×1080"时是 (宽度, 高度)。注意这个反转!
2.2 灰度图像
如果每个像素只需要一个数值来表示亮度(0=黑,255=白),这就是灰度图像 。它的形状是 (H, W),只有一个通道。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import numpy as npblack_img = np.zeros((5 , 5 ), dtype=np.uint8) print (black_img)gradient = np.arange(256 , dtype=np.uint8).reshape(1 , 256 ) print (gradient.shape)
2.3 彩色图像与 BGR 之谜
OpenCV 默认使用 BGR 通道顺序而非 RGB。这不是 Bug,而是历史遗留——早期 OpenCV 基于 Windows 的 BMP 格式,而 BMP 就是 BGR。
1 2 3 4 5 6 7 8 import numpy as npred_pixel_bgr = np.array([[[0 , 0 , 255 ]]], dtype=np.uint8) print (red_pixel_bgr.shape) red_pixel_rgb = np.array([[[255 , 0 , 0 ]]], dtype=np.uint8)
💡 重要原则 :在 OpenCV 中读取的图像永远是 BGR。如果要用 matplotlib 显示,必须先转换。
3. 环境搭建:Python + OpenCV
3.1 安装
1 2 3 4 5 6 7 8 9 python -m venv cv_env cv_env\Scripts\activate pip install opencv-python numpy matplotlib pip install opencv-contrib-python
3.2 验证安装
1 2 3 4 5 6 import cv2import numpy as npimport matplotlib.pyplot as pltprint (f"OpenCV 版本: {cv2.__version__} " )print (f"NumPy 版本: {np.__version__} " )
3.3 辅助函数
在后续代码中,我们会频繁用 matplotlib 来显示 OpenCV 的图像,这里封装一个工具函数:
1 2 3 4 5 6 7 8 9 10 11 def show_image (img, title="Image" , figsize=(10 , 6 ) ): """用 Matplotlib 显示 OpenCV 图像(自动 BGR→RGB 转换)""" if len (img.shape) == 3 : img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) else : img_rgb = img plt.figure(figsize=figsize) plt.imshow(img_rgb, cmap='gray' if len (img.shape) == 2 else None ) plt.title(title) plt.axis('off' ) plt.show()
4. 图像读取、显示与保存
4.1 读取图像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import cv2img_color = cv2.imread('photo.jpg' ) img_gray = cv2.imread('photo.jpg' , cv2.IMREAD_GRAYSCALE) img_unchanged = cv2.imread('photo.jpg' , cv2.IMREAD_UNCHANGED) print (f"彩色图像形状: {img_color.shape} " ) print (f"灰度图像形状: {img_gray.shape} " ) print (f"数据类型: {img_color.dtype} " )
⚠️ 常见陷阱 :cv2.imread() 读取失败时不会报错,而是返回 None!务必检查:
1 2 if img_color is None : print ("图像读取失败,请检查路径!" )
4.2 显示图像
1 2 3 4 cv2.imshow('My Image' , img_color) cv2.waitKey(0 ) cv2.destroyAllWindows()
在 Jupyter Notebook 中更推荐用 matplotlib:
1 show_image(img_color, "我的照片" )
4.3 保存图像
1 2 cv2.imwrite('output.jpg' , img_color) cv2.imwrite('output.png' , img_color)
💡 格式选择 :JPEG 有损压缩,适合照片;PNG 无损压缩,适合截图和带透明通道的图像。
4.4 视频读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 cap = cv2.VideoCapture('video.mp4' ) while cap.isOpened(): ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) cv2.imshow('Video' , gray) if cv2.waitKey(25 ) & 0xFF == ord ('q' ): break cap.release() cv2.destroyAllWindows()
5. 色彩空间:不止 RGB
色彩空间是用数学方式描述颜色的系统。不同的色彩空间有不同的用途。
5.1 BGR ↔ RGB 转换
1 2 3 4 5 6 7 8 9 img = cv2.imread('photo.jpg' ) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(img_rgb) plt.axis('off' ) plt.show()
5.2 HSV 色彩空间
HSV(Hue-色调, Saturation-饱和度, Value-明度)更接近人类感知颜色的方式,在颜色分割任务中极为好用。
1 2 3 4 5 6 7 8 9 img = cv2.imread('photo.jpg' ) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) h, s, v = cv2.split(hsv) show_image(h, "Hue 通道(色调)" ) show_image(s, "Saturation 通道(饱和度)" ) show_image(v, "Value 通道(明度)" )
5.3 实战:用 HSV 提取特定颜色
目标 :从图片中提取所有红色物体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import cv2import numpy as npimport matplotlib.pyplot as pltimg = cv2.imread('photo.jpg' ) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) lower_red1 = np.array([0 , 100 , 100 ]) upper_red1 = np.array([10 , 255 , 255 ]) lower_red2 = np.array([160 , 100 , 100 ]) upper_red2 = np.array([180 , 255 , 255 ]) mask1 = cv2.inRange(hsv, lower_red1, upper_red1) mask2 = cv2.inRange(hsv, lower_red2, upper_red2) mask = mask1 | mask2 result = cv2.bitwise_and(img, img, mask=mask) show_image(result, "红色物体提取" )
为什么 HSV 适合颜色分割? 因为在 HSV 中,色调 H 单独编码了"什么颜色",不受光照影响(光照主要影响 V 通道)。而在 RGB 中,光照变化会同时影响 R、G、B 三个通道,难以用简单的阈值分割。
5.4 色彩空间速查表
色彩空间
用途
OpenCV 常量
BGR
OpenCV 默认
—
RGB
Matplotlib 默认
COLOR_BGR2RGB
HSV
颜色分割、追踪
COLOR_BGR2HSV
GRAY
边缘检测、特征提取
COLOR_BGR2GRAY
LAB
颜色差异计算
COLOR_BGR2LAB
YCrCb
肤色检测
COLOR_BGR2YCrCb
6. 几何变换:缩放、旋转与翻转
6.1 缩放
1 2 3 4 5 6 7 8 9 10 11 12 13 img = cv2.imread('photo.jpg' ) resized = cv2.resize(img, (800 , 600 )) resized_half = cv2.resize(img, None , fx=0.5 , fy=0.5 ) resized_area = cv2.resize(img, (400 , 300 ), interpolation=cv2.INTER_AREA) resized_cubic = cv2.resize(img, (2000 , 1500 ), interpolation=cv2.INTER_CUBIC)
💡 插值方法选择 :缩小图片用 INTER_AREA(效果最好),放大用 INTER_CUBIC(质量高但慢)或 INTER_LINEAR(速度快)。
6.2 旋转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 img = cv2.imread('photo.jpg' ) h, w = img.shape[:2 ] center = (w // 2 , h // 2 ) M = cv2.getRotationMatrix2D(center, angle=45 , scale=1.0 ) rotated = cv2.warpAffine(img, M, (w, h)) def rotate_without_crop (image, angle ): h, w = image.shape[:2 ] center = (w // 2 , h // 2 ) M = cv2.getRotationMatrix2D(center, angle, 1.0 ) cos = np.abs (M[0 , 0 ]) sin = np.abs (M[0 , 1 ]) new_w = int (h * sin + w * cos) new_h = int (h * cos + w * sin) M[0 , 2 ] += (new_w - w) / 2 M[1 , 2 ] += (new_h - h) / 2 return cv2.warpAffine(image, M, (new_w, new_h)) rotated_full = rotate_without_crop(img, 45 ) show_image(rotated_full, "旋转 45°(不裁剪)" )
6.3 翻转
1 2 3 flipped_h = cv2.flip(img, 1 ) flipped_v = cv2.flip(img, 0 ) flipped_b = cv2.flip(img, -1 )
6.4 仿射变换与透视变换
仿射变换 :保持平行线仍然平行(平移 + 旋转 + 缩放 + 剪切)。
1 2 3 4 5 6 7 8 9 img = cv2.imread('photo.jpg' ) h, w = img.shape[:2 ] pts1 = np.float32([[50 , 50 ], [200 , 50 ], [50 , 200 ]]) pts2 = np.float32([[10 , 100 ], [200 , 50 ], [100 , 250 ]]) M = cv2.getAffineTransform(pts1, pts2) dst = cv2.warpAffine(img, M, (w, h))
透视变换 :可以实现"从斜着拍的照片中把矩形内容拉正",这在文档扫描中极为实用。
1 2 3 4 5 6 7 pts1 = np.float32([[56 , 65 ], [368 , 52 ], [28 , 387 ], [389 , 390 ]]) pts2 = np.float32([[0 , 0 ], [300 , 0 ], [0 , 300 ], [300 , 300 ]]) M = cv2.getPerspectiveTransform(pts1, pts2) dst = cv2.warpPerspective(img, M, (300 , 300 ))
7. 图像滤波:模糊与锐化的艺术
滤波是图像处理中最核心的操作之一。它的本质是用一个**卷积核(Kernel)**在图像上滑动,对每个像素的邻域进行加权求和。
7.1 卷积操作图解
假设有一个 3×3 的卷积核:
1 2 3 4 核: 图像的一小块: [1 0 -1] [a b c] [1 0 -1] [d e f] [1 0 -1] [g h i]
卷积结果 = 1×a + 0×b + (-1)×c + 1×d + 0×e + (-1)×f + 1×g + 0×h + (-1)×i
7.2 均值滤波
1 2 3 4 5 6 7 8 img = cv2.imread('photo.jpg' ) blur_mean = cv2.blur(img, (5 , 5 )) kernel_mean = np.ones((5 , 5 ), np.float32) / 25 blur_custom = cv2.filter2D(img, -1 , kernel_mean)
7.3 高斯滤波
高斯滤波比均值滤波更自然,距离中心越远的像素权重越小,符合高斯分布。
1 2 blur_gauss = cv2.GaussianBlur(img, (5 , 5 ), 0 )
💡 核大小必须是奇数 (3, 5, 7, …),因为需要有一个中心点。
7.4 中值滤波
中值滤波取邻域像素的中值,对椒盐噪声 (图像上的黑白噪点)效果极好。
1 blur_median = cv2.medianBlur(img, 5 )
7.5 双边滤波
双边滤波在模糊的同时保留边缘——这是其他滤波器做不到的。原理是同时考虑空间距离和像素值差异。
1 2 blur_bilateral = cv2.bilateralFilter(img, d=9 , sigmaColor=75 , sigmaSpace=75 )
7.6 锐化
1 2 3 4 5 6 7 8 kernel_sharpen = np.array([ [ 0 , -1 , 0 ], [-1 , 5 , -1 ], [ 0 , -1 , 0 ] ], dtype=np.float32) sharpened = cv2.filter2D(img, -1 , kernel_sharpen)
7.7 滤波器对比总结
滤波器
速度
去噪
保边
适用场景
均值
⭐⭐⭐
一般
❌
简单去噪
高斯
⭐⭐⭐
好
❌
预处理、去高斯噪声
中值
⭐⭐
极好(椒盐)
✅(部分)
椒盐噪声
双边
⭐
好
✅
美颜、艺术效果
锐化
⭐⭐⭐
❌
—
增强细节
8. 边缘检测:Canny 算法详解
边缘检测是图像处理到计算机视觉的桥梁——从"像素"到"结构"。
8.1 边缘是什么?
边缘是图像中像素值急剧变化 的地方。数学上,这对应着梯度 (导数)的极值。
8.2 Canny 边缘检测的五个步骤
John Canny 在 1986 年提出的算法至今仍是黄金标准:
1 原始图像 → ①高斯滤波去噪 → ②计算梯度幅值和方向 → ③非极大值抑制 → ④双阈值检测 → ⑤滞后边界追踪 → 边缘图
高斯滤波 :平滑图像,减少噪声对梯度计算的干扰
计算梯度 :用 Sobel 算子计算 x 和 y 方向的梯度,得到幅值和方向
非极大值抑制 :沿梯度方向,只保留局部最大值,让边缘变"细"
双阈值 :高阈值以上为"强边缘",低-高阈值之间为"弱边缘"
滞后追踪 :弱边缘如果与强边缘相连,则保留;否则丢弃
1 2 3 4 5 6 7 8 img = cv2.imread('photo.jpg' , cv2.IMREAD_GRAYSCALE) edges = cv2.Canny(img, threshold1=100 , threshold2=200 ) show_image(edges, "Canny 边缘检测 (100, 200)" )
8.3 阈值调参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 otsu_thresh, _ = cv2.threshold(img, 0 , 255 , cv2.THRESH_OTSU) edges_auto = cv2.Canny(img, otsu_thresh * 0.5 , otsu_thresh) fig, axes = plt.subplots(1 , 3 , figsize=(18 , 6 )) for ax, (t1, t2), title in zip (axes, [(50 , 100 ), (100 , 200 ), (200 , 400 )], ["低阈值 (50,100)" , "中阈值 (100,200)" , "高阈值 (200,400)" ]): e = cv2.Canny(img, t1, t2) ax.imshow(e, cmap='gray' ) ax.set_title(title) ax.axis('off' ) plt.tight_layout() plt.show()
💡 调参技巧 :低阈值 → 检测到更多边缘(含噪声);高阈值 → 只保留最显著的边缘。一般从 (50, 150) 开始尝试。
8.4 其他边缘检测算子
1 2 3 4 5 6 7 8 sobel_x = cv2.Sobel(img, cv2.CV_64F, 1 , 0 , ksize=3 ) sobel_y = cv2.Sobel(img, cv2.CV_64F, 0 , 1 , ksize=3 ) sobel = np.sqrt(sobel_x**2 + sobel_y**2 ).astype(np.uint8) laplacian = cv2.Laplacian(img, cv2.CV_64F) laplacian = np.uint8(np.absolute(laplacian))
9. 阈值处理与形态学操作
9.1 阈值处理
阈值处理将灰度图像转为二值图像(只有黑白两色),是很多后续处理的基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 img = cv2.imread('photo.jpg' , cv2.IMREAD_GRAYSCALE) _, thresh_binary = cv2.threshold(img, 127 , 255 , cv2.THRESH_BINARY) _, thresh_binary_inv = cv2.threshold(img, 127 , 255 , cv2.THRESH_BINARY_INV) _, thresh_trunc = cv2.threshold(img, 127 , 255 , cv2.THRESH_TRUNC) _, thresh_tozero = cv2.threshold(img, 127 , 255 , cv2.THRESH_TOZERO) _, thresh_otsu = cv2.threshold(img, 0 , 255 , cv2.THRESH_BINARY + cv2.THRESH_OTSU) thresh_adapt_mean = cv2.adaptiveThreshold( img, 255 , cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, blockSize=11 , C=2 ) thresh_adapt_gauss = cv2.adaptiveThreshold( img, 255 , cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize=11 , C=2 )
💡 何时用哪种?
光照均匀 → 全局阈值 / Otsu
光照不均匀 → 自适应阈值
不知道选什么 → 先试 Otsu
9.2 形态学操作
形态学操作基于结构元素 (一个小矩阵),在二值图像上进行膨胀或腐蚀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5 , 5 )) eroded = cv2.erode(thresh_binary, kernel, iterations=1 ) dilated = cv2.dilate(thresh_binary, kernel, iterations=1 ) opened = cv2.morphologyEx(thresh_binary, cv2.MORPH_OPEN, kernel) closed = cv2.morphologyEx(thresh_binary, cv2.MORPH_CLOSE, kernel) gradient = cv2.morphologyEx(thresh_binary, cv2.MORPH_GRADIENT, kernel) tophat = cv2.morphologyEx(thresh_binary, cv2.MORPH_TOPHAT, kernel) blackhat = cv2.morphologyEx(thresh_binary, cv2.MORPH_BLACKHAT, kernel)
形态学操作速记:
操作
效果
典型用途
腐蚀
缩小白色区域
去噪点
膨胀
扩大白色区域
连接断裂区域
开运算
去小亮点
去噪
闭运算
填小暗点
填充
梯度
取轮廓
边缘提取
礼帽
取小亮点
背景不均匀校正
10. 特征提取:从 SIFT 到 ORB
特征 是图像中"有趣的"点——角点、斑点、边缘交叉处等。特征提取是传统计算机视觉的核心。
10.1 什么是好的特征?
可重复性 :同一物体在不同图片中能被再次找到
独特性 :不同位置的特征应可区分
局部性 :特征只依赖局部区域,对遮挡鲁棒
10.2 Harris 角点检测
最经典的角点检测方法。核心思想:在角点处,无论往哪个方向移动窗口,像素值都会剧烈变化。
1 2 3 4 5 6 7 8 9 10 11 img = cv2.imread('photo.jpg' ) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) dst = cv2.cornerHarris(gray, blockSize=2 , ksize=3 , k=0.04 ) img_harris = img.copy() img_harris[dst > 0.01 * dst.max ()] = [0 , 0 , 255 ] show_image(img_harris, "Harris 角点检测" )
10.3 SIFT(尺度不变特征变换)
SIFT 是 David Lowe 在 1999 年提出的方法,具有尺度不变性 和旋转不变性 ——即使图片被缩放或旋转,仍能匹配到相同的特征。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 img = cv2.imread('photo.jpg' ) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) sift = cv2.SIFT_create() keypoints, descriptors = sift.detectAndCompute(gray, None ) img_sift = cv2.drawKeypoints( gray, keypoints, None , flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS ) show_image(img_sift, f"SIFT 特征点 ({len (keypoints)} 个)" ) print (f"每个特征点有 {descriptors.shape[1 ]} 维描述子" )
10.4 ORB(Oriented FAST and Rotated BRIEF)
ORB 是 SIFT 的开源替代品,速度更快且不受专利限制。它结合了 FAST 关键点检测和 BRIEF 描述子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 img = cv2.imread('photo.jpg' ) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) orb = cv2.ORB_create(nfeatures=1000 ) keypoints, descriptors = orb.detectAndCompute(gray, None ) img_orb = cv2.drawKeypoints( gray, keypoints, None , flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS ) show_image(img_orb, f"ORB 特征点 ({len (keypoints)} 个)" ) print (f"每个特征点有 {descriptors.shape[1 ]} 维描述子" )
10.5 特征匹配实战
用 ORB 匹配两张图片中的相同物体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 img1 = cv2.imread('object.jpg' , cv2.IMREAD_GRAYSCALE) img2 = cv2.imread('scene.jpg' , cv2.IMREAD_GRAYSCALE) orb = cv2.ORB_create() kp1, des1 = orb.detectAndCompute(img1, None ) kp2, des2 = orb.detectAndCompute(img2, None ) bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True ) matches = bf.match (des1, des2) matches = sorted (matches, key=lambda x: x.distance)[:20 ] img_match = cv2.drawMatches(img1, kp1, img2, kp2, matches, None , flags=2 ) show_image(img_match, "ORB 特征匹配" )
10.6 特征检测器对比
特征
速度
尺度不变
旋转不变
专利
描述子维度
Harris
⭐⭐⭐
❌
❌
无
无(只有位置)
SIFT
⭐
✅
✅
有(已过期)
128
SURF
⭐⭐
✅
✅
有
64
ORB
⭐⭐⭐
✅
✅
无
256
11. 实战项目:文档扫描器
把上面的知识串起来,做一个真正有用的项目——用手机拍文档,自动拉正并增强。
11.1 完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 import cv2import numpy as npimport matplotlib.pyplot as pltdef order_points (pts ): """将四个点按 [左上, 右上, 右下, 左下] 排序""" rect = np.zeros((4 , 2 ), dtype="float32" ) s = pts.sum (axis=1 ) rect[0 ] = pts[np.argmin(s)] rect[2 ] = pts[np.argmax(s)] d = np.diff(pts, axis=1 ) rect[1 ] = pts[np.argmin(d)] rect[3 ] = pts[np.argmax(d)] return rect def four_point_transform (image, pts ): """透视变换:把任意四边形拉成正矩形""" rect = order_points(pts) (tl, tr, br, bl) = rect 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)) return warped def scan_document (image_path ): """完整的文档扫描流程""" img = cv2.imread(image_path) if img is None : print ("无法读取图像!" ) return None orig = img.copy() h, w = img.shape[:2 ] ratio = 800 / max (h, w) img = cv2.resize(img, (int (w * ratio), int (h * ratio))) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5 , 5 ), 0 ) edged = cv2.Canny(blurred, 75 , 200 ) print ("Step 1: 边缘检测完成" ) show_image(edged, "边缘检测" ) contours, _ = cv2.findContours(edged, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) contours = sorted (contours, key=cv2.contourArea, reverse=True )[:5 ] doc_contour = None for c in contours: peri = cv2.arcLength(c, True ) approx = cv2.approxPolyDP(c, 0.02 * peri, True ) if len (approx) == 4 : doc_contour = approx break if doc_contour is None : print ("未找到文档轮廓!" ) return None print ("Step 2: 找到文档轮廓" ) img_contour = img.copy() cv2.drawContours(img_contour, [doc_contour], -1 , (0 , 255 , 0 ), 2 ) show_image(img_contour, "文档轮廓" ) pts = doc_contour.reshape(4 , 2 ) / ratio warped = four_point_transform(orig, pts) warped_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) enhanced = cv2.adaptiveThreshold( warped_gray, 255 , cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize=21 , C=10 ) print ("Step 3: 文档扫描完成!" ) show_image(warped, "拉正后的文档" ) show_image(enhanced, "增强后的文档" ) return enhanced result = scan_document('document_photo.jpg' ) if result is not None : cv2.imwrite('scanned_document.jpg' , result) print ("扫描结果已保存为 scanned_document.jpg" )
11.2 流程回顾
1 拍照 → 灰度化 → 高斯模糊 → Canny 边缘 → 找轮廓 → 透视变换拉正 → 自适应阈值增强 → 清晰文档
这个项目用到了我们之前学过的几乎所有知识:色彩空间转换、滤波、边缘检测、轮廓查找、透视变换、阈值处理。这就是"从像素到特征"的完整链路。
12. 总结与下一步
我们学到了什么?
模块
核心概念
关键函数
图像基础
像素、通道、BGR
cv2.imread() np.array
色彩空间
HSV、灰度
cv2.cvtColor() cv2.inRange()
几何变换
缩放、旋转、透视
cv2.resize() cv2.warpAffine() cv2.warpPerspective()
滤波
模糊、锐化
cv2.GaussianBlur() cv2.filter2D()
边缘检测
梯度、Canny
cv2.Canny() cv2.Sobel()
阈值处理
二值化、自适应
cv2.threshold() cv2.adaptiveThreshold()
形态学
腐蚀、膨胀、开闭运算
cv2.erode() cv2.dilate() cv2.morphologyEx()
特征提取
Harris、SIFT、ORB
cv2.SIFT_create() cv2.ORB_create()
传统方法的局限
上面所有方法都是手工设计 的——我们人为选择了滤波核、阈值、特征描述子。这些方法在简单场景下很有效,但面对复杂的现实世界(光照变化、遮挡、视角变换、海量类别),手工特征就显得力不从心了。
这就是深度学习登场的原因。
在下一篇文章中,我们将从零开始理解卷积神经网络(CNN),用 PyTorch 实现图像分类,并见证"让机器自己学特征"的革命。
下篇预告:《深度学习与计算机视觉:从 CNN 到实战》
我们将深入探讨:
神经网络的基本原理:从感知机到多层网络
CNN 的核心:卷积层、池化层、全连接层
PyTorch 实战:手写数字识别(MNIST)→ 猫狗分类
迁移学习:站在巨人的肩膀上
数据增强:让有限的数据发挥最大价值
本文是「Python 计算机视觉三部曲」的第一篇。如果觉得有帮助,欢迎关注后续更新。