import numpy as np from numpy import sqrt, pi, arctan2, cos, sin, exp from scipy.ndimage import gaussian_filter from .. import img_as_float, draw from ..color import gray2rgb from .._shared.utils import check_nD def daisy(image, 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 ---------- image : (M, N) array Input image (grayscale). 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/software/daisy ''' check_nD(image, 2, 'img') image = img_as_float(image) # 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(image.shape) dy = np.zeros(image.shape) dx[:, :-1] = np.diff(image, n=1, axis=1) dy[:-1, :] = np.diff(image, 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,) + image.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, image.shape[0] - 2 * radius, image.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 = gray2rgb(image) 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 rows, cols, val = draw.circle_perimeter_aa(desc_y, desc_x, int(sigmas[0])) draw.set_color(descs_img, (rows, cols), color, alpha=val) 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) rows, cols, val = draw.line_aa(desc_y, desc_x, int(desc_y + dy), int(desc_x + dx)) draw.set_color(descs_img, (rows, cols), color, alpha=val) 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))) rows, cols, val = draw.circle_perimeter_aa(hist_y, hist_x, int(sigmas[r_num + 1])) draw.set_color(descs_img, (rows, cols), color, alpha=val) 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) rows, cols, val = draw.line_aa(hist_y, hist_x, int(hist_y + dy), int(hist_x + dx)) draw.set_color(descs_img, (rows, cols), color, alpha=val) return descs, descs_img else: return descs