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

---

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

**感谢阅读!下期再见!** 🚀
相关推荐
奔跑中的小象4 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南4 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang4 小时前
LangGraph 状态机实战
人工智能
刹那芳华19925 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作5 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门5 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜5 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟5 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦5 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生6 小时前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能