OpenCV进阶:图像变换、增强与特征检测实战

作者:AI技术分享

专栏:OpenCV计算机视觉实战

发布时间:2025年1月

前言

在上一篇文章中,我们学习了OpenCV的基础知识和图像滤波技术。今天,我们将深入探索更高级的图像处理技术:几何变换、图像增强、形态学操作和特征检测。这些技术是计算机视觉应用的核心,广泛应用于图像矫正、目标识别、图像拼接等领域。

本文的重点实战项目是全景图像拼接,我们将综合运用所学知识,实现一个可以将多张照片拼接成全景图的应用。

一、图像几何变换

1.1 基础变换:缩放、平移、旋转

几何变换是通过数学变换改变图像的空间位置关系。让我们从最基础的变换开始。

python 复制代码
import cv2
import numpy as np
import matplotlib.pyplot as plt

class 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 img
    
    def 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 scaled
    
    def 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 translated
    
    def 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_full
    
    def flip(self, flip_code):
        """
        图像翻转
        flip_code: 0-垂直翻转, 1-水平翻转, -1-同时翻转
        """
        flipped = cv2.flip(self.img, flip_code)
        return flipped
    
    def 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 仿射变换

仿射变换保持了直线的"平直性"和"平行性",包括平移、旋转、缩放和剪切。

python 复制代码
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 img
    
    def 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, M
    
    def 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) * 255
        text = f"仿射矩阵:\n{M3}"
        y0, dy = 50, 30
        for i, line in enumerate(text.split('\n')):
            y = y0 + i * dy
            cv2.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 透视变换

透视变换可以将图像投影到新的视平面,常用于矫正图像的透视畸变。

python 复制代码
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 = 8
        square_size = 40
        start_x, start_y = 150, 100
        
        for i in range(board_size):
            for j in range(board_size):
                if (i + j) % 2 == 0:
                    x1 = start_x + j * square_size
                    y1 = start_y + i * square_size
                    x2 = x1 + square_size
                    y2 = y1 + square_size
                    cv2.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, M
    
    def 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 = 400
                height = 300
                dst_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_pts
        
        return None, None
    
    def 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) * 255
        matrix_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, 40
        for 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 直方图操作

直方图是图像处理中的重要工具,用于分析和改善图像的亮度分布。

python 复制代码
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 img
    
    def calculate_histogram(self, image):
        """计算并绘制直方图"""
        if len(image.shape) == 2:
            # 灰度图
            hist = cv2.calcHist([image], [0], None, [256], [0, 256])
            return hist
        else:
            # 彩色图
            colors = ('b', 'g', 'r')
            hists = []
            for i, color in enumerate(colors):
                hist = cv2.calcHist([image], [i], None, [256], [0, 256])
                hists.append(hist)
            return hists
    
    def 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_color
    
    def 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_color
    
    def 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])
        
        # 计算CDF
        src_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_val
                    break
        
        # 应用映射
        matched = cv2.LUT(self.gray, lookup_table.astype('uint8'))
        
        return matched
    
    def 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)
        
        # CLAHE
        clahe_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 图像增强技术

python 复制代码
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 img
    
    def 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 adjusted
    
    def gamma_correction(self, gamma=1.0):
        """
        伽马校正
        gamma < 1: 图像变亮
        gamma > 1: 图像变暗
        """
        # 构建查找表
        inv_gamma = 1.0 / gamma
        table = np.array([((i / 255.0) ** inv_gamma) * 255
                         for i in np.arange(0, 256)]).astype("uint8")
        
        # 应用查找表
        corrected = cv2.LUT(self.img, table)
        return corrected
    
    def logarithmic_transformation(self):
        """对数变换"""
        # 转换为浮点数
        img_float = self.img.astype(np.float32) / 255.0
        
        # 对数变换
        c = 1.0
        log_transformed = c * np.log(1 + img_float)
        
        # 归一化到0-255
        log_transformed = np.uint8(255 * log_transformed / np.max(log_transformed))
        
        return log_transformed
    
    def 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-255
            if max_val > min_val:
                stretched = ((channel - min_val) / (max_val - min_val) * 255).astype(np.uint8)
            else:
                stretched = channel
            
            stretched_channels.append(stretched)
        
        # 合并通道
        stretched = cv2.merge(stretched_channels)
        return stretched
    
    def 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 sharpened
    
    def 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 1
        scale_g = avg_gray / avg_g if avg_g != 0 else 1
        scale_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 balanced
    
    def 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()

