電卓でのタイムラプスビデオ安定化(IPython + OpenCV)

多くの元素および季節の天体写真愛好家のように、この8月は夜にペルセウス座を捕まえました。 ちょっとしたキャッチがありますが、今は彼についてではなく、そのような釣りの副作用が、それらをタイムアウトにするように要求する一連の写真であったという事実についてです。 しかし、ここに問題があります。カメラの設置は私たちが望むほど厳格ではなく、フレーム間にわずかな変化が現れました。 VirtualDubのシェーク解除プラグインで修正しようとしましたが、結果は満足のいくものではありませんでした。 それから、あなたの自転車を作ることに決めました:結果とそれらがどのように得られるかについての詳細-カットの下で。



従来の「前」と「後」(小さな断片がここに示されています)。 写真は縮小されますが、ここでも「カメラの揺れ」を確認できます。







処理後:







IPythonノートブック+ NumPy + OpenCVで行われます。



必要な警告:投稿には、信号処理の新しい単語も、指定された新しい言語とライブラリもありません。 初心者がここで「プログラミングしない方法」の例を見つけない限り、「IPythonノートブックでアルゴリズムをすばやく見つけてテストする方法」。 専門家のために、私は星を賞賛することを提案します。



なぜ「電卓」なのか、またなぜ自転車を作らなければならなかったのか-叙情的な余談
可能な場合、無料のアナログがある有料プログラムを拒否することにしたので、私はとりわけ、MATLABの交換を探し始めました。 そして、たくさんのIPython + SciPy [+ OpenCV]に落ち着きました。 しかし、私は大きくて非常に便利な電卓の役割でそれらを使用します:アイデアやソリューションのラピッドプロトタイピングや、一度必要なものを自分で説明するのが簡単な場合、これに適したプログラムを探すよりも、支払ったか、私が必要なものではない少しを行う-それは私が投稿で伝えたいこのケースについてです。



データ準備


静止したオブジェクトの可視性を改善するために、すべての画像の特別なバージョンを作成し、その上にすでにあるオフセットを見つけることが決定されました。 各フレームで行われたこと:



これは、元のフレーム(左)と比較して、前処理されたフレーム(右)の外観です。







フレームの読み取りと前処理


ソースファイル(以前にそれらからのバイアスを推定するために準備された)を読み取ります。 簡単にするために、作業ディレクトリはこれらのフレームが配置されているディレクトリと同じものを選択したため、名前をsampledata配列に読み込むだけです。 煩雑にならないように、このような配信コードの詳細は投稿で説明しません。 オリジナルのIPythonノートブックドキュメントで、プロセスで記述されたいくつかのマクロ関数と同様にそれらを見ることができます



それらのすべてをテストするために、いくつかのフレームを取りましょう(0と4にします)。 cv2.absdiff()を減算するだけで、これらのフレームとそれらの違いを示します。







フレームシフトは静止したオブジェクトのエッジが見える方法で見ることができますが、動く星はカメラシフトの評価に役立ちません。 したがって、可能であれば、erode操作を使用してそれらを取り除きます。 カーネルのサイズは経験的に選択されます

そのように
qx,qy=4,4 k=ones((qx,qy)) im1g=cv2.erode(im1g,k) im2g=cv2.erode(im2g,k) show1(im1g,u" ")
      
      











フレーム上に静止した物体が目立っており、これに取り付けることが可能であることがわかります。



画像間のシフトの推定


opencvのfind_obj.pyの例に示すように、隣接する画像で互いに対応するポイントを検索します: Scale Invariant Feature Transform(SIFT)を使用して区別可能なフラグメントを見つけ、結果の配列を比較してフィルターします。

それはどこから来たのですか
関数 `filter_matches`はから直接使用されます。`detectandselectmatches`もその機能をほとんど取ります。 それらに対するすべての権利は、それぞれの著者に留保されています。 私は今、彼らの仕事について詳しくは述べません。希望する人はいつでも助けを見ることができます。

 detector = cv2.SIFT() norm = cv2.NORM_L2 matcher = cv2.BFMatcher(norm) def filter_matches(kp1, kp2, matches, ratio = 0.75): mkp1, mkp2 = [], [] for m in matches: if len(m) == 2 and m[0].distance < m[1].distance * ratio: m = m[0] mkp1.append( kp1[m.queryIdx] ) mkp2.append( kp2[m.trainIdx] ) p1 = np.float32([kp.pt for kp in mkp1]) p2 = np.float32([kp.pt for kp in mkp2]) kp_pairs = zip(mkp1, mkp2) return p1, p2, kp_pairs def detectandselectmatches(fr1a,fr2a): kp1, desc1 = detector.detectAndCompute(fr1a, None) kp2, desc2 = detector.detectAndCompute(fr2a, None) raw_matches = matcher.knnMatch(desc1, trainDescriptors = desc2, k = 2) #2 p1, p2, kp_pairs = filter_matches(kp1, kp2, raw_matches) return p1, p2
      
      



 p1, p2 = detectandselectmatches(im1g,im2g)
      
      





