Assuming the ordinary data structure for a hexagonal grid, you can probably apply a 2d filter to a hexagonal-pixel image, but you need to create a filter that is evaluated at the appropriate coordinates. You see, a 2d filter is just a matrix of values generated by evaluating a function over xy coordinates in a grid. So, a 5x5 Gabor filter matrix is just a Gabor function evaluated at the xy coordinates shown here:
Pixels are "equally spaced" so we can simply pick the distance between each point in the grid to be 1 in x and 1 in y, and we get the Gabor function evaluated at the center of each pixel.
However, hexagonal pixels are not arranged this way. The centers of hexagonal pixels are arranged as thus:
Thus, in order to apply a filter to this, we need to evaluate the appropriate function at these points. Since the stock filters have been evaluated on a rectangular grid, we cannot use them (although they will produce something that probably looks reasonable).
Fortunately, the transformation is relatively easy. If we assume the vertical distance between two rows is 1, then the coordinates are almost just an np.arange
.
import numpy as np
import matplotlib.pyplot as plt
ALTERNATE_ROW_SHIFT = 0+np.sqrt(3)/3 # every other row is "offset" by half a hexagon. If the sides are len 2/3, the shift is root 3 over 3
def hex_grid(rect_grid):
rect_grid = np.copy(rect_grid)
rect_grid[0,:,1::2] += ALTERNATE_ROW_SHIFT
return rect_grid
If you have access to the function that creates your filter, there will usually be some logic that creates a rectangular grid on which the function is subsequently evaluated. Drop the hex_grid
function in on the line after to get hexagonally-spaced coordinates instead.
For example, the wikipedia page on Gabor filters has a python implementation to create a Gabor filter, shown here:
def gabor_fn(sigma, theta, Lambda, psi, gamma):
sigma_x = sigma
sigma_y = float(sigma) / gamma
# Bounding box
nstds = 3 # Number of standard deviation sigma
xmax = max(abs(nstds * sigma_x * np.cos(theta)), abs(nstds * sigma_y * np.sin(theta)))
xmax = np.ceil(max(1, xmax))
ymax = max(abs(nstds * sigma_x * np.sin(theta)), abs(nstds * sigma_y * np.cos(theta)))
ymax = np.ceil(max(1, ymax))
xmin = -xmax
ymin = -ymax
(y,x) = np.meshgrid(np.arange(ymin, ymax + 1), np.arange(xmin, xmax + 1))
# Rotation
x_theta = x * np.cos(theta) + y * np.sin(theta)
y_theta = -x * np.sin(theta) + y * np.cos(theta)
gb = np.exp(-.5 * (x_theta ** 2 / sigma_x ** 2 + y_theta ** 2 / sigma_y ** 2)) * np.cos(2 * np.pi / Lambda * x_theta + psi)
return gb
Note the line involving a np.meshgrid
. This creates a rectagular grid with spacing 1 that is used on subsequent lines. We can simply transform those coordinates to create a new hex_gabor
function (Note that this is 95% identical to the gabor_fn
code):
def hex_gabor_fn(sigma, theta, Lambda, psi, gamma):
sigma_x = sigma
sigma_y = float(sigma) / gamma
# Bounding box
nstds = 3 # Number of standard deviation sigma
xmax = max(abs(nstds * sigma_x * np.cos(theta)), abs(nstds * sigma_y * np.sin(theta)))
xmax = np.ceil(max(1, xmax))
ymax = max(abs(nstds * sigma_x * np.sin(theta)), abs(nstds * sigma_y * np.cos(theta)))
ymax = np.ceil(max(1, ymax))
xmin = -xmax
ymin = -ymax
yx = np.meshgrid(np.arange(ymin, ymax + 1), np.arange(xmin, xmax + 1))
(y,x) = hex_grid(yx)
# Rotation
x_theta = x * np.cos(theta) + y * np.sin(theta)
y_theta = -x * np.sin(theta) + y * np.cos(theta)
gb = np.exp(-.5 * (x_theta ** 2 / sigma_x ** 2 + y_theta ** 2 / sigma_y ** 2)) * np.cos(2 * np.pi / Lambda * x_theta + psi)
return gb
if __name__ == "__main__":
g = gabor_fn(4,np.pi/4,4,0,2)
hg = hex_gabor_fn(4,np.pi/4,4,0,2)
plt.imshow(g)
plt.show()
plt.imshow(hg)
plt.show()
You should be able to drop the resulting kernel into this line cv2.filter2D(img, cv2.CV_8UC3, g_kernel)
.