直交特徴を持つ任意画像上の回転角度の自動検出


9

次のサンプル(マイクロチップ写真の一部)のように、画像の角度を検出する必要があるタスクがあります。画像には直交する特徴が含まれていますが、解像度やシャープネスが異なるため、サイズが異なる場合があります。一部の光学的な歪みと収差のため、画像はわずかに不完全になります。サブピクセル角度検出精度が必要です(つまり、誤差は0.1°未満である必要があり、0.01°程度が許容されます)。参考までに、この画像の最適な角度は約32.19°です。

ここに画像の説明を入力してください 現在、私は2つのアプローチを試しました。どちらも、2°ステップの極小値に対してブルートフォース検索を実行してから、勾配を0.0001°ステップサイズまで下げます。

  1. メリット関数はsum(pow(img(x+1)-img(x-1), 2) + pow(img(y+1)-img(y-1))画像全体で計算されます。水平/垂直線が揃っている場合-水平/垂直方向の変化が少ない。精度は約0.2°でした。
  2. メリット関数は、画像の一部のストライプ幅/高さにわたって(最大-最小)です。このストライプも画像全体にループされ、メリット関数が蓄積されます。このアプローチは、水平/垂直線が整列しているときの明るさの小さな変化にも焦点を当てますが、大きなベース(ストライプ幅-約100ピクセル幅になる可能性があります)全体で小さな変化を検出できます。これにより、最高0.01°までの精度が向上しますが、微調整するパラメータが多数あります(たとえば、ストライプの幅/高さは非常に敏感です)。これは、現実の世界では信頼できない場合があります。

エッジ検出フィルターはあまり役に立ちませんでした。

私の懸念は、ワーストアングルとベストアングルの間の両方のケースでメリット関数の非常に小さな変化(<2x差)です。

角度検出のためのメリット関数の記述について、何か良い提案はありますか?

更新:フルサイズのサンプル画像はここにアップロードされます(51 MiB)

結局のところ、このようになります。


1
stackoverflowからdspに移行されたことは非常に悲しいことです。ここではDSPのようなソリューションが見当たらないため、可能性が大幅に減少しています。DSPアルゴリズムとトリックの99.9%は、このタスクには役に立ちません。ここでは、FFTではなく、カスタムアルゴリズムまたはアプローチが必要なようです。
BarsMonster 2019年

2
悲しいことは全く間違っていると言ってとてもうれしいです。DSP.SEは、これを依頼するのに最適な場所です。(あまりスタックオーバーフローではありません。プログラミングの問題ではありません。プログラミングを知っています。この画像の処理方法がわかりません。)画像は信号であり、DSP.SEは画像処理に非常に関心があります。また、一般的なDSPのトリック大量の(例えば通信信号のために知られたとしても)自分の問題に非常に適用されている:)
マーカス・ミュラー

1
効率はどのくらい重要ですか?
Cedron Dawg

ちなみに、0.04°の解像度で実行している場合でも、回転は32.19°ではなく正確に32°と確信しています。元の写真の解像度はどのくらいですか?幅800ピクセルでは、補正されていない0.01°の回転は高さの差が0.14ピクセルに過ぎないため、sinc補間の下でもほとんど目立ちません。
マーカスミュラー

@CedronDawg確かにリアルタイム要件はありません。8〜12個のコアで10〜60秒の計算を許容できます。
BarsMonster

回答:


12

私があなたの方法1を正しく理解している場合、それを使用して、円対称の領域を使用し、領域の中心を中心に回転させた場合、回転角度に対する領域の依存性を排除し、間のメリット関数によってより公平な比較を得るでしょう。異なる回転角度。基本的にそれと同等の方法を提案しますが、画像全体を使用し、画像の回転を繰り返す必要がなく、ローパスフィルタリングを含めて、ピクセルグリッドの異方性を除去し、ノイズを除去します。

等方性ローパスフィルター処理されたイメージの勾配

最初に、フルサイズのサンプル画像の緑のカラーチャネルの各ピクセルでローカルグラデーションベクトルを計算します。

理想的なローパスフィルターの連続空間インパルス応答をフラットな円形周波数応答で微分することにより、水平および垂直微分カーネルを導出しました。これにより、対角線上で比較される詳細レベルに違いがないことを確認することにより、画像軸の選択の影響を排除します。結果の関数をサンプリングし、回転した余弦ウィンドウを適用することにより、水平または垂直に:

