3、图像预处理(一):图像缩放、裁剪、旋转、仿射变换、透视变换

各位同学,欢迎来到图像预处理的第一讲。

说实话,在车牌识别这个领域摸爬滚打这么多年,我最大的体会就是:预处理做得好,识别成功一半。你想想看,摄像头拍回来的车牌,角度歪的、光线暗的、尺寸不对的,啥情况都有。如果不先把这些「毛刺」处理掉,后面的字符分割和识别根本没法搞。

今天这一章,咱们就把图像预处理中最基础、也最常用的几个操作讲透:缩放、裁剪、旋转、仿射变换、透视变换。这些都是 OpenCV 里的基本功,但也是车牌识别流水线上不可或缺的环节。

3.1 图像缩放:让车牌尺寸统一

车牌识别的第一步,往往是把图像缩放到一个固定尺寸。为什么?因为后续的算法(比如模板匹配、深度学习模型)通常要求输入尺寸一致。

OpenCV 里用 cv2.resize() 搞定。我个人习惯用 INTER_LINEAR 做放大,INTER_AREA 做缩小,效果比较稳。

import cv2

img = cv2.imread('car.jpg')
# 缩放到 640x480
resized = cv2.resize(img, (640, 480))
# 按比例缩放,宽度设为 400,高度自动计算
h, w = img.shape[:2]
ratio = 400 / w
new_h = int(h * ratio)
resized_ratio = cv2.resize(img, (400, new_h))
我的小经验:车牌识别场景下,我一般把车牌区域统一缩放到 300x100 像素。这个比例跟真实车牌(440x140mm)接近,字符不会变形太厉害。

3.2 图像裁剪:只取感兴趣的区域

裁剪说白了就是「切图」。在车牌识别里,我们经常先检测出车牌的大概位置,然后把这个区域裁出来,再做后续处理。

OpenCV 里裁剪特别简单——直接用 NumPy 的切片操作就行。

# 假设我们检测到车牌区域在 (x, y, w, h)
x, y, w, h = 100, 200, 300, 100
plate_roi = img[y:y+h, x:x+w]
cv2.imshow('Plate ROI', plate_roi)

嗯,这里要注意:切片顺序是 [行, 列],也就是先 y 后 x。我刚开始学的时候老搞反,结果裁出来的图都是歪的。

避坑指南:我曾经在项目里直接用了 img[x:x+w, y:y+h],结果调试了半天才发现坐标轴搞反了。记住:OpenCV 里图像是 NumPy 数组,shape 是 (height, width, channels)。

3.3 图像旋转:纠正倾斜的车牌

实际拍摄的车牌,很少有完全水平的。尤其是路边停车抓拍,角度往往偏个 5 到 10 度。这时候就需要旋转校正。

OpenCV 的旋转需要两步走:先算旋转矩阵,再做仿射变换。

def rotate_image(img, angle):
    h, w = img.shape[:2]
    center = (w // 2, h // 2)
    # 计算旋转矩阵
    M = cv2.getRotationMatrix2D(center, angle, 1.0)
    # 执行旋转
    rotated = cv2.warpAffine(img, M, (w, h))
    return rotated

# 顺时针旋转 5 度
corrected = rotate_image(plate_img, -5)

你可能会问:为什么角度是负的?因为 OpenCV 里正角度是逆时针旋转。要顺时针校正,就得传负值。

关键点:旋转后图像边缘会被裁掉。如果你不想丢失信息,可以在 warpAffine 里把输出尺寸设大一些,或者用 cv2.BORDER_REPLICATE 填充边缘。

3.4 仿射变换:更灵活的几何变换

旋转其实是仿射变换的一种特例。仿射变换可以完成:平移、旋转、缩放、错切。它保证变换后直线还是直线,平行线还是平行线。

在车牌识别里,仿射变换常用于校正轻微透视变形。比如车牌左右高度不一致,可以用仿射变换拉平。

# 原图中的三个点
src_pts = np.float32([[50, 50], [200, 50], [50, 200]])
# 目标图中的三个点
dst_pts = np.float32([[10, 100], [200, 50], [100, 250]])

# 计算仿射变换矩阵
M = cv2.getAffineTransform(src_pts, dst_pts)
# 应用变换
affined = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))

说白了,仿射变换就是「三点定乾坤」。你只需要告诉 OpenCV:原图上三个点,要变到目标图的哪三个位置,剩下的它帮你算。

我的做法:在车牌校正时,我通常取车牌的左上、右上、左下三个角点作为源点,然后映射到一个标准矩形。这样能同时校正旋转和轻微透视。

3.5 透视变换:搞定大角度拍摄

仿射变换搞不定的是「近大远小」的透视效果。比如你从侧面拍一辆车,车牌看起来是梯形的。这时候就得用透视变换。

透视变换需要 4 个点,而且这 4 个点不能共线。它可以把任意四边形映射成矩形。

# 原图中的四个角点(比如车牌四个角)
src_pts = np.float32([
    [141, 131], [480, 159],
    [493, 630], [64, 601]
])
# 目标图中的四个角点(标准矩形)
dst_pts = np.float32([
    [0, 0], [300, 0],
    [300, 100], [0, 100]
])

# 计算透视变换矩阵
M = cv2.getPerspectiveTransform(src_pts, dst_pts)
# 应用变换
warped = cv2.warpPerspective(img, M, (300, 100))

你看,代码跟仿射变换很像,只是函数名从 getAffineTransform 变成了 getPerspectiveTransform,而且需要 4 个点。

注意:透视变换的源点顺序必须跟目标点顺序一一对应。我见过有人把左上和右下搞反了,结果变换出来的图像是翻转的。建议按「左上、右上、右下、左下」的顺序排列。

3.6 实战:车牌校正流水线

好了,理论讲完了。咱们来串一个完整的车牌校正流程。这是我实际项目中用到的套路。

def correct_plate(img, corners):
    """
    img: 原始图像
    corners: 车牌四个角点,顺序为左上、右上、右下、左下
    """
    # 1. 计算目标矩形的宽高
    width = 300
    height = 100
    
    # 2. 定义目标点
    dst_pts = np.float32([
        [0, 0],
        [width, 0],
        [width, height],
        [0, height]
    ])
    
    # 3. 透视变换
    M = cv2.getPerspectiveTransform(corners, dst_pts)
    warped = cv2.warpPerspective(img, M, (width, height))
    
    # 4. 灰度化 + 二值化(为后续字符分割做准备)
    gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    
    return warped, binary

这个流程我用了好几年,效果很稳定。核心思路就是:先校正几何变形,再处理图像内容。顺序不能乱。

3.7 本章小结

今天咱们聊了五个基础操作:

  • 缩放:统一尺寸,注意插值方法的选择
  • 裁剪:提取 ROI,注意坐标顺序
  • 旋转:校正倾斜,注意角度正负
  • 仿射变换:三点变换,保持平行性
  • 透视变换:四点变换,校正透视变形

说实话,这些操作单独看都不难。但组合起来,就能解决车牌识别中 80% 的几何变形问题。下一章咱们会继续聊图像预处理的其他内容——滤波、边缘检测、二值化。到时候你会看到,这些基础操作是怎么一步步把一张「脏乱差」的车牌照片,变成干净清晰的识别素材的。

好,今天就到这儿。动手试试吧,把代码跑起来,你会有更深的体会。