OpenCV进阶:图像变换、增强与特征检测实战
作者:AI技术分享
专栏:OpenCV计算机视觉实战
发布时间:2025年1月
前言
在上一篇文章中,我们学习了OpenCV的基础知识和图像滤波技术。今天,我们将深入探索更高级的图像处理技术:几何变换、图像增强、形态学操作和特征检测。这些技术是计算机视觉应用的核心,广泛应用于图像矫正、目标识别、图像拼接等领域。
本文的重点实战项目是全景图像拼接,我们将综合运用所学知识,实现一个可以将多张照片拼接成全景图的应用。
一、图像几何变换
1.1 基础变换:缩放、平移、旋转
几何变换是通过数学变换改变图像的空间位置关系。让我们从最基础的变换开始。
import cv2
import numpy as np
import matplotlib.pyplot as pltclass GeometricTransform:"""图像几何变换类"""def __init__(self, image_path=None):"""初始化"""if image_path and cv2.imread(image_path) is not None:self.img = cv2.imread(image_path)else:# 创建测试图像self.img = self.create_test_image()# 转换为RGB用于matplotlib显示self.img_rgb = cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB)self.h, self.w = self.img.shape[:2]print(f"图像尺寸: {self.w}x{self.h}")def create_test_image(self):"""创建一个带网格的测试图像"""img = np.ones((400, 600, 3), dtype=np.uint8) * 255# 绘制网格for i in range(0, 600, 50):cv2.line(img, (i, 0), (i, 400), (200, 200, 200), 1)for i in range(0, 400, 50):cv2.line(img, (0, i), (600, i), (200, 200, 200), 1)# 添加一些图形作为参考cv2.rectangle(img, (150, 100), (450, 300), (0, 255, 0), 2)cv2.circle(img, (300, 200), 50, (255, 0, 0), -1)cv2.putText(img, 'OpenCV', (220, 200), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 0, 255), 2)# 添加坐标轴cv2.arrowedLine(img, (50, 350), (150, 350), (0, 0, 0), 2)cv2.arrowedLine(img, (50, 350), (50, 250), (0, 0, 0), 2)cv2.putText(img, 'X', (160, 355), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)cv2.putText(img, 'Y', (45, 240), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)return imgdef scale(self, fx=1.5, fy=1.5, interpolation=cv2.INTER_LINEAR):"""图像缩放fx, fy: x和y方向的缩放因子interpolation: 插值方法"""# 方法1:使用缩放因子scaled = cv2.resize(self.img, None, fx=fx, fy=fy, interpolation=interpolation)# 方法2:指定输出大小new_width = int(self.w * fx)new_height = int(self.h * fy)scaled2 = cv2.resize(self.img, (new_width, new_height), interpolation=interpolation)return scaleddef translate(self, tx=50, ty=30):"""图像平移tx: x方向平移量ty: y方向平移量"""# 构建平移矩阵M = np.float32([[1, 0, tx],[0, 1, ty]])# 应用变换translated = cv2.warpAffine(self.img, M, (self.w, self.h))print(f"平移矩阵:\n{M}")return translateddef rotate(self, angle=45, center=None, scale=1.0):"""图像旋转angle: 旋转角度(逆时针为正)center: 旋转中心,默认为图像中心scale: 缩放因子"""if center is None:center = (self.w // 2, self.h // 2)# 获取旋转矩阵M = cv2.getRotationMatrix2D(center, angle, scale)# 应用旋转rotated = cv2.warpAffine(self.img, M, (self.w, self.h))# 计算旋转后的边界框(避免裁剪)cos = np.abs(M[0, 0])sin = np.abs(M[0, 1])new_w = int((self.h * sin) + (self.w * cos))new_h = int((self.h * cos) + (self.w * sin))# 调整旋转矩阵的平移部分M[0, 2] += (new_w / 2) - center[0]M[1, 2] += (new_h / 2) - center[1]# 应用调整后的旋转(不裁剪)rotated_full = cv2.warpAffine(self.img, M, (new_w, new_h),borderValue=(255, 255, 255))return rotated, rotated_fulldef flip(self, flip_code):"""图像翻转flip_code: 0-垂直翻转, 1-水平翻转, -1-同时翻转"""flipped = cv2.flip(self.img, flip_code)return flippeddef demonstrate_all(self):"""演示所有基础变换"""fig = plt.figure(figsize=(15, 10))# 原图plt.subplot(2, 4, 1)plt.imshow(self.img_rgb)plt.title('原始图像')plt.axis('off')# 缩放scaled = cv2.cvtColor(self.scale(0.7, 0.7), cv2.COLOR_BGR2RGB)plt.subplot(2, 4, 2)plt.imshow(scaled)plt.title('缩放 (0.7x)')plt.axis('off')# 平移translated = cv2.cvtColor(self.translate(50, 30), cv2.COLOR_BGR2RGB)plt.subplot(2, 4, 3)plt.imshow(translated)plt.title('平移 (50, 30)')plt.axis('off')# 旋转(裁剪)rotated, _ = self.rotate(30)rotated_rgb = cv2.cvtColor(rotated, cv2.COLOR_BGR2RGB)plt.subplot(2, 4, 4)plt.imshow(rotated_rgb)plt.title('旋转 30° (裁剪)')plt.axis('off')# 旋转(不裁剪)_, rotated_full = self.rotate(30)rotated_full_rgb = cv2.cvtColor(rotated_full, cv2.COLOR_BGR2RGB)plt.subplot(2, 4, 5)plt.imshow(rotated_full_rgb)plt.title('旋转 30° (完整)')plt.axis('off')# 水平翻转h_flip = cv2.cvtColor(self.flip(1), cv2.COLOR_BGR2RGB)plt.subplot(2, 4, 6)plt.imshow(h_flip)plt.title('水平翻转')plt.axis('off')# 垂直翻转v_flip = cv2.cvtColor(self.flip(0), cv2.COLOR_BGR2RGB)plt.subplot(2, 4, 7)plt.imshow(v_flip)plt.title('垂直翻转')plt.axis('off')# 组合变换combined = self.scale(0.8, 0.8)combined = cv2.flip(combined, 1)_, combined = self.rotate(15)combined_rgb = cv2.cvtColor(combined, cv2.COLOR_BGR2RGB)plt.subplot(2, 4, 8)plt.imshow(combined_rgb)plt.title('组合变换')plt.axis('off')plt.tight_layout()plt.show()# 使用示例
if __name__ == "__main__":transform = GeometricTransform()transform.demonstrate_all()
1.2 仿射变换
仿射变换保持了直线的"平直性"和"平行性",包括平移、旋转、缩放和剪切。
class AffineTransform:"""仿射变换演示类"""def __init__(self):self.img = self.create_demo_image()self.h, self.w = self.img.shape[:2]def create_demo_image(self):"""创建演示图像"""img = np.ones((300, 400, 3), dtype=np.uint8) * 255# 绘制一个正方形和一些文字pts = np.array([[50, 50], [200, 50], [200, 200], [50, 200]], np.int32)cv2.polylines(img, [pts], True, (0, 255, 0), 2)cv2.fillPoly(img, [pts], (200, 255, 200))# 添加参考点colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]for i, pt in enumerate(pts):cv2.circle(img, tuple(pt), 5, colors[i], -1)cv2.putText(img, 'Affine', (80, 130), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)return imgdef affine_transform(self, src_pts, dst_pts):"""执行仿射变换src_pts: 源图像中的3个点dst_pts: 目标图像中对应的3个点"""# 计算仿射变换矩阵M = cv2.getAffineTransform(src_pts, dst_pts)# 应用变换transformed = cv2.warpAffine(self.img, M, (self.w, self.h),borderValue=(255, 255, 255))# 在变换后的图像上标记目标点for pt in dst_pts:cv2.circle(transformed, tuple(pt.astype(int)), 5, (255, 0, 255), -1)return transformed, Mdef demonstrate_affine_types(self):"""演示不同类型的仿射变换"""fig = plt.figure(figsize=(15, 10))# 原始图像plt.subplot(2, 3, 1)plt.imshow(cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB))plt.title('原始图像')plt.axis('off')# 定义源点(正方形的3个角)src_pts = np.float32([[50, 50], [200, 50], [50, 200]])# 1. 剪切变换dst_pts1 = np.float32([[50, 50], [250, 70], [50, 200]])transformed1, M1 = self.affine_transform(src_pts, dst_pts1)plt.subplot(2, 3, 2)plt.imshow(cv2.cvtColor(transformed1, cv2.COLOR_BGR2RGB))plt.title('剪切变换')plt.axis('off')# 2. 旋转+缩放dst_pts2 = np.float32([[80, 30], [180, 80], [30, 130]])transformed2, M2 = self.affine_transform(src_pts, dst_pts2)plt.subplot(2, 3, 3)plt.imshow(cv2.cvtColor(transformed2, cv2.COLOR_BGR2RGB))plt.title('旋转+缩放')plt.axis('off')# 3. 任意仿射变换dst_pts3 = np.float32([[100, 80], [280, 60], [80, 250]])transformed3, M3 = self.affine_transform(src_pts, dst_pts3)plt.subplot(2, 3, 4)plt.imshow(cv2.cvtColor(transformed3, cv2.COLOR_BGR2RGB))plt.title('任意仿射变换')plt.axis('off')# 4. 仿射矩阵可视化plt.subplot(2, 3, 5)matrix_img = np.ones((300, 400, 3), dtype=np.uint8) * 255text = f"仿射矩阵:\n{M3}"y0, dy = 50, 30for i, line in enumerate(text.split('\n')):y = y0 + i * dycv2.putText(matrix_img, line, (20, y),cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)plt.imshow(matrix_img)plt.title('变换矩阵')plt.axis('off')plt.tight_layout()plt.show()# 运行演示
affine_demo = AffineTransform()
affine_demo.demonstrate_affine_types()
1.3 透视变换
透视变换可以将图像投影到新的视平面,常用于矫正图像的透视畸变。
class PerspectiveTransform:"""透视变换演示类"""def __init__(self):self.create_demo_scene()def create_demo_scene(self):"""创建一个包含透视的场景"""# 创建棋盘图像self.img = np.ones((500, 700, 3), dtype=np.uint8) * 255# 绘制棋盘board_size = 8square_size = 40start_x, start_y = 150, 100for i in range(board_size):for j in range(board_size):if (i + j) % 2 == 0:x1 = start_x + j * square_sizey1 = start_y + i * square_sizex2 = x1 + square_sizey2 = y1 + square_sizecv2.rectangle(self.img, (x1, y1), (x2, y2), (0, 0, 0), -1)# 添加边框border_pts = np.array([[start_x-2, start_y-2],[start_x + board_size*square_size+2, start_y-2],[start_x + board_size*square_size+2, start_y + board_size*square_size+2],[start_x-2, start_y + board_size*square_size+2]], np.int32)cv2.polylines(self.img, [border_pts], True, (0, 0, 255), 3)# 标记四个角点self.corners = np.float32([[start_x, start_y],[start_x + board_size*square_size, start_y],[start_x + board_size*square_size, start_y + board_size*square_size],[start_x, start_y + board_size*square_size]])for i, corner in enumerate(self.corners):cv2.circle(self.img, tuple(corner.astype(int)), 8, (0, 255, 0), -1)cv2.putText(self.img, str(i+1), tuple(corner.astype(int) + np.array([15, 5])),cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)def apply_perspective(self, dst_points):"""应用透视变换"""# 计算透视变换矩阵M = cv2.getPerspectiveTransform(self.corners, dst_points)# 应用变换transformed = cv2.warpPerspective(self.img, M, (700, 500))# 标记目标点for pt in dst_points:cv2.circle(transformed, tuple(pt.astype(int)), 8, (255, 0, 255), -1)return transformed, Mdef correct_perspective(self, image_with_perspective):"""矫正透视畸变(如拍照的文档)"""gray = cv2.cvtColor(image_with_perspective, cv2.COLOR_BGR2GRAY)# 边缘检测edges = cv2.Canny(gray, 50, 150)# 查找轮廓contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 找到最大的四边形轮廓if contours:largest = max(contours, key=cv2.contourArea)# 多边形近似epsilon = 0.02 * cv2.arcLength(largest, True)approx = cv2.approxPolyDP(largest, epsilon, True)if len(approx) == 4:# 获取四个角点src_pts = approx.reshape(4, 2).astype(np.float32)# 确定目标点(矩形)width = 400height = 300dst_pts = np.float32([[0, 0], [width, 0], [width, height], [0, height]])# 透视变换M = cv2.getPerspectiveTransform(src_pts, dst_pts)corrected = cv2.warpPerspective(image_with_perspective, M, (width, height))return corrected, src_ptsreturn None, Nonedef demonstrate_perspective(self):"""演示透视变换"""fig = plt.figure(figsize=(15, 12))# 原始图像plt.subplot(2, 3, 1)plt.imshow(cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB))plt.title('原始棋盘')plt.axis('off')# 透视变换1:俯视效果dst1 = np.float32([[200, 50], [500, 100], [480, 400], [180, 350]])transformed1, _ = self.apply_perspective(dst1)plt.subplot(2, 3, 2)plt.imshow(cv2.cvtColor(transformed1, cv2.COLOR_BGR2RGB))plt.title('俯视透视')plt.axis('off')# 透视变换2:侧视效果dst2 = np.float32([[100, 100], [450, 50], [500, 450], [50, 400]])transformed2, _ = self.apply_perspective(dst2)plt.subplot(2, 3, 3)plt.imshow(cv2.cvtColor(transformed2, cv2.COLOR_BGR2RGB))plt.title('侧视透视')plt.axis('off')# 透视变换3:扭曲效果dst3 = np.float32([[150, 150], [550, 100], [500, 350], [100, 400]])transformed3, M3 = self.apply_perspective(dst3)plt.subplot(2, 3, 4)plt.imshow(cv2.cvtColor(transformed3, cv2.COLOR_BGR2RGB))plt.title('扭曲透视')plt.axis('off')# 逆透视变换(矫正)M_inv = cv2.getPerspectiveTransform(dst3, self.corners)recovered = cv2.warpPerspective(transformed3, M_inv, (700, 500))plt.subplot(2, 3, 5)plt.imshow(cv2.cvtColor(recovered, cv2.COLOR_BGR2RGB))plt.title('透视矫正')plt.axis('off')# 显示变换矩阵plt.subplot(2, 3, 6)matrix_img = np.ones((500, 700, 3), dtype=np.uint8) * 255matrix_text = "透视变换矩阵(3x3):\n" + "\n".join([" ".join([f"{M3[i,j]:7.2f}" for j in range(3)]) for i in range(3)])y0, dy = 150, 40for i, line in enumerate(matrix_text.split('\n')):cv2.putText(matrix_img, line, (100, y0 + i*dy),cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1)plt.imshow(matrix_img)plt.title('变换矩阵')plt.axis('off')plt.tight_layout()plt.show()# 运行透视变换演示
perspective_demo = PerspectiveTransform()
perspective_demo.demonstrate_perspective()
二、图像增强技术
2.1 直方图操作
直方图是图像处理中的重要工具,用于分析和改善图像的亮度分布。
class HistogramOperations:"""直方图操作类"""def __init__(self, image_path=None):if image_path and cv2.imread(image_path) is not None:self.img = cv2.imread(image_path)else:self.img = self.create_test_image()self.gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)def create_test_image(self):"""创建具有不同亮度区域的测试图像"""img = np.zeros((400, 600, 3), dtype=np.uint8)# 创建不同亮度的区域img[:200, :200] = (30, 30, 30) # 暗区img[:200, 200:400] = (128, 128, 128) # 中等亮度img[:200, 400:] = (220, 220, 220) # 亮区# 添加渐变for i in range(200, 400):gray_value = int(255 * (i - 200) / 200)img[i, :] = (gray_value, gray_value, gray_value)# 添加一些噪声noise = np.random.normal(0, 20, img.shape).astype(np.uint8)img = cv2.add(img, noise)return imgdef calculate_histogram(self, image):"""计算并绘制直方图"""if len(image.shape) == 2:# 灰度图hist = cv2.calcHist([image], [0], None, [256], [0, 256])return histelse:# 彩色图colors = ('b', 'g', 'r')hists = []for i, color in enumerate(colors):hist = cv2.calcHist([image], [i], None, [256], [0, 256])hists.append(hist)return histsdef histogram_equalization(self):"""直方图均衡化"""# 灰度图均衡化equalized_gray = cv2.equalizeHist(self.gray)# 彩色图均衡化(转换到YUV空间)yuv = cv2.cvtColor(self.img, cv2.COLOR_BGR2YUV)yuv[:, :, 0] = cv2.equalizeHist(yuv[:, :, 0])equalized_color = cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR)return equalized_gray, equalized_colordef adaptive_histogram_equalization(self):"""自适应直方图均衡化 (CLAHE)"""# 创建CLAHE对象clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))# 应用于灰度图clahe_gray = clahe.apply(self.gray)# 应用于彩色图lab = cv2.cvtColor(self.img, cv2.COLOR_BGR2LAB)lab[:, :, 0] = clahe.apply(lab[:, :, 0])clahe_color = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)return clahe_gray, clahe_colordef histogram_matching(self, reference_image):"""直方图匹配(规定化)"""# 计算累积分布函数def calculate_cdf(hist):cdf = hist.cumsum()cdf_normalized = cdf / cdf[-1]return cdf_normalized# 计算源图像和参考图像的直方图src_hist = cv2.calcHist([self.gray], [0], None, [256], [0, 256])ref_hist = cv2.calcHist([reference_image], [0], None, [256], [0, 256])# 计算CDFsrc_cdf = calculate_cdf(src_hist)ref_cdf = calculate_cdf(ref_hist)# 建立映射表lookup_table = np.zeros(256)for src_pixel_val in range(256):lookup_val = src_cdf[src_pixel_val]for ref_pixel_val in range(256):if ref_cdf[ref_pixel_val] >= lookup_val:lookup_table[src_pixel_val] = ref_pixel_valbreak# 应用映射matched = cv2.LUT(self.gray, lookup_table.astype('uint8'))return matcheddef demonstrate_histogram_operations(self):"""演示所有直方图操作"""fig = plt.figure(figsize=(18, 12))# 原始图像及其直方图plt.subplot(3, 4, 1)plt.imshow(cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB))plt.title('原始图像')plt.axis('off')plt.subplot(3, 4, 2)hist = cv2.calcHist([self.gray], [0], None, [256], [0, 256])plt.plot(hist)plt.title('原始直方图')plt.xlabel('像素值')plt.ylabel('频数')plt.grid(True)# 直方图均衡化eq_gray, eq_color = self.histogram_equalization()plt.subplot(3, 4, 3)plt.imshow(cv2.cvtColor(eq_color, cv2.COLOR_BGR2RGB))plt.title('直方图均衡化')plt.axis('off')plt.subplot(3, 4, 4)hist_eq = cv2.calcHist([eq_gray], [0], None, [256], [0, 256])plt.plot(hist_eq)plt.title('均衡化后直方图')plt.xlabel('像素值')plt.ylabel('频数')plt.grid(True)# CLAHEclahe_gray, clahe_color = self.adaptive_histogram_equalization()plt.subplot(3, 4, 5)plt.imshow(cv2.cvtColor(clahe_color, cv2.COLOR_BGR2RGB))plt.title('CLAHE')plt.axis('off')plt.subplot(3, 4, 6)hist_clahe = cv2.calcHist([clahe_gray], [0], None, [256], [0, 256])plt.plot(hist_clahe)plt.title('CLAHE后直方图')plt.xlabel('像素值')plt.ylabel('频数')plt.grid(True)# 对比度调整对比plt.subplot(3, 4, 7)plt.imshow(self.gray, cmap='gray')plt.title('原始灰度图')plt.axis('off')plt.subplot(3, 4, 8)plt.imshow(eq_gray, cmap='gray')plt.title('全局均衡化')plt.axis('off')plt.subplot(3, 4, 9)plt.imshow(clahe_gray, cmap='gray')plt.title('自适应均衡化')plt.axis('off')# 累积分布函数对比plt.subplot(3, 4, 10)hist_orig = cv2.calcHist([self.gray], [0], None, [256], [0, 256])cdf_orig = hist_orig.cumsum()cdf_orig_normalized = cdf_orig / cdf_orig[-1]plt.plot(cdf_orig_normalized, label='原始')hist_eq = cv2.calcHist([eq_gray], [0], None, [256], [0, 256])cdf_eq = hist_eq.cumsum()cdf_eq_normalized = cdf_eq / cdf_eq[-1]plt.plot(cdf_eq_normalized, label='均衡化')plt.title('累积分布函数')plt.xlabel('像素值')plt.ylabel('累积概率')plt.legend()plt.grid(True)plt.tight_layout()plt.show()# 运行直方图操作演示
hist_demo = HistogramOperations()
hist_demo.demonstrate_histogram_operations()
2.2 图像增强技术
class ImageEnhancement:"""图像增强技术类"""def __init__(self, image_path=None):if image_path and cv2.imread(image_path) is not None:self.img = cv2.imread(image_path)else:self.img = self.create_test_image()def create_test_image(self):"""创建测试图像"""# 创建一个低对比度的测试图像img = np.zeros((400, 600, 3), dtype=np.uint8)# 添加一些形状cv2.rectangle(img, (50, 50), (250, 200), (60, 60, 60), -1)cv2.circle(img, (400, 150), 80, (90, 90, 90), -1)cv2.ellipse(img, (300, 300), (100, 50), 0, 0, 180, (120, 120, 120), -1)# 添加文字cv2.putText(img, 'Low Contrast', (200, 350),cv2.FONT_HERSHEY_SIMPLEX, 1, (100, 100, 100), 2)return imgdef adjust_brightness_contrast(self, alpha=1.0, beta=0):"""调整亮度和对比度alpha: 对比度控制 (1.0-3.0)beta: 亮度控制 (0-100)"""adjusted = cv2.convertScaleAbs(self.img, alpha=alpha, beta=beta)return adjusteddef gamma_correction(self, gamma=1.0):"""伽马校正gamma < 1: 图像变亮gamma > 1: 图像变暗"""# 构建查找表inv_gamma = 1.0 / gammatable = np.array([((i / 255.0) ** inv_gamma) * 255for i in np.arange(0, 256)]).astype("uint8")# 应用查找表corrected = cv2.LUT(self.img, table)return correcteddef logarithmic_transformation(self):"""对数变换"""# 转换为浮点数img_float = self.img.astype(np.float32) / 255.0# 对数变换c = 1.0log_transformed = c * np.log(1 + img_float)# 归一化到0-255log_transformed = np.uint8(255 * log_transformed / np.max(log_transformed))return log_transformeddef histogram_stretching(self):"""直方图拉伸(对比度拉伸)"""# 分离通道channels = cv2.split(self.img)stretched_channels = []for channel in channels:# 计算当前最小值和最大值min_val = np.min(channel)max_val = np.max(channel)# 拉伸到0-255if max_val > min_val:stretched = ((channel - min_val) / (max_val - min_val) * 255).astype(np.uint8)else:stretched = channelstretched_channels.append(stretched)# 合并通道stretched = cv2.merge(stretched_channels)return stretcheddef unsharp_masking(self, radius=5, amount=1.0):"""非锐化掩模"""# 高斯模糊blurred = cv2.GaussianBlur(self.img, (radius*2+1, radius*2+1), 0)# 计算细节层detail = cv2.subtract(self.img, blurred)# 增强细节sharpened = cv2.addWeighted(self.img, 1, detail, amount, 0)return sharpeneddef white_balance_gray_world(self):"""灰度世界白平衡"""# 计算各通道平均值b, g, r = cv2.split(self.img)avg_b = np.mean(b)avg_g = np.mean(g)avg_r = np.mean(r)# 计算整体平均avg_gray = (avg_b + avg_g + avg_r) / 3# 计算缩放因子scale_b = avg_gray / avg_b if avg_b != 0 else 1scale_g = avg_gray / avg_g if avg_g != 0 else 1scale_r = avg_gray / avg_r if avg_r != 0 else 1# 应用缩放balanced = cv2.merge([np.clip(b * scale_b, 0, 255).astype(np.uint8),np.clip(g * scale_g, 0, 255).astype(np.uint8),np.clip(r * scale_r, 0, 255).astype(np.uint8)])return balanceddef demonstrate_enhancements(self):"""演示所有增强技术"""fig = plt.figure(figsize=(18, 12))# 原始图像plt.subplot(3, 4, 1)plt.imshow(cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB))plt.title('原始图像')plt.axis('off')# 亮度/对比度调整plt.subplot(3, 4, 2)enhanced1 = self.adjust_brightness_contrast(1.5, 30)plt.imshow(cv2.cvtColor(enhanced1, cv2.COLOR_BGR2RGB))plt.title('对比度+亮度')plt.axis('off')# 伽马校正(变亮)plt.subplot(3, 4, 3)gamma_bright = self.gamma_correction(0.5)plt.imshow(cv2.cvtColor(gamma_bright, cv2.COLOR_BGR2RGB))plt.title('伽马校正 (γ=0.5)')plt.axis('off')# 伽马校正(变暗)plt.subplot(3, 4, 4)gamma_dark = self.gamma_correction(2.0)plt.imshow(cv2.cvtColor(gamma_dark, cv2.COLOR_BGR2RGB))plt.title('伽马校正 (γ=2.0)')plt.axis('off')# 对数变换plt.subplot(3, 4, 5)log_trans = self.logarithmic_transformation()plt.imshow(cv2.cvtColor(log_trans, cv2.COLOR_BGR2RGB))plt.title('对数变换')plt.axis('off')# 直方图拉伸plt.subplot(3, 4, 6)stretched = self.histogram_stretching()plt.imshow(cv2.cvtColor(stretched, cv2.COLOR_BGR2RGB))plt.title('直方图拉伸')plt.axis('off')# 非锐化掩模plt.subplot(3, 4, 7)sharpened = self.unsharp_masking(3, 1.5)plt.imshow(cv2.cvtColor(sharpened, cv2.COLOR_BGR2RGB))plt.title('非锐化掩模')plt.axis('off')# 白平衡plt.subplot(3, 4, 8)balanced = self.white_balance_gray_world()plt.imshow(cv2.cvtColor(balanced, cv2.COLOR_BGR2RGB))plt.title('白平衡')plt.axis('off')# 组合增强plt.subplot(3, 4, 9)combined = self.adjust_brightness_contrast(1.2, 20)combined = self.gamma_correction(0.8)combined = self.unsharp_masking(2, 0.5)plt.imshow(cv2.cvtColor(combined, cv2.COLOR_BGR2RGB))plt.title('组合增强')plt.axis('off')# 显示直方图对比plt.subplot(3, 4, 10)gray_orig = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)hist_orig = cv2.calcHist([gray_orig], [0], None, [256], [0, 256])plt.plot(hist_orig, label='原始', alpha=0.7)gray_enhanced = cv2.cvtColor(enhanced1, cv2.COLOR_BGR2GRAY)hist_enhanced = cv2.calcHist([gray_enhanced], [0], None, [256], [0, 256])plt.plot(hist_enhanced, label='增强后', alpha=0.7)plt.title('直方图对比')plt.legend()plt.grid(True)plt.tight_layout()plt.show()# 运行图像增强演示
enhance_demo = ImageEnhancement()
enhance_demo.demonstrate_enhancements()
三、形态学操作
形态学操作是基于形状的图像处理操作,主要用于提取图像成分。
class MorphologicalOperations:"""形态学操作类"""def __init__(self):self.create_test_images()def create_test_images(self):"""创建用于形态学操作的测试图像"""# 创建二值图像self.binary_img = np.zeros((400, 600), dtype=np.uint8)# 添加不同的形状cv2.rectangle(self.binary_img, (50, 50), (200, 150), 255, -1)cv2.circle(self.binary_img, (350, 100), 50, 255, -1)cv2.ellipse(self.binary_img, (150, 300), (80, 40), 0, 0, 360, 255, -1)# 添加噪声noise = np.random.random((400, 600))self.noisy_img = self.binary_img.copy()self.noisy_img[noise > 0.95] = 255 # 添加白噪声self.noisy_img[noise < 0.05] = 0 # 添加黑噪声# 创建带细线的图像self.line_img = np.zeros((400, 600), dtype=np.uint8)cv2.line(self.line_img, (100, 100), (500, 100), 255, 1)cv2.line(self.line_img, (100, 200), (500, 300), 255, 2)cv2.line(self.line_img, (300, 50), (300, 350), 255, 3)def basic_operations(self, kernel_size=3):"""基本形态学操作"""# 创建结构元素kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))# 腐蚀erosion = cv2.erode(self.binary_img, kernel, iterations=1)# 膨胀dilation = cv2.dilate(self.binary_img, kernel, iterations=1)# 开运算(先腐蚀后膨胀)opening = cv2.morphologyEx(self.binary_img, cv2.MORPH_OPEN, kernel)# 闭运算(先膨胀后腐蚀)closing = cv2.morphologyEx(self.binary_img, cv2.MORPH_CLOSE, kernel)return erosion, dilation, opening, closingdef advanced_operations(self):"""高级形态学操作"""kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))# 形态学梯度gradient = cv2.morphologyEx(self.binary_img, cv2.MORPH_GRADIENT, kernel)# 顶帽运算tophat = cv2.morphologyEx(self.binary_img, cv2.MORPH_TOPHAT, kernel)# 黑帽运算blackhat = cv2.morphologyEx(self.binary_img, cv2.MORPH_BLACKHAT, kernel)# 击中击不中变换kernel_hit = np.array([[0, 1, 0],[1, 1, 1],[0, 1, 0]], dtype=np.uint8)hitmiss = cv2.morphologyEx(self.binary_img, cv2.MORPH_HITMISS, kernel_hit)return gradient, tophat, blackhat, hitmissdef noise_removal(self):"""使用形态学操作去噪"""kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))# 开运算去除白噪声denoised1 = cv2.morphologyEx(self.noisy_img, cv2.MORPH_OPEN, kernel)# 闭运算填充黑噪声denoised2 = cv2.morphologyEx(denoised1, cv2.MORPH_CLOSE, kernel)return denoised2def skeleton_extraction(self):"""骨架提取"""img = self.binary_img.copy()skeleton = np.zeros_like(img)# 获取结构元素kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))# 迭代细化while True:# 腐蚀eroded = cv2.erode(img, kernel)# 开运算temp = cv2.dilate(eroded, kernel)# 做差temp = cv2.subtract(img, temp)# 添加到骨架skeleton = cv2.bitwise_or(skeleton, temp)img = eroded.copy()if cv2.countNonZero(img) == 0:breakreturn skeletondef demonstrate_morphology(self):"""演示形态学操作"""fig = plt.figure(figsize=(18, 14))# 原始图像plt.subplot(4, 5, 1)plt.imshow(self.binary_img, cmap='gray')plt.title('原始二值图像')plt.axis('off')# 基本操作erosion, dilation, opening, closing = self.basic_operations(3)plt.subplot(4, 5, 2)plt.imshow(erosion, cmap='gray')plt.title('腐蚀')plt.axis('off')plt.subplot(4, 5, 3)plt.imshow(dilation, cmap='gray')plt.title('膨胀')plt.axis('off')plt.subplot(4, 5, 4)plt.imshow(opening, cmap='gray')plt.title('开运算')plt.axis('off')plt.subplot(4, 5, 5)plt.imshow(closing, cmap='gray')plt.title('闭运算')plt.axis('off')# 高级操作gradient, tophat, blackhat, hitmiss = self.advanced_operations()plt.subplot(4, 5, 6)plt.imshow(gradient, cmap='gray')plt.title('形态学梯度')plt.axis('off')plt.subplot(4, 5, 7)plt.imshow(tophat, cmap='gray')plt.title('顶帽运算')plt.axis('off')plt.subplot(4, 5, 8)plt.imshow(blackhat, cmap='gray')plt.title('黑帽运算')plt.axis('off')# 噪声图像处理plt.subplot(4, 5, 9)plt.imshow(self.noisy_img, cmap='gray')plt.title('噪声图像')plt.axis('off')plt.subplot(4, 5, 10)denoised = self.noise_removal()plt.imshow(denoised, cmap='gray')plt.title('去噪结果')plt.axis('off')# 不同结构元素的效果kernels = {'RECT': cv2.MORPH_RECT,'ELLIPSE': cv2.MORPH_ELLIPSE,'CROSS': cv2.MORPH_CROSS}for i, (name, shape) in enumerate(kernels.items(), 11):kernel = cv2.getStructuringElement(shape, (5, 5))result = cv2.morphologyEx(self.binary_img, cv2.MORPH_GRADIENT, kernel)plt.subplot(4, 5, i)plt.imshow(result, cmap='gray')plt.title(f'梯度 ({name})')plt.axis('off')# 骨架提取plt.subplot(4, 5, 14)plt.imshow(self.binary_img, cmap='gray')plt.title('原始图像')plt.axis('off')plt.subplot(4, 5, 15)skeleton = self.skeleton_extraction()plt.imshow(skeleton, cmap='gray')plt.title('骨架提取')plt.axis('off')# 组合操作示例plt.subplot(4, 5, 16)kernel1 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))kernel2 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))temp = cv2.morphologyEx(self.noisy_img, cv2.MORPH_CLOSE, kernel1)combined = cv2.morphologyEx(temp, cv2.MORPH_OPEN, kernel2)plt.imshow(combined, cmap='gray')plt.title('组合操作')plt.axis('off')plt.tight_layout()plt.show()# 运行形态学操作演示
morph_demo = MorphologicalOperations()
morph_demo.demonstrate_morphology()
四、特征检测与描述
4.1 角点检测
class FeatureDetection:"""特征检测类"""def __init__(self):self.create_test_image()def create_test_image(self):"""创建包含各种特征的测试图像"""self.img = np.ones((500, 700, 3), dtype=np.uint8) * 255# 添加各种形状cv2.rectangle(self.img, (50, 50), (250, 200), (0, 0, 0), 2)cv2.circle(self.img, (400, 150), 80, (0, 0, 0), 2)# 添加棋盘格checker_size = 30for i in range(5):for j in range(5):if (i + j) % 2 == 0:x, y = 450 + j*checker_size, 300 + i*checker_sizecv2.rectangle(self.img, (x, y), (x+checker_size, y+checker_size), (0, 0, 0), -1)# 添加一些线条形成角点points = np.array([[100, 300], [200, 350], [150, 450], [50, 400]], np.int32)cv2.polylines(self.img, [points], True, (0, 0, 0), 2)self.gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)def harris_corner_detection(self):"""Harris角点检测"""# Harris角点检测dst = cv2.cornerHarris(self.gray, blockSize=2, ksize=3, k=0.04)# 膨胀以标记角点dst = cv2.dilate(dst, None)# 阈值化ret, dst = cv2.threshold(dst, 0.01*dst.max(), 255, 0)dst = np.uint8(dst)# 找到角点位置ret, labels, stats, centroids = cv2.connectedComponentsWithStats(dst)# 绘制角点result = self.img.copy()criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.001)corners = cv2.cornerSubPix(self.gray, np.float32(centroids), (5, 5), (-1, -1), criteria)for corner in corners[1:]: # 跳过背景x, y = cornercv2.circle(result, (int(x), int(y)), 5, (0, 0, 255), -1)return result, dstdef shi_tomasi_detection(self):"""Shi-Tomasi角点检测(改进的Harris)"""# 检测角点corners = cv2.goodFeaturesToTrack(self.gray, maxCorners=100,qualityLevel=0.01,minDistance=10)# 绘制角点result = self.img.copy()if corners is not None:corners = np.int0(corners)for corner in corners:x, y = corner.ravel()cv2.circle(result, (x, y), 5, (0, 255, 0), -1)return result, cornersdef fast_detection(self):"""FAST特征点检测"""# 创建FAST检测器fast = cv2.FastFeatureDetector_create(threshold=20,nonmaxSuppression=True)# 检测关键点keypoints = fast.detect(self.gray, None)# 绘制关键点result = cv2.drawKeypoints(self.img, keypoints, None, color=(255, 0, 0), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)return result, keypointsdef orb_detection(self):"""ORB特征检测和描述"""# 创建ORB检测器orb = cv2.ORB_create(nfeatures=500)# 检测关键点和计算描述符keypoints, descriptors = orb.detectAndCompute(self.gray, None)# 绘制关键点result = cv2.drawKeypoints(self.img, keypoints, None, color=(0, 255, 255), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)return result, keypoints, descriptorsdef sift_detection(self):"""SIFT特征检测(需要opencv-contrib-python)"""try:# 创建SIFT检测器sift = cv2.SIFT_create(nfeatures=100)# 检测关键点和计算描述符keypoints, descriptors = sift.detectAndCompute(self.gray, None)# 绘制关键点(带方向和尺度)result = cv2.drawKeypoints(self.img, keypoints, None,flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)return result, keypoints, descriptorsexcept:print("SIFT不可用,请安装opencv-contrib-python")return self.img, None, Nonedef demonstrate_features(self):"""演示所有特征检测方法"""fig = plt.figure(figsize=(15, 10))# 原始图像plt.subplot(2, 3, 1)plt.imshow(cv2.cvtColor(self.img, cv2.COLOR_BGR2RGB))plt.title('原始图像')plt.axis('off')# Harris角点harris_result, harris_dst = self.harris_corner_detection()plt.subplot(2, 3, 2)plt.imshow(cv2.cvtColor(harris_result, cv2.COLOR_BGR2RGB))plt.title('Harris角点检测')plt.axis('off')# Shi-Tomasi角点shi_result, _ = self.shi_tomasi_detection()plt.subplot(2, 3, 3)plt.imshow(cv2.cvtColor(shi_result, cv2.COLOR_BGR2RGB))plt.title('Shi-Tomasi角点检测')plt.axis('off')# FAST特征点fast_result, _ = self.fast_detection()plt.subplot(2, 3, 4)plt.imshow(cv2.cvtColor(fast_result, cv2.COLOR_BGR2RGB))plt.title('FAST特征检测')plt.axis('off')# ORB特征orb_result, _, _ = self.orb_detection()plt.subplot(2, 3, 5)plt.imshow(cv2.cvtColor(orb_result, cv2.COLOR_BGR2RGB))plt.title('ORB特征检测')plt.axis('off')# SIFT特征sift_result, _, _ = self.sift_detection()plt.subplot(2, 3, 6)plt.imshow(cv2.cvtColor(sift_result, cv2.COLOR_BGR2RGB))plt.title('SIFT特征检测')plt.axis('off')plt.tight_layout()plt.show()# 运行特征检测演示
feature_demo = FeatureDetection()
feature_demo.demonstrate_features()
五、实战项目:全景图像拼接
现在让我们综合运用所学知识,实现一个全景图像拼接应用。
import cv2
import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tupleclass PanoramaStitcher:"""全景图像拼接类"""def __init__(self):# 特征检测器和匹配器self.detector = cv2.SIFT_create() # 或使用 cv2.ORB_create()# 特征匹配器FLANN_INDEX_KDTREE = 1index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)search_params = dict(checks=50)self.matcher = cv2.FlannBasedMatcher(index_params, search_params)self.images = []self.keypoints_list = []self.descriptors_list = []def create_test_images(self):"""创建测试用的重叠图像"""# 创建基础场景base_img = np.ones((400, 800, 3), dtype=np.uint8) * 255# 添加渐变背景for i in range(400):base_img[i, :] = [255 - i//2, 200, 100 + i//3]# 添加一些特征cv2.rectangle(base_img, (100, 100), (300, 300), (0, 255, 0), 3)cv2.circle(base_img, (500, 200), 80, (255, 0, 0), 3)cv2.putText(base_img, 'PANORAMA', (250, 200),cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 3)# 添加网格作为特征点for x in range(0, 800, 50):cv2.line(base_img, (x, 0), (x, 400), (200, 200, 200), 1)for y in range(0, 400, 50):cv2.line(base_img, (0, y), (800, y), (200, 200, 200), 1)# 创建三个重叠的视角img1 = base_img[:, 0:400].copy()img2 = base_img[:, 200:600].copy()img3 = base_img[:, 400:800].copy()return [img1, img2, img3]def add_images(self, images: List[np.ndarray]):"""添加要拼接的图像"""self.images = imagesself.detect_features()def detect_features(self):"""检测所有图像的特征点"""self.keypoints_list = []self.descriptors_list = []for img in self.images:gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)kp, des = self.detector.detectAndCompute(gray, None)self.keypoints_list.append(kp)self.descriptors_list.append(des)print(f"检测到 {len(kp)} 个特征点")def match_features(self, idx1: int, idx2: int) -> Tuple[List, np.ndarray, np.ndarray]:"""匹配两幅图像的特征点"""des1 = self.descriptors_list[idx1]des2 = self.descriptors_list[idx2]# 使用FLANN匹配matches = self.matcher.knnMatch(des1, des2, k=2)# 使用Lowe's ratio test筛选好的匹配good_matches = []for match_pair in matches:if len(match_pair) == 2:m, n = match_pairif m.distance < 0.7 * n.distance:good_matches.append(m)print(f"找到 {len(good_matches)} 个好的匹配点")if len(good_matches) < 4:return [], None, None# 提取匹配点src_pts = np.float32([self.keypoints_list[idx1][m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)dst_pts = np.float32([self.keypoints_list[idx2][m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)return good_matches, src_pts, dst_ptsdef find_homography(self, src_pts: np.ndarray, dst_pts: np.ndarray) -> np.ndarray:"""计算单应性矩阵"""M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)return M, maskdef warp_images(self, img1: np.ndarray, img2: np.ndarray, H: np.ndarray) -> np.ndarray:"""根据单应性矩阵拼接图像"""h1, w1 = img1.shape[:2]h2, w2 = img2.shape[:2]# 获取图像1的四个角点corners1 = np.float32([[0, 0], [w1, 0], [w1, h1], [0, h1]]).reshape(-1, 1, 2)# 变换角点corners1_transformed = cv2.perspectiveTransform(corners1, H)# 获取图像2的四个角点corners2 = np.float32([[0, 0], [w2, 0], [w2, h2], [0, h2]]).reshape(-1, 1, 2)# 合并所有角点all_corners = np.concatenate((corners1_transformed, corners2), axis=0)# 找到边界[x_min, y_min] = np.int32(all_corners.min(axis=0).ravel() - 0.5)[x_max, y_max] = np.int32(all_corners.max(axis=0).ravel() + 0.5)# 平移矩阵translation_dist = [-x_min, -y_min]H_translation = np.array([[1, 0, translation_dist[0]],[0, 1, translation_dist[1]],[0, 0, 1]])# 变换图像output_img = cv2.warpPerspective(img1, H_translation.dot(H),(x_max - x_min, y_max - y_min))# 将img2复制到结果图像output_img[translation_dist[1]:h2 + translation_dist[1],translation_dist[0]:w2 + translation_dist[0]] = img2return output_imgdef blend_images(self, img1: np.ndarray, img2: np.ndarray, H: np.ndarray) -> np.ndarray:"""使用渐变混合拼接图像"""h1, w1 = img1.shape[:2]h2, w2 = img2.shape[:2]# 获取输出图像大小corners1 = np.float32([[0, 0], [w1, 0], [w1, h1], [0, h1]]).reshape(-1, 1, 2)corners1_transformed = cv2.perspectiveTransform(corners1, H)corners2 = np.float32([[0, 0], [w2, 0], [w2, h2], [0, h2]]).reshape(-1, 1, 2)all_corners = np.concatenate((corners1_transformed, corners2), axis=0)[x_min, y_min] = np.int32(all_corners.min(axis=0).ravel() - 0.5)[x_max, y_max] = np.int32(all_corners.max(axis=0).ravel() + 0.5)translation_dist = [-x_min, -y_min]H_translation = np.array([[1, 0, translation_dist[0]],[0, 1, translation_dist[1]],[0, 0, 1]])# 创建掩膜output_shape = (y_max - y_min, x_max - x_min)# 变换img1warped_img1 = cv2.warpPerspective(img1, H_translation.dot(H),(output_shape[1], output_shape[0]))warped_mask1 = cv2.warpPerspective(np.ones(img1.shape[:2], dtype=np.uint8) * 255,H_translation.dot(H),(output_shape[1], output_shape[0]))# 创建img2的掩膜mask2 = np.zeros(output_shape, dtype=np.uint8)mask2[translation_dist[1]:h2 + translation_dist[1],translation_dist[0]:w2 + translation_dist[0]] = 255# 创建img2的完整图像warped_img2 = np.zeros((output_shape[0], output_shape[1], 3), dtype=np.uint8)warped_img2[translation_dist[1]:h2 + translation_dist[1],translation_dist[0]:w2 + translation_dist[0]] = img2# 找到重叠区域overlap = cv2.bitwise_and(warped_mask1, mask2)# 创建距离变换用于混合dist1 = cv2.distanceTransform(warped_mask1, cv2.DIST_L2, 5)dist2 = cv2.distanceTransform(mask2, cv2.DIST_L2, 5)# 归一化权重dist1_norm = dist1 / (dist1 + dist2 + 1e-5)dist2_norm = dist2 / (dist1 + dist2 + 1e-5)# 混合图像blended = np.zeros_like(warped_img1)for c in range(3):blended[:, :, c] = (warped_img1[:, :, c] * dist1_norm + warped_img2[:, :, c] * dist2_norm).astype(np.uint8)return blendeddef stitch_pair(self, img1: np.ndarray, img2: np.ndarray, use_blending: bool = True) -> np.ndarray:"""拼接一对图像"""# 检测特征gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)kp1, des1 = self.detector.detectAndCompute(gray1, None)kp2, des2 = self.detector.detectAndCompute(gray2, None)# 匹配特征matches = self.matcher.knnMatch(des1, des2, k=2)good_matches = []for match_pair in matches:if len(match_pair) == 2:m, n = match_pairif m.distance < 0.7 * n.distance:good_matches.append(m)if len(good_matches) < 4:print("匹配点不足")return None# 提取匹配点src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)# 计算单应性矩阵H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)# 拼接图像if use_blending:result = self.blend_images(img1, img2, H)else:result = self.warp_images(img1, img2, H)return resultdef stitch_all(self, use_blending: bool = True) -> np.ndarray:"""拼接所有图像"""if len(self.images) < 2:print("需要至少2张图像")return None# 依次拼接result = self.images[0]for i in range(1, len(self.images)):result = self.stitch_pair(result, self.images[i], use_blending)if result is None:print(f"拼接第{i+1}张图像失败")return Nonereturn resultdef visualize_matches(self, idx1: int = 0, idx2: int = 1):"""可视化特征匹配"""good_matches, src_pts, dst_pts = self.match_features(idx1, idx2)if len(good_matches) == 0:print("没有找到匹配点")return# 绘制匹配img_matches = cv2.drawMatches(self.images[idx1], self.keypoints_list[idx1],self.images[idx2], self.keypoints_list[idx2],good_matches[:20], None,flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)plt.figure(figsize=(15, 6))plt.imshow(cv2.cvtColor(img_matches, cv2.COLOR_BGR2RGB))plt.title(f'特征匹配 (共{len(good_matches)}个匹配)')plt.axis('off')plt.show()# 创建全景拼接实例
def demonstrate_panorama():"""演示全景拼接"""stitcher = PanoramaStitcher()# 创建或加载测试图像test_images = stitcher.create_test_images()stitcher.add_images(test_images)# 显示原始图像fig = plt.figure(figsize=(15, 12))for i, img in enumerate(test_images):plt.subplot(3, 3, i+1)plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))plt.title(f'图像 {i+1}')plt.axis('off')# 可视化特征匹配plt.subplot(3, 1, 2)stitcher.visualize_matches(0, 1)# 拼接结果(无混合)result_no_blend = stitcher.stitch_all(use_blending=False)if result_no_blend is not None:plt.subplot(3, 2, 5)plt.imshow(cv2.cvtColor(result_no_blend, cv2.COLOR_BGR2RGB))plt.title('拼接结果(无混合)')plt.axis('off')# 拼接结果(带混合)result_blend = stitcher.stitch_all(use_blending=True)if result_blend is not None:plt.subplot(3, 2, 6)plt.imshow(cv2.cvtColor(result_blend, cv2.COLOR_BGR2RGB))plt.title('拼接结果(渐变混合)')plt.axis('off')plt.tight_layout()plt.show()return stitcher# 运行全景拼接演示
stitcher = demonstrate_panorama()
六、进阶应用:交互式全景拼接工具b
# 创建窗口cv2.namedWindow(self.window_name)cv2.createTrackbar("Blend Mode", self.window_name, 1, 1, self.on_blend_change)cv2.createTrackbar("Quality", self.window_name, 70,b 100, self.on_quality_change)self.blend_mode = Trueself.match_quality = 0.7def on_blend_change(self, value):"""混合模式改变回调"""self.blend_mode = bool(value)self.update_panorama()def on_quality_change(self, value):"""匹配质量改变回调"""self.match_quality = value / 100.0self.update_panorama()def load_images(self, image_paths=None):"""加载图像"""if image_paths is None:# 使用测试图像self.images = self.stitcher.create_test_images()else:self.images = []for path in image_paths:img = cv2.imread(path)if img is not None:# 调整大小以提高性能height, width = img.shape[:2]if width > 800:scale = 800 / widthnew_width = int(width * scale)new_height = int(height * scale)img = cv2.resize(img, (new_width, new_height))self.images.append(img)self.stitcher.add_images(self.images)print(f"加载了 {len(self.images)} 张图像")def update_panorama(self):"""更新全景图"""if len(self.images) < 2:return# 拼接图像result = self.stitcher.stitch_all(use_blending=self.blend_mode)if result is not None:# 调整显示大小height, width = result.shape[:2]max_width = 1200if width > max_width:scale = max_width / widthnew_width = int(width * scale)new_height = int(height * scale)result = cv2.resize(result, (new_width, new_height))# 添加信息文字info_text = f"Images: {len(self.images)} | " \f"Blending: {'ON' if self.blend_mode else 'OFF'} | " \f"Quality: {self.match_quality:.2f}"cv2.putText(result, info_text, (10, 30),cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)cv2.imshow(self.window_name, result)def save_result(self, filename="panorama_result.jpg"):"""保存结果"""result = self.stitcher.stitch_all(use_blending=self.blend_mode)if result is not None:cv2.imwrite(filename, result)print(f"保存成功: {filename}")def run(self):"""运行交互式工具"""print("全景拼接工具")print("-" * 40)print("操作说明:")print("1. 调整滑动条改变参数")print("2. 按 's' 保存结果")print("3. 按 'r' 重新加载")print("4. 按 'ESC' 退出")print("-" * 40)self.load_images()self.update_panorama()while True:key = cv2.waitKey(1) & 0xFFif key == 27: # ESCbreakelif key == ord('s'):self.save_result()elif key == ord('r'):self.load_images()self.update_panorama()cv2.destroyAllWindows()
运行交互式工具
if name == “main”:
tool = InteractivePanoramaTool()
tool.run()
## 七、总结与展望### 本文总结在这篇进阶教程中,我们深入学习了:1. ✅ **图像几何变换**- 基础变换:缩放、平移、旋转、翻转- 仿射变换:保持平行性的变换- 透视变换:3D投影变换2. ✅ **图像增强技术**- 直方图操作:均衡化、CLAHE、匹配- 亮度对比度调整- 伽马校正与对数变换3. ✅ **形态学操作**- 基本操作:腐蚀、膨胀、开闭运算- 高级操作:梯度、顶帽、黑帽- 实际应用:去噪、骨架提取4. ✅ **特征检测**- 角点检测:Harris、Shi-Tomasi- 特征检测:FAST、ORB、SIFT- 特征描述与匹配5. ✅ **实战项目**- 全景图像拼接完整实现- 特征匹配与单应性变换- 图像混合技术### 关键技术要点1. **几何变换矩阵**:理解2x3仿射矩阵和3x3透视矩阵的意义
2. **直方图均衡化**:改善图像对比度的有效方法
3. **形态学核**:不同形状的结构元素产生不同效果
4. **特征匹配**:使用比率测试筛选好的匹配点
5. **单应性矩阵**:连接两个平面的投影变换### 下一篇预告在系列的第三篇文章中,我们将探索:- **深度学习集成**:使用OpenCV的DNN模块
- **目标检测**:YOLO、SSD实现
- **人脸识别**:检测、识别、关键点定位
- **目标跟踪**:多种跟踪算法对比
- **视频处理**:光流、背景建模
- **实战项目**:智能视频监控系统### 练习建议1. **基础练习**- 实现文档扫描矫正功能- 制作图像增强工具- 尝试不同的形态学操作组合2. **进阶练习**- 改进全景拼接算法,处理曝光差异- 实现自动裁剪功能- 添加图像配准功能3. **挑战项目**- 制作360度全景查看器- 实现增强现实标记识别- 开发图像自动增强系统### 学习资源- OpenCV官方教程:https://docs.opencv.org/master/d9/df8/tutorial_root.html
- 计算机视觉算法详解:https://www.learnopencv.com/
- GitHub代码示例:https://github.com/opencv/opencv-python---**作者寄语**:图像处理和计算机视觉是一个充满挑战和机遇的领域。通过本文的学习,你已经掌握了许多强大的图像处理技术。全景拼接只是这些技术应用的冰山一角,希望你能在实践中发现更多有趣的应用场景。继续探索,继续创造!**感谢阅读!下期再见!** 🚀