-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expand Gabor Kernel Generation to nD #2739
base: main
Are you sure you want to change the base?
Conversation
Hello @kne42! Thanks for updating the PR.
Comment last updated on May 30, 2018 at 21:40 Hours UTC |
c62318b
to
5fbfe7a
Compare
f4dac92
to
923bcb2
Compare
skimage/filters/_gabor.py
Outdated
|
||
# handle deprecation | ||
message = ('Using deprecated, 2D-only interface to' | ||
'gabor_kernel. This interface will be' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need spaces at the end of each sub-string, since concatenation within parentheses won't add spaces. =)
skimage/filters/_gabor.py
Outdated
message = ('Using deprecated, 2D-only interface to' | ||
'gabor_kernel. This interface will be' | ||
'removed in scikit-image 0.16. Use' | ||
'gabor_kernel(frequency, rotation=theta,' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You say rotation=theta
here but the signature does not have a rotation keyword. fwiw I think theta as a kwarg can be maintained, silently adding support for multidimensional theta.
skimage/filters/_gabor.py
Outdated
if sigma_y is None: | ||
sigma_y = _sigma_prefactor(bandwidth) / frequency | ||
def __new__(cls, frequency, thetas=None, bandwidth=1, sigma=None, | ||
sigma_y=None, n_stds=3, offset=None, ndim=2, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that the complexity of a class is necessary. Why did you pick this design instead of extending the existing function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jni I chose this design because it encourages reusing the existing gabor kernel. As of right now, the filters.gabor
function creates a new gabor kernel every time you use it. This becomes very expensive if I, say, want to apply the same gabor filter to 1000 images.
While there is already a method of eliminating this expense by creating a gabor kernel and applying it to the images yourself, I would posit that most users would not consider this as they would immediately gravitate towards the gabor
function, which is not obviously expensive from the documentation when applied many times. Furthermore, if one wanted to reproduce what the gabor
function did, they would have to look at the source code and figure out exactly how we were able to apply the filter to an image.
Although I generally prefer functional design to object-oriented design, I think that this choice is justified for the following reasons:
- NumPy is already object-oriented. If we're representing the kernel as an
np.ndarray
anyways, it would be most useful to extend this interface to apply it as well. - This tags along the filter function to the kernel instead of having to use one function to make the kernel and another to apply it (this would be the functional alternative). This is also a lot more intuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kne42 I think the simplest option is to add to filters.gabor
a kernel
parameter that is the (bare) NumPy array, and avoid recomputing the kernel if this argument is given.
The NumPy community has moved away from inheriting from NumPy arrays because it raises a whole bunch of problems. An example: suppose someone comes up with a fancy new "Gabor++" method that works by creating a Gabor kernel, modifying in some weird way, then applying it. Any call to np.copy(kernel)
will strip away the class and return a bare array. Then you've lost your way of applying the kernel.
The other issue is the "surprise" factor: this is a pattern that does not exist anywhere in skimage, or NumPy/SciPy for that matter. So now users have to wrap their heads around a whole new API for Gabor kernels. Or they run g = filters.gabor_kernel(freq)
and then type type(g)
and get a strange class that they've never seen.
@kne42 this is looking nice! Please ping when it's ready for a full review. Speaking of pinging, did you receive my email? I used the address on your GH profile. |
Excellent points; I’ll change it now.
…On Mon, Apr 30, 2018 at 7:09 AM Juan Nunez-Iglesias < ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In skimage/filters/_gabor.py
<#2739 (comment)>
:
> @@ -72,30 +90,134 @@ def gabor_kernel(frequency, theta=0, bandwidth=1, sigma_x=None, sigma_y=None,
>>> io.imshow(gk.real) # doctest: +SKIP
>>> io.show() # doctest: +SKIP
"""
- if sigma_x is None:
- sigma_x = _sigma_prefactor(bandwidth) / frequency
- if sigma_y is None:
- sigma_y = _sigma_prefactor(bandwidth) / frequency
+ def __new__(cls, frequency, thetas=None, bandwidth=1, sigma=None,
+ sigma_y=None, n_stds=3, offset=None, ndim=2, **kwargs):
@kne42 <https://github.com/kne42> I think the simplest option is to add
to filters.gabor a kernel parameter that is the (bare) NumPy array, and
avoid recomputing the kernel if this argument is given.
The NumPy community has moved away from inheriting from NumPy arrays
because it raises a whole bunch of problems. An example: suppose someone
comes up with a fancy new "Gabor++" method that works by creating a Gabor
kernel, modifying in some weird way, then applying it. Any call to
np.copy(kernel) will strip away the class and return a bare array. Then
you've lost your way of applying the kernel.
The other issue is the "surprise" factor: this is a pattern that does not
exist anywhere in skimage, or NumPy/SciPy for that matter. So now users
have to wrap their heads around a whole new API for Gabor kernels. Or they
run g = filters.gabor_kernel(freq) and then type type(g) and get a
strange class that they've never seen.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2739 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/Ab0F02WkGfDaoOq_2ZKSpRvaSAfua252ks5ttvDogaJpZM4OnDPx>
.
|
…rror that used by scipy.ndimage
…ates as a utility
…'use_homogeneous_coords'
@jni @emmanuelle This PR is now ready for its penultimate review as I start cleaning up documentation/implementation, working out kinks in the functions, and expanding unit test coverage. Some points of discussion:
|
Codecov Report
@@ Coverage Diff @@
## master #2739 +/- ##
=========================================
+ Coverage 86.05% 86.1% +0.05%
=========================================
Files 338 338
Lines 27364 27504 +140
=========================================
+ Hits 23547 23683 +136
- Misses 3817 3821 +4
Continue to review full report at Codecov.
|
skimage/filters/_gabor.py
Outdated
|
||
norm = np.linalg.norm(u) | ||
|
||
if not np.isclose(norm, 1): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this check necessary?
skimage/filters/_gabor.py
Outdated
|
||
|
||
def _compute_projection_matrix(axis, indices=None): | ||
"""Generates a matrix that projects an axis onto the 0th coordinate axis. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the 0th coordinate axis?
skimage/filters/_gabor.py
Outdated
|
||
References | ||
---------- | ||
.. [1] Bart M. ter Haar Romeny. Front-End Vision and Multi-Scale Image |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a collection of articles; could you add the name (and perhaps page number) of the relevant one?
skimage/filters/_gabor.py
Outdated
>>> Y = np.asarray([0.5, 0.5]) | ||
|
||
>>> M = _compute_rotation_matrix(X, Y) | ||
>>> Z = np.matmul(M, X) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Z = M @ X
skimage/filters/_gabor.py
Outdated
Parameters | ||
---------- | ||
image : non-complex array | ||
Linear space to map the filter to. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is the image a linear space here?
skimage/filters/_gabor.py
Outdated
Standard deviation for Gaussian kernel. The standard deviations of the | ||
Gaussian filter are given for each axis as a sequence, or as a single | ||
number, in which case it is equal for all axes. | ||
ndim : int, optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering here: should we allow center and sigma to be scalars? If not, we can derive ndim from center and sigma.
offset : float, optional | ||
Phase offset of harmonic function in radians. | ||
axes : int or sequence of int, optional | ||
Ordering of axes that defines the plane of rotation. Ordering not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what this parameter does?
skimage/filters/_gabor.py
Outdated
if sigma_y is None: | ||
sigma_y = _sigma_prefactor(bandwidth) / frequency | ||
# handle deprecation | ||
message = ('Using deprecated, 2D-only interface to gabor_kernel. ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there no way to support the old style calling convention still?
skimage/filters/_gabor.py
Outdated
m = np.asarray(np.meshgrid(*[range(-c, c + 1) for c in spatial_size], | ||
indexing='ij')) | ||
|
||
rotm = np.matmul(m.T, rot.T).T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rot @ m?
R = np.eye(ndim) # Initial rotation matrix = Identity matrix | ||
|
||
# Loop to create matrices of stages | ||
for step in np.round(2 ** np.arange(np.log2(ndim))).astype(int): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarify what happens here: why the log2, the 2**, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest linking to
https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
and pointing out that this function will compute the correct matrices for rotation along individual axes in the same way that is described for 3D in that page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the same links to the general generate_rotation_matrix
function
skimage/filters/_gabor.py
Outdated
if n + step >= len(w): | ||
break | ||
|
||
r2 = x[w[n]] * x[w[n]] + x[w[n + step]] * x[w[n + step]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
r2 = x[w[n]] ** 2 + x[w[n + step]] ** 2
Ah, but then we take the sqrt, so how about np.hypot
?
skimage/filters/_gabor.py
Outdated
r2 = x[w[n]] * x[w[n]] + x[w[n + step]] * x[w[n + step]] | ||
if r2 > 0: | ||
r = np.sqrt(r2) | ||
pcos = x[w[n]] / r # Calculation of coefficients |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, x[w[n]]
and x[w[n + step]]
. Since these indices are re-used, should we name them?
skimage/filters/_gabor.py
Outdated
dst : (N, ) array | ||
Vector of desired direction. | ||
use_homogeneous_coords : bool, optional | ||
If the input vectors should be treated as homoegeneous coordinates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
homogeneous
r : float | ||
Radial coordinate. | ||
thetas : (N, ) array | ||
Quasipolar angles. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some explanation of where these angles lie would be helpful (given the "quasi" nature you explained to me).
Description
Expands the Gabor filter to work on images of any dimension.
Checklist
./doc/examples
(new features only)[For detailed information on these and other aspects see scikit-image contribution guidelines]
References
For reviewers
(Don't remove the checklist below.)
later.
__init__.py
.doc/release/release_dev.rst
.