图像拼接分为四步
1.特征点提取
2.特征点匹配
3.仿射矩阵计算
4.图像拼接与融合
1.特征提取
找到图像中具有显著性信息点,并计算该点的特征表达
def detectAndDescrible(img):
#构建STFT特征检测器
sift = cv2.SIFT_create()
#特征提取
kps,features = sift.detectAndCompute(img,None)
return kps,features
SIFT 是一种流行的计算机视觉算法,用于检测和描述图像中的关键点,这些关键点对于图像的旋转、尺度变换和亮度变化具有不变性。
SIFT 算法的主要原理可以概括为以下几个步骤:
-
尺度空间极值检测:
- SIFT 算法首先在不同尺度的空间中检测关键点。这是通过构建高斯尺度空间并计算每个像素点的高斯差分来实现的。在每个尺度上,算法会寻找能够成为关键点的局部极值点,这些点在高斯差分图中既是局部最大值也是局部最小值。
-
关键点定位:
- 一旦检测到潜在的关键点,算法会使用泰勒展开来更精确地定位这些关键点。这一步骤确保了关键点的位置和尺度的准确性。
-
方向分配:
- 为了使关键点具有旋转不变性,SIFT 算法为每个关键点分配一个或多个方向。这是通过计算图像局部区域的梯度方向直方图来实现的。每个关键点的方向由其周围区域的主方向决定。
-
关键点描述:
- SIFT 算法为每个关键点生成一个独特的描述子,该描述子捕捉了关键点周围区域的局部图像特征。描述子是通过将关键点周围的区域划分为16个子区域,并为每个子区域计算一个8维的梯度方向直方图来构建的,从而生成一个128维的向量。
-
匹配和筛选:
- 最后,SIFT 算法使用欧氏距离来匹配两幅图像中的关键点描述子。为了提高匹配的稳健性,算法通常会使用最近邻和次近邻距离比率测试来筛选匹配对。
存在问题:
会发现有高度相似的特征点
我们使用阈值ratio,用于确定好的匹配点对
完整代码
def matchKeyPoint(kps_l,kps_r,features_l,features_r,ratio):
Match_idxAndDist = []#存储最近点位置,最近点距离,次近点位置,次近点距离
for i in range(len(features_l)):
#从features_r中找到与i距离最近的2个点
min_IdxDis = [-1,np.inf]#距离最近的点
secMin_IdxDis = [-1,np.inf]#距离第二近的点
for j in range(len(features_r)):
dist = np.linalg.norm(features_l[i] - features_r[j])
if (min_IdxDis[1]>dist):
secMin_IdxDis = np.copy(min_IdxDis)
min_IdxDis = [j,dist]
elif(secMin_IdxDis[1]>dist and secMin_IdxDis[1] != min_IdxDis[1]):
secMin_IdxDis = [j,dist]
Match_idxAndDist.append([min_IdxDis[0],min_IdxDis[1],secMin_IdxDis[0],secMin_IdxDis[1]])
goodMatches = []
#如果i与最近的2个点的距离差较大,那么就不是好的匹配点
for i in range(len(Match_idxAndDist)):
if(Match_idxAndDist[i][1]<=Match_idxAndDist[i][3]*ratio):
goodMatches.append((i,Match_idxAndDist[i][0]))
#获取匹配较好的点对
goodMatches_pos = []
for(idx,correspondingIdx) in goodMatches:
psA = (int(kps_l[idx].pt[0]),int(kps_l[idx].pt[1]))
psB = (int(kps_r[correspondingIdx].pt[0]),int(kps_r[correspondingIdx].pt[1]))
goodMatches_pos.append([psA,psB])
return goodMatches_pos
显示匹配点
#显示匹配点
def drawMatches(img_left,img_right,matches_pos):
hl,wl = img_left.shape[:2]
hr,wr = img_right.shape[:2]
vis = np.zeros((max(hl,hr),wl + wr,3),dtype= np.uint8)
vis[0:hl,0:wl] = img_left #将两幅图像复制到画布上
vis[0:hr,wl:] = img_right
for(img_left_pos,img_right_pos)in matches_pos: #绘制匹配点连线
pos_l = img_left_pos
pos_r = img_right_pos[0] + wl,img_right_pos[1]
cv2.circle(vis,pos_l,3,(0,0,255),1) #绘制红色和绿色的圆圈来标记匹配点
cv2.circle(vis,pos_r,3,(0,255,0),1)
cv2.line(vis,pos_l,pos_r,(255,0,0),1) #绘制黄色线
return vis
仿射矩阵计算
在图像拼接中,仿射矩阵的计算是一个关键步骤,它用于将图像从一个视角转换到另一个视角,从而实现图像的对齐。
以下是计算仿射矩阵的步骤:
-
选择对应点: 通常需要至少三个非共线的点对来唯一确定一个仿射变换。这些点对可以是图像中的角点、边缘或其他特征点。
-
建立方程组: 对于每一对对应点 (x1,y1) 和(x2,y2),我们可以建立以下方程组: 其中,(a11,a12,a13) 和 (a21,a22,a23) 是仿射矩阵的元素。
-
构建矩阵A和向量B: 将上述方程组扩展到所有点对,并将它们写成矩阵形式:对于每个点对,我们添加一行到矩阵A中,并添加对应的 x2,y2 到向量B中。
-
求解仿射矩阵: 使用最小二乘法或其他数值方法求解方程组 ATAX=ATB,其中 AT 是矩阵A的转置。这将给出仿射矩阵 XX 的元素。
-
归一化和平移: 由于仿射矩阵的最后一行通常是 [0,0,1],我们可以通过归一化前两行来确保矩阵的正确性。然后,如果需要,我们可以添加平移向量。
def solve_homography(P, m):
try:
A = []
for r in range(len(P)):
A.append([-P[r, 0], -P[r, 1], -1, 0, 0, 0, P[r, 0] * m[r, 0], P[r, 1] * m[r, 0], m[r, 0]])
A.append([0, 0, 0, -P[r, 0], -P[r, 1], -1, P[r, 0] * m[r, 1], P[r, 1] * m[r, 1], m[r, 1]])
u, s, vt = np.linalg.svd(A)
H = np.reshape(vt[-1], (3, 3)) # Corrected index from 8 to -1
H = H / H[2, 2] # Normalize the matrix
except Exception as e:
print("Error:", e)
return None # Return None or a default value if an error occurs
return H
RANSAC(随机抽样一致性)
是一种用于从包含异常值(外点)的数据集中估计数学模型参数的迭代方法。在图像拼接中,RANSAC算法尤其有用,因为它可以有效地剔除错误匹配的特征点对,从而提高拼接的准确性。
-
RANSAC估计: 使用RANSAC算法从匹配点对中估计最佳单应性矩阵(Homography)。这个过程涉及到随机选择匹配点对,计算单应性矩阵,然后评估这个矩阵的一致性。如果一个匹配点对与单应性矩阵的投影误差小于设定的阈值,则被认为是内点(正确的匹配点);否则,被认为是外点(错误的匹配点)。
def fitHomoMat(matches_pos, nIter=1000, th=5.0):
dstPoints = []
srcPoints = []
for dstPoint, srcPoint in matches_pos:
dstPoints.append(list(dstPoint))
srcPoints.append(list(srcPoint))
dstPoints = np.array(dstPoints)
srcPoints = np.array(srcPoints)
# 利用RANSAC算法,获取最优矩阵
NumSample = len(matches_pos)
threshold = th
NumIter = nIter
NumRandomSubSample = 4
MaxInlier = 0
Best_H = None
for run in range(NumIter):
SubSampleIdx = random.sample(range(NumSample), NumRandomSubSample)
# 计算
H = solve_homography(srcPoints[SubSampleIdx], dstPoints[SubSampleIdx])
NumInlier = 0 # 初始化内点计数
pos_Inlier = [] # 初始化内点位置列表
for i in range(NumSample):
if i not in SubSampleIdx:
concateCoor = np.hstack((srcPoints[i], [1]))
dstCoor = H @ concateCoor.T
if dstCoor[2] <= 1e-8:
continue
dstCoor = dstCoor / dstCoor[2]
# 如果计算的目标点和匹配的目标点距离较近,则将这一对点定义为Inlier
if np.linalg.norm(dstCoor[:2] - dstPoints[i]) < threshold:
NumInlier += 1 # 正确的更新方式
pos_Inlier.append((srcPoints[i], dstPoints[i]))
if MaxInlier < NumInlier:
MaxInlier = NumInlier
Best_H = H
save_Inlier_pos = pos_Inlier
return Best_H, save_Inlier_pos
重叠部分的融合处理
def warp(img_left, img_right, HomoMat,blending_mode="linearrBlending"):
hl, wl= img_left.shape[:2]
(hr, wr) = img_right.shape [ :2]
stitch_img = np.zeros( (max(hl, hr), wl + wr, 3), dtype=np.uint8)
if (blending_mode == "noBlending"): #选择无混合模式,将左图复制到画布左侧
stitch_img[:hl, :wl] =img_left
# 从right img 转换到 left img
inv_H=np.linalg.inv (HomoMat) #计算给定单应性矩阵的逆矩阵
for i in range(stitch_img.shape[0]):#遍历画布的每个像素,使用逆矩阵将右图的坐标转换到左图的坐标中,并复制相应的像素值
for j in range(stitch_img.shape[1]):
#计算左图 i,j 处 对应右图哪个坐标点
coor = np.array([j, i, 1])
img_right_coor = inv_H @ coor # the coordination of right image
img_right_coor /= img_right_coor[2]
#y为宽x为高
y, x = int (round (img_right_coor[0])), int(round(img_right_coor[1]))
#超出范围 不处理
if (x < 0 or x >= hr or y < 0 or y >= wr) :
continue
stitch_img[i, j] = img_right[x, y]
if (blending_mode == "linearBlending"): #选择线性混合模式,调用相应的函数来处理图像重叠区域的融合
stitch_img = linearBlending([img_left, stitch_img])
elif (blending_mode == "linearBlendingWithConstant"):
stitch_img = linearBlendingWithConstantWidth([img_left, stitch_img])
# 去除黑边
stitch_img = removeBlackBorder (stitch_img)
return stitch_img
去除黑边
通过检查图像的每一列和每一行是否全为黑色(即所有像素值均为0),然后移除全黑的列和行来实现。
def removeBlackBorder(img):
h,w = img.shape[:2]
reduced_h,reduced_w = h,w
for col in range(w - 1,-1,-1):
all_black = True
for i in range(h):
if(np.count_nonzero(img[i,col])>0): #检查是否为黑色
all_black = False
break
if (all_black ==True):
reduced_w = reduced_w-1
for row in range(h-1,-1,-1):
all_black = True,
for i in range(reduced_w):
if(np.count_nonzero(img[row,i])>0):
all_black = False,
break
if(all_black == True):
reduced_h = reduced_h-1
return img[:reduced_h,:reduced_w]
图像融合
def linearBlending (imgs) :
img_left, img_right = imgs
(hl, wl) = img_left.shape[:2]
(hr, wr) = img_right.shape[:2]
img_left_mask = np.zeros((hr, wr), dtype=np.uint8)
img_right_mask =np.zeros((hr, wr), dtype=np.uint8)
#找到img_left 和 img_right 的mask部分 即非0部分
for i in range (hl):
for j in range (wl):
if np.count_nonzero(img_left[i, j]) > 0:
img_left_mask[i, j] = 1
for i in range (hr) :
for j in range (wr) :
if np.count_nonzero(img_right[i, j]) > 0:
img_right_mask[i, j] = 1
#找到两图重合的部分
overlap_mask = np.zeros((hr, wr), dtype=np.uint8)
for i in range (hr) :
for j in range (wr) :
if (np.count_nonzero(img_left_mask[i, j]) > 0 and np.count_nonzero(img_right_mask[i, j]) >0):
overlap_mask[i, j] = 1
#计算重叠区域的线性alph值,即将色彩从img_left 到 img_right 逐步过度
alpha_mask = np.zeros((hr, wr)) # alpha value depend on left image,
for i in range (hr) :
minIdx = maxIdx = -1
for j in range (wr) :
if (overlap_mask[i, j] == 1 and minIdx == -1):
minIdx = j
if (overlap_mask[i, j] == 1):
maxIdx = j
if (minIdx == maxIdx):#融合区域过小
continue
decrease_step = 1 / (maxIdx - minIdx)
for j in range (minIdx, maxIdx + 1) :
alpha_mask[i, j] =1- (decrease_step * (j - minIdx))
linearBlending_img = np.copy(img_right)
linearBlending_img[:hl,:wl]=np.copy(img_left)
#线性混合
for i in range (hr) :
for j in range (wr) :
if (np.count_nonzero(overlap_mask[i, j]) > 0):
linearBlending_img[i, j] = alpha_mask[i, j] * img_left[i, j] + (1 - alpha_mask[i, j]) * img_right[i, j]
return linearBlending_img