前言
圖片拼接(image stitching)就是將統一場景的不同拍攝出的圖片拼接到一起,如圖所示
就是拼接全景圖,是圖片拼接的應用之一,手機拍照都有全景拍攝功能
仔細觀察全景圖,尋找它們相似性,圖8-2的全景圖可以通過縮放,旋轉,射影等操作進行拼接而成,我們首先介紹幾個常用的圖像變換
圖像變換
平移變換
平移變換通過向量 ( \mathbf{t} = (t_x, t_y) ) 實現,圖像上點 ( \mathbf{p} = (i, j) ) 平移后得到新點 ( \mathbf{p}' = (i', j') ),滿足: [ \mathbf{p}' = \mathbf{p} + \mathbf{t} ] 其中 ( t_x ) 和 ( t_y ) 分別表示水平和垂直方向的平移距離。
旋轉變換
旋轉變換繞原點逆時針旋轉角度 ( \theta ),點 ( \mathbf{p} = (i, j) ) 旋轉后得到 ( \mathbf{p}' = R\mathbf{p} ),旋轉矩陣 ( R ) 為: [ R = \begin{bmatrix} \cos \theta & -\sin \theta \ \sin \theta & \cos \theta \end{bmatrix} ]
縮放變換
以原點為中心,沿 ( x ) 軸縮放 ( s_x ) 倍,沿 ( y ) 軸縮放 ( s_y ) 倍,點 ( \mathbf{p} = (i, j) ) 縮放后得到 ( \mathbf{p}' = S\mathbf{p} ),縮放矩陣 ( S ) 為: [ S = \begin{bmatrix} s_x & 0 \ 0 & s_y \end{bmatrix} ]
對稱變換
- 關于 ( y ) 軸對稱:點 ( \mathbf{p} = (i, j) ) 變換后為 ( \mathbf{p}' = (-i, j) ),對應矩陣: [ P_y = \begin{bmatrix} -1 & 0 \ 0 & 1 \end{bmatrix} ]
- 關于直線 ( y = x ) 對稱:點 ( \mathbf{p} = (i, j) ) 變換后為 ( \mathbf{p}' = (j, i) ),對應矩陣: [ P_{y=x} = \begin{bmatrix} 0 & 1 \ 1 & 0 \end{bmatrix} ]
射影變換(透視變換)
射影變換是更一般的線性變換,可用齊次坐標表示。對于點 ( \mathbf{p} = (i, j, 1) )(齊次坐標),變換后 ( \mathbf{p}' = H\mathbf{p} ),其中 ( H ) 為 ( 3 \times 3 ) 變換矩陣: [ H = \begin{bmatrix} h_{11} & h_{12} & h_{13} \ h_{21} & h_{22} & h_{23} \ h_{31} & h_{32} & h_{33} \end{bmatrix} ] 射影變換能實現傾斜、透視等復雜幾何變換。
幾何相似性分析
圖8-1的子圖與圖8-2全景圖的相似性體現在:
- 局部與全局關系:子圖通過上述變換(平移、旋轉、縮放、射影)可拼接為全景圖。
- 幾何一致性:變換后的子圖邊緣對齊、視角連貫,滿足幾何約束(如特征點匹配)。
- 變換組合:實際拼接中常組合多種變換,例如先旋轉后平移,或射影校正透視差異。
數學表達統一性
所有變換均可表示為矩陣乘法(齊次坐標下): [ \mathbf{p}' = M\mathbf{p} ] 其中 ( M ) 為對應變換矩陣。平移需擴展為仿射變換: [ M_{\text{平移}} = \begin{bmatrix} 1 & 0 & t_x \ 0 & 1 & t_y \ 0 & 0 & 1 \end{bmatrix} ]
計算變化矩陣
1.通過SIFT計算出兩幅圖片的特征點
2.將兩幅圖片的特征點進行匹配
3.更具匹配的特征點計算圖片變換矩陣
利用RANSAC算法去除誤匹配
當利用SIFT進行特征匹配時,有些時候可能會出現圖8-6的情況。圖8-6中右圖綠色圓圈
內的特征點是與左圖匹配的特征點,但利用SIFT匹配特征點時,會將左圖中部分特征點匹配到
右圖綠色圓圈之外的特征點(如紅色圓圈內的特征點)。這些特征點匹配是錯誤的匹配,應該被
移除,從而保證變換矩陣計算的魯棒性。應該如何移除錯誤的匹配點對呢?
可以用到RANSAC算法
RANSAC算法簡介
RANSAC(Random Sample Consensus)是一種魯棒的模型擬合算法,常用于處理包含大量噪聲或異常值的數據。在計算機視覺中,RANSAC常用于去除特征匹配中的誤匹配(outliers),僅保留滿足幾何約束的正確匹配(inliers)。
算法原理
RANSAC通過隨機采樣最小數據集迭代估計模型參數,并統計支持該模型的樣本數量。算法核心思想是:正確的匹配應滿足某種幾何變換(如單應性矩陣或基礎矩陣),而誤匹配則不符合該約束。
實現步驟
輸入準備
- 兩組匹配的特征點對:
points1
和points2
(形狀為N×2的數組) - 模型類型:單應性矩陣(Homography)或基礎矩陣(Fundamental Matrix)
- 最大迭代次數:
max_iterations
(默認1000) - 內點閾值:
threshold
(像素距離,默認3.0)
核心流程
- 隨機從匹配點對中選取最小樣本集(如單應性矩陣需4對點)
- 根據樣本集計算候選模型參數(如調用
cv2.findHomography
) - 統計所有點在該模型下的投影誤差小于閾值的內點數量
- 保留內點數量最多的模型參數
- 重復上述過程直到達到最大迭代次數
OpenCV代碼實現
import cv2
import numpy as npdef ransac_filter_matches(points1, points2, model='homography', max_iter=1000, threshold=3.0):"""points1, points2: 匹配的點坐標 (N×2 numpy數組)model: 擬合模型類型 ('homography' 或 'fundamental')"""if len(points1) < 4:return np.arange(len(points1)) # 不足4對點時返回所有索引if model == 'homography':H, mask = cv2.findHomography(points1, points2, cv2.RANSAC, threshold, maxIters=max_iter)elif model == 'fundamental':F, mask = cv2.findFundamentalMat(points1, points2, cv2.FM_RANSAC, threshold, max_iter)return mask.ravel().astype(bool) # 返回內點掩碼
參數選擇建議
閾值選擇:通常設置為1-5像素,取決于特征點定位精度。對于SIFT/SURF等特征可設為3,ORB等二進制特征建議設為5
迭代次數:默認1000次可滿足大多數場景。可通過公式估算:
$$ N = \frac{\log(1-p)}{\log(1-(1-\epsilon)^s)} $$
其中p為置信度(如0.99),ε為異常值比例估計值,s為最小樣本數
應用示例
# 假設已有匹配結果
matches = flann.knnMatch(des1, des2, k=2)
good_matches = [m for m,n in matches if m.distance < 0.7*n.distance]# 提取匹配點坐標
pts1 = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1,2)
pts2 = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1,2)# RANSAC過濾
inlier_mask = ransac_filter_matches(pts1, pts2)
final_matches = [good_matches[i] for i in range(len(good_matches)) if inlier_mask[i]]
注意事項
- 匹配點對數量較少時(<10),RANSAC可能失效
- 場景中存在多個運動平面時,需改用多模型擬合方法(如PEARL)
- 對于純旋轉相機運動,建議使用單應性矩陣;一般運動建議用基礎矩陣
圖像變換與縫合
圖像拼接的最后一步是將輸入圖像變換并縫合到一幅圖像中。對于兩幅圖像A和B,在已
經檢測出對應的特征點對,并利用RANSAC算法計算得到變換矩陣T之后,將圖像B轉換為
TB。然后,對轉換后的圖像,即TB,與圖像A在重疊部分的像素值求平均值,以優化圖像縫
合的邊界。如此,便可得到最終縫合好的拼接圖像。
綜上所述,我們把圖像拼接的全過程總結為以下4步:
(1)計算兩幅圖像的特征點;
(2)將兩幅圖像的特征點進行匹配;
(3)根據匹配的特征點對,利用RANSAC算法計算圖像變換矩陣;
(4)將圖像進行拼接。
代碼實現
方法一:使用OpenCV內置的Stitcher類(最簡單)
import cv2# 讀取圖像
image1 = cv2.imread('image1.jpeg')
image2 = cv2.imread('image2.jpeg')# 檢查圖像是否成功讀取
if image1 is None or image2 is None:print("無法讀取圖像文件")exit()# 創建拼接器 效果:拼接結果出現了邊緣黑邊和形變
stitcher = cv2.Stitcher_create() if hasattr(cv2, 'Stitcher_create') else cv2.createStitcher()# 執行拼接
(status, stitched) = stitcher.stitch([image1, image2])# 保存結果
if status == cv2.Stitcher_OK:cv2.imwrite('stitched_output.jpg', stitched)print("拼接成功,結果已保存為 'stitched_output.jpg'")
else:print(f'拼接失敗,錯誤代碼: {status}')
方法二:完整實現
import cv2
import numpy as npdef stitch_images(images, ratio=0.75, reproj_thresh=4.0, show_matches=False):"""圖像拼接函數參數:images: 要拼接的圖像列表ratio: Lowe's ratio test參數reproj_thresh: RANSAC重投影閾值show_matches: 是否顯示特征匹配結果返回:拼接后的圖像"""# 初始化OpenCV的SIFT特征檢測器sift = cv2.SIFT_create()# 檢測關鍵點和描述符(kpsA, featuresA) = sift.detectAndCompute(images[0], None)(kpsB, featuresB) = sift.detectAndCompute(images[1], None)# 匹配特征點matcher = cv2.DescriptorMatcher_create("BruteForce")raw_matches = matcher.knnMatch(featuresA, featuresB, 2)# 應用Lowe's ratio test篩選好的匹配點good_matches = []for m in raw_matches:if len(m) == 2 and m[0].distance < m[1].distance * ratio:good_matches.append((m[0].trainIdx, m[0].queryIdx))# 至少需要4個匹配點才能計算單應性矩陣if len(good_matches) > 4:ptsA = np.float32([kpsA[i].pt for (_, i) in good_matches])ptsB = np.float32([kpsB[i].pt for (i, _) in good_matches])# 計算單應性矩陣(H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reproj_thresh)# 拼接圖像result = cv2.warpPerspective(images[0], H, (images[0].shape[1] + images[1].shape[1], images[0].shape[0]))result[0:images[1].shape[0], 0:images[1].shape[1]] = images[1]# 如果需要顯示匹配結果if show_matches:vis = np.zeros((max(images[0].shape[0], images[1].shape[0]), images[0].shape[1] + images[1].shape[1], 3), dtype=np.uint8)vis[0:images[0].shape[0], 0:images[0].shape[1]] = images[0]vis[0:images[1].shape[0], images[0].shape[1]:] = images[1]for ((trainIdx, queryIdx), s) in zip(good_matches, status):if s == 1:ptA = (int(kpsA[queryIdx].pt[0]), int(kpsA[queryIdx].pt[1]))ptB = (int(kpsB[trainIdx].pt[0]) + images[0].shape[1], int(kpsB[trainIdx].pt[1]))cv2.line(vis, ptA, ptB, (0, 255, 0), 1)cv2.imshow("Feature Matches", vis)cv2.waitKey(0)cv2.destroyAllWindows()return resultreturn None# 示例用法
if __name__ == "__main__":# 讀取兩張要拼接的圖像image1 = cv2.imread("image1.jpeg")image2 = cv2.imread("image2.jpeg")# 確保圖像讀取成功if image1 is None or image2 is None:print("無法讀取圖像文件")exit()# 調整圖像大小(可選)image1 = cv2.resize(image1, (0, 0), fx=0.5, fy=0.5)image2 = cv2.resize(image2, (0, 0), fx=0.5, fy=0.5)# 拼接圖像stitched_image = stitch_images([image1, image2], show_matches=True)if stitched_image is not None:# 顯示并保存結果cv2.imshow("Stitched Image", stitched_image)cv2.waitKey(0)cv2.destroyAllWindows()cv2.imwrite("stitched_result.jpg", stitched_image)else:print("圖像拼接失敗,可能匹配點不足")
使用建議
- 如果只是需要快速拼接,推薦使用第一種方法(Stitcher類)
- 如果需要了解基本原理或進行簡單定制,可以使用第二種方法
- 確保圖像有足夠重疊區域(建議30%以上重疊)
- 圖像大小不宜過大,可以先縮小處理
兩種方法都需要安裝OpenCV:
pip install opencv-python opencv-contrib-python