Wenn ich Ihre Methode 1 richtig verstehe, würden Sie damit, wenn Sie einen kreisförmig symmetrischen Bereich verwenden und die Drehung um den Mittelpunkt des Bereichs durchführen, die Abhängigkeit des Bereichs vom Drehwinkel beseitigen und einen faireren Vergleich durch die Leistungsfunktion zwischen erhalten verschiedene Drehwinkel. Ich werde eine Methode vorschlagen, die im Wesentlichen dieser Methode entspricht, jedoch das Vollbild verwendet und keine wiederholte Bildrotation erfordert. Sie umfasst eine Tiefpassfilterung zum Entfernen der Pixelgitteranisotropie und zum Entrauschen.
Gradient des isotrop tiefpassgefilterten Bildes
Berechnen wir zunächst einen lokalen Gradientenvektor an jedem Pixel für den grünen Farbkanal im Beispielbild in voller Größe.
Ich habe horizontale und vertikale Differenzierungskerne abgeleitet, indem ich die Impulsantwort im kontinuierlichen Raum eines idealen Tiefpassfilters mit einem flachen kreisförmigen Frequenzgang differenziert habe, der den Effekt der Wahl der Bildachsen beseitigt, indem sichergestellt wird, dass kein unterschiedlicher Detaillierungsgrad diagonal verglichen wird horizontal oder vertikal durch Abtasten der resultierenden Funktion und Anwenden eines gedrehten Kosinusfensters:
hx[x,y]=⎧⎩⎨⎪⎪0−ω2cxJ2(ωcx2+y2−−−−−−√)2π(x2+y2)if x=y=0,otherwise,hy[x,y]=⎧⎩⎨⎪⎪0−ω2cyJ2(ωcx2+y2−−−−−−√)2π(x2+y2)if x=y=0,otherwise,(1)
wobei J.2 eine Bessel-Funktion 2. Ordnung der ersten Art ist und ωc die Grenzfrequenz im Bogenmaß ist. Python-Quelle (hat nicht die Minuszeichen von Gleichung 1):
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()
Abbildung 1. 2-d gedrehtes Kosinusfenster.
Abbildung 2. Horizontale isotrope Tiefpass-Differenzierungskerne mit Fenster für unterschiedliche Einstellungen der Grenzfrequenz ωc . Oben : omega_c = np.pi
, Mitte : omega_c = np.pi/4
, Unten : omega_c = np.pi/16
. Das Minuszeichen von Gl. Ich wurde weggelassen. Vertikale Kernel sehen gleich aus, wurden jedoch um 90 Grad gedreht. Eine gewichtete Summe des horizontalen und des vertikalen Kerns mit den Gewichten cos(ϕ) bzw. sin(ϕ) ergibt einen Analysekern des gleichen Typs für den Gradientenwinkel ϕ .
Die Differenzierung der Impulsantwort hat keinen Einfluss auf die Bandbreite, wie aus der 2-D-Fast-Fourier-Transformation (FFT) in Python hervorgeht:
# 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()
Abbildung 3. Größe der 2-d-FFT von hx . Im Frequenzbereich erscheint die Differenzierung als Multiplikation des flachen kreisförmigen Durchlassbandes mit ωx und mit einer Phasenverschiebung von 90 Grad, die in der Größe nicht sichtbar ist.
Um die Faltung für den grünen Kanal durchzuführen und ein 2-D-Gradientenvektorhistogramm zur visuellen Überprüfung in Python zu sammeln:
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)))
Dadurch werden auch die Daten zugeschnitten, wobei (N - 1)//2
Pixel von jeder Kante, die durch die rechteckige Bildgrenze verunreinigt waren, vor der Histogrammanalyse verworfen werden.
π
ππ2
ππ4 π
π8
ππ16
ππ32
ππ64
-0
Abbildung 4. 2D-Histogramme von Gradientenvektoren für verschiedeneEinstellungen derTiefpassfilter-Grenzfrequenzωc. Um: zuerst mitN=41
:omega_c = np.pi
,omega_c = np.pi/2
,omega_c = np.pi/4
(gleiche wie in der PythonListing)omega_c = np.pi/8
,omega_c = np.pi/16
dann:N=81
:omega_c = np.pi/32
,N=161
:omega_c = np.pi/64
. Das Entrauschen durch Tiefpassfilterung schärft die Gradientenorientierungen der Schaltungsspurkante im Histogramm.
Vektorlängengewichtete kreisförmige mittlere Richtung
Es gibt die Yamartino-Methode zum Ermitteln der "durchschnittlichen" Windrichtung aus mehreren Windvektorproben in einem Durchgang durch die Proben. Sie basiert auf dem Mittelwert der Kreisgrößen , der als Verschiebung eines Kosinus berechnet wird, der eine Summe von Kosinus ist, die jeweils um eine Kreisgröße der Periode 2π verschoben sind . Wir können eine vektorlängengewichtete Version derselben Methode verwenden, aber zuerst müssen wir alle Richtungen zusammenfassen, die gleich modulo π/2 . Wir können dies tun, indem wir den Winkel jedes Gradientenvektors [Xk,Yk] mit 4 multiplizieren, indem wir eine komplexe Zahlendarstellung verwenden:
Zk=(Xk+Yki)4X2k+Y2k−−−−−−−√3=X4k−6X2kY2k+Y4k+(4X3kYk−4XkY3k)iX2k+Y2k−−−−−−−√3,(2)
Befriedigung |Zk|=X2k+Y2k−−−−−−−√ und durch spätere Interpretation, dass die Phasen vonZkvon−πbisπWinkel von−π/4bisπ/4, durch Teilen der berechneten kreisförmigen mittleren Phase durch 4:
ϕ=14atan2(∑kIm(Zk),∑kRe(Zk))(3)
Dabei ist ϕ die geschätzte Bildorientierung.
Die Qualität der Schätzung kann indem einen zweiten Durchlauf durch die Daten bewertet werden und indem den mittleren gewichtete Quadratberechnungskreisabstand , MSCD , zwischen den Phasen der komplexen Zahlen Zk und den geschätzten Phasenkreismittel 4ϕ , mit |Zk|als das Gewicht:
MSCD=∑k|Zk|(1−cos(4ϕ−atan2(Im(Zk),Re(Zk))))∑k|Zk|=∑k|Zk|2((cos(4ϕ)−Re(Zk)|Zk|)2+(sin(4ϕ)−Im(Zk)|Zk|)2)∑k|Zk|=∑k(|Zk|−Re(Zk)cos(4ϕ)−Im(Zk)sin(4ϕ))∑k|Zk|,(4)
ϕ
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)")
Aufgrund meiner mpmath
Experimente (nicht gezeigt) denke ich, dass uns die numerische Genauigkeit auch bei sehr großen Bildern nicht ausgehen wird. Für verschiedene Filtereinstellungen (mit Anmerkungen versehen) liegen die Ausgänge zwischen -45 und 45 Grad:
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)
acos(1−MSCD)
Alternative Gewichtsfunktion mit quadratischer Länge
Versuchen wir das Quadrat der Vektorlänge als alternative Gewichtsfunktion durch:
Zk=(Xk+Yki)4X2k+Y2k−−−−−−−√2=X4k−6X2kY2k+Y4k+(4X3kYk−4XkY3k)iX2k+Y2k,(5)
In 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)")
Das quadratische Längengewicht reduziert den RMSCD-Äquivalentwinkel um etwa einen Grad:
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= π/ 64N
1-d-Histogramm
Z.k
# 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()
- π/ 4…π/ 4und gewichtet mit (in der Reihenfolge von unten nach oben am Peak): keine Gewichtung (schwarz), Gradientenvektorlänge (rot), Quadrat der Gradientenvektorlänge (blau). Die Behälterbreite beträgt 0,1 Grad. Der Filter-Cutoff war der omega_c = np.pi/4
gleiche wie in der Python-Liste. Die untere Abbildung wird auf die Spitzen gezoomt.
Lenkbare Filtermathematik
Wir haben gesehen, dass der Ansatz funktioniert, aber es wäre gut, ein besseres mathematisches Verständnis zu haben. Dasx und y differentiation filter impulse responses given by Eq. 1 can be understood as the basis functions for forming the impulse response of a steerable differentiation filter that is sampled from a rotation of the right side of the equation for hx[x,y] (Eq. 1). This is more easily seen by converting Eq. 1 to polar coordinates:
hx(r,θ)=hx[rcos(θ),rsin(θ)]hy(r,θ)=hy[rcos(θ),rsin(θ)]f(r)=⎧⎩⎨0−ω2crcos(θ)J2(ωcr)2πr2if r=0,otherwise=cos(θ)f(r),=⎧⎩⎨0−ω2crsin(θ)J2(ωcr)2πr2if r=0,otherwise=sin(θ)f(r),=⎧⎩⎨0−ω2crJ2(ωcr)2πr2if r=0,otherwise,(6)
where both the horizontal and the vertical differentiation filter impulse responses have the same radial factor function f(r). Any rotated version h(r,θ,ϕ) of hx(r,θ) by steering angle ϕ is obtained by:
h(r,θ,ϕ)=hx(r,θ−ϕ)=cos(θ−ϕ)f(r)(7)
The idea was that the steered kernel h(r,θ,ϕ) can be constructed as a weighted sum of hx(r,θ) and hx(r,θ), with cos(ϕ) and sin(ϕ) as the weights, and that is indeed the case:
cos(ϕ)hx(r,θ)+sin(ϕ)hy(r,θ)=cos(ϕ)cos(θ)f(r)+sin(ϕ)sin(θ)f(r)=cos(θ−ϕ)f(r)=h(r,θ,ϕ).(8)
We will arrive at an equivalent conclusion if we think of the isotropically low-pass filtered signal as the input signal and construct a partial derivative operator with respect to the first of rotated coordinates xϕ, yϕ rotated by angle ϕ from coordinates x, y. (Derivation can be considered a linear-time-invariant system.) We have:
x=cos(ϕ)xϕ−sin(ϕ)yϕ,y=sin(ϕ)xϕ+cos(ϕ)yϕ(9)
Using the chain rule for partial derivatives, the partial derivative operator with respect to xϕ can be expressed as a cosine and sine weighted sum of partial derivatives with respect to x and y:
∂∂xϕ=∂x∂xϕ∂∂x+∂y∂xϕ∂∂y=∂(cos(ϕ)xϕ−sin(ϕ)yϕ)∂xϕ∂∂x+∂(sin(ϕ)xϕ+cos(ϕ)yϕ)∂xϕ∂∂y=cos(ϕ)∂∂x+sin(ϕ)∂∂y(10)
A question that remains to be explored is how a suitably weighted circular mean of gradient vector angles is related to the angle ϕ of in some way the "most activated" steered differentiation filter.
Possible improvements
To possibly improve results further, the gradient can be calculated also for the red and blue color channels, to be included as additional data in the "average" calculation.
I have in mind possible extensions of this method:
1) Use a larger set of analysis filter kernels and detect edges rather than detecting gradients. This needs to be carefully crafted so that edges in all directions are treated equally, that is, an edge detector for any angle should be obtainable by a weighted sum of orthogonal kernels. A set of suitable kernels can (I think) be obtained by applying the differential operators of Eq. 11, Fig. 6 (see also my Mathematics Stack Exchange post) on the continuous-space impulse response of a circularly symmetric low-pass filter.
limh→0∑4N+1N=0(−1)nf(x+hcos(2πn4N+2),y+hsin(2πn4N+2))h2N+1,limh→0∑4N+1N=0(−1)nf(x+hsin(2πn4N+2),y+hcos(2πn4N+2))h2N+1(11)
Figure 6. Dirac delta relative locations in differential operators for construction of higher-order edge detectors.
2) The calculation of a (weighted) mean of circular quantities can be understood as summing of cosines of the same frequency shifted by samples of the quantity (and scaled by the weight), and finding the peak of the resulting function. If similarly shifted and scaled harmonics of the shifted cosine, with carefully chosen relative amplitudes, are added to the mix, forming a sharper smoothing kernel, then multiple peaks may appear in the total sum and the peak with the largest value can be reported. With a suitable mixture of harmonics, that would give a kind of local average that largely ignores outliers away from the main peak of the distribution.
Alternative approaches
It would also be possible to convolve the image by angle ϕ and angle ϕ+π/2 rotated "long edge" kernels, and to calculate the mean square of the pixels of the two convolved images. The angle ϕ that maximizes the mean square would be reported. This approach might give a good final refinement for the image orientation finding, because it is risky to search the complete angle ϕ space at large steps.
Another approach is non-local methods, like cross-correlating distant similar regions, applicable if you know that there are long horizontal or vertical traces, or features that repeat many times horizontally or vertically.