Skip to content
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

Implemented Kadir and Brady's Saliency detection #2299

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

parulsethi
Copy link

@parulsethi parulsethi commented Sep 9, 2016

Description

This PR implements Kadir and Brady's Saliency detection algorithm which calculates the saliency score by measuring the entropy of the region's descriptor(to find locally complex regions) and weigh them by the difference over multiple scales to select features that are globally discriminative.
Result over astronaut():
screen shot 2016-09-09 at 12 07 44 am

As this is my first contribution to not only scikit-image but open-source too, so please suggest the changes that are required in order to merge this, if you are interested in adding this feature.
I haven't yet prepared the doc example as their are some parameters in this algorithm over which i need your suggestion that whether to leave them as optional or set it to the optimized one. These are the saliency and clustering thresholds.

Checklist

References

Kadir, Timor, and Michael Brady. "Saliency, scale and image description." International Journal of Computer Vision 45.2 (2001): 83-105.

@parulsethi parulsethi changed the title Kb saliency detection Implemented Kadir and Brady's Saliency detection Sep 9, 2016
@codecov-io
Copy link

codecov-io commented Sep 9, 2016

Current coverage is 90.68% (diff: 100%)

Merging #2299 into master will increase coverage by 0.06%

@@             master      #2299   diff @@
==========================================
  Files           304        307     +3   
  Lines         21539      21684   +145   
  Methods           0          0          
  Messages          0          0          
  Branches       1852       1865    +13   
==========================================
+ Hits          19520      19665   +145   
  Misses         1665       1665          
  Partials        354        354          

Powered by Codecov. Last update d991a66...481bf43

Copy link
Member

@jni jni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @parulsethi!

I've left a few comments about general style and syntax.

For tests, have a look at the skimage/feature/tests directory for examples. You should add another file, test_kadir_brady.py, that compares the output of the function with known output for a given input.

For the gallery, see in doc/examples/feature_dection/ for some templates that you can copy. Simply add a file next to those, plot_kadir_brady.py, that illustrates the detector.

Finally, as a general comment, some of the functions are a bit long — you should try to keep the function length shorter than 30-50 lines. If it's longer, see whether you can pull some loops into their own functions.

Hope this helps! =)

from .._shared.utils import assert_nD


def kb(image, min_scale=10, max_scale=25, saliency_threshold=0.6, clustering_threshold=7):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you rename this saliency_kadir_brady. kb is definitely not informative enough!



def kb(image, min_scale=10, max_scale=25, saliency_threshold=0.6, clustering_threshold=7):
"""Finds salient regions in the given grayscale image.For each point x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a single sentence followed by a blank line, and should be in present imperative ("Finds salient regions" -> "Find salient regions"). Please review https://www.python.org/dev/peps/pep-0257/ and apply here.

Features with saliency score satisfying threshold will be considered.
clustering_threshold : int, optional
Variance among the regions should be smaller than this threshold
for sufficient clustering.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work on the numpy style parameter list! =)


Returns
-------
A : (n, 3) ndarray
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by convention we use "array of float, shape (n, 3)"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jni I'm sorry, where do we use this notation? I bet that out : (K, 2) ndarray of float is the most oftenly used version all over the package.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soupault you are absolutely right! Oops. Dunno where I got that other idea... =\

@parulsethi sorry about this mistake! But, at least, @soupault agree that the N should be capitalised. =)

assert_nD(image, 2)

# scales for keypoints
scales = np.array([_ for _ in range(min_scale, max_scale, 2)])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three overlapping critiques here:

  1. Only use _ for values that are discarded. In this case, the values are collected in the array, so you would use e.g. np.array([scale for scale in range(min_scale, max_scale, 2)])
  2. But actually, you're just converting to a list, so instead, prefer np.array(list(range(min_scale, max_scale, 2)))
  3. But, actually, you're just converting to an array, so instead use np.arange(min_scale, max_scale, 2).

=)

if s_count >= 1:
# first derivative in entropy space to calculate weights
dif = abs(h - previous_h[:, i])
factor = scales[s_count]**2/(2*scales[s_count]-1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read PEP8 for suggestions about spacing around mathematical operators



def prune(candidate_regions, saliency_threshold, v_th, K=7):
""" It selects highly salient points that have local support
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about docstrings

r, c = np.nonzero(mask)
nPix = len(r)

nScales = len(scales)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use camelCase, use under_scores

# compute the histogram of intensity values in this region
patch = image[min_r:max_r, min_c:max_c]
h = np.histogram(patch, bins=intensity_edges)[0]
h = np.array([_/sum(h) for _ in h])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h /= np.sum(h)

return np.array([gamma, scale, row, column])


def prune(candidate_regions, saliency_threshold, v_th, K=7):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use uppercase arguments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or value names

@parulsethi
Copy link
Author

Hey @jni,

Made all the changes requested above, the code is bit changed for using skimage.util.view_as_windows for pixel iteration and also spread out the functions a bit.

Please suggest if any further improvements are required.

Copy link
Member

@jni jni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parulsethi this is getting much closer! But I think it will benefit from another round of edits.

I also think that, with a bit of thought and care, it could be converted to support nD images, instead of just 2D.

For now, I would really appreciate it if you could address the comments I've left!

from ..util import img_as_ubyte, view_as_windows
from .._shared.utils import assert_nD