(1)hバツ[バツy]={0もし バツ=y=0ωc2バツJ2ωcバツ2+y22πバツ2+y2さもないと、hy[バツy]={0もし バツ=y=0ωc2yJ2ωcバツ2+y22πバツ2+y2さもないと、

ここで、は第1種の2次ベッセル関数、はラジアン単位のカットオフ周波数です。Pythonソース(式1のマイナス記号はありません):J2ωc

import matplotlib.pyplot as plt
import scipy
import scipy.special
import numpy as np

def rotatedCosineWindow(N):  # N = horizontal size of the targeted kernel, also its vertical size, must be odd.
  return np.fromfunction(lambda y, x: np.maximum(np.cos(np.pi/2*np.sqrt(((x - (N - 1)/2)/((N - 1)/2 + 1))**2 + ((y - (N - 1)/2)/((N - 1)/2 + 1))**2)), 0), [N, N])

def circularLowpassKernelX(omega_c, N):  # omega = cutoff frequency in radians (pi is max), N = horizontal size of the kernel, also its vertical size, must be odd.
  kernel = np.fromfunction(lambda y, x: omega_c**2*(x - (N - 1)/2)*scipy.special.jv(2, omega_c*np.sqrt((x - (N - 1)/2)**2 + (y - (N - 1)/2)**2))/(2*np.pi*((x - (N - 1)/2)**2 + (y - (N - 1)/2)**2)), [N, N])
  kernel[(N - 1)//2, (N - 1)//2] = 0
  return kernel

def circularLowpassKernelY(omega_c, N):  # omega = cutoff frequency in radians (pi is max), N = horizontal size of the kernel, also its vertical size, must be odd.
  kernel = np.fromfunction(lambda y, x: omega_c**2*(y - (N - 1)/2)*scipy.special.jv(2, omega_c*np.sqrt((x - (N - 1)/2)**2 + (y - (N - 1)/2)**2))/(2*np.pi*((x - (N - 1)/2)**2 + (y - (N - 1)/2)**2)), [N, N])
  kernel[(N - 1)//2, (N - 1)//2] = 0
  return kernel

N = 41  # Horizontal size of the kernel, also its vertical size. Must be odd.
window = rotatedCosineWindow(N)

# Optional window function plot
#plt.imshow(window, vmin=-np.max(window), vmax=np.max(window), cmap='bwr')
#plt.colorbar()
#plt.show()

omega_c = np.pi/4  # Cutoff frequency in radians <= pi
kernelX = circularLowpassKernelX(omega_c, N)*window
kernelY = circularLowpassKernelY(omega_c, N)*window

# Optional kernel plot
#plt.imshow(kernelX, vmin=-np.max(kernelX), vmax=np.max(kernelX), cmap='bwr')
#plt.colorbar()
#plt.show()

ここに画像の説明を入力してください
図1. 2次元回転コサインウィンドウ。

ここに画像の説明を入力してください
ここに画像の説明を入力してください
ここに画像の説明を入力してください
図2.ウィンドウ化された水平等方性ローパス微分カーネル、異なるカットオフ周波数 ωc設定。上:omega_c = np.pi、中央:omega_c = np.pi/4、下:omega_c = np.pi/16。式のマイナス記号。1つが除外されました。垂直カーネルは同じように見えますが、90度回転されています。重み付きの水平カーネルと垂直カーネルの重み付き合計cosφ そして φ、それぞれ、勾配角度について同じタイプの分析カーネルを提供します φ

Pythonの2次元高速フーリエ変換(FFT)でわかるように、インパルス応答の微分は帯域幅に影響を与えません。

# Optional FFT plot
absF = np.abs(np.fft.fftshift(np.fft.fft2(circularLowpassKernelX(np.pi, N)*window)))
plt.imshow(absF, vmin=0, vmax=np.max(absF), cmap='Greys', extent=[-np.pi, np.pi, -np.pi, np.pi])
plt.colorbar()
plt.show()

ここに画像の説明を入力してください
図3. 2次元FFTの大きさ hバツ。周波数領域では、微分はフラットな円形通過帯域の乗算として表示されます。ωバツ、そしてマグニチュードでは見えない90度の位相シフトによって。

緑のチャネルのたたみ込みを実行し、Pythonで目視検査のために2次元勾配ベクトルヒストグラムを収集するには:

import scipy.ndimage

img = plt.imread('sample.tif').astype(float)
X = scipy.ndimage.convolve(img[:,:,1], kernelX)[(N - 1)//2:-(N - 1)//2, (N - 1)//2:-(N - 1)//2]  # Green channel only
Y = scipy.ndimage.convolve(img[:,:,1], kernelY)[(N - 1)//2:-(N - 1)//2, (N - 1)//2:-(N - 1)//2]  # ...

# Optional 2-d histogram
#hist2d, xEdges, yEdges = np.histogram2d(X.flatten(), Y.flatten(), bins=199)
#plt.imshow(hist2d**(1/2.2), vmin=0, cmap='Greys')
#plt.show()
#plt.imsave('hist2d.png', plt.cm.Greys(plt.Normalize(vmin=0, vmax=hist2d.max()**(1/2.2))(hist2d**(1/2.2))))  # To save the histogram image
#plt.imsave('histkey.png', plt.cm.Greys(np.repeat([(np.arange(200)/199)**(1/2.2)], 16, 0)))

これはまた(N - 1)//2、ヒストグラム分析の前に、長方形の画像の境界によって汚染された各エッジからのピクセルを破棄して、データをトリミングします。

ここに画像の説明を入力してくださいπ ここに画像の説明を入力してくださいπ2 ここに画像の説明を入力してくださいπ4
ここに画像の説明を入力してくださいπ8 ここに画像の説明を入力してくださいπ16 ここに画像の説明を入力してくださいπ32 ここに画像の説明を入力してくださいπ64 ここに画像の説明を入力してください0
図4.さまざまなローパスフィルターのカットオフ周波数に対する、勾配ベクトルの2次元ヒストグラム ωc設定。順番に:第有するN=41omega_c = np.piomega_c = np.pi/2omega_c = np.pi/4(リストのPythonと同じ)、 、omega_c = np.pi/8omega_c = np.pi/16その後:N=81omega_c = np.pi/32N=161omega_c = np.pi/64。ローパスフィルタリングによるノイズ除去により、ヒストグラムの回路トレースエッジグラディエントの方向がシャープになります。

ベクトル長加重円形平均方向

複数の風ベクトルサンプルからサンプルを1回通過することで「平均的な」風向を見つけるYamartinoの方法があります。これは、循環量平均に基づいています。これは、周期の循環量によってシフトされたコサインの合計であるコサインのシフトとして計算されます。2π。同じ方法のベクトル長で重み付けされたバージョンを使用できますが、最初に、モジュロが等しいすべての方向をまとめる必要がありますπ/2。これを行うには、各勾配ベクトルの角度を掛けます[バツkYk] 4で、複素数表現を使用して:

(2)Zk=バツk+Yk4バツk2+Yk2=バツk46バツk2Yk2+Yk4+4バツkYk4バツkYkバツk2+Yk2

満足 |Zk|=バツk2+Yk2 と後で解釈することによって Zk から ππ からの角度を表す π/4π/4、計算された循環平均位相を4で割ることにより、

(3)φ=14atan2ΣkイムZkΣkZk

どこ φ 推定される画像の向きです。

推定の質は、データをもう一度通過し、平均の重み付けされた正方形の円形距離を計算することによって評価できます。MSCD、複素数のフェーズ間 Zk 推定された循環平均位相 4φ|Zk| 重量として:

(4)MSCD=Σk|Zk|1cos4φatan2イムZkZkΣk|Zk|=Σk|Zk|2cos4φZk|Zk|2+4φイムZk|Zk|2Σk|Zk|=Σk|Zk|Zkcos4φイムZk4φΣk|Zk|

によって最小化されました φ式ごとに計算 3. Pythonの場合:

absZ = np.sqrt(X**2 + Y**2)
reZ = (X**4 - 6*X**2*Y**2 + Y**4)/absZ**3
imZ = (4*X**3*Y - 4*X*Y**3)/absZ**3
phi = np.arctan2(np.sum(imZ), np.sum(reZ))/4

sumWeighted = np.sum(absZ - reZ*np.cos(4*phi) - imZ*np.sin(4*phi))
sumAbsZ = np.sum(absZ)
mscd = sumWeighted/sumAbsZ

print("rotate", -phi*180/np.pi, "deg, RMSCD =", np.arccos(1 - mscd)/4*180/np.pi, "deg equivalent (weight = length)")

私のmpmath実験に基づいて(図には示されていません)、非常に大きな画像でも数値の精度が不足することはないと思います。異なるフィルター設定(注釈付き)の場合、出力は-45度から45度の間で報告されます。

rotate 32.29809399495655 deg, RMSCD = 17.057059965741338 deg equivalent (omega_c = np.pi)
rotate 32.07672617150525 deg, RMSCD = 16.699056648843566 deg equivalent (omega_c = np.pi/2)
rotate 32.13115293914797 deg, RMSCD = 15.217534399922902 deg equivalent (omega_c = np.pi/4, same as in the Python listing)
rotate 32.18444156018288 deg, RMSCD = 14.239347706786056 deg equivalent (omega_c = np.pi/8)
rotate 32.23705383489169 deg, RMSCD = 13.63694582160468 deg equivalent (omega_c = np.pi/16)

強力なローパスフィルタリングは有用であると思われ、次のように計算される二乗平均平方根距離(RMSCD)の等価角度を減らします。 acos1MSCD。2次元回転コサインウィンドウがない場合、結果の一部がある程度ずれます(図には示されていません)。これは、分析フィルターの適切なウィンドウ処理を行うことが重要であることを意味します。RMSCDの等価角度は、角度推定の誤差を直接推定するものではないため、はるかに小さくする必要があります。

代替の正方形の長さの重み関数

代替の重み関数として、ベクトルの長さの2乗を試してみましょう。

(5)Zk=バツk+Yk4バツk2+Yk22=バツk46バツk2Yk2+Yk4+4バツkYk4バツkYkバツk2+Yk2

Pythonの場合:

absZ_alt = X**2 + Y**2
reZ_alt = (X**4 - 6*X**2*Y**2 + Y**4)/absZ_alt
imZ_alt = (4*X**3*Y - 4*X*Y**3)/absZ_alt
phi_alt = np.arctan2(np.sum(imZ_alt), np.sum(reZ_alt))/4

sumWeighted_alt = np.sum(absZ_alt - reZ_alt*np.cos(4*phi_alt) - imZ_alt*np.sin(4*phi_alt))
sumAbsZ_alt = np.sum(absZ_alt)
mscd_alt = sumWeighted_alt/sumAbsZ_alt

print("rotate", -phi_alt*180/np.pi, "deg, RMSCD =", np.arccos(1 - mscd_alt)/4*180/np.pi, "deg equivalent (weight = length^2)")

正方形の長さの重みにより、RMSCDの等価角度が約1度減少します。

rotate 32.264713568426764 deg, RMSCD = 16.06582418749094 deg equivalent (weight = length^2, omega_c = np.pi, N = 41)
rotate 32.03693157762725 deg, RMSCD = 15.839593856962486 deg equivalent (weight = length^2, omega_c = np.pi/2, N = 41)
rotate 32.11471435914187 deg, RMSCD = 14.315371970649874 deg equivalent (weight = length^2, omega_c = np.pi/4, N = 41)
rotate 32.16968341455537 deg, RMSCD = 13.624896827482049 deg equivalent (weight = length^2, omega_c = np.pi/8, N = 41)
rotate 32.22062839958777 deg, RMSCD = 12.495324176281466 deg equivalent (weight = length^2, omega_c = np.pi/16, N = 41)
rotate 32.22385477783647 deg, RMSCD = 13.629915935941973 deg equivalent (weight = length^2, omega_c = np.pi/32, N = 81)
rotate 32.284350817263906 deg, RMSCD = 12.308297934977746 deg equivalent (weight = length^2, omega_c = np.pi/64, N = 161)

これは少し良い重み関数のようです。カットオフも追加しましたωc=π/32 そして ωc=π/64。より大きな値を使用するNと、画像のトリミングが異なり、厳密に比較可能なMSCD値は得られません。

1次元ヒストグラム

平方長の重み関数の利点は、次の1次元の重み付きヒストグラムでより明確になります。 Zkフェーズ。Pythonスクリプト:

# Optional histogram
hist_plain, bin_edges = np.histogram(np.arctan2(imZ, reZ), weights=np.ones(absZ.shape)/absZ.size, bins=900)
hist, bin_edges = np.histogram(np.arctan2(imZ, reZ), weights=absZ/np.sum(absZ), bins=900)
hist_alt, bin_edges = np.histogram(np.arctan2(imZ_alt, reZ_alt), weights=absZ_alt/np.sum(absZ_alt), bins=900)
plt.plot((bin_edges[:-1]+(bin_edges[1]-bin_edges[0]))*45/np.pi, hist_plain, "black")
plt.plot((bin_edges[:-1]+(bin_edges[1]-bin_edges[0]))*45/np.pi, hist, "red")
plt.plot((bin_edges[:-1]+(bin_edges[1]-bin_edges[0]))*45/np.pi, hist_alt, "blue")
plt.xlabel("angle (degrees)")
plt.show()

ここに画像の説明を入力してください ここに画像の説明を入力してください
図5.ラップされた勾配ベクトル角度の線形補間された重み付きヒストグラム π/4π/4重み付け(ピークで下から上へ):重み付けなし(黒)、勾配ベクトル長(赤)、勾配ベクトル長の2乗(青)。ビンの幅は0.1度です。フィルターのカットオフはomega_c = np.pi/4、Pythonリストと同じでした。下の図はピークで拡大されています。

操縦可能なフィルター演算

このアプローチが機能することを確認しましたが、より良い数学的理解が得られると良いでしょう。のバツ そして y式によって与えられる微分フィルターインパルス応答。1 は、方程式の右辺の回転からサンプリングされた、ステアリング可能な微分フィルターのインパルス応答を形成するための基底関数として理解できます。hバツ[バツy](方程式1)。これは、Eq。1から極座標:

(6)hバツrθ=hバツ[rcosθrθ]={0もし r=0ωc2rcosθJ2ωcr2πr2さもないと=cosθfrhyrθ=hy[rcosθrθ]={0もし r=0ωc2rθJ2ωcr2πr2さもないと=θfrfr={0もし r=0ωc2rJ2ωcr2πr2さもないと、

ここで、水平と垂直の両方の微分フィルターインパルス応答は同じ動径因子関数を持ちます fr。ローテーションバージョンhrθφhバツrθ ステアリング角度によって φ によって取得されます:

(7)hrθφ=hバツrθφ=cosθφfr

考えは操縦されたカーネル hrθφ の加重和として構築できます hバツrθ そして hバツrθcosφ そして φ 重みとして、そしてそれは確かにそうです:

(8)cosφhバツrθ+φhyrθ=cosφcosθfr+φθfr=cosθφfr=hrθφ

等方性ローパスフィルター処理された信号を入力信号と見なし、最初の回転座標に関する偏微分演算子を作成すると、同等の結論が得られます。 バツφyφ 角度で回転 φ 座標から バツy。(導出は、線形時間不変システムと見なすことができます。)

(9)バツ=cosφバツφφyφy=φバツφ+cosφyφ

偏導関数のチェーンルールを使用すると、バツφ に関する偏導関数のコサインおよびサイン加重和として表すことができます バツ そして y

(10)バツφ=バツバツφバツ+yバツφy=cosφバツφφyφバツφバツ+φバツφ+cosφyφバツφy=cosφバツ+φy

まだ検討されていない問題は、勾配ベクトル角度の適切に重み付けされた円形平均がどのように角度に関係するかです φ ある意味で「最も活性化された」操縦微分フィルター。

可能な改善

結果をさらに改善するために、赤と青のカラーチャネルについても勾配を計算し、「平均」計算に追加データとして含めることができます。

私はこの方法の可能な拡張を念頭に置いています:

1)勾配を検出するのではなく、より大きな分析フィルターカーネルのセットを使用して、エッジを検出します。これは、すべての方向のエッジが等しく扱われるように注意深く作成する必要があります。つまり、任意の角度のエッジ検出器は、直交カーネルの加重和によって取得できる必要があります。式(1)の微分演算子を適用することで、適切なカーネルのセットを取得できます(私はそう思います)。円対称ローパスフィルターの連続空間インパルス応答の11、図6(私の数学スタック交換の投稿も参照)。

(11)リムh0ΣN=04N+11fバツ+hcos2π4N+2y+h2π4N+2h2N+1リムh0ΣN=04N+11fバツ+h2π4N+2y+hcos2π4N+2h2N+1

ここに画像の説明を入力してください
図6. 高次エッジ検出器を構築するための微分演算子のディラックデルタ相対位置。

2)循環量の(加重)平均の計算は、量のサンプルによってシフトされた(および加重によってスケーリングされた)同じ周波数のコサインを合計し、結果の関数のピークを見つけることとして理解できます。慎重に選択された相対振幅を使用して、シフトされた余弦の同様にシフトおよびスケーリングされた高調波が混合に追加され、よりシャープな平滑化カーネルが形成されると、合計に複数のピークが表示され、最大値のピークが報告されます。高調波の適切な混合により、分布のメインピークから離れた外れ値をほとんど無視する一種の局所平均が得られます。

代替アプローチ

画像を角度で畳み込むことも可能です φ と角度 φ+π/2回転した「長いエッジ」カーネル、および2つの畳み込み画像のピクセルの平均二乗を計算するため。角度φ平均二乗を最大化することが報告されます。このアプローチは、完全な角度を検索するのが危険であるため、画像の向きを見つけるための良い最終的な改善をもたらすかもしれませんφ 大きなステップのスペース。

別のアプローチは、非局所的な方法です。たとえば、長い水平または垂直のトレース、または水平または垂直に何度も繰り返されるフィーチャがあることがわかっている場合に適用できる、遠くの類似した領域を相互相関させます。


あなたが得た結果はどのくらい正確ですか?
Royi

@Royi多分0.1度
Olli Niemitalo

限られた解像度を考えると、かなり印象的な@OlliNiemitalo!
マーカスミュラー

3
印象的な@OlliNiemitaloと言えば、これ。回答。です。それ。言葉の。非常に。定義。
マーカスミュラー

@MarcusMüllerありがとうMarcus、最初の拡張機能も非常に興味深いものになると思います。
Olli Niemitalo

5

ここにも同様のDSPトリックがありますが、詳細を正確に覚えていません。

どこかで、どこかで読んだことがあります。これは、向きに関係なく、ファブリックパターンの一致を把握することに関係しています。ですから、それについて調査したいかもしれません。

円のサンプルをつかみます。円のスポークに沿って合計して、円周プロファイルを取得します。その後、DFTを実行しました(結局のところ、それは本質的に循環です)。位相情報をトスして(向きに依存しないようにして)、比較を行います。

次に、2つのファブリックに同じパターンがあるかどうかを確認できました。

あなたの問題も同様です。

最初に試してみるまでもなく、DFT前のプロファイルの特性が方向性を明らかにするように思えます。合計の代わりにスポークに沿って標準偏差を行うと、おそらく両方ともうまくいくはずです。

これで、方向性のある参照画像があれば、それらの手法を使用できます。

セド


精度要件はかなり厳しいです。

私はこれを強打しました。各色のスポークに沿った2つの後続のポイント間の差の絶対値の合計を取ります。

こちらが円周周りのグラフです。値は白いマーカーでプロットされます。

ここに画像の説明を入力してください

あなたはそれをある程度見ることができますが、これがあなたのために働くとは思いません。ごめんなさい。


進捗レポート:いくつか

私は3つのステップのプロセスを決定しました。

1)評価スポットを見つけます。

2)粗い測定

3)精密測定

現在、最初のステップはユーザーの発明です。自動化できるはずですが、気になりません。2番目のステップの大まかなドラフトがあります。試してみたい微調整がいくつかあります。最後に、3番目のステップの候補がいくつかあり、どれが最も効果的かをテストするためにテストを行います。

良いニュースは、それが速く点灯していることです。あなたの唯一の目的がウェブページ上で画像の見た目レベルを作ることであるなら、あなたの許容範囲はあまりにも厳しく、粗い測定は十分に正確でなければなりません。

これは粗い測定です。各ピクセルは約0.6度です。(編集、実際には0.3)

ここに画像の説明を入力してください


進捗レポート:良い結果を得ることができます

ここに画像の説明を入力してください

ほとんどはこれほど良くはありませんが、安価で(そしてかなりローカル)、良い読み取りを行うスポットを見つけるのは簡単です.....人間にとっては。ブルートフォースはプログラムにとってはうまく機能するはずです。

結果は大幅に改善されます。これは単純なベースラインテストです。私はまだ説明をする準備ができていませんし、コードを投稿することもできませんが、このスクリーンショットはフォトショップではありません。


進捗レポート:コードが投稿されました。しばらくこれで終わりです。

このスクリーンショットは、マーカスの45度ショットに取り組んでいるプログラムです。

ここに画像の説明を入力してください

カラーチャンネルは個別に処理されます。

ポイントがスイープの中心として選択されます。

直径は離散的な角度で180度スイープされます

各角度で、「ボラティリティ」は直径全体で測定されます。サンプルを収集するチャネルごとにトレースが作成されます。サンプル値は、サンプルスポットが着地するグリッドスクエアの4つのコーナー値の線形補間です。

各チャネルトレース

サンプルはVonHannウィンドウ関数で乗算されます

A スムーズ/パスを異なりますサンプルに対して行われます

DifferのRMSはボラティリティメジャーとして使用されます

下の行のグラフは次のとおりです。

最初は0から180度のスイープで、各ピクセルは0.5度です。2番目は、選択した角度の周りのスイープです。各ピクセルは0.1度です。3番目は、選択した角度の周りのスイープです。各ピクセルは0.01度です。4番目はトレースの差分曲線です

最初の選択は、3つのチャネルの最小平均ボラティリティです。これは最適な角度に近くなりますが、通常はオンになりません。谷での対称性は、最小値よりも優れた指標です。その近所の最適な放物線は非常に良い答えをもたらすはずです。

ソースコード(GambasのPPA gambas-team / gambas3)は、次の場所にあります。

https://forum.gambas.one/viewtopic.php?f=4&t=707

これは通常のzipファイルなので、ソースを見るためにGambasをインストールする必要はありません。ファイルは「.src」サブディレクトリにあります。

VonHannウィンドウを削除すると、トレースが効果的に長くなるため精度が高くなりますが、ぐらつきが追加されます。中心が重要ではなく、「シーソーが地面に当たったとき」のより早い開始が検出されるので、ダブルVonHannはおそらくより良いでしょう。画像が許す限りトレース長を長くすることで精度を簡単に改善できます(はい、それは自動化可能です)。より良いウィンドウ関数、sinc?

現在の設定で私が取った対策は、3.19値+/-。03 ishを確認します。

これは単なる測定ツールです。画像に適用するために考えられるいくつかの戦略があります。彼らが言うように、それは読者のための練習です。または、この場合はOP。後で自分で試してみるつもりです。

アルゴリズムとプログラムの両方に改善の余地はありますが、すでに非常に有用です。

これが線形補間の仕組みです

'----整数部分

        x =床(rx)
        y = Floor(ry)

'----分数部分

        fx = rx-x
        fy = ry-y

        gx = 1.0-fx
        gy = 1.0-fy

' -  -  加重平均

        vtl = ArgValues [x、y] * gx * gy '左上
        vtr = ArgValues [x + 1、y] * fx * gy '右上
        vbl = ArgValues [x、y + 1] * gx * fy '左下
        vbr = ArgValues [x + 1、y + 1] * fx * fy 'ボトムリグス

        v = vtl + vtr + vbl + vbr

誰かがその慣習的な名前を知っていますか?


1
こんにちは、非常に巧妙なアプローチだったことを後悔する必要はありません。後で同様の問題が発生し、後でここに来る人にとって非常に役立つ場合があります。+1
マーカス・ミュラー

1
@BarsMonster、私は良い進歩をしています。LinuxボックスにGambas(PPA:gambas-team / gambas3)をインストールします。(可能であれば、MarcusとOlliも可能です。)この問題に取り組むだけでなく、他の画像処理タスクの良い基盤としても機能するプログラムに取り組んでいます。
Cedron Dawg

楽しみにしている!
マーカスミュラー

バイリニア補間と呼ばれています@CedronDawgは、なぜここにある代替実装にも示します。
Olli Niemitalo

1
@ OlliNiemitalo、Olliに感謝します。この状況では、バイキュービックを実行しても、バイリニアよりも結果が向上するとは思われません。実際には、有害な場合さえあるのです。後で、直径に沿ったさまざまなボラティリティメトリックとさまざまな形のウィンドウ関数を試してみます。この時点で、パドルや「泥に当たるシーソーのシート」のように、直径の端にあるVonHannを使用することを考えています。曲線の平らな底は、シーソーがまだ地面(エッジ)になっていないところです。2つのコーナーの中間は良い読み物です。現在の設定は、0.1度未満に優れている
Cedron仲の良い友達

4

むしろパフォーマンスを重視しますが、必要に応じて精度を上げる必要があります。

  • エッジ検出画像
  • 必要な精度のために十分なピクセルがあるスペースにハフ変換します。
  • 十分な直交線があるため、ハフ空間の画像には、2つの線上にある最大値が含まれます。これらは簡単に検出でき、目的の角度を提供します。

ニース、まさに私のアプローチ:私が電車に乗る前にそれが見えなかったので、私は答えにそれを組み込んでいなかったので、ちょっと悲しいです。クリア+1!
マーカスミュラー

4

私は先に進んで、基本的にopencvのハフ変換の例をあなたのユースケースに合わせました。アイデアはいいですが、エッジの効いた性質のため、画像には既に多くのエッジがあるので、エッジ検出はあまりメリットがありません。

だから、私が上記の例で言ったのは

  • エッジ検出を省略
  • 入力画像をカラーチャネルに分解し、個別に処理する
  • 特定の角度でラインの出現をカウントします(角度を量子化し、90を法としてそれらを取得した後、十分な直角があるため)。
  • カラーチャンネルのカウンターを組み合わせる
  • これらの回転を修正する

推定の品質をさらに向上させるためにできること(以下に示すように、一番上の推測は正しくありませんでした。2番目は正しくありません)は、異なる画像間の実際の違いを表すグレースケール画像に画像を変換することになるでしょう。最高の素材–明らかに、RGBチャンネルは最高ではありません。あなたは半導体の専門家なので、メタライゼーションとシリコンなどの違いを最大化する方法でカラーチャネルを組み合わせる方法を見つけます。

私のジュピターノートはこちらです。以下の結果を参照してください。

角度分解能を上げるには、QUANT_STEP変数とhough_transform呼び出しの角度精度を上げます。このコードを20分未満で記述したかったため、計算に1分も投資したくなかったので、そうしませんでした。

import cv2
import numpy
from matplotlib import pyplot
import collections

QUANT_STEPS = 360*2
def quantized_angle(line, quant = QUANT_STEPS):
    theta = line[0][1]
    return numpy.round(theta / numpy.pi / 2 * QUANT_STEPS) / QUANT_STEPS * 360 % 90

def detect_rotation(monochromatic_img):
    # edges = cv2.Canny(monochromatic_img, 50, 150, apertureSize = 3) #play with these parameters
    lines = cv2.HoughLines(monochromatic_img, #input
                           1, # rho resolution [px]
                           numpy.pi/180, # angular resolution [radian]
                           200) # accumulator threshold – higher = fewer candidates
    counter = collections.Counter(quantized_angle(line) for line in lines)
    return counter
img = cv2.imread("/tmp/HIKRe.jpg") #Image directly as grabbed from imgur.com
total_count = collections.Counter()
for channel in range(img.shape[-1]):
    total_count.update(detect_rotation(img[:,:,channel]))

most_common = total_count.most_common(5)
for angle,_ in most_common:
    pyplot.figure(figsize=(8,6), dpi=100)
    pyplot.title(f"{angle:.3f}°")
    rotation = cv2.getRotationMatrix2D((img.shape[0]/2, img.shape[1]/2), -angle, 1)
    pyplot.imshow(cv2.warpAffine(img, rotation, img.shape[:2]))

output_4_0

output_4_1

output_4_2

output_4_3

output_4_4


4

これは、私の以前の回答の最初に提案された拡張機能への挑戦です。

理想的な円対称の帯域制限フィルター

半径の円の内側に帯域制限された4つのフィルターの直交バンクを構築しますωc周波数平面上。これらのフィルターのインパルス応答を線形に組み合わせて、方向性エッジ検出カーネルを形成できます。任意に正規化された直交フィルターインパルス応答のセットは、「ビーチボールのような」微分演算子の最初の2つのペアを、円対称の理想的な帯域制限フィルターインパルス応答の連続空間インパルス応答に適用することによって取得されます。hバツy

(1)hバツy=ωc2πバツ2+y2J1ωcバツ2+y2

(2)h0バツバツyαddバツhバツyh0yバツyαddyhバツyh1バツバツyαddバツddバツddy2hバツyh1yバツyαddyddyddバツ2hバツy

(3)h0バツバツy={0もし バツ=y=0ωc2バツJ2ωcバツ2+y22πバツ2+y2さもないと、h0yバツy=h0バツ[yバツ]h1バツバツy={0もし バツ=y=0ωcバツy2バツ2J0ωcバツ2+y2ωcバツ2+y2ωc2バツ2+ωc2y2248J1ωcバツ2+y2ωc2バツ2+ωc2y262πバツ2+y27/2さもないと、h1yバツy=h1バツ[yバツ]

どこ Jαである第一種のベッセル関数オーダーのα そして α「に比例する」を意味します。Wolfram Alphaクエリ((ᵈ/ dx)³ ; ᵈ/ dx ; ᵈ/ dx(ᵈ/ dy)²)を使用して微分を実行し、結果を簡略化しました。

Pythonで切り捨てられたカーネル:

import matplotlib.pyplot as plt
import scipy
import scipy.special
import numpy as np

def h0x(x, y, omega_c):
  if x == 0 and y == 0:
    return 0
  return -omega_c**2*x*scipy.special.jv(2, omega_c*np.sqrt(x**2 + y**2))/(2*np.pi*(x**2 + y**2))

def h1x(x, y, omega_c):
  if x == 0 and y == 0:
    return 0
  return omega_c*x*(3*y**2 - x**2)*(scipy.special.j0(omega_c*np.sqrt(x**2 + y**2))*omega_c*np.sqrt(x**2 + y**2)*(omega_c**2*x**2 + omega_c**2*y**2 - 24) - 8*scipy.special.j1(omega_c*np.sqrt(x**2 + y**2))*(omega_c**2*x**2 + omega_c**2*y**2 - 6))/(2*np.pi*(x**2 + y**2)**(7/2))

def rotatedCosineWindow(N):  # N = horizontal size of the targeted kernel, also its vertical size, must be odd.
  return np.fromfunction(lambda y, x: np.maximum(np.cos(np.pi/2*np.sqrt(((x - (N - 1)/2)/((N - 1)/2 + 1))**2 + ((y - (N - 1)/2)/((N - 1)/2 + 1))**2)), 0), [N, N])

def circularLowpassKernel(omega_c, N):  # omega = cutoff frequency in radians (pi is max), N = horizontal size of the kernel, also its vertical size, must be odd.
  kernel = np.fromfunction(lambda x, y: omega_c*scipy.special.j1(omega_c*np.sqrt((x - (N - 1)/2)**2 + (y - (N - 1)/2)**2))/(2*np.pi*np.sqrt((x - (N - 1)/2)**2 + (y - (N - 1)/2)**2)), [N, N])
  kernel[(N - 1)//2, (N - 1)//2] = omega_c**2/(4*np.pi)
  return kernel

def prototype0x(omega_c, N):  # omega = cutoff frequency in radians (pi is max), N = horizontal size of the kernel, also its vertical size, must be odd.
  kernel = np.zeros([N, N])
  for y in range(N):
    for x in range(N):
      kernel[y, x] = h0x(x - (N - 1)/2, y - (N - 1)/2, omega_c)
  return kernel

def prototype0y(omega_c, N):  # omega = cutoff frequency in radians (pi is max), N = horizontal size of the kernel, also its vertical size, must be odd.
  return prototype0x(omega_c, N).transpose()

def prototype1x(omega_c, N):  # omega = cutoff frequency in radians (pi is max), N = horizontal size of the kernel, also its vertical size, must be odd.
  kernel = np.zeros([N, N])
  for y in range(N):
    for x in range(N):
      kernel[y, x] = h1x(x - (N - 1)/2, y - (N - 1)/2, omega_c)
  return kernel

def prototype1y(omega_c, N):  # omega = cutoff frequency in radians (pi is max), N = horizontal size of the kernel, also its vertical size, must be odd.
  return prototype1x(omega_c, N).transpose()

N = 321  # Horizontal size of the kernel, also its vertical size. Must be odd.
window = rotatedCosineWindow(N)

# Optional window function plot
#plt.imshow(window, vmin=-np.max(window), vmax=np.max(window), cmap='bwr')
#plt.colorbar()
#plt.show()

omega_c = np.pi/8  # Cutoff frequency in radians <= pi
lowpass = circularLowpassKernel(omega_c, N)
kernel0x = prototype0x(omega_c, N)
kernel0y = prototype0y(omega_c, N)
kernel1x = prototype1x(omega_c, N)
kernel1y = prototype1y(omega_c, N)

# Optional kernel image save
plt.imsave('lowpass.png', plt.cm.bwr(plt.Normalize(vmin=-lowpass.max(), vmax=lowpass.max())(lowpass)))
plt.imsave('kernel0x.png', plt.cm.bwr(plt.Normalize(vmin=-kernel0x.max(), vmax=kernel0x.max())(kernel0x)))
plt.imsave('kernel0y.png', plt.cm.bwr(plt.Normalize(vmin=-kernel0y.max(), vmax=kernel0y.max())(kernel0y)))
plt.imsave('kernel1x.png', plt.cm.bwr(plt.Normalize(vmin=-kernel1x.max(), vmax=kernel1x.max())(kernel1x)))
plt.imsave('kernel1y.png', plt.cm.bwr(plt.Normalize(vmin=-kernel1y.max(), vmax=kernel1y.max())(kernel1y)))
plt.imsave('kernelkey.png', plt.cm.bwr(np.repeat([(np.arange(321)/320)], 16, 0)))

ここに画像の説明を入力してください
ここに画像の説明を入力してください
図1.円周対称の帯域制限フィルターインパルス応答のカラーマップ1:1スケールプロット、カットオフ周波数 ωc=π/8。カラーキー:青:ネガティブ、白:ゼロ、赤:最大。

ここに画像の説明を入力してくださいここに画像の説明を入力してください
ここに画像の説明を入力してくださいここに画像の説明を入力してください
ここに画像の説明を入力してください
図2.カットオフ周波数を使用した、フィルターバンク内のフィルターのサンプリングされたインパルス応答のカラーマップ1:1スケールプロット ωc=π/8、 順番に: h0バツh0yh1バツh0y。カラーキー:青:最小、白:ゼロ、赤:最大。

方向エッジ検出器は、これらの加重和として構築できます。Pythonの場合(続き):

composite = kernel0x-4*kernel1x
plt.imsave('composite0.png', plt.cm.bwr(plt.Normalize(vmin=-composite.max(), vmax=composite.max())(composite)))
plt.imshow(composite, vmin=-np.max(composite), vmax=np.max(composite), cmap='bwr')
plt.colorbar()
plt.show()

composite = (kernel0x+kernel0y) + 4*(kernel1x+kernel1y)
plt.imsave('composite45.png', plt.cm.bwr(plt.Normalize(vmin=-composite.max(), vmax=composite.max())(composite)))
plt.imshow(composite, vmin=-np.max(composite), vmax=np.max(composite), cmap='bwr')
plt.colorbar()
plt.show()

ここに画像の説明を入力してくださいここに画像の説明を入力してください
ここに画像の説明を入力してください
図3.図2のカーネルの加重和として構築された方向エッジ検出カーネル。カラーキー:青:最小、白:ゼロ、赤:最大。

図3のフィルターは、勾配フィルター(図2の最初の2つのフィルター)と比較して、連続エッジに対してより適切に調整する必要があります。

ガウスフィルター

図2のフィルターは、厳密な帯域制限のために多くの振動を持っています。おそらく、ガウス微分フィルターのように、より良い開始点はガウス関数でしょう。比較的、数学的に処理する方がはるかに簡単です。代わりに試してみましょう。ガウスの「ローパス」フィルターのインパルス応答の定義から始めます。

(4)hバツyσ=eバツ2+y22σ22πσ2

式の演算子を適用します。2〜hバツyσ 各フィルターを正規化します h 沿って:

(5)hバツyσ2dバツdy=1。

(6)h0バツバツyσ=22πσ2ddバツhバツyσ=2πσ2バツeバツ2+y22σ2h0yバツyσ=h0バツyバツσh1バツバツyσ=2πσ4ddバツddバツddy2hバツyσ=πσ4バツバツy2eバツ2+y22σ2h1yバツyσ=h1バツyバツσ

これらを加重和として、特異性を最大化する垂直エッジ検出フィルターのインパルス応答を構築します S これは、考えられるエッジシフトに対する垂直エッジに対する平均感度です。 s 可能なエッジ回転角度での平均感度と比較して β そして可能なエッジシフト s

(7)S=2πshバツバツyσdバツshバツバツyσdバツdy2dsππshバツcosβバツβyβバツ+cosβydバツshバツcosβバツβyβバツ+cosβydバツdy2dsdβ

加重和のみが必要です h0バツ 変動あり σ2 そして h1バツ最適な分散で。それが判明S インパルス応答によって最大化されます:

(8)hバツバツyσ=76252440561h0バツバツyσ2610597661h1バツバツy5σ=152504880561πσ2バツeバツ2+y22σ2+1830529284575πσ42バツ6バツy2eバツ2+y210σ2=2πσ2152504880561ddバツhバツyσ100πσ4183052928183ddバツddバツddy2hバツy5σ3.8275359956049814σ2ddバツhバツyσ33.044650082417731σ4ddバツddバツddy2hバツy5σ

式によっても正規化されます。5.垂直エッジに対して、このフィルターには次の特性があります。S=10×51/49 + 2 3.661498645、特異性とは対照的に S=2 に関する一次ガウス微分フィルターの バツ。式の最後の部分。8は、Pythonの分離可能な2次元ガウス微分フィルターと互換性のある正規化を備えていますscipy.ndimage.gaussian_filter

import matplotlib.pyplot as plt
import numpy as np
import scipy.ndimage

sig = 8;
N = 161
x = np.zeros([N, N])
x[N//2, N//2] = 1
ddx = scipy.ndimage.gaussian_filter(x, sigma=[sig, sig], order=[0, 1], truncate=(N//2)/sig)
ddx3 = scipy.ndimage.gaussian_filter(x, sigma=[np.sqrt(5)*sig, np.sqrt(5)*sig], order=[0, 3], truncate=(N//2)/(np.sqrt(5)*sig))
ddxddy2 = scipy.ndimage.gaussian_filter(x, sigma=[np.sqrt(5)*sig, np.sqrt(5)*sig], order=[2, 1], truncate=(N//2)/(np.sqrt(5)*sig))

hx = 3.8275359956049814*sig**2*ddx - 33.044650082417731*sig**4*(ddx3 - 3*ddxddy2)
plt.imsave('hx.png', plt.cm.bwr(plt.Normalize(vmin=-hx.max(), vmax=hx.max())(hx)))

h = scipy.ndimage.gaussian_filter(x, sigma=[sig, sig], order=[0, 0], truncate=(N//2)/sig)
plt.imsave('h.png', plt.cm.bwr(plt.Normalize(vmin=-h.max(), vmax=h.max())(h)))
h1x = scipy.ndimage.gaussian_filter(x, sigma=[sig, sig], order=[0, 3], truncate=(N//2)/sig) - 3*scipy.ndimage.gaussian_filter(x, sigma=[sig, sig], order=[2, 1], truncate=(N//2)/sig)
plt.imsave('ddx.png', plt.cm.bwr(plt.Normalize(vmin=-ddx.max(), vmax=ddx.max())(ddx)))
plt.imsave('h1x.png', plt.cm.bwr(plt.Normalize(vmin=-h1x.max(), vmax=h1x.max())(h1x)))
plt.imsave('gaussiankey.png', plt.cm.bwr(np.repeat([(np.arange(161)/160)], 16, 0)))

ここに画像の説明を入力してくださいここに画像の説明を入力してくださいここに画像の説明を入力してくださいここに画像の説明を入力してくださいここに画像の説明を入力してください
図4.カラーマップされた1:1スケールプロットの次の順序:2次元ガウス関数。 バツ、微分演算子 ddバツddバツddy2 ガウス関数に適用される、最適な2コンポーネントガウス派生垂直エッジ検出フィルター hバツバツyσ式の 8.各ガウスの標準偏差はσ=8 最後のプロットの標準偏差を持つ六角形のコンポーネントを除いて 5×8。カラーキー:青:最小、白:ゼロ、赤:最大。

つづく...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.