[OpenCV] 第 22 章: 画像特徴検出のための SIFT アルゴリズム

第 22 章: 画像特徴検出のための SIFT アルゴリズム

この章は第 21 章の続きであり、引き続き画像特徴抽出について説明します。ハリスコーナー検出の最大の欠点は、スケール不変性がないことであり、画像を拡大すると、本来検出できたコーナーがエッジになってしまい検出できなくなることです。この章では、特徴点を抽出するための SIFT アルゴリズムについて説明します。

SIFT (正式名は Scale-Invariant Feature Transform) は、スケーリングに依存しない、つまりスケール不変性を持つ特徴検出です。 SIFT アルゴリズムは、従来の CV 分野における最高峰のアルゴリズムと言え、拡大縮小、変形、ぼかし、明暗の変更、照明の変更、写真へのノイズの追加、さらには別のカメラを使用することもできる非常に強力なアルゴリズムです。さまざまな角度から写真を撮影する場合でも、SIFT は安定した特徴点を検出し、対応関係を確立できます。今日では、ディープラーニングが研究の主流になっていますが、SIFT 特徴点検出は依然として最良の局所特徴検出方法の 1 つであり (速度係数を考慮しない場合、この「1」は削除できます)、SIFT 特徴点検出は SIFT 特徴の説明に記載されています。また、人工的に設計されたフィーチャの時代における多数の勾配方向ヒストグラム フィーチャにも影響を与えました。

SIFT アルゴリズムは、カナダのブリティッシュ コロンビア大学教授 David Lowe によって 1999 年に発表され、2004 年に要約されました。Google Scholar によると、SIFT は 2004 年に 55,841 回も引用されました。


Lowe は非常にビジネスに精通しており、このアルゴリズムの特許をすぐに申請しました。そのため、長い間、OpenCV の SIFT のコードは Non-Free モジュールに含まれていました。特許で保護されているため、気軽に使用することはできません。商業シナリオ。 2020年3月6日をもって、20年の時を経て特許が切れました! SIFT は全人類の公共テクノロジーとなり、誰でも、どの組織でも無料で使用できます。 OpenCV 関係者は、SIFT を Non-Free モジュールから外しました (ただし、業界関係者は、OpenCV によって実装された SIFT はひどいものであると考えており、VLFeat で SIFT を使用することが推奨されています)。

今日でも、SIFT には商業的価値があり、一部の関連分野で広く使用されています。適用範囲には、オブジェクトまたはシーンの認識、ステレオ対応、モーション追跡、複数の画像からの 3D 構造の解決、ロボットのマップ認識とナビゲーション、画像スティッチング、ジェスチャー認識、画像追跡、アクション比較、およびその他の分野が含まれます。
SIFT の唯一の欠点は、非常に遅いことです。静止画像は検出できますが、動画は検出できません。リアルタイム認識は耐えられないほど遅くなります。

もちろんこのアルゴリズムも非常に複雑で面倒なので、opencv の API について簡単に書こうと思ったのですが、よくよく調べてみると手順が多すぎて書かざるを得ませんでした。このアルゴリズムについては別の章を参照してください。このアルゴリズムについてはインターネット上にさまざまな情報があり、論文自体も非常に曖昧であるため、インターネット上には誤解が溢れています。現在の私の知識とプログラミング能力では細かい部分が理解できていない部分もあり、明確に解説している情報も見つかりませんでしたが、長々と書いたのに断念するのは残念なので、このバージョンは最初のバージョンであり、今後もこの記事を改善し続けます。

  • SIFT はどのように検出されますか?
    SIFT 検出は、人間の目でオブジェクトを見るプロセスも利用します。人間の目でオブジェクトを見るとき、オブジェクトの局所的な詳細だけでなく、オブジェクトの全体的な輪郭も見る必要があります。人間の目で物体を近距離で見ると、物体は比較的大きく、遠くから見ると小さく見えます。これはカメラで写真を撮るのと同じで、近くでは大きく、遠くでは小さくなります。 SIFT は、画像を異なるスケール空間の一連の画像に処理します。この一連の画像には、局所的な鮮明な詳細と全体的にぼやけた輪郭の両方があり、また、異なるサイズ (近くと遠くの大小の効果) が含まれます。一連の写真の中から特徴点抽出として安定した点を見つけます。
  • SIFT はどのような特徴点を検出できますか?
    SIFT で検索される特徴点は、コーナー点、エッジ点、暗部の輝点、明部の暗点、局所的な平坦部などの非常に目立つ点です。この点はこのローカルエリアを表します。また、これらの点は、平行移動、回転、拡大縮小などのアフィン変換、照明の輝度変換、ノイズなどの影響により検出できないことはありません。したがって、SIFT アルゴリズムを使用して検出された特徴点は非常に良好な結果をもたらします。 SIFT を使用して検出された特徴点は、マッチング、位置決め、追跡などに非常に効果的です。論文の原文は次のとおりです。この方法では、さまざまな視野角からのオブジェクトやシーンの信頼できるマッチングを実現でき、アルゴリズムによって抽出された特徴は画像のスケールや回転に対して不変です。画像がアフィン歪み、3D 視点の変更、ノイズの追加、照明の変更などを受けた場合、抽出した特徴はすべて、非常に優れた堅牢性を備えていることを示しています。さらに、この方法は、リアルタイム認識プロセス中に、認識されたオブジェクトが乱雑であるかブロックされているかを認識でき、優れたロバスト性を備えています。この認識アプローチでは、ほぼリアルタイムのパフォーマンスを達成しながら、クラスターおよびオクルージョン間でオブジェクトを確実に識別できます。

同時に、このアルゴリズムは画像全体のスケールと位置を密にカバーする多数の特徴点を抽出することができ、これらの特徴は独立性が高くなります。たとえば、500*500 ピクセルの画像の場合、約 2000 の安定した特徴が生成されます。

SIFT によって抽出された特徴点は、キーポイント、極値ポイント、kp とも呼ばれます。これらの点は、x および y 座標情報 (つまり、元の画像内で kp 点が存在する位置情報) だけでなく、 点の方向とサイズ は、アフィン変換やライティングなどの影響を受けない変数の集合です。特徴点の記述子を使用すると、記述子はアフィン変換、照明、ノイズなどの要因の影響を受けないため、記述子に基づいて 2 つの画像内の同じ特徴点を照合して、照合の目的を達成できます。下の写真の左側のテンプレートは、右の写真の 3 階の 2 番目の部屋の窓の写真です。SIFT アルゴリズムを使用して、右の写真のこの窓を見つけます。 特徴点に寄与する周囲のピクセルの情報 または des とも呼ばれ、長さは 128 で、このベクトルのセットはこれを表します。 記述子

画像マッチングは、コンピュータ ビジョンの分野における多くの問題の中でも基本的な側面です。キーポイント記述子の基本的な機能は画像マッチングに使用されます。特徴マッチングアルゴリズム (次章で説明) を使用して、検索画像の DES 記述子と画像ライブラリ画像の DES を照合できます。を押すと、一致する値が取得されます。これは画像検索です。画像のスプライシングでは、2 つの画像で同じ des が見つかった場合、一致する kp ペアが決定され、画像のスプライシングを開始できます。2 つの画像間に同一の特徴記述子がない場合、スプライシングは実行できません。

  • SIFT アルゴリズムのステップ
    Lowe は、SIFT アルゴリズムを 4 つのステップに分解します。
    1. スケール空間の極値検出 -- スケール空間の極値検出
    2. キーポイントの位置特定 -- キーポイントの位置決め
    3. 方向の割り当て -- キーポイントの方向の決定
    4. キーポイント記述子 --重要なポイントの説明

このステップは Lowe による大まかな全体的な説明であり、中間には以下の各ステップの内訳を参照することで理解できる詳細が多数あります。

1. スケール空間極値検出

SIFT アルゴリズムはさまざまなスケール スペースでキー ポイントを検索するため、最初のステップとして画像のさまざまなスケール スペースを構築し、次にこれらのさまざまなスケール スペースで 予備の極値検出 。
異なるスケールの空間を構築する手順は 2 つの小さな手順に分かれています。最初の小さな手順はガウス ピラミッドを構築することであり、2 番目の小さな手順はガウス ピラミッドの隣接する層を減算することです。ガウス差分ピラミッドを取得します。したがって、スケール空間は、画像のガウス ピラミッドとガウス差分ピラミッドです。
予備的な極値検出も 2 つの小さなステップに分割されます。最初の小さなステップは、しきい値を使用していくつかのノイズ極値点を除去することであり、2 番目の小さなステップは ガウス差分ピラミッドで、同じレイヤー内の垂直方向の 26 の近傍内で、最初に極値点を見つけます< a i=8> 。

このステップは次のように分解できます。ガウス ピラミッドの構築 - ガウス差分ピラミッドの構築 - しきい値処理 - 極値の予備検出 4 つの小さなステップ。