三、形态学操作

形态学操作是基于形状的图像处理操作,主要用于提取图像成分。

python 复制代码
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, closing
    
    def 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, hitmiss
    
    def 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 denoised2
    
    def 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:
                break
        
        return skeleton
    
    def 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 角点检测

python 复制代码
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 = 30
        for i in range(5):
            for j in range(5):
                if (i + j) % 2 == 0:
                    x, y = 450 + j*checker_size, 300 + i*checker_size
                    cv2.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 = corner
            cv2.circle(result, (int(x), int(y)), 5, (0, 0, 255), -1)
        
        return result, dst
    
    def 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, corners
    
    def 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, keypoints
    
    def 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, descriptors
    
    def 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, descriptors
        except:
            print("SIFT不可用,请安装opencv-contrib-python")
            return self.img, None, None
    
    def 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()

五、实战项目:全景图像拼接

现在让我们综合运用所学知识,实现一个全景图像拼接应用。

python 复制代码
import cv2
import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tuple

class PanoramaStitcher:
    """全景图像拼接类"""
    
    def __init__(self):
        # 特征检测器和匹配器
        self.detector = cv2.SIFT_create()  # 或使用 cv2.ORB_create()
        
        # 特征匹配器
        FLANN_INDEX_KDTREE = 1
        index_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 = images
        self.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_pair
                if 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_pts
    
    def 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, mask
    
    def 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]] = img2
        
        return output_img
    
    def 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)
        
        # 变换img1
        warped_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 blended
    
    def 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_pair
                if 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 result
    
    def 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 None
        
        return result
    
    def 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 = True
    self.match_quality = 0.7
    
def on_blend_change(self, value):
    """混合模式改变回调"""
    self.blend_mode = bool(value)
    self.update_panorama()

def on_quality_change(self, value):
    """匹配质量改变回调"""
    self.match_quality = value / 100.0
    self.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 / width
                    new_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 = 1200
        if width > max_width:
            scale = max_width / width
            new_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) & 0xFF
        
        if key == 27:  # ESC
            break
        elif 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

---

**作者寄语**:图像处理和计算机视觉是一个充满挑战和机遇的领域。通过本文的学习,你已经掌握了许多强大的图像处理技术。全景拼接只是这些技术应用的冰山一角,希望你能在实践中发现更多有趣的应用场景。继续探索,继续创造!

**感谢阅读!下期再见!** 🚀
相关推荐
说私域3 小时前
开源链动2+1模式、AI智能名片与S2B2C商城小程序:社群经济的数字化重构路径
人工智能·小程序·开源
lingchen19063 小时前
卷积神经网络中的卷积运算原理
深度学习·计算机视觉·cnn
rengang663 小时前
智能化的重构建议:大模型分析代码结构,提出可读性和性能优化建议
人工智能·性能优化·重构·ai编程
灵遁者书籍作品3 小时前
语言的拓扑学约束公理:语言对实在的描述具有拓扑不变量——某些真理必须通过悖论、沉默或隐喻表达
人工智能·计算机视觉
一尘之中3 小时前
觉醒的拓扑学:在量子纠缠与神经幻象中重构现实认知
人工智能·重构
金宗汉3 小时前
《宇宙递归拓扑学:基于自指性与拓扑流形的无限逼近模型》
大数据·人工智能·笔记·算法·观察者模式
Joy T4 小时前
海南蓝碳:生态财富与科技驱动的新未来
大数据·人工智能·红树林·海南省·生态区建设
N0nename4 小时前
TR3--Transformer之pytorch复现
人工智能·pytorch·python
北京耐用通信4 小时前
电力自动化新突破:Modbus如何变身Profinet?智能仪表连接的终极解决方案
人工智能·物联网·网络安全·自动化·信息与通信