感谢你提供的完整文件路径列表!我注意到你的路径中有一些 **明显的复制粘贴错误**(比如 `/Users/mac/Desktop/...` 被重复拼接),但从中我可以准确提取出:
---
## ✅ 你拥有的真实图像数据
### 📁 `grail` 序列(18 张)
```
grail00.jpg 到 grail17.jpg (共 18 张)
```
### 📁 `library` 序列(14 张)
```
1.jpg, 2.jpg, ..., 14.jpg
```
### 📁 `parrington` 序列(18 张)
```
prtn00.jpg 到 prtn17.jpg (共 18 张)
```
### 📁 `Xue-Mountain-Entrance` 序列(17 张)
```
DSC_0171.jpg 到 DSC_0187.jpg (跳过了 DSC_0181.jpg?共 17 张)
```
✅ 所有这些都在:
```
/Users/mac/Desktop/Problem_Set_2-3/Problem2Images/panoramas/
```
---
## ✅ 当前问题诊断
尽管你拥有所有图像,但可能仍然只生成了 **3 张成对图像的拼接结果**,而没有生成那 **4 个全景图**。
根据你给的日志片段,我发现路径出现了异常重复,例如:
> `/Users/mac/Desktop/Problem_Set_2-3/Problem2Images/panoramas/grail/grail05.jpg /Users/Users/mac/Desktop/...`
这说明你在手动复制路径时发生了错误 —— 但这不是程序的问题。
---
## ✅ 我将为你提供一个 **终极调试版脚本**
它会:
1. 自动扫描并列出每个场景下的所有图像
2. 明确告诉你“是否找到了图片”
3. 如果找不到,打印详细的排查建议
4. 拼接全部 **3 对 + 4 组全景 = 7 张图**
---
### ✅ 最终可运行代码(带详细调试输出)
```python
# homework2.py - 全功能图像拼接(支持6图+4序列)
import numpy as np
from scipy import ndimage, spatial
import cv2
import os
import time
from glob import glob
import re
# ---------------------------
# 路径配置(请确认与你的实际一致)
# ---------------------------
ROOT_DIR = '/Users/mac/Desktop/Problem_Set_2-3'
PROBLEM2_IMAGES_DIR = os.path.join(ROOT_DIR, 'Problem2Images')
PANORAMAS_ROOT = os.path.join(PROBLEM2_IMAGES_DIR, 'panoramas')
OUTPUT_DIR = os.path.join(ROOT_DIR, 'outputs')
os.makedirs(OUTPUT_DIR, exist_ok=True)
# ---------------------------
# 参数设置
# ---------------------------
HARRIS_K = 0.04
HARRIS_WIN = 5
HARRIS_TH_RATIO = 0.01
HARRIS_MIN_DIST = 6
PATCH_SIZE = 16
HOG_BINS = 8
RATIO_TEST = 0.75
RANSAC_ITERS = 2000
RANSAC_INLIER_TH = 4.0
DOWNSAMPLE_FOR_SPEED = 0.5 # 提高速度;设为 1.0 可获得更高精度
EPS = 1e-9
# ---------------------------
# 自然排序函数
# ---------------------------
def natural_sort_key(s):
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', s)]
# ---------------------------
# 加载图像
# ---------------------------
def load_image(path, scale=1.0):
if not os.path.isfile(path):
raise FileNotFoundError(f"Image not found: {path}")
img = cv2.imread(path)
if img is None:
raise ValueError(f"Cannot read image: {path}")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
if scale != 1.0:
h_new = int(img.shape[0] * scale)
w_new = int(img.shape[1] * scale)
img = cv2.resize(img, (w_new, h_new), interpolation=cv2.INTER_AREA)
return img
# ---------------------------
# Sobel 梯度
# ---------------------------
def gradient_x(img):
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) if img.ndim == 3 else img
return cv2.Sobel(gray.astype(np.float32), cv2.CV_32F, 1, 0, ksize=3, borderType=cv2.BORDER_REFLECT)
def gradient_y(img):
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) if img.ndim == 3 else img
return cv2.Sobel(gray.astype(np.float32), cv2.CV_32F, 0, 1, ksize=3, borderType=cv2.BORDER_REFLECT)
# ---------------------------
# Harris 角点响应
# ---------------------------
def harris_response(img, k=HARRIS_K, win_s=HARRIS_WIN):
Ix = gradient_x(img)
Iy = gradient_y(img)
sigma = max(win_s / 6.0, 0.5)
Ix2 = ndimage.gaussian_filter(Ix * Ix, sigma=sigma, mode='reflect')
Iy2 = ndimage.gaussian_filter(Iy * Iy, sigma=sigma, mode='reflect')
Ixy = ndimage.gaussian_filter(Ix * Iy, sigma=sigma, mode='reflect')
det = Ix2 * Iy2 - Ixy * Ixy
trace_sq = (Ix2 + Iy2) ** 2
R = det - k * trace_sq
return R
# ---------------------------
# 非极大值抑制选择角点
# ---------------------------
def corner_selection(R, th_ratio=HARRIS_TH_RATIO, min_dist=HARRIS_MIN_DIST):
H, W = R.shape
Rmax = np.max(R)
if Rmax <= 0:
return []
thresh = th_ratio * Rmax
R_thresh = (R > thresh) * R
size = 2 * min_dist + 1
local_max = ndimage.maximum_filter(R_thresh, size=size, mode='reflect')
mask = (R_thresh == local_max) & (R_thresh > 0)
coords = np.argwhere(mask)
responses = [R[r, c] for r, c in coords]
order = np.argsort(responses)[::-1]
selected = []
occupied = np.zeros((H, W), dtype=bool)
for idx in order:
r, c = coords[idx]
if occupied[r, c]:
continue
selected.append((int(c), int(r)))
r0 = max(0, r - min_dist); r1 = min(H, r + min_dist + 1)
c0 = max(0, c - min_dist); c1 = min(W, c + min_dist + 1)
occupied[r0:r1, c0:c1] = True
return selected
# ---------------------------
# HOG 描述子
# ---------------------------
def histogram_of_gradients(img, pix, patch_size=PATCH_SIZE, bins=HOG_BINS):
if len(pix) == 0:
return np.empty((0, bins), dtype=np.float32)
Ix = gradient_x(img)
Iy = gradient_y(img)
mag = np.sqrt(Ix * Ix + Iy * Iy) + EPS
ang = np.arctan2(Iy, Ix)
half = patch_size // 2
feats = []
for (x, y) in pix:
x = int(round(x)); y = int(round(y))
h, w = img.shape[:2]
x1 = max(0, x - half); x2 = min(w, x + half)
y1 = max(0, y - half); y2 = min(h, y + half)
pmag = mag[y1:y2, x1:x2].ravel()
pang = ang[y1:y2, x1:x2].ravel()
if pmag.size == 0:
feats.append(np.zeros(bins, dtype=np.float32))
continue
hist, _ = np.histogram(pang, bins=bins, range=(-np.pi, np.pi), weights=pmag)
norm = np.linalg.norm(hist) + EPS
hist = hist / norm
hist = np.clip(hist, 0, 0.2)
hist = hist / (np.linalg.norm(hist) + EPS)
feats.append(hist.astype(np.float32))
return np.stack(feats, axis=0)
# ---------------------------
# 特征匹配
# ---------------------------
def feature_matching(img1, img2, ratio=RATIO_TEST):
R1 = harris_response(img1)
R2 = harris_response(img2)
pts1 = corner_selection(R1)
pts2 = corner_selection(R2)
if len(pts1) < 4 or len(pts2) < 4:
raise ValueError(f"Not enough corners: {len(pts1)}, {len(pts2)}")
f1 = histogram_of_gradients(img1, pts1)
f2 = histogram_of_gradients(img2, pts2)
dist = spatial.distance.cdist(f1, f2, metric='euclidean')
matches1, matches2 = [], []
for i in range(dist.shape[0]):
if dist.shape[1] < 2:
continue
idxs = np.argsort(dist[i])[:2]
if dist[i, idxs[0]] / (dist[i, idxs[1]] + EPS) <= ratio:
j = idxs[0]
if np.argmin(dist[:, j]) == i:
matches1.append(pts1[i])
matches2.append(pts2[j])
if len(matches1) < 4:
raise ValueError("Not enough good matches")
return matches1, matches2
# ---------------------------
# 归一化点用于 DLT
# ---------------------------
def normalize_points(pts):
pts = np.asarray(pts, dtype=np.float64)
mean = pts.mean(axis=0)
std = pts.std(axis=0).mean()
s = np.sqrt(2) / (std + EPS)
T = np.array([[s, 0, -s * mean[0]],
[0, s, -s * mean[1]],
[0, 0, 1]])
pts_h = np.hstack([pts, np.ones((pts.shape[0], 1))])
norm_h = (T @ pts_h.T).T
norm = norm_h[:, :2] / (norm_h[:, 2:3] + EPS)
return norm, T
# ---------------------------
# 计算单应性矩阵
# ---------------------------
def compute_homography(pixels1, pixels2):
p1 = np.asarray(pixels1, dtype=np.float64)
p2 = np.asarray(pixels2, dtype=np.float64)
if p1.shape[0] < 4:
raise ValueError("Need at least 4 point pairs.")
p1n, T1 = normalize_points(p1)
p2n, T2 = normalize_points(p2)
A = []
for (x1, y1), (x2, y2) in zip(p1n, p2n):
A.append([-x1, -y1, -1, 0, 0, 0, x1*x2, y1*x2, x2])
A.append([0, 0, 0, -x1, -y1, -1, x1*y2, y1*y2, y2])
A = np.array(A)
_, _, Vt = np.linalg.svd(A)
H_norm = Vt[-1].reshape(3, 3)
H = np.linalg.inv(T2) @ H_norm @ T1
return H / H[2, 2]
# ---------------------------
# RANSAC
# ---------------------------
def align_pair(pixels1, pixels2, max_iters=RANSAC_ITERS, inlier_th=RANSAC_INLIER_TH):
p1 = np.asarray(pixels1, dtype=np.float64)
p2 = np.asarray(pixels2, dtype=np.float64)
N = p1.shape[0]
if N < 4:
raise ValueError("Need >=4 matches for RANSAC")
best_H = None
best_inliers = []
rng = np.random.default_rng(seed=42)
for it in range(max_iters):
idx = rng.choice(N, 4, replace=False)
try:
H = compute_homography(p1[idx], p2[idx])
except:
continue
p1h = np.hstack([p1, np.ones((N, 1))])
proj = (H @ p1h.T).T
proj_xy = proj[:, :2] / (proj[:, 2:3] + EPS)
dists = np.linalg.norm(proj_xy - p2, axis=1)
inliers = np.where(dists < inlier_th)[0]
if len(inliers) > len(best_inliers):
best_inliers = inliers
best_H = H
if len(inliers) > 0.85 * N:
break
if best_H is None or len(best_inliers) < 4:
raise RuntimeError("RANSAC failed")
best_H = compute_homography(p1[best_inliers], p2[best_inliers])
return best_H, best_inliers
# ---------------------------
# 图像融合
# ---------------------------
def create_distance_alpha(img):
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) if img.ndim == 3 else img
binary = ((gray > 0).astype(np.uint8)) * 255
dist = cv2.distanceTransform(binary, cv2.DIST_L2, 5).astype(np.float32)
if dist.max() > 0:
dist /= dist.max()
return dist
def stitch_blend(img_src, img_dst, H):
h_src, w_src = img_src.shape[:2]
h_dst, w_dst = img_dst.shape[:2]
corners_src = np.float32([[0, 0], [w_src, 0], [w_src, h_src], [0, h_src]])
corners_h = np.hstack([corners_src, np.ones((4, 1))])
proj = (H @ corners_h.T).T
proj_xy = proj[:, :2] / (proj[:, 2:3] + EPS)
all_x = np.concatenate([proj_xy[:, 0], [0, w_dst]])
all_y = np.concatenate([proj_xy[:, 1], [0, h_dst]])
x_min = int(np.floor(all_x.min()))
x_max = int(np.ceil(all_x.max()))
y_min = int(np.floor(all_y.min()))
y_max = int(np.ceil(all_y.max()))
out_w = x_max - x_min
out_h = y_max - y_min
if out_w <= 0 or out_h <= 0:
raise ValueError("Invalid canvas size.")
xs = np.arange(x_min, x_max)
ys = np.arange(y_min, y_max)
xv, yv = np.meshgrid(xs, ys)
grid = np.stack([xv.ravel(), yv.ravel(), np.ones_like(xv).ravel()], axis=0)
invH = np.linalg.inv(H + EPS)
src_coords = (invH @ grid).T
src_coords = src_coords[:, :2] / (src_coords[:, 2:3] + EPS)
map_x = src_coords[:, 0].reshape(out_h, out_w).astype(np.float32)
map_y = src_coords[:, 1].reshape(out_h, out_w).astype(np.float32)
warped_src = cv2.remap(
img_src, map_x, map_y,
interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT, borderValue=0
)
canvas = np.zeros((out_h, out_w, 3), dtype=np.uint8)
dx = -x_min
dy = -y_min
roi_h = min(h_dst, out_h - dy)
roi_w = min(w_dst, out_w - dx)
if roi_h > 0 and roi_w > 0:
canvas[dy:dy+roi_h, dx:dx+roi_w] = img_dst[:roi_h, :roi_w]
alpha_src = create_distance_alpha(warped_src)
alpha_dst = create_distance_alpha(canvas)
alpha_sum = alpha_src + alpha_dst
alpha_sum[alpha_sum == 0] = EPS
merged = (
warped_src.astype(np.float32) * alpha_src[..., None] +
canvas.astype(np.float32) * alpha_dst[..., None]
) / alpha_sum[..., None]
return np.clip(merged, 0, 255).astype(np.uint8)
# ---------------------------
# 裁剪黑边
# ---------------------------
def crop_black(img):
if img.ndim == 3:
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
else:
gray = img
coords = cv2.findNonZero(gray)
if coords is None or len(coords) == 0:
return img
x, y, w, h = cv2.boundingRect(coords)
return img[y:y+h, x:x+w]
# ---------------------------
# 生成全景图(顺序拼接)
# ---------------------------
def generate_panorama(image_list):
if len(image_list) < 2:
return image_list[0] if len(image_list) > 0 else None
pano = image_list[0].copy()
for i in range(1, len(image_list)):
img = image_list[i]
success = False
for attempt in ['normal', 'reverse']:
try:
if attempt == 'normal':
pts_pano, pts_img = feature_matching(pano, img)
H, _ = align_pair(pts_pano, pts_img)
pano = stitch_blend(pano, img, H)
else:
pts_img, pts_pano = feature_matching(img, pano)
H_ab, _ = align_pair(pts_img, pts_pano)
H_inv = np.linalg.inv(H_ab + EPS)
pano = stitch_blend(pano, img, H_inv)
success = True
break
except Exception as e:
if attempt == 'reverse':
print(f"[FAIL] Both directions failed at image {i}: {e}")
if not success:
print(f"[SKIP] Skipping image {i} due to matching failure.")
return pano
# ---------------------------
# 主程序入口
# ---------------------------
if __name__ == '__main__':
t0 = time.time()
print("="*70)
print("🖼️ 开始图像拼接任务")
print("="*70)
# --- Step 1: 成对图像 ---
print("\n🔍 正在处理成对图像...")
pair_files = [
('1_1.jpg', '1_2.jpg'),
('2_1.jpg', '2_2.jpg'),
('3_1.jpg', '3_2.jpg') # 如果是 .JPG,请改为 .JPG
]
for name1, name2 in pair_files:
path1 = os.path.join(PROBLEM2_IMAGES_DIR, name1)
path2 = os.path.join(PROBLEM2_IMAGES_DIR, name2)
# 尝试自动补全扩展名
for p, n in [(path1, name1), (path2, name2)]:
if not os.path.exists(p):
base = os.path.splitext(n)[0]
for ext in ['.JPG', '.jpeg', '.JPEG']:
alt_path = os.path.join(PROBLEM2_IMAGES_DIR, base + ext)
if os.path.exists(alt_path):
if p == path1:
path1 = alt_path
else:
path2 = alt_path
print(f"🔧 自动修正路径: {n} → {os.path.basename(alt_path)}")
break
try:
img1 = load_image(path1, DOWNSAMPLE_FOR_SPEED)
img2 = load_image(path2, DOWNSAMPLE_FOR_SPEED)
print(f" ✅ 加载成功: {os.path.basename(path1)}, {os.path.basename(path2)}")
pts1, pts2 = feature_matching(img1, img2)
H, _ = align_pair(pts1, pts2)
pano = stitch_blend(img1, img2, H)
pano = crop_black(pano)
suffix1 = os.path.splitext(os.path.basename(path1))[0]
suffix2 = os.path.splitext(os.path.basename(path2))[0]
output_path = os.path.join(OUTPUT_DIR, f"pair_{suffix1}_{suffix2}.jpg")
cv2.imwrite(output_path, cv2.cvtColor(pano, cv2.COLOR_RGB2BGR))
print(f" 💾 已保存: {output_path}")
except Exception as e:
print(f" ❌ 拼接失败 {name1}-{name2}: {e}")
# --- Step 2: 全景序列 ---
print("\n🧩 正在处理全景序列...")
scene_names = ['grail', 'library', 'parrington', 'Xue-Mountain-Entrance']
for scene_name in scene_names:
scene_dir = os.path.join(PANORAMAS_ROOT, scene_name)
print(f"\n📁 场景: {scene_name}")
print(f" 路径: {scene_dir}")
if not os.path.isdir(scene_dir):
print(f" ❌ 错误:目录不存在!")
continue
patterns = ['*.jpg', '*.JPG', '*.jpeg', '*.JPEG']
image_paths = []
for pat in patterns:
image_paths.extend(glob(os.path.join(scene_dir, pat)))
image_paths = sorted(set(image_paths), key=natural_sort_key)
print(f" ✅ 找到 {len(image_paths)} 张图像:")
for p in image_paths[:3]:
print(f" - {os.path.basename(p)}")
if len(image_paths) > 3:
print(f" ... 还有 {len(image_paths)-3} 张")
if len(image_paths) < 2:
print(f" ⚠️ 图像不足,跳过")
continue
try:
imgs = [load_image(p, DOWNSAMPLE_FOR_SPEED) for p in image_paths]
print(f" 🔄 开始拼接...")
pano = generate_panorama(imgs)
if pano is not None:
pano = crop_black(pano)
output_path = os.path.join(OUTPUT_DIR, f"panorama_{scene_name}.png")
cv2.imwrite(output_path, cv2.cvtColor(pano, cv2.COLOR_RGB2BGR))
print(f" 💾 已保存: {output_path}")
except Exception as e:
print(f" ❌ 失败: {e}")
total_time = time.time() - t0
print(f"\n🎉 所有任务完成!总耗时: {total_time:.1f} 秒")
print(f"📁 输出目录: {OUTPUT_DIR}")
print(f"📄 输出文件应在: {OUTPUT_DIR}/ 下看到 7 个文件。")
```
---
## ✅ 如何确保成功运行?
### 1. 确保结构正确
```
Problem_Set_2-3/
├── Problem2Images/
│ ├── 1_1.jpg, 1_2.jpg, ...
│ └── panoramas/
│ ├── grail/
│ ├── library/
│ ├── parrington/
│ └── Xue-Mountain-Entrance/
└── outputs/ ← 自动生成
```
### 2. 安装依赖
```bash
pip install opencv-python numpy scipy
```
### 3. 运行
```bash
cd /Users/mac/Desktop/Problem_Set_2-3
python homework2.py
```
---
###