注: 一部の参考資料では、第 1 ステップをガウス スケール空間の構築と呼び、第 2 ステップを極点検出と呼んでいますが、実際、個人的には、この方が明確であると感じています。ただし、論文の原文はスケール空間の極値検出に関するものであるため、論文自体が非常に曖昧であり、他の解釈も多岐にわたるため、著者に敬意を表し、著者の手順に従うことにします。

  • まず、ガウス ピラミッドがどのようなものかを見てみましょう。

    このピラミッドは、第 11 章で説明したピラミッドとは多少異なります。全体的に、このピラミッドは数字のように見えます。普通のピラミッドを重ね合わせたもの。
  • ここで、最初にいくつかの概念を明確にする必要があります:
    (1) 上のガウス ピラミッドの「層」は「層」とは呼ばれません。「グループ」または直接「グループ」と呼ばれます。オクターブは文字通りオクターブを意味します。
    (2) ピラミッドの各グループ (各オクターブ) には複数の「レイヤー」があります。つまり、各オクターブには、同じ画像サイズ (または画像解像度) (レイヤー) 画像を持つ複数の画像があります。つまり、異なるレイヤーのピクチャのサイズは同じですが、異なるグループのピクチャのサイズは異なります。
    (3) 上のピラミッドを ガウス ピラミッド と呼び、場所によっては とも呼ばれます。 < a i=6>ガウス スケール空間 (またはガウス スケール空間 S(x,y,δ) (4) ガウス差分 (DOG) は、ガウス ピラミッドの隣接する層間の差分によって得られます。したがって、DOG の全体的な外観はガウス ピラミッドと同じであり、オクターブ グループの数は同じですが、各グループの層の数が異なり、DOG の層はガウス ピラミッドの層より 1 つ少なくなります。違いにより、レイヤーが 1 つ少なくなります。ガウス ピラミッドに 5 つの層がある場合、DOG ピラミッドには 4 つの層があります。

参考文献が多数あるため、ここで名称や用語を統一したのですが、統一した上で論文原文や他の参考文献を見たときに混乱することはありません。

1. ガウスピラミッドの構築

  • 建設完了後の効果は次のとおりです。

    上の画像はガウス自身のアバターです
  • この論文の実装手順:
    最初の手順: 元の画像を 2 倍にする
    双線形補間方法を使用します (第 5 章 この補間方法 (この補間方法は、画像をスケーリングするときに説明しました)画像の幅と高さがそれぞれ 2 倍になります。画像の解像度を2倍にし、2倍に拡大することを意味します。上の最初の行にある 1 枚の写真です。
    この論文の原文は次のとおりです: アルゴリズムはまず、双線形補間を使用して入力の幅と高さを 2 倍にします。それが上の最初の画像、つまりその行にある画像です。 < a i=4> 注: 最初に画像を 2 倍にする理由は、元の画像情報をできるだけ多く保持するためなので、最初にアップサンプリングする必要があります。 2 倍になったイメージは Octave-1 と呼ばれます。ただし、このステップを省略して元の画像を直接操作できる場合もありますが、このステップは必要ありません。このステップを実行する必要がある場合は、opencv で cv2.resize() 関数を呼び出すだけです。ステップ 2: ガウス コンボリューション演算を実行する 最初のステップで取得した画像に対してガウス コンボリューション演算を実行し、より大きなガウス コンボリューション カーネルを使用してコンボリューション結果をコンボリューションし続けます。ぼやけた画像にロールします。 論文の元の文言は次のとおりです: この画像はガウス畳み込みを使用してぼやけています。それはオレンジ色の矢印で示されています。 以下は次のとおりです。標準偏差が増加するさらなる畳み込みのシーケンス。さらに右にある各画像は、緑色の矢印で示されているように、その左隣の画像を畳み込んだ結果です。 ガウス畳み込みの関数は次のとおりであることがわかっています。画像がぼやけるように画像を滑らかにします。このステップの結果は、上の図の 2 行目の 6 枚の画像です。これは、ガウス ピラミッドの最初のグループ (Octave1) の各層の画像です。この一連の写真は、私たちが目で見たときの物体の細部と輪郭を模倣しており、澄んでいれば細部が見え、ぼやけていれば輪郭が見えます。 このステップを完了するには、最初にガウス カーネルを構築し、次に畳み込みを実行します。これは重要なポイントです!ここで Lowe はさまざまなガウス カーネルを使用して画像を畳み込み、ガウス スケール空間を取得します。 画像はすべて 2 次元であるため、2 次元のガウス カーネルを使用する必要があります。 2 次元ガウス カーネルの生成に使用される数式は 2 次元ガウス関数です。 このうち、δ はスケール空間係数とも呼ばれます。 /span> ここで構築したものはスケール スペースと呼ばれます。これを行うポイントは、さまざまなスケールの観察をシミュレートし (表の下に移動するにつれて)、細かいスケールの構造を抑制すること (右に移動するにつれて) です。 この論文の原文は次のとおりです: 最後に、各行の最後から 2 番目の画像がダウンサンプリングされます - 青い矢印を参照してください。これにより、別の畳み込み行が開始されます。画像が小さくなりすぎるまで、このプロセスを繰り返します。 (ちなみに、サンプリング レートはステージごとに 2 分の 1 ずつ減少するため、通常、各行はオクターブと呼ばれます。) と呼ばれます。 ガウス スケール空間 S(x,y,δ) または ガウス スケール空間 S(x, y, δ) とも呼ばれるガウス ピラミッドが得られます この時点で、 この操作ステップは、近くと遠くのオブジェクトのサイズに対する人間の目の影響を模倣することに似ています。オブジェクトは、近い距離から見ると非常に大きく、近くから見ると非常に小さくなります。これはカメラの撮影効果とも一致します。 は標準偏差であり、論文では増加する標準偏差です。このガウス カーネルはスケール スペース フィルターとも呼ばれます。異なるガウス カーネルとは、異なるシガムのガウス カーネルを指します。ガウシアンカーネルではコンボリューション計算を行うことができますが、コンボリューション計算はコンボリューショナルニューラルネットワークの計算方法と同じで、対応する位置を乗算したり加算したりするため、ここでは説明を省略します。ステップ 3: ダウンサンプリングを実行し、2 番目のステップを繰り返してオクターブの最後の 4 つのグループを取得し、ガウス ピラミッドの構築を完了します。











  • 具体的な詳細と手順:

(1) ガウスピラミッドのグループ数はどうやって決めるのですか?
次の式を使用します。 ここで、M、N は元の画像の幅と高さです。
この式を使用して、どのくらいの高さのピラミッドを構築する必要があるか、つまりステップ 2 を何回繰り返す必要があるかを確認します。この公式の具体的な理由は Lowe の経験にあるのかもしれませんが、とにかく、それが論文で求められたことですが、説明は見つかりませんでした。

(2) 事前に簡単に説明しておきますが、ガウス差分ピラミッドはどのように生成されるのでしょうか。
下の図を見てください:

図の左側はガウス ピラミッド、右側はガウス差分ピラミッドです。左下は Octave1 (ガウス ピラミッドの最初のグループ) で、5 つの画像が含まれています。左上は Octave2 (ガウス ピラミッドの ​​2 番目のグループ) で、これも 5 つの画像が含まれています。画像の右下は、ガウス差分ピラミッドの最初のグループ Octave1 であり、4 つの画像が含まれています。右上は、ガウス差分ピラミッドの ​​2 番目のグループである Octave2 で、これも 4 枚の画像が含まれています。ガウス差分ピラミッドは、ガウス ピラミッドの隣接するレイヤーを減算することによって取得されるため、図内の丸で囲まれたマイナス記号は実際には単純な減算です。したがって、ガウスピラミッドとガウス差分ピラミッドのグループ数は同じであり、対応するグループ内の画像サイズも同じですが、その数が1つ少なくなります(つまり、レイヤー数が1つ少なくなります)。

(3) 事前に簡単に説明しますが、ガウス差分ピラミッドを生成した後、ガウス差分ピラミッド上の極値点を抽出するにはどうすればよいでしょうか。
私たちが生成したガウス差分ピラミッド内のすべての画像は、異なるスケールの連続空間内の一連の画像です。ある画像上の点が極点であるかどうかを判断したい場合、その点の周囲の点、つまり同じレイヤー内の他の点を見る必要があるだけでなく、画像上の点も見る必要があります。この点に対応する上層と下層のサイズにより、その点が極点であるかどうかを判断できます。ある点が極点であるかどうかを判断するには、同じ層だけでなく、上下に隣接する層も考慮する必要があります。

(4) オクターブの各グループ内のピクチャの数を決定するにはどうすればよいですか?
式を使用します: S=n+3、n は各グループ内の特徴を抽出する写真の数を表します。各グループの 2 枚の写真から特徴を抽出したい場合は、次のようになります。式 2+3=5 では、最初に 5 枚の画像を生成する必要があります。つまり、ステップ 2 では、異なるぼかしレベルの 5 枚の写真を取得するために、ガウスぼかしを 5 回行う必要があります。なぜプラス3の関係になるのでしょうか?生成される画像は 5 枚だけなので、ガウス ピラミッドの各オクターブは 5 層ですが、ガウス ピラミッドからガウス差分ピラミッドを生成すると、各オクターブは 4 層になります。極点を探すときは、同じ画像内で極点を見つけるだけでなく、垂直方向の極点であるかどうかを比較する必要があります。各グループの最初の層の画像と最後の層の画像には垂直方向の画像が存在しないため、導出して垂直方向の極値を求めることは不可能であり、極値点は最初の層と最後の層以外の領域を選択し、その層の外側にある中間層を見つけます。したがって、特徴抽出に使用できる画像は 2 つだけです。つまりプラス3の関係になります。

(5) ガウスブラーが適用されるたびにシグマを決定するにはどうすればよいですか?
下の図を見てください:

画像内の数式の n は前の n と同じで、各グループ内の特徴を抽出する画像の数を示します。 K は n を通じて計算でき、各ピクチャのシグマは k を通じて知ることができます。
最初のグループの最初の層は sigma0 を使用し、最初のグループの 2 番目の層は k*sigma0 を使用し、最初のグループの 3 番目の層は k^2*sigma0,,, を使用すると仮定します。下に進むと、最初の画像セットのガウス カーネルがあります。
つまり、元の画像 (2 倍にすることも拡大しないこともできる) に基づいて、元の画像は sigma0、k*sigma0、k^2*sigma0... などのサイズに分割されます。一連のガウス カーネルがぼかしに使用され、ぼかした画像は最初のグループの各レイヤーの画像です。

第 2 グループの最初のレイヤーは、最初のグループの最後から 3 番目のレイヤーの画像を取り出し、偶数行と偶数列を削除します。つまり、1 点おきに点を取得します。画像 B は、2sigma0 のガウス カーネルを使用して畳み込まれ、ぼかされています。
2 番目のグループの 2 番目の層は、画像 B をぼかすための k*2sigma0 のガウス カーネルです。3 番目の層は k^2*2sigma0 などで、同じものを生成します。最初のグループとして番号を指定します。

3番目のグループの各レイヤーも同様で、2番目のグループから最後から3番目のレイヤーの写真を取り出し、偶数行偶数列、つまり1点おきに点を取り、半分に縮小した後、 、それが画像 C であると仮定し、畳み込みブラーには 4sigma0 ガウス カーネルが使用されます。なぜ 4sigma0 なのか? k=2^1/n、k^n+2δってただの4δじゃないですか!同様に、3 つのグループの各層のシグマは、4sigma0、k*4sigma0、k^2*4sigma0... となります。

前のグループの最後から 2 番目のレイヤーを次のグループの開始ブラー画像として使用する必要があるのはなぜですか?
グループに n 個のレイヤーがある場合、最後から 3 番目のレイヤーのシグマは k^n*sigma0, k=2^(1/n) であるため、最初のレイヤーのシグマは2 番目のグループは 2sigma0 で、実際にはこの 2sigma0 を取得するだけです。このようにして作成されたスケール空間は、可能な限り多くのスケールを含む連続した画像サイズ空間です。この方法でのみ、後で安定した効果的な特徴点を見つけるための基礎を築くことができます。

では、sigma0 はどうやって決定するのでしょうか?
sigma0 については、lewo は多くの実験を行った結果、経験値 1.6 を与えました。しかし、私たちのカメラには写真撮影後に自動的にガウスぼけ補正が備わっています。カメラのガウス カーネルは 0.5 であると仮定します。シグマ。
ガウス カーネルの分散にはピタゴラスの定理の形式に従う特性があるため、つまり、最初に分散 0.5 のガウス カーネルで画像をぼかし、次に分散を 0.5 にしてぼかした場合、 2 回目は、分散 1.52 ブラーのガウス カーネルを使用しました。これら 2 つのブラーの結果は、ガウス カーネル 1.6 のブラーの効果と同等です。上の図のδ0の式ですが、ピタゴラスの定理の性質から、sigma0は1.52が使えます。

(6) ガウスピラミッドの各グループの画像はどのように生成されるのでしょうか? (ここでの主な目的は、ダウンサンプリングを個別に強調することです)
最初のグループは、5 つの異なるガウス カーネルを使用して、元の画像または 2 倍になった元の画像に基づいています (具体的には、どの 5 つのカーネルもぼやけています (前に理解されているように)、5 つのレイヤーの最初のグループである 5 つの画像を生成します。
5 層画像の 2 番目のグループは次のとおりです。まず、最初のグループの最後から 3 番目の層画像を取り出し (これを取り出した理由は前述しました)、次にダウンサンプリングします。ここでのダウンサンプリングとは、通常のピラミッドを構築する際のダウンサンプリングと同じで、前述したように1点おきに点を取るか、偶数の行や列を削除して画像サイズを半分に縮小することです。次に、ダウンサンプリングされた画像を 5 つの異なるガウス カーネル (特定の 5 つのカーネルについても前に詳しく説明しました) でぼかし、2 番目のグループの 5 つのレイヤーである 5 つの画像レイヤーを生成します。
類推すると、すべてのグループのすべての層が生成され、ガウス ピラミッドが完成します。

概要:
(1) ガウス ピラミッドのグループと層の数は自分で調整できますが、通常は Lowe によって与えられた経験値を採用します。
(2) 同じグループ内で、異なるレイヤーの画像のサイズは同じで、後の画像レイヤーのガウス平滑化係数シグマは、前のレイヤーの平滑化係数の k 倍です。画像の数。異なるグループでは、後者のグループの最初の画像は、前のグループの最後から 3 番目の画像の半分のサンプルであり、画像サイズは前のグループの半分です。
(3) シグマの違いにより画像のぼけ具合が決まります。スケールが大きい(δ 値が大きい)場合は画像がぼやけており、画像の一般的な情報を表します。スケールが小さい(δ 値が小さい)場合は、画像がぼやけています。画像は鮮明で、画像の詳細な情報を表現します。
(4) SIFT がスケーリングに対して不変である理由は、SIFT がピラミッドを構築する方法が絶妙であるためです。一方では、ピラミッドを下から上に見ます。このピラミッドは、物体を見るときの人間の目の効果をシミュレートしています。つまり、近くでは大きく、遠くでは小さく見えます。つまり、物体は上に向かって大きく見えます。ピラミッドの一番下にある物体は大きく、上に行くほど小さくなり、物体は非常に小さくなります。一方、ピラミッド グループを見ると、グループ内の画像はさまざまな分散のガウス カーネルで畳み込まれており、実際にシミュレートされるのは、近くで見ると鮮明で、遠くで見るとぼやけるという効果です。したがって、ガウス ピラミッドは、人間の目の遠方および近方の観察と局所的および全体的な観察をシミュレートする 2 つのレベルのシミュレーションです。画像の輪郭(全体)。)ので、画像のスケール空間と呼ばれます。これが、SIFT にさまざまな不変性がある重要な理由の 1 つです。

説明:
(1) ガウス関数の離散近似を計算する場合、約 3δ の距離を超えるピクセルは無効とみなされ、これらのピクセルの計算は可能です。無視される。したがって、実際のアプリケーションでは、(6δ+1)*(6δ+1) のガウス コンボリューション カーネルを計算するだけで、関連するピクセルの影響を保証できます。
(2) それは数学的に証明されています。論文「スケール空間理論: さまざまなスケールで構造を解析するための基本ツール」では、ガウス カーネルがスケール空間変換を達成する唯一の変換カーネルであることが証明されています。 、そしてこれは唯一の線形カーネルであり、明確な近くと遠くのぼやけをシミュレートできる唯一の線形カーネルです。つまり、他のカーネルにもブラー効果があっても使用できず、ガウスカーネルのみが使用可能となります。

  • スケール空間をもう一度見てください
    画像のスケール空間によって解決される問題は、すべてのスケールで画像をどのように記述するかということです。ガウス ピラミッドでは、異なるスケールの L 層画像のグループが合計 O 個生成されます。これら 2 つの量 (O、L) を組み合わせたものがガウス ピラミッドのスケール空間を構成します。つまり、ガウス ピラミッドのグループ O二次元座標系として を使用し、ある座標と異なる層 L を別の座標として使用すると、与えられた座標セット (O, L) によってガウス ピラミッド内の画像を一意に決定できます。スケール空間の画像表現:

2. ガウス差分ピラミッドを構築する

ガウス ピラミッドの差分 DOG (Difference of Gaussian) は、ガウス ピラミッドに基づいて構築されますが、実際、ガウス ピラミッドを生成する目的は、DOG ピラミッドを構築することです。抽出する極点は DOG から抽出されます。

ガウス差分ピラミッドの第 1 グループと第 1 層は、ガウス ピラミッドの第 1 グループと第 2 層から第 1 グループと第 1 層を減算することで得られます。同様に、各差分画像はグループごと、レイヤーごとに生成され、すべての差分画像が差分ピラミッドを構成します。これを要約すると、DOG ピラミッドの第 0 グループの i 層の画像は、第 0 グループの i 層からガウス ピラミッドの第 0 グループの i+1 層を減算することによって得られます。

この写真は上に表示されています。左側はガウス ピラミッド、右側はガウス差分ピラミッド、中央の丸で囲まれたマイナス記号は単純なマイナス記号です。ガウス差分ピラミッドでは、ガウス ピラミッドよりもグループあたりのレイヤーが 1 つ少なくなります。

説明: これから探している特徴点は、このピラミッド内にあります。しかし、ここでは、なぜこの空間が安定したキーポイントを検出できるのかを証明するための数学的証明の大部分を省略しました。 Tony Lindeberg は、スケール正規化された LoG (ガウスのラプラシオン) 演算子には真のスケール不変性があることを指摘し (この演算子は前の章で説明しました)、Lowe はガウス差分ピラミッドを使用して LoG 演算子を近似します。 Mikolajczyk は 2002 年に、ガウス差分ピラミッドを使用して LoG 演算子を近似できる理由も証明しました。ここでは数学的導出は書きません。詳細な説明が必要な学生は、スケール不変特徴 、この記事は非常に明確です。

  • 論文の差分ピラミッドの視覚効果を見てください:

    上記の表現は正規化されていることに注意してください。その下部にある灰色のグラフを参照してください。これは、次の場合に特に顕著です。低コントラストの画像。(フル コントラストの入力では 0.00 で黒、1.00 で白になります。)
    上の図は、DOG 画像を正規化した後の Lowe の視覚化効果です (正規化されていない場合、可視化後は真っ黒で、人間の目では何も見えません。DOG 画像にはまだ多くの特徴が含まれていることがわかります。これらの特徴は、ぼかしの度合いやスケールが異なる場合でも依然として存在します。つまり、これらの点は次のとおりです。どのように変換されても安定しています。不変とは、有効な情報を含む点を意味します。これらの点は、Sift が抽出したい「安定した」特徴点でもあります。
  • 3. しきい値処理 (ノイズ極値点の除去)
    ガウス差分画像から抽出したい特徴は、非常に明るい点または非常に暗い点のほんの一部です。これらの点は次のとおりです。抽出したい安定した特徴であり、これらの点はまさに極端な位置にある点であり、いくつかの最大値点と最小値点です。ただし、すぐに極値点を探すのではなく、まずしきい値を使用していくつかの点を削除します。

Lowe が与えた式は次のとおりです: abs(val) > 0.5*T/n (T=0.04 は Lowe が与えた経験値です。n は上記の n のままで、各グループに入りたい回数です)差分ピラミッドの画像から特徴を抽出します。

ここでも n=2 を例として取り上げます。この場合、abs(val) を保持するには、abs(val) が 0.01 より大きい必要があります。これは、点の絶対値がしきい値 0.01 より小さい場合、この点を除外することを意味します。つまり、この点は探している点ではなく、ノイズ ポイントである可能性が高くなります (多くの参考文献)資料にはノイズ ポイントと書かれていますが、個人的には、目立った特徴のない点、または比較的平坦な領域にあるいくつかの点だと思います。ノイズ ポイントと言う理由は何ですか?)。
また、ここでなぜ絶対値を採用するのかという質問があれば、それは前の内容を注意深く読んでいないことを意味します。このステップは差分ピラミッドにあります。差分は引き算なので、当然のことながら、は正です。負の値もあります。ここでの数値は整数ではなくなり、すべて浮動小数点数になります。

  • 4. 極値点の予備検出 (ガウス差分ピラミッドで極値点を見つける)
    ここで述べた予備的とは、さらなる処理が必要であることを意味し、さらなる処理ステップは次のとおりです。 2番目の大きなステップ。 (とても無茶ですが、元の論文はそのように書かれています)
    閾値処理の後、実際にガウス差分ピラミッドの極値点のスクリーニングを開始します。スクリーニング方法は次のとおりです。

    ガウス差分イメージの最大値と最小値は、現在のスケールと隣接するスケール (丸でマーク) で 3x3 領域内の 26 個の近傍ピクセル (X でマーク) を比較することによって検出されます。 たとえば、中間レイヤーで X が描画されているピクセルが極点であるかどうかを確認したい場合、 、比較のために、同じレイヤーに 8 ピクセル、上下のレイヤーに 9 ピクセル、合計 9+8+9=26 ピクセルを配置します。この手順は非常に簡単で、比較するだけで、周囲の 26 点より大きいか小さい場合、それが極点になります。これは局所的な極値であり、重要なポイントとなる可能性があります。
    D(x,y,sigma) の極大値と極小値を検出するために、各サンプル点が現在の画像内の 8 つの近傍と、上下のスケール内の 9 つの近傍と比較されます。隣接するすべてのピクセルよりも大きいか、すべての隣接ピクセルよりも小さい場合にのみ選択されます。

最初の大きなステップがついに終わり、今から第二の大きなステップが始まります。

2. 要点の位置付け

この手順には次の内容が含まれます:
1. 極値ポイントの位置を調整し、極値ポイントを正確に特定します。
2. 低コントラストのポイントを削除します< /span> a>
3. エッジ効果を削除します

1. 極値の位置を調整し、極値を正確に特定します。
以前に極値を見つけましたが、これらの極値は実際の極値ではありません。なぜでしょうか。
まず、同じレイヤー上の画像であっても、画像のピクセル値は不連続で離散的な値になります。第 2 に、異なるレイヤー上の画像の場合、ピクセル値はさらに大きくなります。 、上層のガウスぼかしカーネルは下層の k 倍など、より離散的になります。つまり、ピクセルは離散的であり、異なるスケールの空間はさらに離散的です。以下の図に示すように:

したがって、最初に見つけた極値点は、実際の極値点ではありませんが、実際の極値点に近い点である可能性が高く、以下ではこれらの極値点と呼ぶことにします。は擬似極値点です。したがって、実際の極値点を見つけるには、擬似極値点の位置を調整する必要があります。

  • では、最初に見つかった既知の擬似極値点を使用して、実際の極値点を見つけるにはどうすればよいでしょうか?
    テイラー展開公式を使用する テイラー展開の本質は、ある点における関数の情報を使用して、その付近の値の公式を記述することです。つまり、多項式関数を使用して関数を近似し、関数上の特定の点から近似を拡張します。
    したがって、最初に検出された擬似極点 X(x0,y0,sigma0).T で 3 値の 2 次テイラー展開を行うことができます。ここでの演算アイデアは、以下から参照することができます。コーナー点検出 コーナー検出もテイラー展開で行うという考え方ですが、コーナー検出にはパラメーター sigma が存在しないため、2 値の 2 次テイラー展開になります。ここでは 3 値の 2 次テイラー展開です。テイラー展開は、点 X(x0,y0,sigma0).T における元の関数 f(x,y,sigma) の近似として考えることができます。

展開されたベクトル形式は次のとおりです。

つまり、擬似極点 X(x0,y0,sigma0).T の元の関数が見つかりました。元の関数を使用して、元の関数の極点を見つけたい場合は、自然に元の関数を導出し、導関数を 0 に設定すると、実際の極値点の x、y、シグマがわかります。次に、x、y、シグマを元の関数に戻すと、次のことができます。実際の極値点の値を見つけます。数学的導出は次のとおりです。
f(x) を導出:

導関数をゼロに等しくします:、この値は変位に相当します。

f(x) を代入して極値点の値を求めます。

  • 注 1: このステップのアルゴリズム実装プロセスは直接計算ではなく反復プロセスであるため、反復回数には制限があります。元の論文には次のように記載されています。
    ケース 1: 得られた x、y、sigma の 3 つの変位がすべて 0.5 未満である場合、変位はすでに非常に小さい、つまり収束している、つまり極限に非常に近いことを意味します。この時点で見つかった点は正確な極点です。
    ケース 2: 反復回数を超えても x、y、sigma がすべて 0.5 未満にならない場合は、収束できない可能性があると考え、反復を停止して破棄します。この点、つまりこの極端な点は必要ありません。
    ケース 3: 収束しても、元の関数に戻した後の x、y、sigma の値が一定の範囲を超えた場合、テイラー展開は次のとおりであるため、この点は放棄します。この時点で、近くの元の関数をシミュレートします。そのため、解はこの点から大きく外れることはありません。偏差が大きすぎる場合は、テイラー展開が元の関数にうまく適合していないことを意味し、導関数を導出するのは無意味です。元の機能に戻るので諦める。
  • 注 2: テイラー展開には導関数があります。次の図に示すように、画像の場合、導関数は差です。
     
    f に対応する下付き文字は次のとおりです。絵。以上が微分導出の全体的な処理であり、読者は該当する画像に対応するコードを記述することができる。
    この時点で、実際の極値点の位置決めが完了しました。このステップの数学的導出に怖気づく必要はありません。テイラー展開をより深く理解していれば、このステップの原理と解決策は正常に感じられるでしょう。
  • 2. コントラストの低い点を削除する

テイラー展開を使用して実際の極値点を見つけた後、コントラストの低い点を破棄する必要もあります。 Lewo が与えた式は次のとおりです。
|f(X)|
これは、上記のテイラー展開で得られた極値が T/n より小さい場合でも、n は 2 であるとみなされ、0.02 未満であることを意味します。この極値を考慮します。ポイントのコントラストが低すぎるため、破棄されます。

  • 3. エッジ応答の除去

ガウス差分関数はエッジに沿って強い応答を示すため (これを理解するには、第 9 章の画像勾配の図を参照できます。この図は非常に直感的です)、つまり、この関数は次の影響に非常に敏感です。強いエッジ応答が生成されます。つまり、上記のレイヤーでフィルタリングされた点の一部はエッジ ポイントであり、エッジ ポイントは必要な特徴点ではありませんが、コーナー ポイントであれば、必要な特徴点になります。 Harris のコーナー ポイント検出では、コーナー ポイントは非常に優れた特性を持つ点の一種であると述べています。したがって、ここでもいくつかのエッジポイントを削除する必要があります。そこに着く方法?実際、コーナー点検出では、どれがコーナー点であるかを区別できるだけでなく、どれがエッジ点であり、どれが平坦領域内の点であるかを区別できることをコーナー点検出ですでに詳しく述べました。区別の鍵となるのは M 行列で、M 行列の固有値の相対的な大小関係によって、どれがコーナー点、どれがエッジ点、どれが平坦領域点であるかを区別できます。したがって、ここでも同じ原理と考え方を使用し、M マトリックスを使用してエッジ ポイントを削除します。
多くの場所でヘッセ行列について直接説明しています。実際、このヘッセ行列は M 行列です。ここで理解できない学生は、コーナー検出について詳しく調べることをお勧めします。そこで、ここでは M 行列をヘッセ行列とも呼んでいますが、実際には、これはもともとヘッセ行列であり、実対称行列です。

コーナー点の検出は、行列の 2 つの固有値が比較的大きい点を見つけることであり、ここではエッジ効果を排除する、つまりエッジ点を削除したいため、ここでは非常に異なる固有値を持つ 2 つの点を削除します。両方の固有値が大きい点(コーナー点)と、両方の固有値が小さい点(平坦な領域の点)を保持し、固有値の差が大きい2点(エッジ点)を除去することです。 )。実際、Harris アルゴリズムはこれら 3 種類の点を区別するのに非常に優れており、エッジ検出に Harris を使用している人を見たことがありますが、検出されたエッジ効果は依然として非常に良好でした。このステップは、Harris コーナー検出器の構築に似ています。

ここには 2 つの状況が考えられます。
ケース 1: ヘッセ行列の行列式が <0 の場合は、それを単に破棄します。行列式が 0 未満であるということは、行列の 2 つの固有値の符号が異なることを意味し、両方の符号が異なる場合、その差は非常に大きくなり、エッジ点となるため、直接破棄されます。
ケース 2: 2 つの固有値の符号は同じですが、それらの差が大きすぎて特定の経験的しきい値より大きい場合、それらは破棄されます。このステップで Lowe が与えた式は次のとおりです。

このうち、H はヘッセ行列、α と β は H の 2 つの固有値、Tr(H) は行列のトレースです。 , Det(H) 行列の行列式です。なぜハリスコーナー検出の章を読むことをお勧めするかというと、行列の固有値を見つけるのは難しいですが、行列式とトレースを見つけるのは簡単です。
大きな固有値を α、小さな固有値を β とします。Γ = α/β
「α と β には大きな違いがない」ことを願っています。 「Γ ができるだけ 1 に近いこと」を望みます (Lowe はここで Γ=10 を取ることを推奨しています)
α>β であるため、Γ>1 であり、上記の式は [ で単調増加します。 1, +∞] であるため、「Γ はできる限り 1 に近づける必要がある」は次の条件に変換されます:

この時点で、SIFT アルゴリズムの 2 番目のステップが完了します。

3. キーポイントの方向を決定する

2 番目のステップで極値点が見つかった後、まず極値点をガウス ピラミッドの対応する位置にマッピングし直す必要があり、その後、方向の割り当てが実行されます。プロセスを次の図に示します。

  • 1. マッピング方法は?
    まず、2 番目のステップで除外された極値点にはすべて、x、y、シグマの 3 つのパラメータがあり、x、y、シグマを設定する必要はないことを理解する必要があります。 integers. sigma テイラー展開から導出される x、y、およびシグマは上で行われるため、どのレイヤにも該当する必要はありません。したがって、次のように見つける必要があります。たとえば、その層の極点の場合です。そして、x、yはその層の極点Xの座標位置です。このようにして、極値点がガウス ピラミッドの特徴点にマッピングされます。
    この小さなステップは難しくありません。極点のシグマに従って極点をガウス ピラミッドにマッピングすることです。このようにして、ガウス差分ピラミッドの極点はすべてガウス ピラミッドにマッピングされ、これらの点を特徴点と呼びます。
  • 2. 特徴点が位置する局所領域内のすべてのピクセルの勾配振幅と勾配方向を見つけます
    特徴点を円の中心とし、スケールをとります。特徴点が位置するガウス画像 半径 1.5 倍の円内で、円内のすべてのピクセルの勾配振幅と勾配方向をカウントします。
    勾配振幅と勾配方向は次の式を使用します。

    勾配振幅と勾配方向については、実際には第 10 章の鋭いエッジ検出で詳しく説明します。画素の勾配には水平方向の差分と垂直方向の差分の2方向があり、最終的には両方向の差分の二乗和の平方根を求めます。ピクセルのグラデーション値。
    勾配の方向は arctan (垂直勾配/水平勾配) です。角度を求める、逆正接関数。
  • 3. 特徴点の勾配方向と勾配振幅を決定する
    上の丸で囲まれたすべてのピクセルの勾配方向は (-π, π) からであり、-π をπ は平均して 8 つの区間に分割されます (論文では 36 区間に分割されており、1 区間は 10 度です) 全ピクセルの勾配方向がどの区間に該当するかに数値が加算されます。この数値は次のように計算されます。 1.5 倍のシグマを使用します。ガウス フィルタリングは、トラップされたピクセルの勾配振幅に重みを与え、それをこのピクセルの振幅として間隔列に累積します。これは加重投票プロセスであり、加重はシグマの 1.5 倍です。これにより、上の画像の右の画像が得られます。
    次に、右図の最も高い方向を特徴点の主方向とし、その列の振幅を特徴点の振幅とします。

注: 特徴点には 2 つの方向がある場合があります。1 つは主方向、もう 1 つは補助方向です。補助方向とは、他の柱のサイズが主方向の柱のサイズの80%を超えており、他の柱がこの特徴点の補助方向であるためである。このような特徴点については、同じ位置、同じスケール、異なる方向にある2つの特徴点に過ぎない、2つの特徴点であると考える。

この時点で、ガウス ピラミッド上のすべての画像の特徴点 (x、y 座標) が決定され、これらの点の勾配の大きさと勾配の方向も決まりました。
これらの特徴点を視覚化したい場合は、x、y 座標を通じて元の画像内の対応する座標を見つける必要があります。この特徴点がガウス ピラミッドの ​​2 番目のグループの 2 番目のレベルにある場合、この特徴点は最初に最初のグループのスケールにマッピングされ、次に元の画像にマッピングされる必要があります。

4. 要点の説明

このセクションでは、記述子の生成方法について説明します。
2 つの画像を照合する場合は、2 つの画像の特徴点をそれぞれ見つけて、同じ特徴点を照合します。すでに特徴点のxy座標、画素値、勾配振幅、勾配方向の4次元データは得られていますが、2枚の写真の大きさ、変形、照明、ノイズなどが異なるため、どちらの写真であるかは分かりません。特徴点を一致させたい場合は、記述子を使用する必要があります。記述子はマッチングに特別に使用され、128 次元のベクトルです。 2 つの特徴点の記述子が非常に近い場合、それらは一致する可能性があり、2 つの特徴点が一致することを意味します。2 つの特徴点を直線で結ぶだけです。それがこの記事の冒頭にある一致する画像です。
記述子を生成する手順: 次の図に示すように、領域の検出 - 領域の回転 - 勾配方向の検出 - 128 次元ベクトルの生成:

  • エリアの検索
    上の図 1 を参照してください。図 1 はガウス ピラミッドの中央の画像です。これは、探している特徴点がガウス ピラミッド内にあるためです。 、画像のさまざまなスケール空間で。
    特徴点を中心に円を描き、いくつかのピクセルを囲みます。
    論文に描かれた円の半径は次のとおりです: mδ(d+1)*√2/2、論文では m を 3 とします。mδ は a の辺の長さを指します。小領域 d は小領域をいくつ取るかを示します。これは図 3 の式です。
  • 回転領域
    上の図 2 を参照してください。記述子には回転の不変性が必要なので、つまり、カメラがどの角度からこの特徴点を捉えても、この特徴点はの記述子はすべて変更されていないため、一致させることができます。そのため、丸で囲ったピクセルを回転する必要があります。これは、アフィン変換を行うこと、つまり、ピクセルの主方向に回転することを意味します (図 2)。
    回転プロセスに違いがあるはずです。論文ではトライリニア補間を使用していますが、ここでは他の補間方法を使用でき、影響は大きくありません。
  • 勾配の方向を見つけて、記述子である 128 次元のベクトルを生成します。図 4 は、2 行目の最初の画像です。この画像は、回転補間後に丸で囲んだ領域です。 small 領域は 16 個あり、各小領域は 8 次元ベクトル、つまり 8 方向の勾配振幅を持つため、16 個の小領域のベクトルを書くと 128 次元のベクトルが得られます。この 128 次元のベクトルが、必要な記述子です。
    各小領域の 8 次元ベクトルはどのようにして得られるのでしょうか?それぞれの小領域には 16 個のピクセルがあり (16 個ではない場合もありますが、最初のステップの式に従って計算され、計算された数が数値になります)、各ピクセルの勾配方向と勾配振幅を計算し、これらを入力します。勾配方向を 8 方向間隔に分割すると、8 次元ベクトルが形成されます。 16 の小領域にも同じことが当てはまります。

この時点で、SIFT の実装手順と原則が導入されました。私は疲れ果てて、二度と原則を書き留めないことを誓いました。

5. マニュアルコードの説明

1. バックログ:

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

2. ガウスカーネルの構築

# 构建高斯核
def GuassianKernel(sigma,ksize):  #生成高斯核的函数,第一个参数是δ,第二个参数是核的尺寸,就是你想生成3x3还是5x5的高斯核  
    temp = [t - (ksize // 2) for t in range(ksize)]  #生成一个列表,假如ksize=5,temp=[-2, -1, 0, 1, 2]
    kernel_x = []
    for i in range(ksize):          #用循环生成一个方形的核
        kernel_x.append(temp)  
    kernel_x = np.array(kernel_x)  #转array类型  
    gaussian = (1.0 / (2*sigma*sigma*np.pi)) * np.exp(-(kernel_x**2 + (kernel_x.T)**2)/(2*sigma*sigma))  #二元高斯函数计算公式
    result = gaussian/gaussian.sum()   #归一化处理,这步一定要有,gaussian.sum()返回0.4787148085297054,所以一定要归一化  
    return result
#测试上面的函数,我们生成一个δ=1.5,尺寸为3的高斯核
GuassianKernel(1.5,3) #生成一个σ=1.5、尺寸是3x3的高斯核
array([[0.09474166, 0.11831801, 0.09474166]],
       [0.11831801、0.14776132、0.11831801]、
       [0.09474166、0.11831801、0.09474166]])

上記のコードによって達成される効果は次のとおりです。

いくつかの場所で提供されているコードは正規化されていません。これは個人的には間違っていると思います。
注: GuassianKernel(10,3) によって返されるガウス カーネルの値はほぼ 0.11 です。したがって、分散が大きいほど、視覚化後のガウス カーネルは平坦になり、分散が小さいほど、ガウス カーネルは尖って高くなります。ガウス関数は確率関数であるため、分散が小さいほど、データが平均値付近に集中していることを意味するため、平均点での確率が大きくなり、ガウス カーネルはよりシャープで高くなりま す。平坦であるほど、データの分散が大きくなり、データがより分散し、平均からの距離が非常に大きくなるため、平均に近い数値の確率が低くなり、平坦になります。

  • シグマを直観的に見てみましょう:
    ここで n を 2 と仮定すると、ガウス ピラミッドの層の数は 5 になります。元の画像は(600, 868, 3) の場合、ピラミッドのグループの数は 6 です。k は根数 2 です。Lowe が提案した sigma0 が 1.52 であると仮定すると、
    、すべてのシグマ結果このガウス ピラミッドのグループとすべての層は、次のコードを実行した結果です:
import cv2
img = cv2.imread(r'C:\Users\25584\Desktop\building.jpg')  #img.shape返回(600, 868, 3)
n = 2   #想在2个层里面提取特征
S= n+3  #5,高斯金字塔每组的层数
O = int(np.log2(min(img.shape[0],img.shape[1])))-3   #计算结果O=6,所以金字塔有6组,所以高斯金字塔有30张图片,应该有30个sigma
k = 2**(1.0/n)   #k=根号2
sigma0=1.52
sigma = [[(k**s)*sigma0*(1<<o) for s in range(S)] for o in range(O)]
sigma
[[1.52,
  2.149604614807105、
  3.040000000000001、
  4.29920922961421、
  6.080000000000002]、
 [3.04、
  4.29920922961421、
  6.080000000000002、
  8.59841845922842、
  12.160000000000004]、
 [6.08、
  8.59841845922842、
  12.160000000000004、
  17.19683691845684、
  24.320000000000007]、
 [12.16、
  17.19683691845684、
  24.320000000000007、
  34.39367383691368、
  48.640000000000015]、
 [24.32、
  34.39367383691368、
  48.640000000000015、
  68.78734767382736、
  97.28000000000003]、
 [48.64、
  68.78734767382736、
  97.28000000000003、
  137.5746953476547、
  194.56000000000006]]

結果からわかること: ガウス ピラミッドの最初のグループの最初の層のシグマは 1.52、2 番目の層は √2*1.52、3 番目の層は √2*√2*1.52、そして4 番目のレイヤーは √2*√2*√2*1.52、5 番目のレベルは √2*√2*√2*√2*1.52
2 番目のレベルの最初のレベルのシグマガウスピラミッドのグループは (√ 2*√2*1.52)、2 番目の層は √2*(√2*√2*1.52)、3 番目の層は √2*√2*(√2*√2*1.52) )、第 4 層は √ 2*√2*√2*(√2*√2*1.52)、第 5 層は √2*√2*√2*√2*(√2*√2*1.52) )
ガウス ピラミッドの ​​3 番目のグループの最初の層のシグマは (√2*√2*√2*√2*1.52)、2 番目の層は √2*(4* 1.52)、3 番目の層は √2*√ 2*(4*1.52)、4 番目の層は √2*√2*√2*(4*1.52)、5 番目の層は √2*√2*√ 2*√2*(4*1.52) ガウス ピラミッド No. 6 つのグループの最初の層のシグマは (32*1.52)、2 番目の層は √2*( 32*1.52)、第 3 層は √2*√2*(32*1.52)、第 4 層は √2* √2*√2*(32*1.52)、第 5 層は √2*√2 *√2*√2*(32*1.52) ガウス ピラミッドの第 5 グループの最初の層のシグマは (16*1.52)、2 番目の層は √2*(16*1.52) です。 、3番目の層は√2*√2*(16*1.52)、4番目の層は1番目の層は√2*√2*√2*(16*1.52)、5番目の層は√2*√2です*√2*√2*(16*1.52)
ガウス ピラミッドの第 4 グループの最初の層のシグマは (8*1.52)、2 番目の層は √2*(8*1.52) )、3 番目のレベルは √2*√2*(8* 1.52)、4 番目のレベルは √2*√2*√2*(8*1.52)、5 番目のレベルは √2*√2*√2 *√2*(8*1.52)

