Si je comprends bien votre méthode 1, si vous utilisiez une région circulairement symétrique et effectuiez la rotation autour du centre de la région, vous élimineriez la dépendance de la région à l'angle de rotation et obtiendriez une comparaison plus juste par la fonction de mérite entre angles de rotation différents. Je proposerai une méthode qui est essentiellement équivalente à cela, mais utilise l'image complète et ne nécessite pas de rotation d'image répétée, et inclura un filtrage passe-bas pour supprimer l'anisotropie de la grille de pixels et pour le débruitage.
Gradient de l'image filtrée passe-bas isotrope
Tout d'abord, calculons un vecteur de gradient local à chaque pixel pour le canal de couleur verte dans l'image échantillon en taille réelle.
J'ai dérivé des noyaux de différenciation horizontale et verticale en différenciant la réponse impulsionnelle dans l'espace continu d'un filtre passe-bas idéal avec une réponse en fréquence circulaire plate qui supprime l'effet du choix des axes d'image en s'assurant qu'il n'y a pas de niveau de détail différent en diagonale par rapport à horizontalement ou verticalement, en échantillonnant la fonction résultante, et en appliquant une fenêtre de cosinus tourné:
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)
où J2 est une fonction de Bessel du premier ordre du premier type, et ωcest la fréquence de coupure en radians. Source Python (n'a pas les signes moins de l'équation 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()
Figure 1. Fenêtre cosinus pivotée en 2D.
Figure 2. Noyaux de différenciation isotropes passe-bas horizontaux fenêtrés, pour différentes fréquences de coupure ωcréglages. Top: omega_c = np.pi
, milieu: omega_c = np.pi/4
, en bas: omega_c = np.pi/16
. Le signe moins de l'équation. 1 a été omis. Les noyaux verticaux se ressemblent mais ont été tournés de 90 degrés. Une somme pondérée des noyaux horizontaux et verticaux, avec des poidscos(ϕ) et sin(ϕ), respectivement, donne un noyau d'analyse du même type pour l'angle de gradient ϕ.
La différenciation de la réponse impulsionnelle n'affecte pas la bande passante, comme le montre sa transformée de Fourier rapide 2-d (FFT), en Python:
# 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()
Figure 3. Amplitude de la FFT 2D de hx. Dans le domaine fréquentiel, la différenciation apparaît comme une multiplication de la bande passante circulaire plate parωx, et par un déphasage de 90 degrés qui n'est pas visible dans l'amplitude.
Pour effectuer la convolution du canal vert et collecter un histogramme vectoriel à gradient 2D, pour inspection visuelle, en Python:
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)))
Cela recadre également les données, en supprimant les (N - 1)//2
pixels de chaque bord qui ont été contaminés par la limite rectangulaire de l'image, avant l'analyse de l'histogramme.
π
π2
π4
π8
π16
π32
π64
-0
Figure 4. Histogrammes 2D de vecteurs de gradient, pour différentes fréquences de coupure du filtre passe-bas ωcréglages. Pour: d' abord avec N=41
: omega_c = np.pi
, omega_c = np.pi/2
, omega_c = np.pi/4
(comme dans la liste Python), omega_c = np.pi/8
, omega_c = np.pi/16
puis: N=81
: omega_c = np.pi/32
, N=161
: omega_c = np.pi/64
. Le débruitage par filtrage passe-bas accentue les orientations de gradient de bord de trace de circuit dans l'histogramme.
Sens moyen circulaire circulaire pondéré en fonction de la longueur du vecteur
Il existe la méthode Yamartino pour trouver la direction "moyenne" du vent à partir de plusieurs échantillons de vecteurs de vent en un seul passage à travers les échantillons. Il est basé sur la moyenne des quantités circulaires , qui est calculée comme le décalage d'un cosinus qui est une somme de cosinus décalés chacun d'une quantité circulaire de période2π. Nous pouvons utiliser une version pondérée par la longueur vectorielle de la même méthode, mais nous devons d'abord regrouper toutes les directions qui sont égales moduloπ/2. Nous pouvons le faire en multipliant l'angle de chaque vecteur de gradient[Xk,Yk] par 4, en utilisant une représentation numérique complexe:
Zk=(Xk+Yki)4X2k+Y2k−−−−−−−√3=X4k−6X2kY2k+Y4k+(4X3kYk−4XkY3k)iX2k+Y2k−−−−−−−√3,(2)
satisfaisant |Zk|=X2k+Y2k−−−−−−−√ et en interprétant plus tard que les phases de Zk de −π à π représenter les angles de −π/4 à π/4, en divisant la phase moyenne circulaire calculée par 4:
ϕ=14atan2(∑kIm(Zk),∑kRe(Zk))(3)
où ϕ est l'orientation estimée de l'image.
La qualité de l'estimation peut être évaluée en effectuant un autre passage dans les données et en calculant la distance circulaire carrée moyenne pondérée ,MSCD, entre les phases des nombres complexes Zk et la phase moyenne circulaire estimée 4ϕ, avec |Zk| comme le poids:
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)
qui a été minimisé par ϕcalculé par Eq. 3. En 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)")
Sur la base de mes mpmath
expériences (non montrées), je pense que nous ne manquerons pas de précision numérique même pour de très grandes images. Pour différents paramètres de filtre (annotés), les sorties sont, comme indiqué entre -45 et 45 degrés:
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)
Un filtrage passe-bas puissant semble utile, réduisant l'angle équivalent à la distance circulaire quadratique moyenne (RMSCD) calculé comme acos( 1 - MSCD ). Sans la fenêtre de cosinus tourné en 2D, certains résultats seraient faussés d'un degré ou deux (non représentés), ce qui signifie qu'il est important de faire un fenêtrage approprié des filtres d'analyse. L'angle équivalent RMSCD n'est pas directement une estimation de l'erreur dans l'estimation de l'angle, qui devrait être beaucoup moins.
Fonction alternative de poids carré
Essayons le carré de la longueur du vecteur comme fonction de pondération alternative, en:
Zk=(Xk+Ouikje)4X2k+Oui2k-------√2=X4k- 6X2kOui2k+Oui4k+ ( 4X3kOuik- 4XkOui3k) jeX2k+Oui2k,(5)
En 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)")
Le poids de longueur carrée réduit l'angle équivalent RMSCD d'environ un degré:
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)
Cela semble une fonction de poids légèrement meilleure. J'ai aussi ajouté des coupuresωc= π/ 32 et ωc= π/ 64. Ils utilisent une plus grande N
résultant en un recadrage différent de l'image et des valeurs MSCD pas strictement comparables.
Histogramme 1-d
L'avantage de la fonction de poids carré est plus apparent avec un histogramme pondéré 1 d de Zkphases. Script 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()
Figure 5. Histogramme pondéré interpolé linéairement des angles de vecteur de gradient, enveloppé - π/ 4…π/ 4et pondéré par (dans l'ordre du bas vers le haut au sommet): pas de pondération (noir), longueur du vecteur de gradient (rouge), carré de la longueur du vecteur de gradient (bleu). La largeur du bac est de 0,1 degré. La coupure du filtre était la omega_c = np.pi/4
même que dans la liste Python. La figure du bas est zoomée sur les pics.
Filtre mathématique orientable
Nous avons vu que l'approche fonctionne, mais il serait bon d'avoir une meilleure compréhension mathématique. leX et yréponses impulsionnelles du filtre de différenciation données par l'équation. 1 peut être compris comme les fonctions de base pour former la réponse impulsionnelle d'un filtre de différenciation orientable qui est échantillonné à partir d'une rotation du côté droit de l'équation pourhX[ x , y](Éq.1). Cela se voit plus facilement en convertissant l'égaliseur. 1 aux coordonnées polaires:
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.