import cv2
import numpy as np
import os
import re
from tqdm import tqdm # 用于显示进度条
def resize_image(img, max_dimension=1024):
"""
将图片缩小到最大边长为 max_dimension
"""
height, width = img.shape[:2]
if max(height, width) > max_dimension:
scale = max_dimension / max(height, width)
img = cv2.resize(img, (int(width * scale), int(height * scale)), interpolation=cv2.INTER_AREA)
return img
def find_homography_and_warp(img1, img2):
"""
计算单应性矩阵并变换第二张图片
"""
# 使用 AKAZE 特征检测算法
akaze = cv2.AKAZE_create()
kp1, desc1 = akaze.detectAndCompute(img1, None)
kp2, desc2 = akaze.detectAndCompute(img2, None)
# 使用 FLANN 匹配器
FLANN_INDEX_LSH = 6
index_params = dict(algorithm=FLANN_INDEX_LSH, table_number=6, key_size=12, multi_probe_level=1)
search_params = dict(checks=50)
flann = cv2.FlannBasedMatcher(index_params, search_params)
# 匹配描述符
matches = flann.knnMatch(desc1, desc2, k=2)
# 筛选好的匹配
good = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
good.append(m)
# 至少需要 4 个匹配点
if len(good) < 4:
raise ValueError("匹配点不足,无法计算单应性矩阵")
# 获取匹配点的位置
pt1 = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
pt2 = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
# 计算单应性矩阵
H, mask = cv2.findHomography(pt2, pt1, cv2.RANSAC, ransacReprojThreshold=4.0)
# 使用单应性矩阵变换第二张图片
height = img1.shape[0] + img2.shape[0]
width = img1.shape[1] + img2.shape[1]
canvas = np.zeros((height, width, 3), np.uint8)
warped_img2 = cv2.warpPerspective(img2, H, (canvas.shape[1], canvas.shape[0]))
return warped_img2
def multi_band_blending(img1, img2):
"""
使用多频段融合消除鬼影
"""
# 创建多频段融合器
blender = cv2.detail_MultiBandBlender()
blender.prepare((0, 0, max(img1.shape[1], img2.shape[1]), max(img1.shape[0], img2.shape[0])))
# 创建单通道的 mask
mask1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
mask1 = (mask1 > 0).astype(np.uint8) * 255 # 将非零区域设置为 255
mask2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
mask2 = (mask2 > 0).astype(np.uint8) * 255 # 将非零区域设置为 255
# 将图像添加到融合器
blender.feed(img1.astype(np.uint8), mask1, (0, 0)) # 使用 uint8 类型和单通道 mask
blender.feed(img2.astype(np.uint8), mask2, (0, 0)) # 使用 uint8 类型和单通道 mask
# 融合图像
result, _ = blender.blend(None, None)
return result.astype(np.uint8)
def crop_black_borders(img):
"""
裁剪黑色边缘
"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return img
x, y, w, h = cv2.boundingRect(contours[0])
return img[y:y+h, x:x+w]
def stitch_images(image_list):
"""
拼接多张图片
"""
if len(image_list) < 2:
raise ValueError("至少需要两张图片进行拼接")
# 初始化拼接结果为第一张图片
result = image_list[0]
# 依次拼接剩余的图片
for i in tqdm(range(1, len(image_list)), desc="拼接进度"):
img2 = image_list[i]
try:
warped_img2 = find_homography_and_warp(result, img2)
result = multi_band_blending(result, warped_img2) # 使用多频段融合
except ValueError as e:
print(f"跳过第 {i+1} 张图片:{e}")
continue
# 裁剪黑色边缘
result = crop_black_borders(result)
return result
def load_images_from_folder(folder, max_dimension=1024):
"""
从文件夹中加载所有图片,并按数字编号排序
"""
images = []
filenames = []
# 获取文件夹中所有图片文件名
for filename in os.listdir(folder):
if filename.endswith(('.jpg', '.jpeg', '.png', '.bmp')):
filenames.append(filename)
# 按数字编号排序
filenames.sort(key=lambda f: int(re.sub('\D', '', f)))
# 加载图片并缩小
for filename in filenames:
img_path = os.path.join(folder, filename)
img = cv2.imread(img_path)
if img is not None:
img = resize_image(img, max_dimension) # 缩小图片
images.append(img)
print(f"已加载图片: {filename}")
else:
print(f"无法加载图片: {filename}")
return images
# 从文件夹中加载图片
folder_path = "images" # 替换为你的图片文件夹路径
images = load_images_from_folder(folder_path, max_dimension=1024) # 限制图片最大边长为 1024
if len(images) < 2:
print("文件夹中至少需要两张图片进行拼接")
else:
# 拼接图片
final_image = stitch_images(images)
# 显示拼接结果
cv2.imshow('Stitched Image', final_image)
# 保存拼接结果
output_path = "hecheng.jpg"
cv2.imwrite(output_path, final_image)
print(f"拼接结果已保存为: {output_path}")
cv2.waitKey(0)
cv2.destroyAllWindows()