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