Merge pull request #384 from andersbll/master

Dense DAISY feature description
This commit is contained in:
Johannes Schönberger
2012-12-30 00:40:09 -08:00
9 changed files with 449 additions and 4 deletions
+3
View File
@@ -126,3 +126,6 @@
- Steven Silvester, Karel Zuiderveld
Adaptive Histogram Equalization
- Anders Boesen Lindbo Larsen
Dense DAISY feature description, circle perimeter drawing.
+28
View File
@@ -0,0 +1,28 @@
"""
===============================
Dense DAISY feature description
===============================
The DAISY local image descriptor is based on gradient orientation histograms
similar to the SIFT descriptor. It is formulated in a way that allows for fast
dense extraction which is useful for e.g. bag-of-features image
representations.
In this example a limited number of DAISY descriptors are extracted at a large
scale for illustrative purposes.
"""
from skimage.feature import daisy
from skimage import data
import matplotlib.pyplot as plt
img = data.camera()
descs, descs_img = daisy(img, step=180, radius=58, rings=2, histograms=6,
orientations=8, visualize=True)
plt.axis('off')
plt.imshow(descs_img)
descs_num = descs.shape[0] * descs.shape[1]
plt.title('%i DAISY descriptors extracted:' % descs_num)
plt.show()
+6 -2
View File
@@ -13,7 +13,7 @@ This example shows how to fill several different shapes:
import matplotlib.pyplot as plt
from skimage.draw import line, polygon, circle, ellipse
from skimage.draw import line, polygon, circle, circle_perimeter, ellipse
import numpy as np
@@ -42,5 +42,9 @@ img[rr,cc,:] = (255, 255, 0)
rr, cc = ellipse(300, 300, 100, 200, img.shape)
img[rr,cc,2] = 255
# circle
rr, cc = circle_perimeter(120, 400, 50)
img[rr, cc, :] = (255, 0, 255)
plt.imshow(img)
plt.show()
plt.show()
+1 -1
View File
@@ -1,2 +1,2 @@
from ._draw import line, polygon, ellipse, circle
from ._draw import line, polygon, ellipse, circle, circle_perimeter, set_color
bresenham = line
+66
View File
@@ -187,3 +187,69 @@ def circle(double cy, double cx, double radius, shape=None):
``img[rr, cc] = 1``.
"""
return ellipse(cy, cx, radius, radius, shape)
def circle_perimeter(int cy, int cx, int radius):
"""Generate circle perimeter coordinates.
Parameters
----------
cy, cx : int
Centre coordinate of circle.
radius: int
Radius of circle.
Returns
-------
rr, cc : (N,) ndarray of int
Indices of pixels that belong to the circle perimeter.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
"""
cdef list rr = list()
cdef list cc = list()
cdef int x = 0
cdef int y = radius
cdef int d = 3 - 2 * radius
while y >= x:
rr.extend([y, -y, y, -y, x, -x, x, -x])
cc.extend([x, x, -x, -x, y, y, -y, -y])
if d < 0:
d += 4 * x + 6
else:
d += 4 * (x - y) + 10
y -= 1
x += 1
return np.array(rr) + cy, np.array(cc) + cx
@cython.boundscheck(False)
@cython.wraparound(False)
def set_color(img, coords, color):
"""Set pixel color in the image at the given coordiantes. Coordinates that
exceeed the shape of the image will be ignored.
Parameters
----------
img : (M, N, D) ndarray
Image
coords : ((P,) ndarray, (P,) ndarray)
Coordinates of pixels to be colored.
color : (D,) ndarray
Color to be assigned to coordinates in the image.
Returns
-------
img : (M, N, D) ndarray
The updated image.
"""
rr, cc = coords
rr_inside = np.logical_and(rr >= 0, rr < img.shape[0])
cc_inside = np.logical_and(cc >= 0, cc < img.shape[1])
inside = np.logical_and(rr_inside, cc_inside)
img[rr[inside], cc[inside]] = color
+32 -1
View File
@@ -1,7 +1,7 @@
from numpy.testing import assert_array_equal
import numpy as np
from skimage.draw import line, polygon, circle, ellipse
from skimage.draw import line, polygon, circle, circle_perimeter, ellipse
def test_line_horizontal():
@@ -150,6 +150,37 @@ def test_circle():
assert_array_equal(img, img_)
def test_circle_perimeter():
img = np.zeros((15, 15), 'uint8')
rr, cc = circle_perimeter(7, 7, 0)
img[rr, cc] = 1
assert(np.sum(img) == 1)
img = np.zeros((17, 15), 'uint8')
rr, cc = circle_perimeter(7, 7, 7)
img[rr, cc] = 1
img_ = np.array(
[[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
)
assert_array_equal(img, img_)
def test_ellipse():
img = np.zeros((15, 15), 'uint8')
+1
View File
@@ -1,3 +1,4 @@
from ._daisy import daisy
from ._hog import hog
from .texture import greycomatrix, greycoprops, local_binary_pattern
from .peak import peak_local_max
+217
View File
@@ -0,0 +1,217 @@
import numpy as np
from scipy import sqrt, pi, arctan2, cos, sin, exp
from scipy.ndimage import gaussian_filter
import skimage.color
from skimage import img_as_float, draw
def daisy(img, step=4, radius=15, rings=3, histograms=8, orientations=8,
normalization='l1', sigmas=None, ring_radii=None, visualize=False):
'''Extract DAISY feature descriptors densely for the given image.
DAISY is a feature descriptor similar to SIFT formulated in a way that
allows for fast dense extraction. Typically, this is practical for
bag-of-features image representations.
The implementation follows Tola et al. [1]_ but deviate on the following
points:
* Histogram bin contribution are smoothed with a circular Gaussian
window over the tonal range (the angular range).
* The sigma values of the spatial Gaussian smoothing in this code do not
match the sigma values in the original code by Tola et al. [2]_. In
their code, spatial smoothing is applied to both the input image and
the center histogram. However, this smoothing is not documented in [1]_
and, therefore, it is omitted.
Parameters
----------
img : (M, N) array
Input image (greyscale).
step : int, optional
Distance between descriptor sampling points.
radius : int, optional
Radius (in pixels) of the outermost ring.
rings : int, optional
Number of rings.
histograms : int, optional
Number of histograms sampled per ring.
orientations : int, optional
Number of orientations (bins) per histogram.
normalization : [ 'l1' | 'l2' | 'daisy' | 'off' ], optional
How to normalize the descriptors
* 'l1': L1-normalization of each descriptor.
* 'l2': L2-normalization of each descriptor.
* 'daisy': L2-normalization of individual histograms.
* 'off': Disable normalization.
sigmas : 1D array of float, optional
Standard deviation of spatial Gaussian smoothing for the center
histogram and for each ring of histograms. The array of sigmas should
be sorted from the center and out. I.e. the first sigma value defines
the spatial smoothing of the center histogram and the last sigma value
defines the spatial smoothing of the outermost ring. Specifying sigmas
overrides the following parameter.
``rings = len(sigmas)-1``
ring_radii : 1D array of int, optional
Radius (in pixels) for each ring. Specifying ring_radii overrides the
following two parameters.
| ``rings = len(ring_radii)``
| ``radius = ring_radii[-1]``
If both sigmas and ring_radii are given, they must satisfy the
following predicate since no radius is needed for the center
histogram.
``len(ring_radii) == len(sigmas)+1``
visualize : bool, optional
Generate a visualization of the DAISY descriptors
Returns
-------
descs : array
Grid of DAISY descriptors for the given image as an array
dimensionality (P, Q, R) where
| ``P = ceil((M-radius*2)/step)``
| ``Q = ceil((N-radius*2)/step)``
| ``R = (rings*histograms + 1)*orientations``
descs_img : (M, N, 3) array (only if visualize==True)
Visualization of the DAISY descriptors.
References
----------
.. [1] Tola et al. "Daisy: An efficient dense descriptor applied to wide-
baseline stereo." Pattern Analysis and Machine Intelligence, IEEE
Transactions on 32.5 (2010): 815-830.
.. [2] http://cvlab.epfl.ch/alumni/tola/daisy.html
'''
# Validate image format.
if img.ndim > 2:
raise ValueError('Only grey-level images are supported.')
if img.dtype.kind != 'f':
img = img_as_float(img)
# Validate parameters.
if sigmas is not None and ring_radii is not None \
and len(sigmas) - 1 != len(ring_radii):
raise ValueError('len(sigmas)-1 != len(ring_radii)')
if ring_radii is not None:
rings = len(ring_radii)
radius = ring_radii[-1]
if sigmas is not None:
rings = len(sigmas) - 1
if sigmas is None:
sigmas = [radius * (i + 1) / float(2 * rings) for i in range(rings)]
if ring_radii is None:
ring_radii = [radius * (i + 1) / float(rings) for i in range(rings)]
if normalization not in ['l1', 'l2', 'daisy', 'off']:
raise ValueError('Invalid normalization method.')
# Compute image derivatives.
dx = np.zeros(img.shape)
dy = np.zeros(img.shape)
dx[:, :-1] = np.diff(img, n=1, axis=1)
dy[:-1, :] = np.diff(img, n=1, axis=0)
# Compute gradient orientation and magnitude and their contribution
# to the histograms.
grad_mag = sqrt(dx ** 2 + dy ** 2)
grad_ori = arctan2(dy, dx)
orientation_kappa = orientations / pi
orientation_angles = [2 * o * pi / orientations - pi
for o in range(orientations)]
hist = np.empty((orientations,) + img.shape, dtype=float)
for i, o in enumerate(orientation_angles):
# Weigh bin contribution by the circular normal distribution
hist[i, :, :] = exp(orientation_kappa * cos(grad_ori - o))
# Weigh bin contribution by the gradient magnitude
hist[i, :, :] = np.multiply(hist[i, :, :], grad_mag)
# Smooth orientation histograms for the center and all rings.
sigmas = [sigmas[0]] + sigmas
hist_smooth = np.empty((rings + 1,) + hist.shape, dtype=float)
for i in range(rings + 1):
for j in range(orientations):
hist_smooth[i, j, :, :] = gaussian_filter(hist[j, :, :],
sigma=sigmas[i])
# Assemble descriptor grid.
theta = [2 * pi * j / histograms for j in range(histograms)]
desc_dims = (rings * histograms + 1) * orientations
descs = np.empty((desc_dims, img.shape[0] - 2 * radius,
img.shape[1] - 2 * radius))
descs[:orientations, :, :] = hist_smooth[0, :, radius:-radius,
radius:-radius]
idx = orientations
for i in range(rings):
for j in range(histograms):
y_min = radius + int(round(ring_radii[i] * sin(theta[j])))
y_max = descs.shape[1] + y_min
x_min = radius + int(round(ring_radii[i] * cos(theta[j])))
x_max = descs.shape[2] + x_min
descs[idx:idx + orientations, :, :] = hist_smooth[i + 1, :,
y_min:y_max,
x_min:x_max]
idx += orientations
descs = descs[:, ::step, ::step]
descs = descs.swapaxes(0, 1).swapaxes(1, 2)
# Normalize descriptors.
if normalization != 'off':
descs += 1e-10
if normalization == 'l1':
descs /= np.sum(descs, axis=2)[:, :, np.newaxis]
elif normalization == 'l2':
descs /= sqrt(np.sum(descs ** 2, axis=2))[:, :, np.newaxis]
elif normalization == 'daisy':
for i in range(0, desc_dims, orientations):
norms = sqrt(np.sum(descs[:, :, i:i + orientations] ** 2,
axis=2))
descs[:, :, i:i + orientations] /= norms[:, :, np.newaxis]
if visualize:
descs_img = skimage.color.gray2rgb(img)
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
# Draw center histogram sigma
color = (1, 0, 0)
desc_y = i * step + radius
desc_x = j * step + radius
coords = draw.circle_perimeter(desc_y, desc_x, sigmas[0])
draw.set_color(descs_img, coords, color)
max_bin = np.max(descs[i, j, :])
for o_num, o in enumerate(orientation_angles):
# Draw center histogram bins
bin_size = descs[i, j, o_num] / max_bin
dy = sigmas[0] * bin_size * sin(o)
dx = sigmas[0] * bin_size * cos(o)
coords = draw.line(desc_y, desc_x, desc_y + dy,
desc_x + dx)
draw.set_color(descs_img, coords, color)
for r_num, r in enumerate(ring_radii):
color_offset = float(1 + r_num) / rings
color = (1 - color_offset, 1, color_offset)
for t_num, t in enumerate(theta):
# Draw ring histogram sigmas
hist_y = desc_y + int(round(r * sin(t)))
hist_x = desc_x + int(round(r * cos(t)))
coords = draw.circle_perimeter(hist_y, hist_x,
sigmas[r_num + 1])
draw.set_color(descs_img, coords, color)
for o_num, o in enumerate(orientation_angles):
# Draw histogram bins
bin_size = descs[i, j, orientations + r_num *
histograms * orientations +
t_num * orientations + o_num]
bin_size /= max_bin
dy = sigmas[r_num + 1] * bin_size * sin(o)
dx = sigmas[r_num + 1] * bin_size * cos(o)
coords = draw.line(hist_y, hist_x, hist_y + dy,
hist_x + dx)
draw.set_color(descs_img, coords, color)
return descs, descs_img
else:
return descs
+95
View File
@@ -0,0 +1,95 @@
import numpy as np
from numpy.testing import assert_raises, assert_almost_equal
from numpy import sqrt, ceil
from skimage import data
from skimage import img_as_float
from skimage.feature import daisy
def test_daisy_color_image_unsupported_error():
img = np.zeros((20, 20, 3))
assert_raises(ValueError, daisy, img)
def test_daisy_desc_dims():
img = img_as_float(data.lena()[:128, :128].mean(axis=2))
rings = 2
histograms = 4
orientations = 3
descs = daisy(img, rings=rings, histograms=histograms,
orientations=orientations)
assert(descs.shape[2] == (rings * histograms + 1) * orientations)
rings = 4
histograms = 5
orientations = 13
descs = daisy(img, rings=rings, histograms=histograms,
orientations=orientations)
assert(descs.shape[2] == (rings * histograms + 1) * orientations)
def test_descs_shape():
img = img_as_float(data.lena()[:256, :256].mean(axis=2))
radius = 20
step = 8
descs = daisy(img, radius=radius, step=step)
assert(descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step)))
assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step)))
img = img[:-1, :-2]
radius = 5
step = 3
descs = daisy(img, radius=radius, step=step)
assert(descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step)))
assert(descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step)))
def test_daisy_incompatible_sigmas_and_radii():
img = img_as_float(data.lena()[:128, :128].mean(axis=2))
sigmas = [1, 2]
radii = [1, 2]
assert_raises(ValueError, daisy, img, sigmas=sigmas, ring_radii=radii)
def test_daisy_normalization():
img = img_as_float(data.lena()[:64, :64].mean(axis=2))
descs = daisy(img, normalization='l1')
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
assert_almost_equal(np.sum(descs[i, j, :]), 1)
descs_ = daisy(img)
assert_almost_equal(descs, descs_)
descs = daisy(img, normalization='l2')
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
assert_almost_equal(sqrt(np.sum(descs[i, j, :] ** 2)), 1)
orientations = 8
descs = daisy(img, orientations=orientations, normalization='daisy')
desc_dims = descs.shape[2]
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
for k in range(0, desc_dims, orientations):
assert_almost_equal(sqrt(np.sum(
descs[i, j, k:k + orientations] ** 2)), 1)
img = np.zeros((50, 50))
descs = daisy(img, normalization='off')
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
assert_almost_equal(np.sum(descs[i, j, :]), 0)
assert_raises(ValueError, daisy, img, normalization='does_not_exist')
def test_daisy_visualization():
img = img_as_float(data.lena()[:128, :128].mean(axis=2))
descs, descs_img = daisy(img, visualize=True)
assert(descs_img.shape == (128, 128, 3))
if __name__ == '__main__':
from numpy import testing
testing.run_module_suite()