3. 畳み込み演算を定義する

#定义卷积操作
def convolve(guassionKernel, img, padding, strides):  #高斯核;要卷积的图片;四周用0填充,是一个四个向量的数组;卷积操作的步长,是一个两个向量的数组  
    img_size = img.shape
    kernel_size = guassionKernel.shape
    if len(img_size)==3:
        channel=[]
        for i in range(img_size[-1]):
            pad_img = np.pad(img[:,:,i], ((padding[0],padding[1]), (padding[2], padding[3])), 'constant')  #对每个通道都填充 A
            temp = []
            for j in range(0, img_size[0], strides[1]):
                temp.append([])
                for k in range(0, img_size[1], strides[0]):
                    val = (guassionKernel*pad_img[j*strides[1]:j*strides[1]+kernel_size[0], k*strides[0]:k*strides[0]+kernel_size[1]]).sum()
                    temp[-1].append(val)
            channel.append(np.array(temp))
        channel=tuple(channel)
        result = np.dstack(channel)
    elif len(img_size)==2:
        channel=[]
        pad_img = np.pad(img, ((padding[0], padding[1]),(padding[2], padding[3])), 'constant')
        for j in range(0, img_size[0], strides[1]):
            channel.append([])
            for k in range(0, img_size[1], strides[0]):
                val = (guassionKernel*pad_img[j*strides[1]:j*strides[1]+kernel_size[0],k*strides[0]:k*strides[0]+kernel_size[1]]).sum()
                channel[-1].append(val)
        result = np.array(channel)
    return result

