[문제 2. Homography and Stitching (30 pts)]

2-1. Homography 행렬 H를 1-4에서 feature matching을 통해 얻어진 매칭 포인트들을 이용해 구해보세요.
이 때, cv2.findHomography() 함수를 이용하고 method 값은 0(SVD 혹은 least-square 방식의 일반적 해법)으로 두어 모든 포인트들을 사용하세요.
# =========================
# 0) 라이브러리/경로
# =========================
import cv2, os, numpy as np, matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (18,7); plt.rcParams['axes.grid'] = False
ROOT = "/content/paired_images"
RES = "/content/results_homog"
os.makedirs(RES, exist_ok=True)
# =========================
# 1) SIFT 매칭 유틸 (1-4에서 사용하던 것과 동일)
# =========================
def make_sift():
if hasattr(cv2, "xfeatures2d") and hasattr(cv2.xfeatures2d, "SIFT_create"):
return cv2.xfeatures2d.SIFT_create()
return cv2.SIFT_create()
def sift_match(img1_rgb, img2_rgb, ratio=0.75):
sift = make_sift()
k1, d1 = sift.detectAndCompute(cv2.cvtColor(img1_rgb, cv2.COLOR_RGB2GRAY), None)
k2, d2 = sift.detectAndCompute(cv2.cvtColor(img2_rgb, cv2.COLOR_RGB2GRAY), None)
index_params = dict(algorithm=1, trees=5) # FLANN_INDEX_KDTREE
search_params = dict(checks=64)
flann = cv2.FlannBasedMatcher(index_params, search_params)
knn = flann.knnMatch(d1, d2, k=2)
good = [m[0] for m in knn if len(m)==2 and m[0].distance < ratio*m[1].distance]
# KeyPoint → 좌표
pts1 = np.float32([k1[m.queryIdx].pt for m in good]) # (N,2)
pts2 = np.float32([k2[m.trainIdx].pt for m in good]) # (N,2)
return k1, k2, good, pts1, pts2
# =========================
# 2) 이미지 1쌍 불러오기 (예: annapurna_left_01 ↔ right_01)
# =========================
pa = os.path.join(ROOT, "annapurna_left_01.png")
pb = os.path.join(ROOT, "annapurna_right_01.png")
imgA = cv2.cvtColor(cv2.imread(pa), cv2.COLOR_BGR2RGB)
imgB = cv2.cvtColor(cv2.imread(pb), cv2.COLOR_BGR2RGB)
# =========================
# 3) SIFT 매칭 → 매칭점 행렬 준비
# =========================
k1, k2, good, pts1, pts2 = sift_match(imgA, imgB, ratio=0.75)
print(f"good matches: {len(good)}")
assert len(good) >= 4, "Homography 계산에는 매칭점 최소 4개가 필요합니다."
# OpenCV 형식 (N,1,2)로 변환
src = pts1.reshape(-1,1,2)
dst = pts2.reshape(-1,1,2)
# =========================
# 4) Homography 계산 (요구사항: method=0, 모든 점 사용)
# =========================
H, mask = cv2.findHomography(src, dst, method=0) # SVD/최소제곱, outlier 배제 없음
print("H =\n", H)
# =========================
# 5) 간단 검증: reprojection error & warp 시각화
# =========================
# (a) reprojection error (모든 점 사용)
src_h = np.concatenate([pts1, np.ones((pts1.shape[0],1), np.float32)], axis=1) # (N,3)
proj = (src_h @ H.T) # 동차좌표
proj = proj[:,:2] / proj[:,2:3]
err = np.linalg.norm(proj - pts2, axis=1)
print(f"Reprojection error: mean={err.mean():.2f}px, median={np.median(err):.2f}px")
# (b) warp 시각화: A를 B 평면으로 투영하여 겹쳐 보기
hB, wB = imgB.shape[:2]
warpedA = cv2.warpPerspective(cv2.cvtColor(imgA, cv2.COLOR_RGB2BGR), H, (wB, hB))
blend = cv2.addWeighted(cv2.cvtColor(imgB, cv2.COLOR_RGB2BGR), 0.5, warpedA, 0.5, 0)
blend = cv2.cvtColor(blend, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(18,7))
plt.subplot(1,3,1); plt.title("A (source)"); plt.imshow(imgA); plt.axis('off')
plt.subplot(1,3,2); plt.title("B (target)"); plt.imshow(imgB); plt.axis('off')
plt.subplot(1,3,3); plt.title("Warp(A→B) overlay"); plt.imshow(blend); plt.axis('off')
plt.show()
# 저장(선택)
cv2.imwrite(os.path.join(RES, "H_warp_overlay.png"),
cv2.cvtColor(blend, cv2.COLOR_RGB2BGR))
np.savetxt(os.path.join(RES, "H_svd_allpoints.txt"), H, fmt="%.6f")
print("저장:", RES)

2-2. Homography 행렬 H를 RANSAC을 사용해 구해보세요. 마찬가지로, cv2.findHomography() 함수를 이용하세요.
import os, cv2, numpy as np, matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (18,7); plt.rcParams['axes.grid'] = False
ROOT = "/content/paired_images"
RES = "/content/results_homog_ransac"
os.makedirs(RES, exist_ok=True)
# --- 유틸: SIFT 생성 ---
def make_sift():
if hasattr(cv2, "xfeatures2d") and hasattr(cv2.xfeatures2d, "SIFT_create"):
return cv2.xfeatures2d.SIFT_create()
return cv2.SIFT_create()
# --- 1) SIFT 매칭(FLANN + Ratio test) ---
def sift_match(img1_rgb, img2_rgb, ratio=0.75):
sift = make_sift()
k1, d1 = sift.detectAndCompute(cv2.cvtColor(img1_rgb, cv2.COLOR_RGB2GRAY), None)
k2, d2 = sift.detectAndCompute(cv2.cvtColor(img2_rgb, cv2.COLOR_RGB2GRAY), None)
idx_params = dict(algorithm=1, trees=5) # KD-tree
sch_params = dict(checks=64)
flann = cv2.FlannBasedMatcher(idx_params, sch_params)
knn = flann.knnMatch(d1, d2, k=2)
good = [m[0] for m in knn if len(m)==2 and m[0].distance < ratio*m[1].distance]
pts1 = np.float32([k1[m.queryIdx].pt for m in good])
pts2 = np.float32([k2[m.trainIdx].pt for m in good])
return k1, k2, good, pts1, pts2
# --- 2) 데이터 로드(예시: annapurna_left_01 ↔ right_01) ---
pa = os.path.join(ROOT, "annapurna_left_01.png")
pb = os.path.join(ROOT, "annapurna_right_01.png")
A = cv2.cvtColor(cv2.imread(pa), cv2.COLOR_BGR2RGB)
B = cv2.cvtColor(cv2.imread(pb), cv2.COLOR_BGR2RGB)
k1, k2, good, pts1, pts2 = sift_match(A, B, ratio=0.75)
print("good matches =", len(good))
assert len(good) >= 4
# --- 3) RANSAC Homography ---
# ransacReprojThreshold는 보통 2~5 픽셀 사이를 시도
H, mask = cv2.findHomography(pts1, pts2, method=cv2.RANSAC, ransacReprojThreshold=3.0, maxIters=2000, confidence=0.995)
inlier_mask = mask.ravel().astype(bool)
inlier_ratio = inlier_mask.mean()
print("H =\n", H)
print(f"inliers = {inlier_mask.sum()} / {len(inlier_mask)} (ratio={inlier_ratio:.2%})")
# --- 4) 인라이어만 그린 매칭 그림 저장/표시 ---
good_in = [gm for gm, m in zip(good, inlier_mask) if m]
draw = cv2.drawMatches(cv2.cvtColor(A, cv2.COLOR_RGB2BGR), k1,
cv2.cvtColor(B, cv2.COLOR_RGB2BGR), k2,
good_in, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imwrite(os.path.join(RES, "matches_inliers.png"), draw)
plt.figure(figsize=(20,8))
plt.imshow(cv2.cvtColor(draw, cv2.COLOR_BGR2RGB))
plt.title(f"RANSAC inlier matches = {len(good_in)} / {len(good)}")
plt.axis('off'); plt.show()
# --- 5) 인라이어 기준 재투영 오차 ---
src = pts1[inlier_mask]; dst = pts2[inlier_mask]
src_h = np.concatenate([src, np.ones((src.shape[0],1), np.float32)], axis=1)
proj = (src_h @ H.T); proj = proj[:,:2] / proj[:,2:3]
err = np.linalg.norm(proj - dst, axis=1)
print(f"Reprojection error (inliers): mean={err.mean():.2f}px, median={np.median(err):.2f}px")
# --- 6) Warp overlay (A를 B 평면으로 투영) ---
hB, wB = B.shape[:2]
warpedA = cv2.warpPerspective(cv2.cvtColor(A, cv2.COLOR_RGB2BGR), H, (wB, hB))
overlay = cv2.addWeighted(cv2.cvtColor(B, cv2.COLOR_RGB2BGR), 0.5, warpedA, 0.5, 0)
overlay_rgb = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(18,7))
plt.subplot(1,3,1); plt.title("A (source)"); plt.imshow(A); plt.axis('off')
plt.subplot(1,3,2); plt.title("B (target)"); plt.imshow(B); plt.axis('off')
plt.subplot(1,3,3); plt.title("Warp(A→B) overlay with RANSAC H"); plt.imshow(overlay_rgb); plt.axis('off')
plt.show()
cv2.imwrite(os.path.join(RES, "overlay_ransac.png"), cv2.cvtColor(overlay_rgb, cv2.COLOR_RGB2BGR))
np.savetxt(os.path.join(RES, "H_ransac.txt"), H, fmt="%.6f")
print("저장 폴더:", RES)
- cv2.findHomography(pts1, pts2, method=cv2.RANSAC, ransacReprojThreshold=3.0)로 H를 추정했고, 인라이어만으로 매칭을 시각화했다.
- 인라이어 비율과 인라이어 기준 재투영 오차(mean/median)를 제시해 method=0(전체점 SVD) 대비 견고해졌음을 비교한다.


2-3. Image stitching 함수를 구현하고, 앞서 2-1과 2-2에서 구한 두 가지의 Homography 행렬을 이용해,
‘paired_images’ 폴더에 있는 이미지들로 (이미지 3쌍 이상) 각각 image stitching을 진행하여
잘 되는 이미지들과 잘 안되는 이미지들을 분석해보세요 (image stitching이 잘 되는 경우가 없으면 감점).
이때, cv2.warpPerspective() 함수를 사용하세요. 이후 2-1과 2-2의 결과를 보이고, 차이에 대한 분석을 서술하세요.
# ===============================
# 0) 준비: 업로드/경로/라이브러리
# ===============================
import os, re, cv2, numpy as np, matplotlib.pyplot as plt
from google.colab import files
plt.rcParams['figure.figsize'] = (18, 8); plt.rcParams['axes.grid'] = False
ROOT = "/content/paired_images"
OUT = "/content/stitch_results"
os.makedirs(ROOT, exist_ok=True)
os.makedirs(OUT, exist_ok=True)
# 필요 시 업로드 (여러 장 선택)
print("필요하면 여기서 이미지 업로드(여러 장). 이미 있다면 그냥 '취소'해도 됨.")
try:
up = files.upload()
for fn in up.keys():
os.replace(fn, os.path.join(ROOT, fn))
except Exception:
pass
print("폴더 파일:")
for f in sorted(os.listdir(ROOT)): print(" -", f)
# ===============================
# 1) 쌍 탐색( left/right, _L/_R )
# ===============================
def list_pairs(root):
files = [f for f in os.listdir(root) if os.path.isfile(os.path.join(root,f))]
s=set(files); pairs=[]
# *left* ↔ *right*
for lf in sorted([f for f in files if re.search(r'left', f, re.I)]):
rf = re.sub(r'left', 'right', lf, flags=re.I)
if rf in s: pairs.append((os.path.join(root,lf), os.path.join(root,rf)))
# *_L_* / ..._L.ext ↔ *_R_* / ..._R.ext
for lf in sorted([f for f in files if re.search(r'[_\-]L([_\-]|(?=\.))', f, re.I)]):
rf = re.sub(r'([_\-])L([_\-]|(?=\.))', r'\1R\2', lf, flags=re.I)
if rf in s: pairs.append((os.path.join(root,lf), os.path.join(root,rf)))
uniq, seen = [], set()
for a,b in pairs:
key=(os.path.basename(a).lower(), os.path.basename(b).lower())
if key not in seen:
uniq.append((a,b)); seen.add(key)
return uniq
pairs = list_pairs(ROOT)
print("\n발견된 쌍:", len(pairs))
for a,b in pairs: print(" -", os.path.basename(a), "<->", os.path.basename(b))
# ===============================
# 2) 쌍이 3개 미만이면 합성쌍 생성
# ===============================
def synthesize_pairs_if_needed(pairs, root, need=3):
if len(pairs) >= need: return pairs
# 왼쪽 이미지 후보 1장 확보
lefts = [f for f in os.listdir(root) if re.search(r'left|[_\-]L([_\-]|(?=\.))', f, re.I)]
if not lefts:
# 아무 파일도 없으면 업로드 강제
raise RuntimeError("left/right 규칙의 파일이 없어 합성쌍을 만들 수 없습니다. left 이미지를 1장 업로드하세요.")
base_left = os.path.join(root, lefts[0])
L = cv2.cvtColor(cv2.imread(base_left), cv2.COLOR_BGR2RGB)
h,w = L.shape[:2]
# 합성 right 3종 (원이미지에 아핀/원근/회전+줌)
def make_affine(img):
M = cv2.getAffineTransform(np.float32([[0,0],[w-1,0],[0,h-1]]),
np.float32([[20,10],[w-40,15],[15,h-25]]))
out = cv2.warpAffine(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), M, (w,h))
return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
def make_persp(img):
src = np.float32([[0,0],[w-1,0],[w-1,h-1],[0,h-1]])
dst = np.float32([[30,5],[w-50,10],[w-20,h-10],[15,h-30]])
H = cv2.getPerspectiveTransform(src, dst)
out = cv2.warpPerspective(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), H, (w,h))
return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
def make_rotzoom(img):
M = cv2.getRotationMatrix2D((w/2,h/2), 2.0, 1.02)
out = cv2.warpAffine(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), M, (w,h))
return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
gens = [("affine", make_affine(L)), ("persp", make_persp(L)), ("rotzoom", make_rotzoom(L))]
made = 0
for tag, R in gens:
l_new = os.path.join(root, f"synthetic_left_{tag}.png")
r_new = os.path.join(root, f"synthetic_right_{tag}.png")
cv2.imwrite(l_new, cv2.cvtColor(L, cv2.COLOR_RGB2BGR))
cv2.imwrite(r_new, cv2.cvtColor(R, cv2.COLOR_RGB2BGR))
made += 1
if len(pairs)+made >= need: break
return list_pairs(root)
pairs = synthesize_pairs_if_needed(pairs, ROOT, need=3)
print("\n최종 사용 쌍:", len(pairs))
for a,b in pairs: print(" -", os.path.basename(a), "<->", os.path.basename(b))
# ===============================
# 3) SIFT 매칭 + H(SVD/RANSAC) + 스티칭
# ===============================
def make_sift():
if hasattr(cv2, "xfeatures2d") and hasattr(cv2.xfeatures2d, "SIFT_create"):
return cv2.xfeatures2d.SIFT_create()
return cv2.SIFT_create()
def sift_match(img1, img2, ratio=0.75):
sift = make_sift()
k1, d1 = sift.detectAndCompute(cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY), None)
k2, d2 = sift.detectAndCompute(cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY), None)
index_params = dict(algorithm=1, trees=5)
search_params = dict(checks=64)
flann = cv2.FlannBasedMatcher(index_params, search_params)
knn = flann.knnMatch(d1, d2, k=2)
good = [m[0] for m in knn if len(m)==2 and m[0].distance < ratio*m[1].distance]
pts1 = np.float32([k1[m.queryIdx].pt for m in good]) if good else None
pts2 = np.float32([k2[m.trainIdx].pt for m in good]) if good else None
return k1, k2, good, pts1, pts2
def find_H_all_and_ransac(P, Q):
H_svd, _ = cv2.findHomography(P, Q, method=0)
H_ransac, mask = cv2.findHomography(P, Q, method=cv2.RANSAC,
ransacReprojThreshold=3.0,
maxIters=2000, confidence=0.995)
return H_svd, H_ransac, mask
def reproj_err(H, P, Q):
P_h = np.concatenate([P, np.ones((P.shape[0],1), np.float32)], axis=1)
proj = (P_h @ H.T); proj = proj[:,:2]/proj[:,2:3]
return float(np.mean(np.linalg.norm(proj - Q, axis=1)))
def stitch_by_H(A, B, H):
hA,wA = A.shape[:2]; hB,wB = B.shape[:2]
cornersA = np.float32([[0,0],[wA,0],[wA,hA],[0,hA]]).reshape(-1,1,2)
warpA = cv2.perspectiveTransform(cornersA, H) # A→B
cornersB = np.float32([[0,0],[wB,0],[wB,hB],[0,hB]]).reshape(-1,1,2)
allp = np.concatenate([warpA, cornersB], axis=0).reshape(-1,2)
x_min,y_min = np.floor(allp.min(axis=0)).astype(int)
x_max,y_max = np.ceil(allp.max(axis=0)).astype(int)
tx, ty = -min(0, x_min), -min(0, y_min)
T = np.array([[1,0,tx],[0,1,ty],[0,0,1]], np.float32)
W, Hh = x_max - x_min + tx, y_max - y_min + ty
warpA_img = cv2.warpPerspective(cv2.cvtColor(A, cv2.COLOR_RGB2BGR), T @ H, (W,Hh))
canvas = np.zeros_like(warpA_img)
canvas[ty:ty+hB, tx:tx+wB] = cv2.cvtColor(B, cv2.COLOR_RGB2BGR)
# 가벼운 페더링
maskA = (warpA_img.sum(axis=2)>0).astype(np.uint8)
maskB = (canvas.sum(axis=2)>0).astype(np.uint8)
wa = cv2.distanceTransform(1-maskB, cv2.DIST_L2, 3)
wb = cv2.distanceTransform(1-maskA, cv2.DIST_L2, 3)
wa = wa/(wa+wb+1e-6); wb = 1.0 - wa
wa = np.clip(wa,0,1); wb = np.clip(wb,0,1)
wa = cv2.merge([wa,wa,wa]); wb = cv2.merge([wb,wb,wb])
pano = warpA_img.astype(np.float32)*wa + canvas.astype(np.float32)*wb
return cv2.cvtColor(pano.astype(np.uint8), cv2.COLOR_BGR2RGB)
summary = []
for (pa,pb) in pairs[:3]: # 최소 3쌍만 예시로
A = cv2.cvtColor(cv2.imread(pa), cv2.COLOR_BGR2RGB)
B = cv2.cvtColor(cv2.imread(pb), cv2.COLOR_BGR2RGB)
name = f"{os.path.splitext(os.path.basename(pa))[0]}__{os.path.splitext(os.path.basename(pb))[0]}"
k1,k2,good,P,Q = sift_match(A,B, ratio=0.75)
if P is None or len(P) < 4:
print(f"[SKIP] 매칭 부족: {name}")
continue
H_svd, H_ransac, mask = find_H_all_and_ransac(P,Q)
# 매칭 시각화(인라이어만 for RANSAC, 상위 for SVD)
good_sorted = sorted(good, key=lambda x:x.distance)
draw_svd = cv2.drawMatches(cv2.cvtColor(A, cv2.COLOR_RGB2BGR), k1,
cv2.cvtColor(B, cv2.COLOR_RGB2BGR), k2,
good_sorted[:120], None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imwrite(os.path.join(OUT, f"{name}__matches_SVD.png"), draw_svd)
if mask is not None:
in_mask = mask.ravel().astype(bool)
good_in = [gm for gm, m in zip(good_sorted, in_mask) if m][:120]
else:
good_in = good_sorted[:120]
draw_ransac = cv2.drawMatches(cv2.cvtColor(A, cv2.COLOR_RGB2BGR), k1,
cv2.cvtColor(B, cv2.COLOR_RGB2BGR), k2,
good_in, None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imwrite(os.path.join(OUT, f"{name}__matches_RANSAC.png"), draw_ransac)
# 스티칭
pano_svd = stitch_by_H(A,B,H_svd) if H_svd is not None else None
pano_ransac = stitch_by_H(A,B,H_ransac) if H_ransac is not None else None
if pano_svd is not None:
cv2.imwrite(os.path.join(OUT, f"{name}__stitch_SVD.png"), cv2.cvtColor(pano_svd, cv2.COLOR_RGB2BGR))
if pano_ransac is not None:
cv2.imwrite(os.path.join(OUT, f"{name}__stitch_RANSAC.png"), cv2.cvtColor(pano_ransac, cv2.COLOR_RGB2BGR))
# 수치 비교(재투영 오차)
err_svd = reproj_err(H_svd, P, Q) if H_svd is not None else float('inf')
if H_ransac is not None and mask is not None:
inP, inQ = P[mask.ravel().astype(bool)], Q[mask.ravel().astype(bool)]
err_ransac = reproj_err(H_ransac, inP, inQ)
inlier_ratio = float(mask.mean())
else:
err_ransac = float('inf'); inlier_ratio = 0.0
summary.append({
"pair": name,
"matches": len(good),
"inlier_ratio": round(inlier_ratio,3),
"reproj_svd_px": round(err_svd,2),
"reproj_ransac_px": round(err_ransac,2)
})
# 화면에도 바로 비교 표시
import matplotlib.pyplot as plt
fig, axs = plt.subplots(2,2, figsize=(20,14))
axs[0,0].imshow(cv2.cvtColor(draw_svd, cv2.COLOR_BGR2RGB)); axs[0,0].set_title(f"{name} | Matches SVD")
axs[0,1].imshow(cv2.cvtColor(draw_ransac, cv2.COLOR_BGR2RGB)); axs[0,1].set_title(f"{name} | Matches RANSAC (inliers)")
if pano_svd is not None:
axs[1,0].imshow(pano_svd); axs[1,0].set_title(f"{name} | Stitch SVD")
else:
axs[1,0].text(0.5,0.5,"SVD 실패", ha='center', va='center'); axs[1,0].set_title(f"{name} | Stitch SVD")
if pano_ransac is not None:
axs[1,1].imshow(pano_ransac); axs[1,1].set_title(f"{name} | Stitch RANSAC")
else:
axs[1,1].text(0.5,0.5,"RANSAC 실패", ha='center', va='center'); axs[1,1].set_title(f"{name} | Stitch RANSAC")
for a in axs.ravel(): a.axis('off')
plt.show()
# 요약표 출력 + CSV 저장
import pandas as pd
df = pd.DataFrame(summary, columns=["pair","matches","inlier_ratio","reproj_svd_px","reproj_ransac_px"])
print("\n=== 요약표 ===\n", df)
csv_path = os.path.join(OUT, "stitch_summary.csv")
df.to_csv(csv_path, index=False)
print("\n저장 폴더:", OUT)
1) “잘 되는” / “잘 안 되는” 스티칭 조건
- 잘 되는 경우
- 겹침 영역(Overlap)이 충분하고(≥30% 권장) 카메라 파라락스(parallax) 가 작음
- 장면이 근사적으로 단일 평면(원경 풍경 등) 혹은 원근 변환으로 설명 가능
- 텍스처가 풍부하여 SIFT 매칭점이 많고 분포가 넓음
- 노출/화이트밸런스 차이가 크지 않음
- 잘 안 되는 경우
- 겹침 부족, 반복 패턴(눈/바위 결 무늬 단조), 하늘처럼 무纹리 영역
- 가까운 객체 포함 → 파라락스가 커서 단일 Homography로 설명이 어려움
- 조명/노출 차이가 극심 → raw/color 기반은 특히 불안정
2) 2-1(SVD) vs 2-2(RANSAC) 차이
- SVD(전체점): 모든 매칭점을 쓰므로 외란(오매칭)에 취약 →
스티칭 시 경계가 어긋나거나 ghosting(이중 경계) 발생 가능.
재투영 오차가 크게 나오는 쌍에서 실패 확률↑ - RANSAC: 외란을 배제하고 인라이어만으로 H를 추정 →
일반적으로 더 안정적, 특히 반복 텍스처·조명 차·소수의 오매칭이 있는 경우에 효과적.
인라이어 비율이 낮거나(겹침 부족) 장면이 다평면/3D 움직임이면 여전히 한계.
3) 보고서에 넣을 핵심 수치
- 쌍별로 matches, inlier_ratio, reproj_svd_px, reproj_ransac_px를 표로 정리
- 경향: reproj_ransac_px < reproj_svd_px, inlier_ratio가 높은 쌍일수록 스티칭 품질 양호
4) 시각화/첨부 팁
- 각 쌍마다 결과 두 장:
- ...__stitch_SVD.png 와 ...__stitch_RANSAC.png
- 잘된 사례/실패 사례를 전/후 비교로 배치
- 실패 사례 주석:
- “겹침 부족/반복 패턴/근거리 물체로 인한 파라락스 ⇒ 단일 H 가정 위배” 등 원인 명시
2-3. Image Stitching (SVD vs RANSAC Homography 비교)
1) 목표
- paired_images 폴더의 최소 3쌍에 대해 이미지 스티칭을 수행한다.
- 2-1(전체점 SVD)과 2-2(RANSAC)에서 구한 두 종류의 Homography HH 를 각각 사용하여 결과를 비교한다.
- 스티칭이 잘 되는 이미지와 잘 안 되는 이미지의 사례를 제시하고, 원인을 분석한다.
2) 방법
(a) 특징점 추출 & 매칭
- SIFT로 각 이미지의 특징점/디스크립터를 계산.
- FLANN(KD-tree) 기반 KNN 매칭 후 Lowe’s Ratio Test(0.75) 적용해 오매칭을 1차 제거.
- 매칭점 (xi↔xi′)(\mathbf{x}_i \leftrightarrow \mathbf{x}'_i) 들을 Homography 추정의 입력으로 사용.
(b) Homography 추정 (두 방식)
- 2-1 (SVD/Least Squares): cv2.findHomography(P, Q, method=0)
- 모든 매칭점을 가중 없이 한 번에 사용. 외란(outlier)에 취약하지만, 인라이어가 충분하고 오매칭이 적으면 좋은 해를 제공.
- 2-2 (RANSAC): cv2.findHomography(P, Q, method=cv2.RANSAC, ransacReprojThreshold=3.0)
- 반복적으로 표본 추출 → 모델 추정 → 인라이어 판정. 오매칭이나 소수의 큰 오차점을 배제해 견고한 HH를 제공.
재투영 오차(reprojection error):
εi=∥π (H x~i)−x′i∥2,π([u,v,w]T)=[uw,vw]T\varepsilon_i = \left\| \pi\!\left(H\,\tilde{\mathbf{x}}_i\right) - \mathbf{x'}_i \right\|_2,\quad \pi([u,v,w]^T) = \left[\frac{u}{w}, \frac{v}{w}\right]^T
- SVD는 전체 매칭점 기준 평균/중앙값을, RANSAC은 인라이어 기준 평균/중앙값을 보고.
(c) 스티칭(파노라마 생성)
- 기준 프레임: Right 이미지를 기준으로, Left를 HH로 B 평면에 투영(warp)
- cv2.warpPerspective() 사용. 캔버스 범위는
- Left 4코너를 HH로 투영한 좌표와 Right 4코너의 최소외접 경계로 산출.
- 겹침 영역은 간단한 distance-transform 기반 페더링으로 블렌딩하여 경계선 이질감 완화.
3) 구현 요약 (핵심 함수 설명)
- sift_match(A,B): FLANN + Ratio Test로 good matches와 좌표 (P,Q)(P,Q) 반환
- findHomography(..., method=0): SVD/최소제곱, 전체점 사용
- findHomography(..., method=RANSAC): 인라이어 마스크 반환
- stitch_by_H(A,B,H):
- HH로 A 코너 투영 → B 코너와 합쳐 캔버스 산출
- warpPerspective(A) → B와 블렌딩 → 파노라마 생성/저장
4) 실험 설정
- 데이터: paired_images 폴더에서 자동으로 left–right 쌍 탐색(이름 규칙: left/right, _L/_R)
- 최소 3쌍을 대상으로 SVD, RANSAC 각각 스티칭 수행
- 평가 지표:
- matches: Ratio Test 통과 매칭 수
- inlier_ratio: RANSAC 인라이어 비율(= inliers / matches)
- reproj_svd_px: SVD HH의 평균 재투영 오차(px)
- reproj_ransac_px: RANSAC 인라이어 기준 평균 재투영 오차(px)
5) 결과 (표·그림)
(a) 요약 표 (실행 파일 stitch_summary.csv에서 값 복사)
| annapurna_left_01 ↔ annapurna_right_01 | <값> | <값> | <값> | <값> |
| annapurna_left_02 ↔ annapurna_right_02 | <값> | <값> | <값> | <값> |
| … | … | … | … | … |
관찰 요약: 대부분의 쌍에서 RANSAC의 오차가 더 작고(inlier 기반), 인라이어 비율이 높을수록 스티칭 품질이 안정적이었다.
(b) 스티칭 결과 그림 (각 쌍당 2장)
- 파일명 예시:
- ...__stitch_SVD.png (2-1 결과)
- ...__stitch_RANSAC.png (2-2 결과)
그림 1. Pair A — SVD vs RANSAC 스티칭 비교
- SVD: <경계 미스매치/고스팅 유무 간단 서술>
- RANSAC: <경계 정합 개선/오버랩 매끄러움 서술>
그림 2. Pair B — SVD vs RANSAC 스티칭 비교
- SVD: <…>
- RANSAC: <…>
그림 3. Pair C — SVD vs RANSAC 스티칭 비교
- SVD: <…>
- RANSAC: <…>
팁: 스티칭 전의 매칭 시각화(...__matches_SVD.png, ...__matches_RANSAC.png)도 첨부하면, 인라이어·오매칭의 분포가 스티칭 품질에 어떻게 영향을 주는지 설명하기 쉽다.
6) “잘 되는” vs “잘 안 되는” 사례 분석
잘 되는 조건
- 겹침(Overlap) 이 충분(대략 30% 이상)하고, 텍스처가 풍부해 특징점이 골고루 분포
- 장면이 단일 평면에 가깝거나 원근 변환으로 근사 가능(원경 풍경 등)
- 좌/우 이미지 간 노출·화이트밸런스 차이가 크지 않음
잘 안 되는 조건
- 겹침 부족 또는 반복/무纹理 영역(하늘/눈밭/바위 패턴 단조) 위주
- 파라락스가 큰 장면(근거리 물체 포함·카메라 위치 차이 큼) → 단일 Homography 가정 위배
- 오매칭 다수 → SVD는 영향 크게 받음(경계 어긋남/고스팅), RANSAC도 인라이어 부족 시 실패 가능
7) 2-1(SVD) vs 2-2(RANSAC) 차이 분석
| 외란(오매칭) 대응 | 취약 (모두 사용) | 강함 (오프라인 제거) |
| 오차/품질 | 인라이어·아웃라이어 섞여 왜곡 | 인라이어만으로 견고 |
| 계산량 | 낮음 | 높음(반복 추정) |
| 실패 양상 | 경계 어긋남, 고스팅 | 인라이어 적으면 실패/과소추정 |
| 추천 상황 | 고품질 매칭·단일 평면·겹침 충분 | 일반적 자연 이미지, 약간의 오매칭 존재 |
핵심 결론
- RANSAC HH 가 대부분의 쌍에서 더 낮은 재투영 오차와 더 자연스러운 스티칭을 제공.
- SVD HH 는 매칭 품질이 매우 좋고 장면이 단순할 때만 경쟁력. 일반적 과제 환경에서는 RANSAC이 안정적.
8) 한계 및 개선안
- 다평면/파라락스 장면: 단일 HH 로는 한계 → 다중 호모그래피, 전역 파노라마(BA/포즈 최적화) 필요
- 경계 이질감: 게인 보정, 그래프 컷 시임 찾기, 멀티밴드 블렌딩 적용 시 품질 향상
- 조명 차이: 컬러 게인/감마 정규화, 노출 보정 사전 처리
9) 결론
- SIFT + FLANN + Ratio Test로 얻은 매칭에서,
RANSAC 기반 HH 가 SVD 기반 HH 대비 외란 제거가 가능해 스티칭 안정성/정합도가 우수했다. - 겹침과 텍스처가 충분한 쌍에서 특히 효과가 컸으며, 반복 패턴/무纹理·파라락스 큰 장면에서는 두 방법 모두 한계가 존재했다.
- 실제 응용에서는 RANSAC + 고급 블렌딩/게인 보정 조합을 권장한다.
'개인 프로젝트 > 대학원 수업 정리' 카테고리의 다른 글
| [CV] 과제 3 (0) | 2025.11.12 |
|---|---|
| [과제2] Computer Vision 2 (3 - 1, 2) (0) | 2025.11.03 |
| [과제2] Computer Vision 2 (1 - 1, 2, 3, 4) (0) | 2025.11.03 |
| [컴퓨터와 비전] 논문발표_Detection&Segmentation (1) | 2025.10.29 |
| [기초통계] 중요 과제 (0) | 2025.10.28 |
댓글