結果の配列p1、p2は、それぞれフレーム1および2上の一致点のx、y座標のセットです。

例えば
 [665.927307129,17.939201355] [668.513000488,19.468919754]
 [744.969177246,60.6581344604] [747.49786377,61.8129844666]
 [746.388549805,77.1945953369] [749.15411377,78.5462799072]
 [892.944763184,169.295532227] [895.570373535,170.530929565]
 [906.57824707,185.634231567] [908.093933105,186.593307495]
 ...


それらの差をとると、各ポイントのバージョンに応じたフレームオフセットの配列を取得します。 この段階では、別のフィルタリングを行うことができます(一部のポイントは誤った一致により非常に順序が乱れる場合があります)が、平均の代わりに中央値を使用する場合、これらの誤った値はすべて重要ではありません。 これは写真ではっきりとわかります。青はポイントの各ペアのオフセットを表し、選択した値は赤で示され、比較のために緑は単純な平均です。

 dp=p2-p1 mx,my=np.median(dp[:,0]),np.median(dp[:,1])
      
      









  dx = 2.68393 dy = 1.34059 




逆シフトを実行する


実際のオフセットを生成するために残ります。 対応する値を適切な場所に挿入するだけで、 アフィン変換行列を手動で構築します。 これにはopencvの特別な関数を使用できますが、回転と変位の場合、マトリックスは非常に単純に見えます。



M = \ begin {bmatrix} cos(\ phi)&amp; -sin(\ phi)&amp; dx \\ sin(\ phi)&amp; cos(\ phi)&amp; dy \ end {bmatrix}






どこで \ピ 回転角であり、 dx そして dy -対応する変位値

 def getshiftmatrix( (dx,dy)): return array([[ 1., 0, dx], [ 0, 1., dy]])
      
      





実際に変換を開始します

 def shiftimg(im2,shift): tr1=getshiftmatrix(shift) return cv2.warpAffine(im2,tr1,tuple(reversed(im2.shape[:2]) )) im2r= shiftimg(im2,tuple(-array(shift)))
      
      





結果を見てみましょう。最初と同様に、単純な減算で比較します。







出来上がり! 必要なのは、すべての固定オブジェクトの代わりに黒でした。 したがって、フレームは結合されます。



すべてのフレームを処理する


すべてが機能します。 処理を処理できますか...まだですか? いくつかの詳細が残っています:



私たちはこれをすべて考慮して書きます。



 #   basepath4orig        arshifts=[] im1= cv2.imread(sampledata[0]) # base frame im1g = preprocess(im1) kp1, desc1 = detector.detectAndCompute(im1g, None) imgprev=cv2.imread(basepath4orig+sampledata[0]) #base original frame for i,x in enumerate(sampledata): print x, im2g = preprocess(cv2.imread(x)) kp2, desc2 = detector.detectAndCompute(im2g, None) raw_matches = matcher.knnMatch(desc1, trainDescriptors = desc2, k = 2) #2 p1, p2, kp_pairs = filter_matches(kp1, kp2, raw_matches) dp=p2-p1 if len(dp)<=0: shift=0,0 else: dx,dy=np.median(dp[:,0]),np.median(dp[:,1]) print dx,dy #process original frame imgr= shiftimg(cv2.imread(basepath4orig+x),(-dx,-dy)) if -dy>0: imgr[:int(ceil(abs(dy))),:,:] = imgprev[:int(ceil(abs(dy))),:,:] if -dy<0: imgr[-int(ceil(abs(dy))):,:,:] = imgprev[-int(ceil(abs(dy))):,:,:] if -dx>0: imgr[:,:int(ceil(abs(dx))),:] = imgprev[:,:int(ceil(abs(dx))),:] if -dx<0: imgr[:,-int(ceil(abs(dx))):,:] = imgprev[:,-int(ceil(abs(dx))):,:] imgprev=imgr cv2.imwrite('shifted_'+x+'.JPG',imgr)
      
      





それだけです-結果が得られます:不動のオブジェクト-接着されたもののように、星は本来のように回転し、雲は彼らのビジネスに従って浮きます、そして私は他に何を脱ぐべきかを考えます-すでに特別にタイムラプスのために。



youtube.comの結果:







***
最も注意深い人は、前のバージョンがyoutubeになったことに気付く可能性があります-エッジの黒いバーの修正なし-私はそれをもう交換しません、通常のバージョンは投稿のgifですでに与えられています




すべてが行われたIPythonノートブックドキュメントのコピーへのリンク



使用される材料と手段:




また、ありがとう



All Articles