OpenCV Python图像处理实战:从基础到人脸检测与车牌识别 OpenCVOpen Source Computer Vision Library是一个开源的计算机视觉库由Intel开发并持续维护。它提供了丰富的图像处理和计算机视觉算法支持Python、C、Java等多种编程语言。无论是图像处理、视频分析、人脸检测还是车牌识别OpenCV都是开发者的首选工具。这次我们来看OpenCV在Python环境下的完整应用教程。重点不是概念有多复杂而是能不能快速上手解决实际问题。如果你关心图像处理、视频分析、目标检测等实际应用这篇文章可以直接收藏。OpenCV最核心的特点是跨平台、功能全面、社区活跃。从基础的图像读写、几何变换到高级的边缘检测、形态学处理、轮廓分析OpenCV都提供了完善的API支持。更重要的是它不需要高端显卡普通CPU就能运行大部分功能适合各种硬件环境。本文会带你完成OpenCV的完整学习路径从环境搭建开始逐步深入图像处理的各种技术最后实现人脸检测和车牌识别等实战项目。每个环节都会提供可运行的代码示例和效果验证。1. OpenCV核心能力速览能力项说明支持平台Windows、Linux、macOS全平台支持编程语言Python、C、Java、MATLAB等核心功能图像处理、视频分析、目标检测、机器学习硬件要求普通CPU即可运行部分功能可GPU加速安装方式pip一键安装支持多种版本适合场景学术研究、工业检测、安防监控、自动驾驶2. 环境准备与安装2.1 Python环境配置首先确保已安装Python 3.6及以上版本。推荐使用Anaconda管理Python环境# 创建虚拟环境 conda create -n opencv_env python3.8 conda activate opencv_env2.2 OpenCV安装通过pip安装OpenCV核心包和扩展模块# 安装基础版 pip install opencv-python # 安装完整版包含额外模块 pip install opencv-contrib-python # 安装常用依赖 pip install numpy matplotlib2.3 验证安装创建测试脚本验证安装是否成功import cv2 import numpy as np print(fOpenCV版本: {cv2.__version__}) print(fNumPy版本: {np.__version__}) # 测试基本功能 img np.zeros((100, 100, 3), dtypenp.uint8) cv2.imshow(Test, img) cv2.waitKey(1000) cv2.destroyAllWindows()如果能看到一个黑色窗口显示1秒后关闭说明安装成功。3. 图像基础操作3.1 图像读取与显示import cv2 import matplotlib.pyplot as plt # 读取图像 img cv2.imread(image.jpg) # 检查图像是否读取成功 if img is None: print(图像读取失败请检查文件路径) else: # BGR转RGBmatplotlib显示需要 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 显示图像 plt.figure(figsize(10, 6)) plt.subplot(121), plt.imshow(img_rgb), plt.title(原始图像) plt.axis(off) # 显示图像信息 plt.subplot(122) plt.text(0.1, 0.9, f尺寸: {img.shape}, fontsize12) plt.text(0.1, 0.7, f数据类型: {img.dtype}, fontsize12) plt.text(0.1, 0.5, f最大值: {img.max()}, fontsize12) plt.text(0.1, 0.3, f最小值: {img.min()}, fontsize12) plt.axis(off) plt.show()3.2 图像保存# 保存处理后的图像 cv2.imwrite(processed_image.jpg, img) # 设置JPEG质量参数 cv2.imwrite(high_quality.jpg, img, [cv2.IMWRITE_JPEG_QUALITY, 95])4. 图像几何变换4.1 图像缩放def resize_image(image, scale_factorNone, target_sizeNone): 图像缩放函数 :param image: 输入图像 :param scale_factor: 缩放比例 :param target_size: 目标尺寸 (width, height) :return: 缩放后的图像 if target_size: # 按目标尺寸缩放 resized cv2.resize(image, target_size, interpolationcv2.INTER_LINEAR) elif scale_factor: # 按比例缩放 height, width image.shape[:2] new_size (int(width * scale_factor), int(height * scale_factor)) resized cv2.resize(image, new_size, interpolationcv2.INTER_LINEAR) else: return image return resized # 测试缩放功能 img cv2.imread(image.jpg) img_small resize_image(img, scale_factor0.5) # 缩小50% img_large resize_image(img, scale_factor1.5) # 放大50% img_target resize_image(img, target_size(300, 200)) # 指定尺寸4.2 图像旋转def rotate_image(image, angle, centerNone, scale1.0): 图像旋转函数 :param image: 输入图像 :param angle: 旋转角度正数逆时针 :param center: 旋转中心 :param scale: 缩放比例 :return: 旋转后的图像 height, width image.shape[:2] if center is None: center (width // 2, height // 2) # 获取旋转矩阵 rotation_matrix cv2.getRotationMatrix2D(center, angle, scale) # 计算旋转后的图像尺寸 cos_val abs(rotation_matrix[0, 0]) sin_val abs(rotation_matrix[0, 1]) new_width int((height * sin_val) (width * cos_val)) new_height int((height * cos_val) (width * sin_val)) # 调整旋转矩阵的中心点 rotation_matrix[0, 2] (new_width / 2) - center[0] rotation_matrix[1, 2] (new_height / 2) - center[1] # 执行旋转 rotated cv2.warpAffine(image, rotation_matrix, (new_width, new_height)) return rotated # 测试旋转功能 img_rotated_45 rotate_image(img, 45) # 逆时针旋转45度 img_rotated_90 rotate_image(img, 90) # 逆时针旋转90度 img_rotated_180 rotate_image(img, 180) # 旋转180度4.3 仿射变换def affine_transform(image, src_points, dst_points): 仿射变换 :param image: 输入图像 :param src_points: 源图像三个点 :param dst_points: 目标图像三个点 :return: 变换后的图像 # 确保点格式正确 src_points np.float32(src_points) dst_points np.float32(dst_points) # 计算仿射变换矩阵 M cv2.getAffineTransform(src_points, dst_points) # 执行变换 rows, cols image.shape[:2] transformed cv2.warpAffine(image, M, (cols, rows)) return transformed # 示例图像平移 def translate_image(image, x_offset, y_offset): 图像平移 :param image: 输入图像 :param x_offset: x轴偏移量 :param y_offset: y轴偏移量 :return: 平移后的图像 rows, cols image.shape[:2] # 构造平移矩阵 M np.float32([[1, 0, x_offset], [0, 1, y_offset]]) translated cv2.warpAffine(image, M, (cols, rows)) return translated5. 图像滤波与平滑处理5.1 均值滤波def mean_filter(image, kernel_size5): 均值滤波 :param image: 输入图像 :param kernel_size: 卷积核大小 :return: 滤波后的图像 return cv2.blur(image, (kernel_size, kernel_size)) # 测试不同核大小的效果 img_mean_3 mean_filter(img, 3) # 3x3均值滤波 img_mean_5 mean_filter(img, 5) # 5x5均值滤波 img_mean_7 mean_filter(img, 7) # 7x7均值滤波5.2 高斯滤波def gaussian_filter(image, kernel_size5, sigma1.0): 高斯滤波 :param image: 输入图像 :param kernel_size: 卷积核大小必须为奇数 :param sigma: 高斯核标准差 :return: 滤波后的图像 return cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma) # 测试不同参数的高斯滤波 img_gauss_3 gaussian_filter(img, 3, 0.5) # 小核小标准差 img_gauss_5 gaussian_filter(img, 5, 1.0) # 中等核中等标准差 img_gauss_7 gaussian_filter(img, 7, 1.5) # 大核大标准差5.3 中值滤波def median_filter(image, kernel_size5): 中值滤波对椒盐噪声特别有效 :param image: 输入图像 :param kernel_size: 卷积核大小必须为奇数 :return: 滤波后的图像 return cv2.medianBlur(image, kernel_size) # 测试中值滤波 img_median_3 median_filter(img, 3) # 3x3中值滤波 img_median_5 median_filter(img, 5) # 5x5中值滤波5.4 滤波效果对比def compare_filters(original_image): 比较不同滤波器的效果 # 添加椒盐噪声用于测试 noisy_image add_salt_pepper_noise(original_image, 0.05) # 应用不同滤波器 mean_filtered mean_filter(noisy_image, 5) gaussian_filtered gaussian_filter(noisy_image, 5, 1.0) median_filtered median_filter(noisy_image, 5) # 显示结果 plt.figure(figsize(15, 10)) images [original_image, noisy_image, mean_filtered, gaussian_filtered, median_filtered] titles [原图, 加噪图像, 均值滤波, 高斯滤波, 中值滤波] for i in range(5): plt.subplot(2, 3, i1) if len(images[i].shape) 3: plt.imshow(cv2.cvtColor(images[i], cv2.COLOR_BGR2RGB)) else: plt.imshow(images[i], cmapgray) plt.title(titles[i]) plt.axis(off) plt.tight_layout() plt.show() def add_salt_pepper_noise(image, noise_ratio0.05): 添加椒盐噪声 noisy_image np.copy(image) height, width image.shape[:2] # 添加盐噪声白点 salt_pixels int(noise_ratio * height * width * 0.5) coords [np.random.randint(0, i-1, salt_pixels) for i in image.shape[:2]] noisy_image[coords[0], coords[1]] 255 # 添加椒噪声黑点 pepper_pixels int(noise_ratio * height * width * 0.5) coords [np.random.randint(0, i-1, pepper_pixels) for i in image.shape[:2]] noisy_image[coords[0], coords[1]] 0 return noisy_image6. 边缘检测技术6.1 Canny边缘检测def canny_edge_detection(image, low_threshold50, high_threshold150, aperture_size3): Canny边缘检测 :param image: 输入图像灰度图 :param low_threshold: 低阈值 :param high_threshold: 高阈值 :param aperture_size: Sobel算子孔径大小 :return: 边缘图像 # 转换为灰度图 if len(image.shape) 3: gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray image # 应用Canny边缘检测 edges cv2.Canny(gray, low_threshold, high_threshold, apertureSizeaperture_size) return edges # 测试不同参数的Canny检测 edges_weak canny_edge_detection(img, 30, 90) # 弱边缘检测 edges_medium canny_edge_detection(img, 50, 150) # 中等边缘检测 edges_strong canny_edge_detection(img, 100, 200) # 强边缘检测6.2 Sobel边缘检测def sobel_edge_detection(image, dx1, dy1, ksize3): Sobel边缘检测 :param image: 输入图像 :param dx: x方向导数阶数 :param dy: y方向导数阶数 :param ksize: Sobel核大小 :return: 边缘强度图像 if len(image.shape) 3: gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray image # 应用Sobel算子 sobelx cv2.Sobel(gray, cv2.CV_64F, dx, 0, ksizeksize) sobely cv2.Sobel(gray, cv2.CV_64F, 0, dy, ksizeksize) # 计算梯度幅值 gradient_magnitude np.sqrt(sobelx**2 sobely**2) gradient_magnitude np.uint8(255 * gradient_magnitude / gradient_magnitude.max()) return gradient_magnitude # 测试Sobel检测 sobel_edges sobel_edge_detection(img)6.3 Laplacian边缘检测def laplacian_edge_detection(image, ksize3): Laplacian边缘检测 :param image: 输入图像 :param ksize: 拉普拉斯核大小 :return: 边缘图像 if len(image.shape) 3: gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray image # 应用Laplacian算子 laplacian cv2.Laplacian(gray, cv2.CV_64F, ksizeksize) laplacian np.uint8(np.absolute(laplacian)) return laplacian # 边缘检测效果对比 def compare_edge_detectors(image): 比较不同边缘检测算法的效果 # 转换为灰度图 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) 3 else image # 应用不同边缘检测算法 canny_edges canny_edge_detection(gray) sobel_edges sobel_edge_detection(gray) laplacian_edges laplacian_edge_detection(gray) # 显示结果 plt.figure(figsize(15, 5)) images [gray, canny_edges, sobel_edges, laplacian_edges] titles [原图, Canny边缘, Sobel边缘, Laplacian边缘] for i in range(4): plt.subplot(1, 4, i1) plt.imshow(images[i], cmapgray) plt.title(titles[i]) plt.axis(off) plt.tight_layout() plt.show()7. 形态学处理7.1 腐蚀与膨胀def morphological_operations(image, operationerode, kernel_size5, iterations1): 形态学基本操作 :param image: 输入图像二值图 :param operation: 操作类型 erode/dilate :param kernel_size: 结构元素大小 :param iterations: 迭代次数 :return: 处理后的图像 # 创建结构元素 kernel np.ones((kernel_size, kernel_size), np.uint8) if operation erode: result cv2.erode(image, kernel, iterationsiterations) elif operation dilate: result cv2.dilate(image, kernel, iterationsiterations) else: result image return result # 测试腐蚀和膨胀 binary_image cv2.imread(binary_image.jpg, 0) # 读取二值图像 eroded morphological_operations(binary_image, erode, 3, 1) dilated morphological_operations(binary_image, dilate, 3, 1)7.2 开运算与闭运算def advanced_morphological_operations(image, operationopen, kernel_size5): 高级形态学操作 :param image: 输入图像 :param operation: 操作类型 open/close/gradient/tophat/blackhat :param kernel_size: 结构元素大小 :return: 处理后的图像 kernel np.ones((kernel_size, kernel_size), np.uint8) if operation open: result cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel) elif operation close: result cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel) elif operation gradient: result cv2.morphologyEx(image, cv2.MORPH_GRADIENT, kernel) elif operation tophat: result cv2.morphologyEx(image, cv2.MORPH_TOPHAT, kernel) elif operation blackhat: result cv2.morphologyEx(image, cv2.MORPH_BLACKHAT, kernel) else: result image return result # 测试各种形态学操作 opened advanced_morphological_operations(binary_image, open, 5) closed advanced_morphological_operations(binary_image, close, 5) gradient advanced_morphological_operations(binary_image, gradient, 5)8. 阈值处理8.1 全局阈值处理def global_threshold(image, threshold_value127, methodbinary): 全局阈值处理 :param image: 输入图像灰度图 :param threshold_value: 阈值 :param method: 阈值处理方法 :return: 二值图像 methods { binary: cv2.THRESH_BINARY, binary_inv: cv2.THRESH_BINARY_INV, trunc: cv2.THRESH_TRUNC, tozero: cv2.THRESH_TOZERO, tozero_inv: cv2.THRESH_TOZERO_INV } if method not in methods: method binary _, binary cv2.threshold(image, threshold_value, 255, methods[method]) return binary # 测试不同全局阈值方法 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) binary_global global_threshold(gray, 127, binary)8.2 自适应阈值处理def adaptive_threshold(image, methodmean, block_size11, C2): 自适应阈值处理 :param image: 输入图像灰度图 :param method: 自适应方法 mean/gaussian :param block_size: 邻域大小必须为奇数 :param C: 常数项 :return: 二值图像 methods { mean: cv2.ADAPTIVE_THRESH_MEAN_C, gaussian: cv2.ADAPTIVE_THRESH_GAUSSIAN_C } if method not in methods: method mean # 确保block_size为奇数 if block_size % 2 0: block_size 1 adaptive_binary cv2.adaptiveThreshold( image, 255, methods[method], cv2.THRESH_BINARY, block_size, C ) return adaptive_binary # 测试自适应阈值 adaptive_mean adaptive_threshold(gray, mean, 11, 2) adaptive_gaussian adaptive_threshold(gray, gaussian, 11, 2)8.3 Otsu阈值处理def otsu_threshold(image): Otsu自动阈值处理 :param image: 输入图像灰度图 :return: 二值图像和最佳阈值 # Otsu阈值处理 ret, binary cv2.threshold(image, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) return binary, ret # 测试Otsu阈值 otsu_binary, optimal_threshold otsu_threshold(gray) print(fOtsu算法计算的最佳阈值: {optimal_threshold})9. 轮廓检测与分析9.1 轮廓查找与绘制def find_contours(binary_image, modeexternal, methodsimple): 查找图像轮廓 :param binary_image: 二值图像 :param mode: 轮廓检索模式 external/list/ccomp/tree :param method: 轮廓近似方法 simple/none :return: 轮廓列表和层次结构 modes { external: cv2.RETR_EXTERNAL, list: cv2.RETR_LIST, ccomp: cv2.RETR_CCOMP, tree: cv2.RETR_TREE } methods { simple: cv2.CHAIN_APPROX_SIMPLE, none: cv2.CHAIN_APPROX_NONE } contours, hierarchy cv2.findContours( binary_image, modes.get(mode, cv2.RETR_EXTERNAL), methods.get(method, cv2.CHAIN_APPROX_SIMPLE) ) return contours, hierarchy def draw_contours(image, contours, color(0, 255, 0), thickness2): 在图像上绘制轮廓 :param image: 原始图像 :param contours: 轮廓列表 :param color: 轮廓颜色 :param thickness: 轮廓线粗细 :return: 绘制轮廓后的图像 result image.copy() cv2.drawContours(result, contours, -1, color, thickness) return result # 轮廓检测示例 contours, hierarchy find_contours(binary_global, external, simple) contour_image draw_contours(img, contours)9.2 轮廓特征分析def analyze_contours(contours): 分析轮廓特征 :param contours: 轮廓列表 :return: 轮廓特征字典 features [] for i, contour in enumerate(contours): # 计算轮廓面积 area cv2.contourArea(contour) # 计算轮廓周长 perimeter cv2.arcLength(contour, True) # 计算轮廓边界矩形 x, y, w, h cv2.boundingRect(contour) # 计算最小外接矩形 rect cv2.minAreaRect(contour) box cv2.boxPoints(rect) box np.int0(box) # 计算最小外接圆 (x_circle, y_circle), radius cv2.minEnclosingCircle(contour) # 计算轮廓近似多边形拟合 epsilon 0.02 * perimeter approx cv2.approxPolyDP(contour, epsilon, True) features.append({ index: i, area: area, perimeter: perimeter, bounding_rect: (x, y, w, h), min_area_rect: rect, min_enclosing_circle: ((x_circle, y_circle), radius), approx_polygon: approx, vertices_count: len(approx) }) return features # 分析轮廓特征 contour_features analyze_contours(contours) # 打印轮廓信息 for feature in contour_features[:5]: # 只显示前5个轮廓 print(f轮廓{feature[index]}: 面积{feature[area]:.2f}, f周长{feature[perimeter]:.2f}, f顶点数{feature[vertices_count]})10. 实战应用人脸检测10.1 使用Haar级联分类器def face_detection_haar(image, scale_factor1.1, min_neighbors5, min_size(30, 30)): 使用Haar级联分类器进行人脸检测 :param image: 输入图像 :param scale_factor: 图像缩放比例 :param min_neighbors: 最小邻居数 :param min_size: 最小人脸尺寸 :return: 检测到的人脸位置列表 # 加载预训练的人脸检测器 face_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml) # 转换为灰度图 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 人脸检测 faces face_cascade.detectMultiScale( gray, scaleFactorscale_factor, minNeighborsmin_neighbors, minSizemin_size ) return faces def draw_faces(image, faces, color(255, 0, 0), thickness2): 在图像上绘制检测到的人脸框 :param image: 原始图像 :param faces: 人脸位置列表 :param color: 框颜色 :param thickness: 框线粗细 :return: 绘制人脸框后的图像 result image.copy() for (x, y, w, h) in faces: cv2.rectangle(result, (x, y), (xw, yh), color, thickness) return result # 人脸检测示例 faces face_detection_haar(img) face_image draw_faces(img, faces) print(f检测到 {len(faces)} 个人脸)10.2 使用DNN模块进行人脸检测def face_detection_dnn(image, confidence_threshold0.5): 使用DNN模块进行人脸检测更准确 :param image: 输入图像 :param confidence_threshold: 置信度阈值 :return: 检测到的人脸位置和置信度 # 加载预训练模型 model_file opencv_face_detector_uint8.pb config_file opencv_face_detector.pbtxt net cv2.dnn.readNetFromTensorflow(model_file, config_file) # 准备输入图像 blob cv2.dnn.blobFromImage(image, 1.0, (300, 300), [104, 117, 123]) net.setInput(blob) # 前向传播 detections net.forward() faces [] confidences [] # 解析检测结果 for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence confidence_threshold: x1 int(detections[0, 0, i, 3] * image.shape[1]) y1 int(detections[0, 0, i, 4] * image.shape[0]) x2 int(detections[0, 0, i, 5] * image.shape[1]) y2 int(detections[0, 0, i, 6] * image.shape[0]) faces.append((x1, y1, x2-x1, y2-y1)) confidences.append(confidence) return faces, confidences # DNN人脸检测示例 dnn_faces, confidences face_detection_dnn(img) dnn_face_image draw_faces(img, dnn_faces) for i, (face, confidence) in enumerate(zip(dnn_faces, confidences)): print(f人脸{i1}: 位置{face}, 置信度{confidence:.3f})11. 实战应用车牌识别11.1 车牌区域检测def license_plate_detection(image): 车牌区域检测 :param image: 输入图像 :return: 检测到的车牌区域 # 转换为灰度图 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 使用高斯模糊减少噪声 blurred cv2.GaussianBlur(gray, (5, 5), 0) # 使用Sobel算子检测边缘 sobelx cv2.Sobel(blurred, cv2.CV_8U, 1, 0, ksize3) # 二值化处理 _, binary cv2.threshold(sobelx, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 形态学操作闭运算连接区域 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 查找轮廓 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 筛选可能的车牌区域 plates [] for contour in contours: # 计算轮廓的边界矩形 x, y, w, h cv2.boundingRect(contour) # 根据长宽比和面积筛选 aspect_ratio w / h area w * h # 典型车牌的长宽比约为4:1面积适中 if 2 aspect_ratio 5 and 1000 area 50000: plates.append((x, y, w, h)) return plates def draw_license_plates(image, plates, color(0, 255, 0), thickness2): 在图像上绘制检测到的车牌区域 result image.copy() for (x, y, w, h) in plates: cv2.rectangle(result, (x, y), (xw, yh), color, thickness) return result # 车牌检测示例 plates license_plate_detection(img) plate_image draw_license_plates(img, plates) print(f检测到 {len(plates)} 个车牌区域)11.2 车牌字符识别基础版def preprocess_plate(plate_region): 预处理车牌区域用于字符识别 :param plate_region: 车牌区域图像 :return: 预处理后的图像 # 转换为灰度图 gray cv2.cvtColor(plate_region, cv2.COLOR_BGR2GRAY) if len(plate_region.shape) 3 else plate_region # 二值化 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 形态学操作去除噪声 kernel np.ones((3, 3), np.uint8) cleaned cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return cleaned def character_segmentation(plate_image): 车牌字符分割 :param plate_image: 预处理后的车牌图像 :return: 分割出的字符图像列表 # 查找字符轮廓 contours, _ cv2.findContours(plate_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) characters [] character_boxes [] # 筛选字符轮廓 for contour in contours: x, y, w, h cv2.boundingRect(contour) # 根据宽高比和面积筛选字符 aspect_ratio w / h area w * h if 0.2 aspect_ratio 1.0 and area 100: character_boxes.append((x, y, w, h)) # 按x坐标排序从左到右 character_boxes.sort(keylambda box: box[0]) # 提取字符图像 for x, y, w, h in character_boxes: character plate_image[y:yh, x:xw] characters.append(character) return characters # 车牌字符识别示例 if plates: # 提取第一个车牌区域 x, y, w, h plates[0] plate_region