第四章:图像几何变换——让图像动起来

各位同学,欢迎来到第四章。这一章我们聊聊图像的几何变换。

说白了,几何变换就是让图像在空间里「动一动」——放大缩小、平移、旋转、甚至扭曲。听起来简单,但这里面的门道可不少。我刚开始做图像处理时,以为缩放就是resize一下完事,结果在项目中吃了大亏。嗯,咱们今天就把这些坑一个个填上。

4.1 图像缩放——不只是变大变小

图像缩放,最基础的操作。OpenCV里用 cv2.resize() 搞定。

import cv2
import numpy as np

img = cv2.imread('lena.jpg')

# 方法一:指定目标尺寸
resized = cv2.resize(img, (300, 300))

# 方法二:指定缩放因子
resized_by_factor = cv2.resize(img, None, fx=0.5, fy=0.5)

# 方法三:指定插值方式
resized_interp = cv2.resize(img, (300, 300), interpolation=cv2.INTER_CUBIC)

这里有个关键点——插值方式。你想想看,图像缩小的时候,像素变少了,怎么决定哪些像素保留?图像放大的时候,像素变多了,多出来的像素怎么填?

插值方法 适用场景 特点
INTER_NEAREST 快速预览 速度最快,但锯齿明显
INTER_LINEAR 默认选项,通用场景 速度与质量平衡
INTER_CUBIC 放大图像 质量好,但计算量大
INTER_LANCZOS4 高质量缩放 效果最好,速度最慢
我的经验: 项目中做图像预处理时,我习惯用 INTER_LINEAR。速度快,效果也够用。只有在最后输出展示时,我才会用 INTER_CUBIC 或 INTER_LANCZOS4 来提升视觉效果。

4.2 图像平移——让图像「走两步」

平移其实就是一个矩阵乘法的事。OpenCV里用仿射变换来实现。

import cv2
import numpy as np

img = cv2.imread('lena.jpg')
rows, cols = img.shape[:2]

# 定义平移矩阵:向右平移100像素,向下平移50像素
M = np.float32([[1, 0, 100],
                [0, 1, 50]])

# 执行平移
translated = cv2.warpAffine(img, M, (cols, rows))

这里要注意,warpAffine 的第三个参数是输出图像的尺寸。如果平移后图像超出了边界,超出的部分会被裁掉。我曾经在做一个拼接项目时,平移后图像边缘信息丢失了,找了半天bug才发现是尺寸没设对。

避坑指南: 平移后如果不想丢失边缘信息,可以把输出尺寸设大一些。比如 (cols + 100, rows + 50),这样平移后的图像就能完整显示。

4.3 图像旋转——绕哪个点转?

旋转也是仿射变换的一种。OpenCV提供了 cv2.getRotationMatrix2D() 来生成旋转矩阵。

import cv2
import numpy as np

img = cv2.imread('lena.jpg')
rows, cols = img.shape[:2]

# 绕图像中心旋转45度,缩放因子为1
center = (cols // 2, rows // 2)
M = cv2.getRotationMatrix2D(center, 45, 1.0)

# 执行旋转
rotated = cv2.warpAffine(img, M, (cols, rows))

为什么需要指定旋转中心?你想想看,绕左上角旋转和绕中心旋转,结果完全不一样。我刚开始学的时候,默认绕(0,0)点旋转,结果图像直接飞出去了,画面一片黑。嗯,这个坑我替你们踩过了。

核心要点: 旋转后图像可能会超出边界。如果想保留完整图像,需要计算新的边界尺寸。OpenCV的 cv2.getRotationMatrix2D 返回的矩阵可以配合 cv2.warpAffine 的边界填充模式来处理。

4.4 仿射变换——三个点定天下

仿射变换的本质是:直线还是直线,平行线还是平行线。它由三个点唯一确定。

import cv2
import numpy as np

img = cv2.imread('lena.jpg')
rows, cols = 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)

# 执行仿射变换
affined = cv2.warpAffine(img, M, (cols, rows))