注: A: np.pad() の最初のパラメータは塗りつぶされる画像です。3 つのチャネルを個別に取り出し、すべてを塗りつぶす必要があります。 2 番目のパラメーターは次のとおりです。padding[0] 行はイメージの上部にパディングされ、padding[1] 行は下部にパディングされ、padding[2] 列は左側にパディングされ、padding[3] 列はイメージにパディングされます。権利。 3 番目のパラメータは 0 で埋めることを意味します。

4. ダウンサンプリング操作を定義します。これは、偶数行と偶数列を削除し、1 つおきにポイントを選択します。

#定义下采样操作
def undersampling(img, step=2):
    return img[::step, ::step]

5. ガウスピラミッドとガウス差分ピラミッドの生成

#构建高斯金字塔和高斯差分金字塔  
def getDoG(img,n,sigma0,S= None,O=None):   #img:原图;n:想要在多少层中提取特征;sigma0:高斯核;S:高斯金字塔每组有多少层图像;O: 金字塔有几组;
    if S == None:
        S = n + 3 # 高斯金字塔的层数,n最小取1,就有4层
    if O == None:
        O = int(np.log2(min(img.shape[0], img.shape[1]))) - 3  # 高斯金字塔的组数,lewo给出的公式: O=log2(min(img长,img宽))-3
    k = 2 ** (1.0/n)
    sigma = [[(k ** s) * sigma0 * (1 << o) for s in range(S)] for o in range(O)]  # 参照上面关于sigma的讲解
    sample = [undersampling(img, 1 << o) for o in range(O)]  # 降采样取图片作为该层的输入
    Guass_Pyramid = []
    for i in range(O):
        Guass_Pyramid.append([]) #声明二维空数组
        for j in range(S):
            ksize = int (6*sigma[i][j]+1) # 通常,图像处理只需要计算(6*sigma+1)*(6*sigma+1)的矩阵就可以保证相关像素影响,前面也说过这个点
            #ksize = int(9)  #留一个对比的代码
            if ksize % 2 == 0: #防止高斯核不是奇数
                ksize += 1
            Guass_Pyramid[-1].append(convolve(GuassianKernel(sigma[i][j], ksize), sample[i],[ksize//2, ksize//2, ksize//2, ksize//2],[1, 1]))  
    DoG_Pyramid = [[Guass_Pyramid[o][s + 1] - Guass_Pyramid[o][s] for s in range(S - 1)] for o in range(O)]  #每一层中 上一张减去下一张得到高斯差分金字塔
    return Guass_Pyramid, DoG_Pyramid, O  #返回高斯金字塔和高斯差分金字塔
  • 上記のコードをテストして、ガウス ピラミッドとガウス差分ピラミッドを確認します。
  • import cv2
    img = cv2.imread(r'C:\Users\25584\Desktop\building.jpg')  #img.shape返回(600, 868, 3)
    Guass_Pyramid,DoG_Pyramid,O = getDoG(img,2,1.52,S= None,O=None)   #假设n=2,sigma0=1.52

    実行結果:
    len(Guass_Pyramid) #Return 6、つまり 6 つのグループがあります
    len(Guass_Pyramid[0]), len (Guass_Pyramid[ 1])、len(Guass_Pyramid[2])、len(Guass_Pyramid[3])、len(Guass_Pyramid[4])、len(Guass_Pyramid[5]) #all は 5 を返します。つまり、各グループには 5 があります。写真のレイヤー Guass_Pyramid[4][0].shape の戻り値: (38, 55, 3 )< /span> Guass_Pyramid[5][0].shape は (19, 28, 3) を返します Guass_Pyramid[2][0].shape は (150, 217, 3) を返します< a i=6> Guass_Pyramid[3][0].shape の戻り値: (75, 109, 3) Guass_Pyramid[1][0].shape 。 。 。 Guass_Pyramid[1][4].shape は (300, 434, 3) も返します
    Guass_Pyramid[0][0].shape、Guass_Pyramid[0][1].shape、Guass_Pyramid[0][2].shape、Guass_Pyramid[0][3]。形状、Guass_Pyramid[0 ][4].shape、すべて (600, 868, 3) を返し、各レイヤーの画像サイズは同じです




    len(DoG_Pyramid) #return 6
    len(DoG_Pyramid[0])\len(DoG_Pyramid[1])....すべては 4 を返します。つまり、各グループは4 層の画像
    DoG_Pyramid[0][0].shape、戻り値: (600, 868, 3)
    他のものは正しいのでリストしません。彼ら。

    6. キー ポイントの位置決め: しきい値を設定してノイズ ポイントを除去し、最初にキー ポイントを特定します。

# 函数2.1.1 adjustLocalExtrema
# 功能:通过泰勒展开精调位置精调位置
def adjustLocalExtrema(DoG, o, s, x, y, contrastThreshold, edgeThreshold, sigma, n, SIFT_FIXPT_SCALE):
    # 在检测到的极值点(x,y,sigma)做三元二阶泰勒展开
    SIFT_MAX_INTERP_STEPS = 5
    SIFT_IMG_BORDER = 5

    point = []

    img_scale = 1.0 / (255 * SIFT_FIXPT_SCALE)
    deriv_scale = img_scale * 0.5
    second_deriv_scale = img_scale
    cross_deriv_scale = img_scale * 0.25

    img = DoG[o][s]
    i = 0
    while i < SIFT_MAX_INTERP_STEPS:
        if s < 1 or s > n or y < SIFT_IMG_BORDER or y >= img.shape[1] - SIFT_IMG_BORDER or x < SIFT_IMG_BORDER or x >= \
                img.shape[0] - SIFT_IMG_BORDER:
            return None, None, None, None

        img = DoG[o][s]
        prev = DoG[o][s - 1]
        next = DoG[o][s + 1]

        dD = [(img[x, y + 1] - img[x, y - 1]) * deriv_scale,
              (img[x + 1, y] - img[x - 1, y]) * deriv_scale,
              (next[x, y] - prev[x, y]) * deriv_scale]

        v2 = img[x, y] * 2
        dxx = (img[x, y + 1] + img[x, y - 1] - v2) * second_deriv_scale
        dyy = (img[x + 1, y] + img[x - 1, y] - v2) * second_deriv_scale
        dss = (next[x, y] + prev[x, y] - v2) * second_deriv_scale
        dxy = (img[x + 1, y + 1] - img[x + 1, y - 1] - img[x - 1, y + 1] + img[x - 1, y - 1]) * cross_deriv_scale
        dxs = (next[x, y + 1] - next[x, y - 1] - prev[x, y + 1] + prev[x, y - 1]) * cross_deriv_scale
        dys = (next[x + 1, y] - next[x - 1, y] - prev[x + 1, y] + prev[x - 1, y]) * cross_deriv_scale

        H = [[dxx, dxy, dxs],
             [dxy, dyy, dys],
             [dxs, dys, dss]]

        X = np.matmul(np.linalg.pinv(np.array(H)), np.array(dD))

        xi = -X[2]
        xr = -X[1]
        xc = -X[0]

        if np.abs(xi) < 0.5 and np.abs(xr) < 0.5 and np.abs(xc) < 0.5:
            break

        y += int(np.round(xc))
        x += int(np.round(xr))
        s += int(np.round(xi))

        i += 1

    if i >= SIFT_MAX_INTERP_STEPS:
        return None, x, y, s
    if s < 1 or s > n or y < SIFT_IMG_BORDER or y >= img.shape[1] - SIFT_IMG_BORDER or x < SIFT_IMG_BORDER or x >= \
            img.shape[0] - SIFT_IMG_BORDER:
        return None, None, None, None

    t = (np.array(dD)).dot(np.array([xc, xr, xi]))

    contr = img[x, y] * img_scale + t * 0.5
    # 确定极值点位置第四步:舍去低对比度的点 :|fx| < T/n
    if np.abs(contr) * n < contrastThreshold:
        return None, x, y, s

    # 确定极值点位置第五步:边缘效应的去除。 利用Hessian矩阵的迹和行列式计算主曲率的比值
    # H(x,y)=[Dxx Dxy
    #         Dxy Dyy]
    tr = dxx + dyy
    det = dxx * dyy - dxy * dxy
    if det <= 0 or tr * tr * edgeThreshold >= (edgeThreshold + 1) * (edgeThreshold + 1) * det:
        return None, x, y, s

    point.append((x + xr) * (1 << o))
    point.append((y + xc) * (1 << o))
    point.append(o + (s << 8) + (int(np.round((xi + 0.5)) * 255) << 16))
    point.append(sigma * np.power(2.0, (s + xi) / n) * (1 << o) * 2)

    return point, x, y, s

 

def GetMainDirection(img, r, c, radius, sigma, BinNum):
    expf_scale = -1.0 / (2.0 * sigma * sigma)

    X = []
    Y = []
    W = []
    temphist = []

    for i in range(BinNum):
        temphist.append(0.0)

    # 图像梯度直方图统计的像素范围
    k = 0
    for i in range(-radius, radius + 1):
        y = r + i
        if y <= 0 or y >= img.shape[0] - 1:
            continue
        for j in range(-radius, radius + 1):
            x = c + j
            if x <= 0 or x >= img.shape[1] - 1:
                continue

            dx = (img[y, x + 1] - img[y, x - 1])
            dy = (img[y - 1, x] - img[y + 1, x])

            X.append(dx)
            Y.append(dy)
            W.append((i * i + j * j) * expf_scale)
            k += 1

    length = k

    W = np.exp(np.array(W))
    Y = np.array(Y)
    X = np.array(X)
    Ori = np.arctan2(Y, X) * 180 / np.pi
    Mag = (X ** 2 + Y ** 2) ** 0.5

    # 计算直方图的每个bin
    for k in range(length):
        bin = int(np.round((BinNum / 360.0) * Ori[k]))
        if bin >= BinNum:
            bin -= BinNum
        if bin < 0:
            bin += BinNum
        temphist[bin] += W[k] * Mag[k]

    # smooth the histogram
    # 高斯平滑
    temp = [temphist[BinNum - 1], temphist[BinNum - 2], temphist[0], temphist[1]]
    temphist.insert(0, temp[0])
    temphist.insert(0, temp[1])
    temphist.insert(len(temphist), temp[2])
    temphist.insert(len(temphist), temp[3])  # padding

    hist = []
    for i in range(BinNum):
        hist.append(
            (temphist[i] + temphist[i + 4]) * (1.0 / 16.0) + (temphist[i + 1] + temphist[i + 3]) * (4.0 / 16.0) +
            temphist[i + 2] * (6.0 / 16.0))

    # 得到主方向
    maxval = max(hist)

    return maxval, hist
# 函数2.1 LocateKeyPoint
# 功能:关键点定位,共分为5步
def LocateKeyPoint(DoG, sigma, GuassianPyramid, n, BinNum=36, contrastThreshold=0.04, edgeThreshold=10.0):
    SIFT_ORI_SIG_FCTR = 1.52
    SIFT_ORI_RADIUS = 3 * SIFT_ORI_SIG_FCTR
    SIFT_ORI_PEAK_RATIO = 0.8

    SIFT_INT_DESCR_FCTR = 512.0
    # SIFT_FIXPT_SCALE = 48
    SIFT_FIXPT_SCALE = 1

    KeyPoints = []
    O = len(DoG)
    S = len(DoG[0])
    for o in range(O):
        for s in range(1, S - 1):
            # 第一步:设定阈值
            threshold = 0.5 * contrastThreshold / (n * 255 * SIFT_FIXPT_SCALE)# 用于阈值化,去噪
            img_prev = DoG[o][s - 1]
            img = DoG[o][s]
            img_next = DoG[o][s + 1]
            for i in range(img.shape[0]):
                for j in range(img.shape[1]):
                    val = img[i, j]
                    eight_neiborhood_prev = img_prev[max(0, i - 1):min(i + 2, img_prev.shape[0]), max(0, j - 1):min(j + 2, img_prev.shape[1])]
                    eight_neiborhood = img[max(0, i - 1):min(i + 2, img.shape[0]), max(0, j - 1):min(j + 2, img.shape[1])]
                    eight_neiborhood_next = img_next[max(0, i - 1):min(i + 2, img_next.shape[0]), max(0, j - 1):min(j + 2, img_next.shape[1])]
                    # 第二步:阈值化,在高斯差分金字塔中找极值
                    if np.abs(val) > threshold and \
                            ((val > 0 and (val >= eight_neiborhood_prev).all() and (val >= eight_neiborhood).all() and (
                                    val >= eight_neiborhood_next).all())
                             or (val < 0 and (val <= eight_neiborhood_prev).all() and (
                                            val <= eight_neiborhood).all() and (val <= eight_neiborhood_next).all())): # 如果某点大于阈值,并且 比周围8个点、上下2*9个点共26个点都大或都小,则认为是关键点
                        # 第三步:精调位置,通过函数2.1.1 adjustLocalExtrema:实现
                        point, x, y, layer = adjustLocalExtrema(DoG, o, s, i, j, contrastThreshold, edgeThreshold,
                                                                sigma, n, SIFT_FIXPT_SCALE)
                        if point == None:
                            continue
                        scl_octv = point[-1] * 0.5 / (1 << o)
                        # GetMainDirection:(确定极值点的位置以后就)求主方向
                        omax, hist = GetMainDirection(GuassianPyramid[o][layer], x, y,
                                                      int(np.round(SIFT_ORI_RADIUS * scl_octv)),
                                                      SIFT_ORI_SIG_FCTR * scl_octv, BinNum)
                        mag_thr = omax * SIFT_ORI_PEAK_RATIO
                        for k in range(BinNum):
                            if k > 0:
                                l = k - 1
                            else:
                                l = BinNum - 1
                            if k < BinNum - 1:
                                r2 = k + 1
                            else:
                                r2 = 0
                            if hist[k] > hist[l] and hist[k] > hist[r2] and hist[k] >= mag_thr:
                                bin = k + 0.5 * (hist[l] - hist[r2]) / (hist[l] - 2 * hist[k] + hist[r2])
                                if bin < 0:
                                    bin = BinNum + bin
                                else:
                                    if bin >= BinNum:
                                        bin = bin - BinNum
                                temp = point[:]
                                temp.append((360.0 / BinNum) * bin)
                                KeyPoints.append(temp)

    return KeyPoints

 

# calcSIFTDescriptor:更小的计算描述符函数
def calcSIFTDescriptor(img, ptf, ori, scl, d, n, SIFT_DESCR_SCL_FCTR=3.0, SIFT_DESCR_MAG_THR=0.2,
                       SIFT_INT_DESCR_FCTR=512.0, FLT_EPSILON=1.19209290E-07):
    dst = []
    pt = [int(np.round(ptf[0])), int(np.round(ptf[1]))]  # 坐标点取整
    # 旋转到主方向
    cos_t = np.cos(ori * (np.pi / 180))  # 余弦值
    sin_t = np.sin(ori * (np.pi / 180))  # 正弦值
    bins_per_rad = n / 360.0
    exp_scale = -1.0 / (d * d * 0.5)
    hist_width = SIFT_DESCR_SCL_FCTR * scl
    # radius: 统计区域边长的一半
    radius = int(np.round(hist_width * 1.4142135623730951 * (d + 1) * 0.5))
    cos_t /= hist_width
    sin_t /= hist_width

    rows = img.shape[0]
    cols = img.shape[1]

    hist = [0.0] * ((d + 2) * (d + 2) * (n + 2))
    X = []
    Y = []
    RBin = []
    CBin = []
    W = []

    k = 0
    for i in range(-radius, radius + 1):
        for j in range(-radius, radius + 1):

            c_rot = j * cos_t - i * sin_t
            r_rot = j * sin_t + i * cos_t
            rbin = r_rot + d // 2 - 0.5
            cbin = c_rot + d // 2 - 0.5
            r = pt[1] + i
            c = pt[0] + j

            if rbin > -1 and rbin < d and cbin > -1 and cbin < d and r > 0 and r < rows - 1 and c > 0 and c < cols - 1:
                dx = (img[r, c + 1] - img[r, c - 1])
                dy = (img[r - 1, c] - img[r + 1, c])
                X.append(dx)
                Y.append(dy)
                RBin.append(rbin)
                CBin.append(cbin)
                W.append((c_rot * c_rot + r_rot * r_rot) * exp_scale)
                k += 1

    length = k
    Y = np.array(Y)
    X = np.array(X)
    Ori = np.arctan2(Y, X) * 180 / np.pi
    Mag = (X ** 2 + Y ** 2) ** 0.5
    W = np.exp(np.array(W))

    for k in range(length):
        rbin = RBin[k]
        cbin = CBin[k]
        obin = (Ori[k] - ori) * bins_per_rad
        mag = Mag[k] * W[k]

        r0 = int(rbin)
        c0 = int(cbin)
        o0 = int(obin)
        rbin -= r0
        cbin -= c0
        obin -= o0

        if o0 < 0:
            o0 += n
        if o0 >= n:
            o0 -= n

        # histogram update using tri-linear interpolation
        v_r1 = mag * rbin
        v_r0 = mag - v_r1

        v_rc11 = v_r1 * cbin
        v_rc10 = v_r1 - v_rc11

        v_rc01 = v_r0 * cbin
        v_rc00 = v_r0 - v_rc01

        v_rco111 = v_rc11 * obin
        v_rco110 = v_rc11 - v_rco111

        v_rco101 = v_rc10 * obin
        v_rco100 = v_rc10 - v_rco101

        v_rco011 = v_rc01 * obin
        v_rco010 = v_rc01 - v_rco011

        v_rco001 = v_rc00 * obin
        v_rco000 = v_rc00 - v_rco001

        idx = ((r0 + 1) * (d + 2) + c0 + 1) * (n + 2) + o0
        hist[idx] += v_rco000
        hist[idx + 1] += v_rco001
        hist[idx + (n + 2)] += v_rco010
        hist[idx + (n + 3)] += v_rco011
        hist[idx + (d + 2) * (n + 2)] += v_rco100
        hist[idx + (d + 2) * (n + 2) + 1] += v_rco101
        hist[idx + (d + 3) * (n + 2)] += v_rco110
        hist[idx + (d + 3) * (n + 2) + 1] += v_rco111

    # finalize histogram, since the orientation histograms are circular
    for i in range(d):
        for j in range(d):
            idx = ((i + 1) * (d + 2) + (j + 1)) * (n + 2)
            hist[idx] += hist[idx + n]
            hist[idx + 1] += hist[idx + n + 1]
            for k in range(n):
                dst.append(hist[idx + k])

    # copy histogram to the descriptor,
    # apply hysteresis thresholding
    # and scale the result, so that it can be easily converted
    # to byte array
    nrm2 = 0
    length = d * d * n
    for k in range(length):
        nrm2 += dst[k] * dst[k]
    thr = np.sqrt(nrm2) * SIFT_DESCR_MAG_THR

    nrm2 = 0
    for i in range(length):
        val = min(dst[i], thr)
        dst[i] = val
        nrm2 += val * val
    nrm2 = SIFT_INT_DESCR_FCTR / max(np.sqrt(nrm2), FLT_EPSILON) # 归一化
    for k in range(length):
        dst[k] = min(max(dst[k] * nrm2, 0), 255)

    return dst
# calcDescriptors:计算描述符
def calcDescriptors(gpyr, keypoints, SIFT_DESCR_WIDTH=4, SIFT_DESCR_HIST_BINS=8):
    # SIFT_DESCR_WIDTH = 4,描述直方图的宽度
    # SIFT_DESCR_HIST_BINS = 8
    d = SIFT_DESCR_WIDTH
    n = SIFT_DESCR_HIST_BINS
    descriptors = []

    # keypoints(x,y,低8位组数次8位层数,尺度,主方向)
    for i in range(len(keypoints)):
        kpt = keypoints[i]
        o = kpt[2] & 255  # 组序号
        s = (kpt[2] >> 8) & 255  # 该特征点所在的层序号
        scale = 1.0 / (1 << o)  # 缩放倍数
        size = kpt[3] * scale  # 该特征点所在组的图像尺寸
        ptf = [kpt[1] * scale, kpt[0] * scale]  # 该特征点在金字塔组中的坐标
        img = gpyr[o][s]  # 该点所在的金字塔图像

        descriptors.append(calcSIFTDescriptor(img, ptf, kpt[-1], size * 0.5, d, n))  # calcSIFTDescriptor:更小的计算描述符函数
    return descriptors
def SIFT(img, showDoGimgs=False):
    # 1. 建立高斯差分金字塔,
    SIFT_SIGMA = 1.6
    SIFT_INIT_SIGMA = 0.5  # 假设的摄像头的尺度
    sigma0 = np.sqrt(SIFT_SIGMA ** 2 - SIFT_INIT_SIGMA ** 2) #初始sigma0
    n = 2######

    DoG, GuassianPyramid,octaves = getDoG(img, n, sigma0)  # 函数1.1,getDoG:得到高斯金字塔和高斯差分金字塔

    if showDoGimgs:
        plt.figure(1)
        for i in range(octaves):
            for j in range(n + 3):
                array = np.array(GuassianPyramid[i][j], dtype=np.float32)
                plt.subplot(octaves, n + 3, j + (i) * octaves + 1)
                plt.imshow(array.astype(np.uint8), cmap='gray')
                plt.axis('off')
        plt.show()

        plt.figure(2)
        for i in range(octaves):
            for j in range(n + 2):
                array = np.array(DoG[i][j], dtype=np.float32)
                plt.subplot(octaves, n + 3, j + (i) * octaves + 1)
                plt.imshow(array.astype(np.uint8), cmap='gray')
                plt.axis('off')
        plt.show()

    #2. 确定关键点位置,为关键点赋予方向
    KeyPoints = LocateKeyPoint(DoG, SIFT_SIGMA, GuassianPyramid, n)  # 函数2.1,LocateKeyPoint:关键点定位

    #3. 计算关键点的描述符
    discriptors = calcDescriptors(GuassianPyramid, KeyPoints)  # 函数3.1,calcDescriptors:计算描述符

    return KeyPoints, discriptors
def Lines(img, info, color=(255, 0, 0), err=700):
    if len(img.shape) == 2:
        result = np.dstack((img, img, img))
    else:
        result = img
    k = 0
    for i in range(result.shape[0]):
        for j in range(result.shape[1]):
            temp = (info[:, 1] - info[:, 0])
            A = (j - info[:, 0]) * (info[:, 3] - info[:, 2])
            B = (i - info[:, 2]) * (info[:, 1] - info[:, 0])
            temp[temp == 0] = 1e-9
            t = (j - info[:, 0]) / temp
            e = np.abs(A - B)
            temp = e < err
            if (temp * (t >= 0) * (t <= 1)).any():
                result[i, j] = color
                k += 1
    #print(k)

    return result
def drawLines(X1, X2, Y1, Y2, dis, img, num=10):
    info = list(np.dstack((X1, X2, Y1, Y2, dis))[0])
    info = sorted(info, key=lambda x: x[-1])
    info = np.array(info)
    info = info[:min(num, info.shape[0]), :]
    img = Lines(img, info)
    # plt.imsave('./sift/3.jpg', img)

    if len(img.shape) == 2:
        plt.imshow(img.astype(np.uint8), cmap='gray')
    else:
        plt.imshow(img.astype(np.uint8))
    plt.axis('off')
    # plt.plot([info[:,0], info[:,1]], [info[:,2], info[:,3]], 'c')
    # fig = plt.gcf()
    # fig.set_size_inches(int(img.shape[0]/100.0),int(img.shape[1]/100.0))
    plt.savefig('result.jpg')
    plt.show()
if __name__ == '__main__':
    origimg = plt.imread('./index_1.bmp')  # 读第一张图片
    if len(origimg.shape) == 3:#如果是彩色图,就按照三通道取均值的方式转成灰度图
        img = origimg.mean(axis=-1)
    else:
        img = origimg
    keyPoints, discriptors = SIFT(img)  # 用SIFT算法计算关键点(x坐标,y坐标,sigma,主方向,梯度幅值)和描述符(128维的向量)

    origimg2 = plt.imread('./index_3.bmp')  # 读第二张图片
    if len(origimg.shape) == 3:
        img2 = origimg2.mean(axis=-1)
    else:
        img2 = origimg2
    ScaleRatio = img.shape[0] * 1.0 / img2.shape[0]
    img2 = np.array(Image.fromarray(img2).resize((int(round(ScaleRatio * img2.shape[1])), img.shape[0]), Image.BICUBIC))
    keyPoints2, discriptors2 = SIFT(img2)  # 用SIFT算关键点和描述符

    indexs = []
    deltas = []
    for i in range(len(keyPoints2)):
        ds = discriptors2[i]
        mindetal = 10000000
        index = -1
        detal = 0
        for j in range(len(keyPoints)):
            ds0 = discriptors[j]
            d = np.array(ds)-np.array(ds0)
            detal = d.dot(d)
            if( detal <= mindetal):
                mindetal = detal
                index = j
        indexs.append(index)
        deltas.append(mindetal)


    keyPoints = np.array(keyPoints)[:,:2]
    keyPoints2 = np.array(keyPoints2)[:,:2]

    keyPoints2[:, 1] = img.shape[1] + keyPoints2[:, 1]

    origimg2 = np.array(Image.fromarray(origimg2).resize((img2.shape[1],img2.shape[0]), Image.BICUBIC))
    result = np.hstack((origimg,origimg2))


    keyPoints = keyPoints[indexs[:]]

    X1 = keyPoints[:, 1]
    X2 = keyPoints2[:, 1]
    Y1 = keyPoints[:, 0]
    Y2 = keyPoints2[:, 0]
    drawLines(X1,X2,Y1,Y2,deltas,result) #把匹配的结果放到这里画线

注: 私のコーディング能力は後で制限されるため、上記のコードは github で見つけたものであり、自分で書いたものではありません。​ 

6. opencv の SIFT アルゴリズム API
  • cv2.xfeatures2d.SIFT_create()
    これはクラスです。SIFT アルゴリズムは基本パッケージではなく opencv 拡張パッケージに含まれているため、このクラスは cv2.xfeatures2d モジュールの下にあります。パッケージ。 。
    (1) まず sift クラスをインスタンス化します: sift = cv2.xfeatures2d.SIFT_create()
    (2) キー ポイント kp を検出して取得します: kp = sift。 detect(img, ...)
    (3) キーポイントの記述子 des を取得します: kp, des = sift.compute(img, kp)、最初のパラメーターはどの画像を参照しますかは記述子の計算に使用され、2 番目のパラメーターがキー ポイントで、kp 記述子と des 記述子が同時に返されます
    、または 2 つと 3 つのステップが結合されます: kp, des = sift.detectAndCompute (img,mask), このメソッドは kp と des を同時に返すことができます。パラメーター img は、kp と des を見つけたい画像を指します。マスクは、img 内のどの領域が計算されるかを指します。実際には、通常これを使用しますメソッド
    (4) 描画 kp: cv2.drawKeypoints(image, kp, Outputimage, color, flags)
    画像: 元の画像
    kp: キーポイント情報、画像上に描画されます
    Outputimage: 元の画像となる出力画像
    color: の色を変更します。 (b, g, r) の値を変更してブラシを作成します。
    flags: 描画関数のフラグ設定
      cv2.DRAW_MATCHES_FLAGS_DEFAULT: 出力画像行列を作成します。既存の出力イメージを使用して、一致するペアと特徴点を描画します。キーポイントごとに、中間点のみが描画されます。
      cv2.DRAW_MATCHES_FLAGS_DRAW_OVER_OUTIMG: 出力イメージ マトリックスを作成しませんが、出力イメージ上に一致するペアを描画します。
      cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS: それぞれのサイズと合計を描画します。特徴点 方向キー ポイントのグラフィックス
      cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS: 単一点の特徴点は描画されません 
#例22.1 调用SIFT算法检测图像的特征点  
import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread(r'C:\Users\25584\Desktop\building.jpg')  #原图  (600, 868, 3)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)    #转灰度图   (600, 868)

#------------分两步提取特征点-----------------------------------------
sift0 = cv2.xfeatures2d.SIFT_create()              #创建sift对象
kp0 = sift0.detect(img_gray, None)        #检测关键点。第二个参数是一个掩码,可以对img_gray中的某个区域进行检测,对整个图片进行检测时就设置为none
kp_, des0 = sift0.compute(img_gray, kp0)

img0 = img.copy()                       #绘制keypoint
cv2.drawKeypoints(img_gray, kp0, img0)
print(des0)    
print('----------------------------------------')  

#---------------一步提取特征点----------------------------------
sift1 = cv2.xfeatures2d.SIFT_create()    #创建sift对象
kp1, des1 = sift1.detectAndCompute(img_gray, None)        #进行检测

img1 = img.copy()                       #绘制keypoint
cv2.drawKeypoints(img1, kp1, img1, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
print(des1)  

#---------------可视化----------------------------------
plt.figure(figsize=(10,8), dpi=100)
ret = np.hstack((img[:,:,::-1], img0[:,:,::-1], img1[:,:,::-1]))
plt.imshow(ret), plt.title('SIFT detect'), plt.xticks([]), plt.yticks([])
plt.show()
[[ 9. 6. 9. ... 0. 0. 0.]
 [ 2. 1. 7. ... 1. 1. 3.]
 [ 0. 0. 0. ... 8. 1. 2.]
 ...
 [ 71. 4. 0. ... 0. 0. 20.]
 [102. 2. 0. ... 0. 0. 15.]
 [ 8. 2. 0. ... 0. 0. 0.]]
----------------------------------------
[[ 9. 6. 9. ... 0. 0. 0.]
 [ 2. 1. 7. ... 1. 1. 3.]
 [ 0. 0. 0. ... 8. 1. 2.]
 ...
 [ 71. 4. 0. ... 0. 0. 20.]
 [102. 2. 0. ... 0. 0. 15.]
 [ 8. 2. 0. ... 0. 0. 0.]]

説明: 1 つまたは 2 つのステップで抽出された特徴点はまったく同じであり、記述子もまったく同じです。
len(kp) は 4566 を返します。これは、4566 個のキー ポイントが見つかったことを意味します。 des.shape は (4566, 128) を返します。これは、各キー ポイントが長さ 128 のベクトルを返し、このベクトルがこのキー ポイントを記述するために使用されることを意味します。

おすすめ

転載: blog.csdn.net/friday1203/article/details/134846584