1
# -*- coding: utf-8 -*-
7
Wolfgang Förstner, Timo Dickscheid and Falko Schindler
9
Detecting Interpretable and Accurate Scale-Invariant Keypoints, International Conference on Computer Vision (ICCV'09), Kyoto, 200
12
Falko Schindler (falko.schindler@uni-bonn.de)
13
Department of Photogrammetry
14
Institute of Geodesy and Geoinformation
19
Ported to Python from matlab by B.Nouvel(bertrand.nouvel@gmail.com)
26
For research and academic use only.
27
No commercial application.
30
No warranty for validity of this implementation.
34
import numpy, scipy,os
36
from scipy import ndimage
37
import pycvfext.keypoints.lib
40
## MATLAB Equivalents + Utility functions
45
return [ [ (i/p[1])%p[0] for i in ind] for p in zip(shp,reduce( lambda x,y:x+[x[-1]*y] ,shp[:-1],[1],) ) ]
48
return (scipy.ndimage.maximum_filter(I,size=(3,3,3))==I) #*(1- ((scipy.ndimage.minimum_filter(I,size=(3,3,3))==I)))
51
return scipy.stats.gamma.cdf(a/2.0,b/2.0)
54
return 1/(numpy.linalg.norm(A,1)*numpy.linalg.norm(numpy.linalg.inv(A), 1))
57
a=numpy.ones(map(lambda x,y:x+2*y,img.shape,p))*v
58
a[p[0]:-p[0],p[1]:-p[1]]=img
63
return img[p[0]:-p[0],p[1]:-p[1]]
70
strides=strides[n:]+strides[:n]
71
shape=shape[n:]+shape[:n]
72
return numpy.ndarray(buffer=M.copy('C').data,strides=strides,shape=shape,dtype=M.dtype)
76
LUT_time = numpy.load(os.path.join(pycvfext.keypoints.lib.__path__[0],'fftLUT_time.npy'));
82
'G':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer(Hy, Hx)),
83
'Gx':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer(Hy, (Hx*dx))),
84
'Gxx':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer(Hy, (Hx*dxx))),
85
'Gy':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer((Hy*dy), Hx)),
86
'Gyy':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer((Hy*dyy), Hx)),
87
'y2G':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer((Hy*dyy), Hx)*sigma[0]**4+numpy.outer(Hy, Hx)*sigma[1]**2),
88
'x2G':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer(Hy, (Hx*dxx))*sigma[1]**4+numpy.outer(Hy, Hx)*sigma[1]**2),
89
'xyG':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: numpy.outer((Hy*dy), (Hx*dx))*sigma[0]*sigma[1]**2)
94
## These are separated versions for CUDA (later)
98
'G':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: (Hy, Hx)),
99
'Gx':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: (Hy, (Hx*dx))),
100
'Gxx':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: (Hy, (Hx*dxx))),
101
'Gy':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: ((Hy*dy), Hx)),
102
'Gyy':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: ((Hy*dyy), Hx)),
103
'y2G':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: ((Hy*dyy), Hx)*sigma[0]**4+numpy.outer(Hy, Hx)*sigma[1]**2),
104
'x2G':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: (Hy, (Hx*dxx))*sigma[1]**4+numpy.outer(Hy, Hx)*sigma[1]**2),
105
'xyG':(lambda Hy,Hx,dx,dy,dxx,dyy,sigma: ((Hy*dy), (Hx*dx))*sigma[0]*sigma[1]**2)
114
D0=numpy.array([[16., 8., 16.], [8., 4., 8.],[ 16., 8., 16.]])**-1
115
D1=numpy.array([[16., 8., 16.], [numpy.inf, numpy.inf, numpy.inf],[ -16, -8, -16]])**-1
116
D33 = numpy.dstack([D0, -2 * D0, D0])
117
D11 = shiftdim(D33, 2);
118
D22 = shiftdim(D11, 2);
119
D13 = numpy.dstack([ D1, numpy.zeros((3,3)), -D1])
120
D12 = shiftdim(D13, 2);
121
D23 = shiftdim(D12, 2);
122
DA=numpy.array([ D11, D12, D13, D12, D22, D23, D13, D23, D33])
125
GD3 = numpy.dstack([D0, numpy.zeros((3,3)), -D0])
126
GD1 = shiftdim(GD3, 2)
127
GD2 = shiftdim(GD1, 2)
128
DGD=numpy.array([GD1,GD2,GD3])
131
## SPECIFIC IMPLEMENTATION UTILITY FUNC
136
dx=numpy.inner(DA.reshape(9,27) , C.reshape(1,27))
137
H = dx.reshape(( 3, 3))
142
return numpy.inner(DGD.reshape((3,27)) , C.reshape((1,27)));
147
def cubicInterp3(ar,x,y,z):
149
return numpy.array([])
150
#print ar.shape,ar.dtype
151
#print x.shape,x.min(),x.max()
152
#print y.shape,y.min(),y.max()
153
#print z.shape,z.min(),z.max()
154
coordinates=numpy.hstack([x.reshape((-1,1)),y.reshape((-1,1)),z.reshape((-1,1))])
155
#print coordinates.shape
156
#print numpy.isnan(ar).sum()
157
#assert(not numpy.isnan(ar).any())
158
#print coordinates,coordinates.dtype
159
r=ndimage.map_coordinates(ar.astype(numpy.float32),coordinates.T.astype(numpy.float32),mode='nearest')
161
#print numpy.isnan(r).sum()
162
#assert(not numpy.isnan(r).any())
166
def gaussFFT(img, sigma,ffttime, *kernels):
168
% GAUSSFFT convolves an image IMG with Gaussian kernels via FFT.
170
% Kernels with SIGMA smaller than 0.4 pixel will be neglected since the
171
% algorithm gets very slow and thus not suited for small scales.
174
% img image to be convolved
175
% sigma kernel parameter of length 1 or 2
176
% varargin first optional argument can be a look-up table for fft2
177
% computational costs in order to predict optimal image
178
% sizes (vector format),
179
% all other arguments are kernel names, possible names are:
182
% 'Gxx', 'Gyy', 'Gxy',
183
% 'x2G', 'y2G', 'xyG'
186
% varargout convolved images, corresponding to one kernel each
191
% fftResult = gaussFFT(img, 20, 'G');
192
% imshow(fftResult, []);
193
%% use standard convolution
194
% varargout = cell(size(varargin));
195
% for i = 2 : numel(varargin)
196
% varargout{i - 1} = conv2(img, getFilter(sigma, varargin{i}), 'same');
201
sigma = numpy.array([sigma, sigma])
202
sigma = sigma * (sigma >= 0.4); # < neglect small values
205
# pad and transform image
206
# image padding in spatial domain, depending on sigma
207
padding = (4,)*len(sigma)
208
# default size of padding
209
paddingv = map(lambda x,y:round(x*y), sigma , padding)
210
if paddingv[0]>0 or paddingv[1]>0:
211
f = pad(img,paddingv,1)
216
# Fast Fourier Transformation of the image
217
F = numpy.fft.fft2(f);
220
#% gaussians Hc for rows and columns separately
221
# transition function: gaussians
223
# build them for rows and columns separately
225
#circular repetitions in frequency domain
227
nShah = numpy.ceil(6 / sigma[dim])
231
# multiple repetitions of each pixel
232
j, n = numpy.mgrid[-nShah : (nShah+1), 0 : N[dim] ]
234
# gaussian value at each position
235
Hc[dim] = numpy.exp(-2 * (sigma[dim] * numpy.pi * (j + n / N[dim]))**2);
236
# sum over all repetitions
237
Hx_temp =Hc[1].sum(axis=0);
238
Hy_temp = Hc[0].sum(axis=0);
240
# normalization (divide by N, so it won't have to be done when using fft2 instead of ifft2)
244
# see handwritten notes
245
dy = - 1j * numpy.sin(2 * numpy.pi * numpy.arange(0,1,1./N[0]))
246
dx = 1j * numpy.sin(2 * numpy.pi * numpy.arange(0,1,1./N[1]))
247
dyy = - 2 * (1 - numpy.cos(2 * numpy.pi * numpy.arange(0,1,1./N[0])))
248
dxx = - 2 * (1 - numpy.cos(2 * numpy.pi * numpy.arange(0,1,1./N[1])))
250
#return map(lambda k:unpad(numpy.real(numpy.fft.ifft2((F * gauss_kernels[k](Hy,Hx,dx,dy,dxx,dyy,sigma)))),paddingv),kernels)
251
return map(lambda k:numpy.flipud(unpad(numpy.real(numpy.fft.fft2((F * gauss_kernels[k](Hy,Hx,dx,dy,dxx,dyy,sigma)).T).T),paddingv)),kernels)
256
def omega(alphaDeg, g, sig, M, fftTime):
258
OMEGA computes the model error for a certain ALPHA on a gradient image G.
262
alpha = alphaDeg / 180. * numpy.pi;
265
gAlpha_r = numpy.cos(alpha) * g[0] + numpy.sin(alpha) * g[1];
266
gAlpha_c = - numpy.sin(alpha) * g[0] + numpy.cos(alpha) * g[1];
268
# compute error messure
269
return M * ( gaussFFT(gAlpha_r * gAlpha_r, sig, fftTime, 'y2G')[0]
270
+ 2 * gaussFFT(gAlpha_r * gAlpha_c, sig, fftTime, 'xyG')[0]
271
+ gaussFFT(gAlpha_c * gAlpha_c, sig, fftTime, 'x2G')[0])
275
def buildScaleSpace(img, params):
277
L2, P = numpy.zeros((img.shape)+(params.nS,)), numpy.zeros(img.shape+(params.nS,))
278
for s in range(params.nS):
280
## FIRST We COMPUTE L2
284
gTau_r, gTau_c = gaussFFT(img, params.tau(s), LUT_time, 'Gy', 'Gx')
285
M = 12 * params.sig(s)**2 + 1
288
## smallest eigenvalue of the structure tensor
291
N0_rr = gaussFFT(gTau_r * gTau_r, params.sig(s), LUT_time, 'G')[0]*M
292
N0_cc = gaussFFT(gTau_c * gTau_c, params.sig(s), LUT_time, 'G')[0]*M
293
N0_rc = gaussFFT(gTau_r * gTau_c, params.sig(s), LUT_time, 'G')[0]*M
296
det = N0_rr * N0_cc - N0_rc**2
297
L2[:, :, s] = numpy.real(0.5 * (tr - numpy.sqrt(tr**2 - 4 * det)))
304
if params._type== 'min':
305
S0 = omega( 0, (gTau_r,gTau_c), params.sig(s), M, LUT_time)
306
S1 = omega( 60, (gTau_r,gTau_c), params.sig(s), M, LUT_time)
307
S2 = omega(120, (gTau_r,gTau_c), params.sig(s), M, LUT_time)
308
# model: S = a - b * cos(2 * alpha - 2 * alpha0)
309
a = (S0 + S1 + S2) / 3;
310
b = 2 / 3 * numpy.sqrt( S0**2 + S1**2 + S2**2 - S0 * S1 - S1 * S2 - S2 * S0);
311
# alpha0 = 1 / 2 * atan2(sqrt(3) * (S2 - S1), S1 + S2 - 2 * S0);
314
P[:, :, s] = L2[:, :, s] * R / S
315
elif type(params._type) in [float,int]:
316
S = omega(params._type, (gTau_r,gTau_c), params.sig(s), M, LUT_time)
318
P[:, :, s] = L2[:, :, s] * R / S
320
raise ValueError,('Invalid scale \"type\". Must be \"min\" or \"numeric\".');
322
# remove imaginary and undefined precisions
324
P[numpy.isnan(P)] = -numpy.inf;
331
#######################################################################################################################
332
### PART II KEYPOINT DETECTION
333
########################################################################################################################
336
def nonMax(P, r, c, s, params):
338
NONMAX does a non-maximum suppression at (R, C, S) w. r. t. precision P.
341
table = numpy.hstack([r.reshape(-1,1), c.reshape(-1,1), s.reshape(-1,1), cubicInterp3(P, r, c, s).reshape(-1,1)]) #, (len(r), 4));
342
if table.shape[0]==0:
344
idx = numpy.argsort(table[:,3])[::-1]
347
for line1 in range(table.shape[0]):
348
# compare to following points (with lower precision)
349
line2=numpy.arange(line1 + 1, table.shape[0])
352
sig1 = params.sig(table[line1, 2])
353
sig2 = params.sig(table[line2, 2])
357
# Mahalanobis distance
358
d2 = (((table[line1, 0] - table[line2, 0])**2 + (table[line1, 1] - table[line2, 1])**2) / (sig1**2 + sig2**2)
359
+ (table[line1, 2] - table[line2, 2])**2 / ((params.nonmaxOctave * params.layersPerOctave)**2))
361
# remove all points closer than threshold
362
table[line2[d2 < params.nonmaxTd2], :] = None
364
return table[:,0],table[:,1],table[:,2],table[:,3]
369
def detectPoints(P, L2, params,eps=1e-27):
371
DETECTPOINTS extracts interest points from the scale space.
377
r, c, s = imregionalmax(P).nonzero()
378
print 'Found %d local maxima, '%(len(r),)
380
#% remove points close to the boundary
381
## slice assigning wourl be better hre ?
382
border = 2.5 * params.sig(s)
384
valid = (r - 1 >= border)
385
valid *= ((P.shape[0] - r) > border)
386
valid *=( c - 1 >= border)
387
valid *= ((P.shape[1] - c) > border )
388
valid *= (s - 1 >= 1)
389
valid *= ((P.shape[2] - s) > 1)
391
#print "valid=",valid,valid.sum(),valid.shape
393
r, c, s = r[valid], c[valid], s[valid];
395
print('%d off boundary, '% len(r))
397
#% threshold on p and on lambda
398
h = params.noise**2 / 16 / numpy.pi / params.tau(s)**4
402
Tl2 = h * params.Tlambda2 * chi2inv(0.999, 24 * params.sig(s)**2 + 2);
404
ciP=P[(r.reshape((-1,1)),c.reshape((-1,1)),s.reshape((-1,1)))] #P[numpy.hstack([r.reshape((-1,1)),c.reshape((-1,1)),s.reshape((-1,1))]) ]
405
ciL=L2[(r.reshape((-1,1)),c.reshape((-1,1)),s.reshape((-1,1)))]
406
print valid.shape, ciP.shape, ciL.shape,params.Tp,Tl2.shape
407
valid = numpy.asarray(ciP > params.Tp).ravel() * numpy.asarray(ciL > Tl2).ravel()
408
print valid.shape, valid.dtype
409
print ('%d above thresholds, '%( valid.sum(),))
411
#% optimize each point
412
R, C, S = numpy.mgrid[-1:2,-1:2,-1:2]
417
valid=valid.reshape(-1,1)
418
for p in valid.nonzero()[0]:
419
# iterate multiple times
421
for it in range( params.koetheMaxIter + 1):
422
# gradient and Hessian in current 27-neighborhood
424
# P27 = P(r[p] + R, c[p] + C, s[p] + S);
426
#print R.shape, r[p].shape,r.shape,p
427
#P27 = cubicInterp3(P, R+r[p], C+c[p] , S+s[p]);
429
P27=P[( R+r[p], C+c[p] , S+s[p])]
438
break # update position, but check for image boundary
440
if numpy.isnan(H).any():
443
if (numpy.linalg.eig(H)[0] > 0).all():
450
break # update position, but check for image boundary
453
## adding bias to positions
458
bias = (numpy.asmatrix(H) * G**-1)#numpy.linalg.inv(numpy.asmatrix(G)))
460
r[p] = r[p] + bias[0]
461
c[p] = c[p] + bias[1]
462
s[p] = s[p] + bias[2]
464
if min(r[p], c[p], s[p], P.shape[0] - r[p], P.shape[1]-c[p], P.shape[2]-s[p]) < 2:
469
if numpy.linalg.norm(bias) < params.koetheEpsilon:
472
if it == params.koetheMaxIter:
476
print('%d converged'%( valid.sum()));
478
print('removing non maxima')
479
#% supress non-maxima
480
x = nonMax(P, r[valid], c[valid], s[valid], params);
482
valid=(1-numpy.isnan(x[0])).nonzero()
483
#print valid,x[0].shape,valid.shape
485
return r[valid],c[valid],s[valid]
491
## Ok we just create a class as in the matlab implementation to store the parameters
496
class Parameters(object):
499
tau = staticmethod(lambda sigma: sigma / 3)
512
self.sigmaFrom = 2**(1 + 1 / self.layersPerOctave)
515
sigStep = 2**(1. / self.layersPerOctave);
516
sigFrom = self.sigmaFrom / sigStep**2;
517
sigTo = self.sigmaFrom * sigStep**2 * 2**self.numberOfOctaves;
518
self.nS = int( numpy.floor(numpy.log(sigTo / sigFrom) / numpy.log(sigStep)));
519
self.sig = lambda s: sigFrom * sigStep**(s - 1);
521
self.tau = lambda s: self.otau(self.sig(s))
525
def sfop(img,**kwargs):
527
Please see license usage restrictions.
529
kwargs may be used to change one of these default parameter
532
tau = staticmethod(lambda sigma: sigma / 3)
545
img = img.mean(axis=2)
546
img=img.astype(numpy.float32)
550
for k in kwargs.items():
551
setattr(params,k[0],k[1])
556
P, L2 = buildScaleSpace(img, params);
557
r, c, s = detectPoints(P, L2, params);
560
return numpy.hstack([r.reshape(-1,1),c.reshape(-1,1),s.reshape(-1,1)])
565
if __name__=="__main__":
566
#print sfop(scipy.lena()[:256,:512])
567
print sfop(scipy.lena()[:512,:512])