为什么是三个点?因为仿射变换有6个自由度(2x3矩阵),三个点提供6个方程,刚好解出来。我在做文档矫正时,经常用仿射变换来校正倾斜的扫描件。选三个角点,映射到矩形位置,搞定。

我的习惯: 选点的时候尽量选在图像边缘,这样变换后的效果更稳定。别选太靠近的点,否则矩阵会不稳定。

4.5 透视变换——四个点改变世界

透视变换比仿射变换更强大。它允许直线变成直线,但平行线不一定保持平行。说白了,就是模拟人眼从不同角度观察的效果。

import cv2
import numpy as np

img = cv2.imread('card.jpg')
rows, cols = img.shape[:2]

# 原图中的四个点(比如一张名片四个角)
pts1 = np.float32([[56, 65],
                   [368, 52],
                   [28, 387],
                   [389, 390]])

# 目标图中的四个点(矫正后的矩形)
pts2 = np.float32([[0, 0],
                   [300, 0],
                   [0, 400],
                   [300, 400]])

# 计算透视变换矩阵
M = cv2.getPerspectiveTransform(pts1, pts2)

# 执行透视变换
warped = cv2.warpPerspective(img, M, (300, 400))

透视变换需要4个点,因为它的自由度是8(3x3矩阵,最后一个元素固定为1)。四个点提供8个方程,刚好求解。

经典应用: 文档扫描、车牌矫正、广告牌替换。我做过一个项目,需要从倾斜拍摄的身份证照片中提取正面图像,就是用透视变换搞定的。选四个角点,映射到标准尺寸,效果立竿见影。

4.6 实战:综合应用——文档矫正

咱们来一个完整的例子。假设你拍了一张倾斜的文档照片,怎么把它矫正成正面视图?

import cv2
import numpy as np

def correct_document(image_path):
    img = cv2.imread(image_path)
    orig = img.copy()
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 边缘检测
    edges = cv2.Canny(gray, 50, 150)
    
    # 找轮廓
    contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # 找到最大的四边形轮廓
    for c in sorted(contours, key=cv2.contourArea, reverse=True)[:5]:
        peri = cv2.arcLength(c, True)
        approx = cv2.approxPolyDP(c, 0.02 * peri, True)
        
        if len(approx) == 4:
            doc_pts = approx.reshape(4, 2).astype(np.float32)
            break
    
    # 排序四个角点:左上、右上、右下、左下
    doc_pts = order_points(doc_pts)
    
    # 目标尺寸
    width = 500
    height = 700
    dst_pts = np.float32([[0, 0],
                          [width, 0],
                          [width, height],
                          [0, height]])
    
    # 透视变换
    M = cv2.getPerspectiveTransform(doc_pts, dst_pts)
    warped = cv2.warpPerspective(orig, M, (width, height))
    
    return warped

def order_points(pts):
    # 按左上、右上、右下、左下排序
    rect = np.zeros((4, 2), dtype=np.float32)
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    return rect

# 使用
result = correct_document('skewed_doc.jpg')
cv2.imwrite('corrected_doc.jpg', result)

这个例子把今天讲的内容串起来了:边缘检测找轮廓、轮廓近似找四边形、透视变换矫正。我在实际项目中,这个流程的成功率能达到90%以上。剩下的10%?嗯,光照太差或者文档背景太复杂的时候,需要加一些预处理。

注意: 角点排序很重要!如果顺序不对,矫正出来的图像可能是翻转的。我刚开始写这个函数时,就因为这个bug调试了半天。

4.7 总结与对比

变换类型 所需点数 变换矩阵 特性 典型应用
缩放 无(直接resize) 改变尺寸 图像预处理
平移 2x3 保持形状 图像对齐
旋转 2x3 绕点旋转 图像校正
仿射变换 3个点 2x3 保持平行线 文档矫正、图像配准
透视变换 4个点 3x3 改变视角 文档扫描、车牌识别

好了,这一章的内容就到这里。几何变换是图像处理的基础,也是很多高级应用(比如图像拼接、SLAM)的基石。我个人建议,把今天这几个函数多练几遍,尤其是仿射变换和透视变换,面试时经常被问到。

下一章我们聊聊图像滤波,到时候见。