パノラマ画像のステッチ
- パノラマ画像スティッチングの手動実現
- 環境:python3.6 + opencv3.4.2.16
##サンプル画像
この実験で使用した画像ステッチング素材は、次の3つの画像です。
https://andreame.com/2019/11/12/stitch.html
この実験の目的は、これら3つの画像を円筒面に投影し、パノラマステッチを実行することです。
Opencvの組み込み実装
まず、opencvにはstitchクラスが組み込まれています。これには、パノラマ画像のステッチに使用できるメソッドが含まれています。
基本的な使用法は非常に単純で、コアコードは2行で実装されています
import cv2
from cv2 import Stitcher
def stitcher(images)
# images是待拼接图像的list
Stitcher = cv2.Stitcher.create(cv2.Stitcher_PANORAMA)
(status, pano) = Stitcher.stitch(images)
return pano
Python
コピー
そして、デフォルトのステッチ効果はかなり良いです
したがって、画像スティッチング機能の完了を実現するために、上記の2行のコードはすでに解決できます。
分析
もちろん、呼び出すことを学ぶだけでは十分ではありません。手動で実装することをお勧めします。残念ながら、opencvは呼び出される基になるcppライブラリであり、Pythonの実装ではありません。ソースコードを直接変更するというアイデア実装できません。カプセル化されたステッチクラスのパイプラインは次のとおりです。
opencvのドキュメントでは、このクラスには主に次のモジュールが含まれていると指摘されています。
- 機能検索と画像マッチング
- 回転推定
- 自動校正
- 画像ワーピング
- シーム推定
- 露出補正
- イメージブレンダー
一部のスプライシングプロセスは遅く、通常のパノラマ画像スプライシングでは、このような面倒な手順は必要ないため、モジュール構成を自分で設計できます。
設計と実装
コードの実装は、主にこのブログとこのチュートリアルを参照しています。
主な手順は次のとおりです。
- 画像の円筒図法変換
- ステッチする画像の特徴点と説明点を計算します
- 画像間のフィーチャ記述位置距離を計算します
- 最高の特徴点を選別する
- RANSACを使用してホモグラフィ行列を計算します
- ワーピング
- シームフュージョン
- ステッチ
- 画像のトリミング
円筒図法
回路図はブログ1を参照できます
簡単に言えば、元の画像の特定のピクセル(x、y)を焦点距離fの円柱面に投影します。
\ begin {array} {l} {\ text {1.} \ theta = \ arctan \ left(\ frac {x} {f} \ right)} \\ {\ text {2.} x ^ {\ prime} = f \ times \ theta} \\ {\ text {3.} \ frac {y} {\ frac {f} {\ cos(\ theta)}} = \ frac {y ^ {\ prime}} {f} } \ end {array}1.θ= arctan(f x)2。x ′= f×θ3。cos(θ)f y = f y ′
上記の式により、$(x、y)$から$(x ^ {\ prime}、y ^ {\ prime})$への変更を実現できます。コードは次のとおりです。
def cylindricalProjection(img) :
rows = img.shape[0]
cols = img.shape[1]
#f = cols / (2 * math.tan(np.pi / 8))
result = np.zeros_like(img)
center_x = int(cols / 2)
center_y = int(rows / 2)
alpha = math.pi / 4
f = cols / (2 * math.tan(alpha / 2))
for y in range(rows):
for x in range(cols):
theta = math.atan((x- center_x )/ f)
point_x = int(f * math.tan( (x-center_x) / f) + center_x)
point_y = int( (y-center_y) / math.cos(theta) + center_y)
if point_x >= cols or point_x < 0 or point_y >= rows or point_y < 0:
pass
else:
result[y , x, :] = img[point_y , point_x ,:]
return result
Python
コピー
変換の結果は次のとおりです
右の写真は、左の写真を円筒図法で投影した結果です。
もちろん、黒い境界線が画像のステッチの効果に影響を与えないようにするために、事前に黒い境界線を削除する必要もあります。黒い境界線を削除する方法については、後で説明します。
特徴点の抽出
特徴点の抽出
特徴点の抽出は、opencvの組み込み関数を使用して実装できますdetectAndCompute()
。
opencv-python4より上のバージョンでは、著作権の問題により、この関数は開かれなくなり、バージョンをopencv3に戻す必要があることに注意してください。著者が使用する環境は
- python3.6
- opencv-contrib-python 3.4.2.16
- opencv-python 3.4.2.16
関数呼び出しは次のとおりです。
def getSURFFeatures(im)
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
surf = cv2.xfeatures2d.SURF_create()
kp, des = surf.detectAndCompute(gray, None)
#sift = cv2.xfeatures2d.SIFT_create()
#kp, des = sift.detectAndCompute(im, None)
cv2.imwrite('imageWithKeypoints.png',cv2.drawKeypoints(im,kp,None))
return {'kp':kp, 'des':des}
Python
コピー
ここでは、opencvに組み込まれているsiftメソッドまたはsurfメソッドを使用できます。次の図は、図3のこれらの特徴点を示しています。
特徴点距離の計算
2つの画像の特徴点を取得したら、次のステップは、2つの写真の特徴点間の対応を見つけることです。2つの画像を大きな画像につなぎ合わせるには、それらの間の一致点を見つける必要があります。これらの一致点を使用して、最初の画像を回転、ズーム、および変形して、次の画像とつなぎ合わせる方法を決定することができます。これは対応を見つけることです。
ここでは、opencvが提供する2つのメソッドを使用して、イメージのFLANNまたはBFMatcherメソッドを照合できます。
特定の実装コードは次のとおりです。
# matches = self.flann.knnMatch(
# imageSet2['des'],
# imageSet1['des'],
# k=2
# )
match = cv2.BFMatcher()
matches = match.knnMatch(
imageSet2['des'],
imageSet1['des'],
k=2
)
Python
コピー
k = 2の場合、このパラメーターは、一致によって2つの最良の一致が得られるようにすることを意味します。
draw_params = dict(matchColor = (0,255,0), # draw matches in green color
singlePointColor = None,
flags = 2)
img3 = cv2.drawMatchesKnn(i1,imageSet1['kp'],i2,imageSet2['kp'],matches,None,**draw_params)
cv2.imwrite("correspondences.png", img3)
print(len(matches))
Python
コピー
効果を描画します。次の図は、2つの画像間の特徴点のマッチングを示しています。
明らかに、特徴点が多すぎて、特徴点の数は615です。
最適な特徴点をフィルタリングする
通常、画像では、特徴点が画像内の複数の場所に存在するため、さらにスクリーニングが必要です。
good = []
for i, (m, n) in enumerate(matches):
if m.distance < 0.3*n.distance:
good.append((m.trainIdx, m.queryIdx))
Python
コピー
この場合、2つの画像間で最適な一致が得られ、次のステップはホモグラフィ行列を計算することです。
この比率を0.3に設定すると、フィルタリング効果は次の図に示されます。
現時点では、特徴点の数は39です。
ホモグラフィ行列の計算
ホモグラフィ行列の機能は、最適なマッチングポイントに基づいて2つの画像の相対的な方向変換を推定することです。ここでは、cvの組み込みRANSACメソッドを使用して以下を解決できます。
H, s = cv2.findHomography(matchedPointsCurrent, matchedPointsPrev, cv2.RANSAC, 4)
Python
コピー
得られた$ H $は、解くべきホモグラフィ行列です。
I_x = H \ times I_yI x = H×Iy
結果のホモグラフィマトリックスは次のとおりです
\ begin {bmatrix} h_ \ text {11}&h_ \ text {12}&h_ \ text {13} \\ h_ \ text {21}&h_ \ text {22}&h_ \ text {23} \\ h_ \ text {31}&h_ \ text {32}&h_ \ text {33} \ end {bmatrix}⎣⎡h11h 21 h 31 h 12 h 22 h 32 h 13 h 23h33⎦⎤
ラップ
前のステップでホモグラフィマトリックスを取得し、次のステップは画像をステッチすることです。
まず、2つの間のホモグラフィ行列を取得します。最初の画像の観点から、2番目の画像がどのように見えるかを理解できます。画像を変えて新しい空間に変える必要があります。次に、ワープ変換のプロセスが必要です。
反り変換には次のものが含まれます
- 平面変換
- 円筒形の変形
- 球面変換
次の関数を呼び出すだけで完了できます。
warped_image = cv2.warpPerspective(image, homography_matrix, dimension_of_warped_image)
Python
コピー
上記の関数で、imageは変換される画像を表し、homography_matrixは取得したホモグラフィ行列であり、dimension_of_warped_imageは変換後の画像のサイズです。
したがって、反り変換を行うには、反り変換後のサイズも取得して、スプライシングの準備をする必要があります。
H = self.matcher_obj.match(a, b, 'left')
xh = np.linalg.inv(H)
ds = np.dot(xh, np.array([a.shape[1], a.shape[0], 1]))
ds = ds / ds[-1] #
f1 = np.dot(xh, np.array([0, 0, 1]))
f1 = f1 / f1[-1]
xh[0][-1] += abs(f1[0])
xh[1][-1] += abs(f1[1]) #
ds = np.dot(xh, np.array([a.shape[1], a.shape[0], 1]))
offsety = abs(int(f1[1]))
offsetx = abs(int(f1[0]))
dsize = (int(ds[0]) + offsetx, int(ds[1]) + offsety)
Python
コピー
最終結果は、ワーピング変換後に取得できます。
ホモグラフィ行列$ H $があります。各画像の開始座標が$(0,0)$で、終了座標が$(r_e、c_e)$の場合、この変更によってワープした後の画像サイズを取得できます。
開始点の計算$ Point_ {strat}:= H \ times(0,0)$から終了点の計算$ point_ {end}:= H \ times(re、c_e)$まで。
もちろん、スプライシングの次のステップは簡単tmp[offsety:b.shape[0]+offsety, offsetx:b.shape[1]+offsetx] = b
に実行できますが、これにより継ぎ目が非常に明確になるため、調整も必要になります。
効果は次のとおりです。
非常にはっきりとした継ぎ目が見られますが、このような単純な処理では、目的の効果が得られないことは明らかです。
シームフュージョン
それで、継ぎ目を取り除く方法は、ここに加重融合の方法です。
2つの画像の重なり部分を計算するという考え方です。重なり部分については、ピクセルを段階的に遷移させる方法を使用して、肉眼で観察できる急激な変化を排除します。
次に、加重融合を使用して問題を解決することを検討できます。つまり、スプライシングパーツのピクセルを、同じ座標を持つ2つの画像のピクセルの加重和にします。
point_ {result} = \ alpha \ times point_ {a} +(1- \ alpha)\ times point_ {b}ポイント結果=α×ポイントa +(1-α)×ポイントb
式で$ \ alpha $を決定する方法は、重みを計算するための距離です。一致境界から画像境界までの画像です。この重みは1から0までスムーズになります。
def seamEstimation(self,tmp,leftimage,rightimage,offsetx,offsety):
alpha = 0.0
processwidth = leftimage.shape[1] - offsetx
print("initial_width",processwidth)
for x in range(offsetx,tmp.shape[1]):
test_y = int((offsety + leftimage.shape[0])/2)
if np.array_equal(tmp[test_y,x],np.array([0,0,0])):
#print(tmp[test_y,x])
processwidth = x - offsetx
break
print("now_width",processwidth)
x_max = np.minimum(offsetx+rightimage.shape[1],tmp.shape[1])
y_max = np.minimum(offsety+rightimage.shape[0],tmp.shape[0])
for x in range(offsetx,x_max):
for y in range(offsety,y_max):
rx = x - offsetx
ry = y - offsety
if not np.array_equal(tmp[y,x],np.array([0,0,0])) and not np.array_equal(rightimage[ry,rx],np.array([0,0,0])):
alpha = np.minimum(rx/processwidth,1.0)
#alpha = rx/processwidth
tmp[y,x] = alpha*rightimage[ry,rx]+(1-alpha)*tmp[y,x]
elif np.array_equal(tmp[y,x],np.array([0,0,0])):
tmp[y,x] = rightimage[ry,rx]
elif np.array_equal(rightimage[ry,rx],np.array([0,0,0])):
tmp[y,x] = tmp[y,x]
else:
print("Some problems happen!")
return tmp
Python
コピー
次に、複数の画像を連続してステッチして、ステッチされた画像を取得します。
全体的な効果は大丈夫ですが、明らかなスプライシング部分にはゴースト画像があります。もちろん、これはピクセルフュージョンによって引き起こされる必然的な結果であり、後で改善する必要があります。
MultipleImagesStitching
以上の説明により、画像のステッチの基本作業は完了しました。その後、上記の2つのステッチ作業を連続して繰り返すだけで済みます。
平面変更を採用する場合、過度のずれを避けるために、真ん中の画像を左右のステッチのベースラインとして使用する必要があることに注意してください。そして、写真の数が多すぎる場合は、バッチでスプライスする必要があります。
ただし、ここでは円筒形のスプライシングが使用されており、順次スプライシングによって深刻な結果が生じることはありません。スプライシングコードは次のとおりです。
def shift(self):
a = self.images[0]
for b in self.images[1:]:
H = self.matcher_obj.match(a, b, 'left') # 特征点匹配
xh = np.linalg.inv(H)
ds = np.dot(xh, np.array([a.shape[1], a.shape[0], 1]))
ds = ds / ds[-1] #
f1 = np.dot(xh, np.array([0, 0, 1]))
f1 = f1 / f1[-1]
xh[0][-1] += abs(f1[0])
xh[1][-1] += abs(f1[1]) #
ds = np.dot(xh, np.array([a.shape[1], a.shape[0], 1]))
offsety = abs(int(f1[1])) # y偏移量 需要了解单应性矩阵的作用
offsetx = abs(int(f1[0])) # x偏移量
dsize = (int(ds[0]) + offsetx, int(ds[1]) + offsety) # 图片大小统计
tmp = cv2.warpPerspective(a, xh, dsize)
tmp = self.seamEstimation(tmp,a,b,offsetx,offsety)
a = tmp # 为循环做准备
return a
Python
コピー
画像のトリミング
もちろん、この時点で取得した画像にはまだ多数の黒いフレームが含まれているため、画像操作でトリミングする必要があります。
画像をグレースケールに変換する
img = cv2.medianBlur(image,3) #中值滤波
b=cv2.threshold(img,0,255,cv2.THRESH_BINARY)
binary_image=b[1] #二值图--具有三通道
binary_image=cv2.cvtColor(binary_image,cv2.COLOR_BGR2GRAY)
Python
コピー
ここでは、単に画像操作を使用して、画像をバイナリ画像に変換します。
さらに、次のステップに役立つように、彼のすべての境界線が露出されるように、引き続き10ピクセルをアウトリーチする必要があります。
最大輪郭を計算する
ret, thresh = cv2.threshold(binary_image, 0, 255, cv2.THRESH_BINARY)
binary ,cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
Python
コピー
opencvの組み込み関数を使用して、この画像の最大輪郭を計算します。
最大の外接長方形を描画します
mask = np.zeros(thresh.shape, dtype="uint8")
x, y, w, h = cv2.boundingRect(cnt)
Python
コピー
連続腐食操作
最初の外接ボックスを最大の外接長方形ボックスに設定し、これを開始点として使用して、選択したボックスに背景のピクセル位置が含まれていないことを認識して、選択したボックスの面積を減らすために継続的に衣類の操作を実行します、必要なバウンディングボックスを見つけることができます。$(x、y、w、h)$
下の写真は、トリミング後のステッチ画像です。
境界線を削除するコードは次のとおりです。
def ExtractImage(image):
# 去除边框,提取图像内容
plt.figure(num='ExtractImage')
image = cv2.copyMakeBorder(image, 10, 10, 10, 10, cv2.BORDER_CONSTANT, (0, 0, 0))
binary_image = getBinaryImage(image)
cv2.imwrite("binary_image.png", binary_image)
ret, thresh = cv2.threshold(binary_image, 0, 255, cv2.THRESH_BINARY)
binary ,cnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea) # 获取最大轮廓
mask = np.zeros(thresh.shape, dtype="uint8")
x, y, w, h = cv2.boundingRect(cnt)
# 绘制最大外接矩形框(内部填充)
cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)
cv2.imwrite("mask.png", mask)
minRect = mask.copy()
sub = mask.copy()
print(sub.shape[0] * sub.shape[1])
# 连续腐蚀操作,直到sub中不再有前景像素
while cv2.countNonZero(sub) > 0:
minRect = cv2.erode(minRect, None)
sub = cv2.subtract(minRect, thresh)
binary ,cnts, hierarchy = cv2.findContours(minRect.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = max(cnts, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
image = image[y:y + h, x:x + w]
return image
Python
コピー
穴埋め
前のステップでは、腐食操作のサイクル条件を「候補フレームに背景ピクセルがない」に設定しました。次に、背景ピクセルをどのように判断しますか?背景ピクセルとしてのピクセル値は0であると簡単に考えることができます。ただし、元の画像またはその他の操作により、ピクセル値が0のピクセルが画像コンテンツに表示される場合は、次のようになります。
2つの画像をつなぎ合わせた後、結果の画像は2値画像に変換されます。画像には穴があり、急にトリミングされた場合は上半分が差し引かれます。
このような画像を直接トリミングすると、次の結果が得られます。
したがって、トリミングの次のステップを実行する前に、画像に穴がないことを確認するために、高度な穴の塗りつぶしが必要です。
塗りつぶし後、トリミングの結果は次のようになります。
最終結果
接合する前に
ステッチ後
ズーム
時間コスト:5.6752543449401855
参照
- 円筒図法:https://blog.csdn.net/zwx1995zwx/article/details/81005454
- 個人ブログ:https://www.andreame.com/2019/11/09/stitch.html
- https://kushalvyas.github.io/stitching.html
- https://medium.com/pylessons/image-stitching-with-opencv-and-python-1ebd9e0a6d78
- 「opencv文档」:https://docs.opencv.org/3.4.2/d1/d46/group__stitching.html