def saliency_kadir_brady(image, min_scale=5, max_scale=13, saliency_threshold=0.6, clustering_threshold=2):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parulsethi PEP8 requires all lines to be wrapped at 80 characters wide.

What editor are you using? Most modern editors will automatically wrap lines for you. I recommend PyCharm.

for row in patches:
for patch in row:
h = np.histogram(patch, bins=intensity_edges)[0]
h = h / np.sum(h)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you can just use np.histogram(..., normed=True)

for s_idx, scale in enumerate(scales):
# shape for windows according to current scale
radius = scale + 1
window_shape = (radius, radius)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is better described as "sidelen" or "diameter" than "radius"

nr, nc = image.shape
# find pixels that we are going to examine
mask = np.ones((nr - max(scales), nc - max(scales)))
r, c = np.nonzero(mask) + max(scales) / 2 + 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are trying to add a tuple (np.nonzero(.)) to a float (max(scales) / 2), which is an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r and c are assigned here the 2 arrays inside the tuple, so it's actually array+float

In [96]: np.nonzero(mask)
Out[96]: (array([0, 0, 1, 1]), array([0, 1, 0, 1]))

In [97]: r, c = np.nonzero(mask)

In [98]: r
Out[98]: array([0, 0, 1, 1])

In [99]: c
Out[99]: array([0, 1, 0, 1])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parulsethi Python evaluates the RHS entirely before assignment, so it first will try to add a tuple to a float, and then unpack the arrays within the tuple:

In [1]: import numpy as np

In [2]: mask = np.random.rand(5, 5) > 0.5

In [3]: r, c = np.nonzero(mask) + 1.5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-036d7e86f56f> in <module>()
----> 1 r, c = np.nonzero(mask) + 1.5

TypeError: can only concatenate tuple (not "float") to tuple

# iterate through scales
for s_idx, scale in enumerate(scales):
# shape for windows according to current scale
radius = scale + 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the +1?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make no. of iterating windows equal to n_pix.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or to just have those windows whose center pixel equals the pixels which are considered from n_pix (or mask)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, can you elaborate a bit more here? I still don't understand why we don't use the scale as the radius — it seems to be some form of convention, but it doesn't seem justified to me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In [68]: scales = [5,3]
    ...: image= np.random.rand(10, 10)
    ...: nr, nc = image.shape
    ...: 
    ...: mask = np.ones((nr - max(scales), nc - max(scales)))
    ...: r, c = np.nonzero(mask)
    ...: n_pix = len(r)
    ...: 

In [69]: n_pix
Out[69]: 25

let current_scale = scales[0]
and if i set diameter(thanks for pointing radius being misleading) without doing +1

In [73]: patches = util.view_as_windows(image, current_scale)

In [74]: patches.shape
Out[74]: (6, 6, 5, 5)

which basically gives no. of patches => 6*6 = 36
but then with +1

In [75]: patches = util.view_as_windows(image, current_scale+1)

In [76]: patches.shape
Out[76]: (5, 5, 6, 6)

now, no. of patches => 5*5 = 25 which is equal to n_pix computed above.

This is basically because the no. of windows are image.shape - scale+1, so i can either do diameter = scale+1 or do mask = np.ones((nr-max(scales)+1, nc-max(scales)+1)) above which would then give n_pix=36 if we take diameter same as scale.

Copy link
Author

@parulsethi parulsethi Oct 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's n_pix which should be calculated with (nr-max(scales)+1, nc-max(scales)+1), and diameter should be left same as scale.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parulsethi yes, this last statement makes the most sense to me. =)

h = np.histogram(patch, bins=intensity_edges)[0]
h = h / np.sum(h)

# index of histogram values greater than zero
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In comments, don't simply state what the code is doing (that's what the code is for), but why you're doing it. In this case, "find histogram values greater than zero to prevent NaNs when computing 0log(0) during entropy computation"

t_r : (N,) ndarray of float
Row index of selected regions.
t_c : (N,) ndarray of float
Cloumn index of selected regions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column

Parameters
----------
candidate_regions : (4, N) ndarray of float
A 2d array with each column representing gamma,scale,y,x.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use "row, column" or "r, c" instead of "y, x".

column = np.append(column, center[0])
row = np.append(row, center[1])
scale = np.append(scale, center[2])
gamma = np.append(gamma, t_gamma[index])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extremely inefficient... Use lists to append, and convert to array at the end!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for this!

cluster = np.zeros((3, k + 1))

# pruning process
for index in range(n):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what you're doing here? It seems to me that you should be able to simplify the logic quite a bit. For example, the code block for the first region (under the else) is the same as the code block for a sufficiently-far region. I'm sure you'd be able to condense both code paths into a single clause.

@sciunto
Copy link
Member

sciunto commented Apr 25, 2017

@parulsethi What's the status of this PR for you please? Do you still want to go through @jni's comments?

@parulsethi
Copy link
Author

Sorry for the delay, I made most of the changes in the review above, will push them here, but was stuck at one of them - replied in the corresponding review comment.

@sciunto
Copy link
Member

sciunto commented Apr 27, 2017

Thanks @parulsethi! Feel free to push the modifications you made so far.

Could you also tick the box (if not done yet) on the right of this page. This is to allow to push on your branch as well. That could be useful for us.

thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants