본문 바로가기
개인 프로젝트/대학원 수업 정리

[과제2] Computer Vision 2 (2 - 1, 2, 3)

by 응_비 2025. 11. 3.

[문제 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를 HHB 평면에 투영(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):
    1. HH로 A 코너 투영 → B 코너와 합쳐 캔버스 산출
    2. 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에서 값 복사)

Pair (L↔R)matchesinlier_ratioreproj_svd_pxreproj_ransac_px
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) 차이 분석

항목SVD(전체점)RANSAC
외란(오매칭) 대응 취약 (모두 사용) 강함 (오프라인 제거)
오차/품질 인라이어·아웃라이어 섞여 왜곡 인라이어만으로 견고
계산량 낮음 높음(반복 추정)
실패 양상 경계 어긋남, 고스팅 인라이어 적으면 실패/과소추정
추천 상황 고품질 매칭·단일 평면·겹침 충분 일반적 자연 이미지, 약간의 오매칭 존재

핵심 결론

  • RANSAC HH대부분의 쌍에서 더 낮은 재투영 오차더 자연스러운 스티칭을 제공.
  • SVD HH 는 매칭 품질이 매우 좋고 장면이 단순할 때만 경쟁력. 일반적 과제 환경에서는 RANSAC이 안정적.

8) 한계 및 개선안

  • 다평면/파라락스 장면: 단일 HH 로는 한계 → 다중 호모그래피, 전역 파노라마(BA/포즈 최적화) 필요
  • 경계 이질감: 게인 보정, 그래프 컷 시임 찾기, 멀티밴드 블렌딩 적용 시 품질 향상
  • 조명 차이: 컬러 게인/감마 정규화, 노출 보정 사전 처리

9) 결론

  • SIFT + FLANN + Ratio Test로 얻은 매칭에서,
    RANSAC 기반 HHSVD 기반 HH 대비 외란 제거가 가능해 스티칭 안정성/정합도가 우수했다.
  • 겹침과 텍스처가 충분한 쌍에서 특히 효과가 컸으며, 반복 패턴/무纹理·파라락스 큰 장면에서는 두 방법 모두 한계가 존재했다.
  • 실제 응용에서는 RANSAC + 고급 블렌딩/게인 보정 조합을 권장한다.

댓글