Si entiendo su método 1 correctamente, con él, si utilizó una región simétrica circular e hizo la rotación sobre el centro de la región, eliminaría la dependencia de la región en el ángulo de rotación y obtendría una comparación más justa por la función de mérito entre diferentes ángulos de rotación Sugeriré un método que es esencialmente equivalente a eso, pero usa la imagen completa y no requiere rotación repetida de la imagen, e incluirá un filtro de paso bajo para eliminar la anisotropía de la cuadrícula de píxeles y para eliminar el ruido.
Gradiente de imagen filtrada de paso bajo isotrópico
Primero, calculemos un vector de gradiente local en cada píxel para el canal de color verde en la imagen de muestra de tamaño completo.
Derivé núcleos de diferenciación horizontal y vertical al diferenciar la respuesta al impulso de espacio continuo de un filtro de paso bajo ideal con una respuesta de frecuencia circular plana que elimina el efecto de la elección de los ejes de imagen al garantizar que no haya un nivel de detalle diferente en comparación diagonal horizontal o verticalmente, muestreando la función resultante y aplicando una ventana de coseno girado:
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)
dónde J2 es una función de Bessel de segundo orden del primer tipo, y ωces la frecuencia de corte en radianes. Fuente de Python (no tiene los signos menos de la ecuación 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()
Figura 1. Ventana coseno rotada en 2-d.
Figura 2. Núcleos de diferenciación isotrópicos de paso bajo horizontales con ventana, para diferentes frecuencias de corte ωcajustes Top: omega_c = np.pi
, medio: omega_c = np.pi/4
, abajo: omega_c = np.pi/16
. El signo menos de la ecuación. 1 quedó fuera. Los granos verticales se ven iguales pero se han girado 90 grados. Una suma ponderada de los granos horizontales y verticales, con pesoscos(ϕ) y sin(ϕ), respectivamente, proporciona un núcleo de análisis del mismo tipo para el ángulo de gradiente ϕ.
La diferenciación de la respuesta al impulso no afecta el ancho de banda, como se puede ver por su transformada rápida de Fourier (FFT) de 2 días, 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()
Figura 3. Magnitud de la 2-d FFT de hx. En el dominio de la frecuencia, la diferenciación aparece como una multiplicación de la banda de paso circular plana porωx, y por un cambio de fase de 90 grados que no es visible en la magnitud.
Para hacer la convolución para el canal verde y recolectar un histograma de vector de gradiente 2D, para inspección visual, 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)))
Esto también recorta los datos, descartando (N - 1)//2
píxeles de cada borde que estaban contaminados por el límite rectangular de la imagen, antes del análisis del histograma.
π
π2
π4
π8
π16
π32
π64
-0
Figura 4. Histogramas bidimensionales de vectores de gradiente, para diferentes frecuencias de corte de filtro de paso bajo ωcajustes En orden: primero con N=41
: omega_c = np.pi
, omega_c = np.pi/2
, omega_c = np.pi/4
(igual que en el Python de la lista), omega_c = np.pi/8
, omega_c = np.pi/16
, entonces: N=81
: omega_c = np.pi/32
, N=161
: omega_c = np.pi/64
. La eliminación del ruido mediante el filtrado de paso bajo agudiza las orientaciones de gradiente de borde de traza del circuito en el histograma.
Dirección media circular ponderada de la longitud del vector
Existe el método de Yamartino para encontrar la dirección "promedio" del viento a partir de múltiples muestras de vectores de viento en un solo paso a través de las muestras. Se basa en la media de cantidades circulares , que se calcula como el desplazamiento de un coseno que es una suma de cosenos cada uno desplazado por una cantidad circular de período2π. Podemos usar una versión ponderada de longitud de vector del mismo método, pero primero necesitamos agrupar todas las direcciones que son iguales móduloπ/2. Podemos hacer esto multiplicando el ángulo de cada vector gradiente[Xk,Yk] por 4, usando una representación de números complejos:
Zk=(Xk+Yki)4X2k+Y2k−−−−−−−√3=X4k−6X2kY2k+Y4k+(4X3kYk−4XkY3k)iX2k+Y2k−−−−−−−√3,(2)
satisfactorio |Zk|=X2k+Y2k−−−−−−−√ y luego interpretando que las fases de Zk desde −π a π representar ángulos desde - π/ 4 a π/ 4, dividiendo la fase media circular calculada por 4:
ϕ =14 4atan2(∑kSoy(Zk) ,∑kRe(Zk) )(3)
dónde ϕ es la orientación estimada de la imagen.
La calidad de la estimación se puede evaluar haciendo otro pase a través de los datos y calculando la distancia circular cuadrada media ponderada ,MSCD, entre fases de los números complejos Zk y la fase media circular estimada 4 ϕ, con El |ZkEl | como el peso:
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)
que fue minimizado por ϕcalculado por la ecuación 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)")
Basado en mis mpmath
experimentos (no mostrados), creo que no nos quedaremos sin precisión numérica incluso para imágenes muy grandes. Para diferentes configuraciones de filtro (anotado) las salidas son, como se informa entre -45 y 45 grados:
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)
El filtrado de paso bajo fuerte parece útil, ya que reduce el ángulo equivalente de la distancia circular cuadrática media (RMSCD) calculada como acos( 1 - MSCD ). Sin la ventana de coseno rotado en 2-d, algunos de los resultados estarían apagados en un grado más o menos (no se muestra), lo que significa que es importante hacer una correcta ventana de los filtros de análisis. El ángulo equivalente de RMSCD no es directamente una estimación del error en la estimación del ángulo, que debería ser mucho menor.
Función alternativa de peso de longitud cuadrada
Probemos al cuadrado de la longitud del vector como una función de peso alternativa, por:
Zk=(Xk+Ykyo)4 4X2k+Y2k-------√2=X4 4k- 6X2kY2k+Y4 4k+ ( 4X3kYk- 4XkY3k) iX2k+Y2k,(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)")
El peso de la longitud cuadrada reduce el ángulo equivalente RMSCD en aproximadamente un grado:
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)
Esto parece una función de peso ligeramente mejor. También agregué cortesωC= π/ 32 y ωC= π/ 64. Utilizan mayor, lo que N
da como resultado un recorte diferente de la imagen y valores MSCD no estrictamente comparables.
Histograma 1-d
El beneficio de la función de peso de longitud cuadrada es más evidente con un histograma ponderado 1-d de Zketapas. Script de 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()
Figura 5. Histograma ponderado interpolado linealmente de ángulos vectoriales de gradiente, envuelto en - π/ 4...π/ 4y ponderado por (en orden de abajo hacia arriba en el pico): sin ponderación (negro), longitud del vector gradiente (rojo), cuadrado de la longitud del vector gradiente (azul). El ancho del contenedor es de 0.1 grados. El corte del filtro fue el omega_c = np.pi/4
mismo que en la lista de Python. La figura inferior se amplía en los picos.
Matemáticas de filtro orientable
Hemos visto que el enfoque funciona, pero sería bueno tener una mejor comprensión matemática. losX y yrespuestas de impulso de filtro de diferenciación proporcionadas por la ecuación. 1 puede entenderse como las funciones básicas para formar la respuesta al impulso de un filtro de diferenciación orientable que se muestrea a partir de una rotación del lado derecho de la ecuación parahX[ x , y](Ec. 1). Esto se ve más fácilmente al convertir la ecuación. 1 a coordenadas polares:
hX( r , θ ) =hX[ r cos( θ ) , r sin( θ ) ]hy( r , θ ) =hy[ r cos( θ ) , r sin( θ ) ]F( r )=⎧⎩⎨0 0-ω2Cr cos( θ )J2(ωCr )2 πr2si r = 0 ,de otra manera= cos( θ ) f( R ) ,=⎧⎩⎨0 0-ω2Cr pecado( θ )J2(ωCr )2 πr2si r = 0 ,de otra manera= pecado( θ ) f( R ) ,=⎧⎩⎨0 0-ω2CrJ2(ωCr )2 πr2si r = 0 ,de otra manera,(6)
donde las respuestas de impulso del filtro de diferenciación horizontal y vertical tienen la misma función de factor radial F( r ). Cualquier versión rotadah ( r , θ , ϕ ) de hX( r , θ ) por ángulo de dirección ϕ se obtiene por:
h ( r , θ , ϕ ) =hX( r , θ - ϕ ) = cos( θ - ϕ ) f( r )(7)
La idea era que el núcleo dirigido h ( r , θ , ϕ ) se puede construir como una suma ponderada de hX( r , θ ) y hX( r , θ ), con cos( ϕ ) y pecado( ϕ ) como los pesos, y ese es el caso:
cos( ϕ )hX( r , θ ) + pecado( ϕ )hy( r , θ ) = cos( ϕ ) cos( θ ) f( r ) + pecado( ϕ ) pecado( θ ) f( r ) = cos( θ - ϕ ) f(r)=h(r,θ,ϕ).(8)
Llegaremos a una conclusión equivalente si pensamos en la señal filtrada de paso bajo isotrópico como la señal de entrada y construimos un operador derivado parcial con respecto a la primera de las coordenadas rotadas xϕ, yϕ girado por ángulo ϕ de coordenadas x, y. (La derivación puede considerarse un sistema invariante de tiempo lineal). Tenemos:
x=cos(ϕ)xϕ−sin(ϕ)yϕ,y=sin(ϕ)xϕ+cos(ϕ)yϕ(9)
Usando la regla de la cadena para derivadas parciales, el operador de derivada parcial con respecto axϕ puede expresarse como una suma ponderada de coseno y seno de derivadas parciales con respecto a x y 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.