-
-
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
draw_random_shapes: add background argument and avoid invisible objects #3216
base: main
Are you sure you want to change the base?
Conversation
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.
Thanks for your contribution, Christoph. I've left some initial feedback.
skimage/draw/_random_shapes.py
Outdated
else: | ||
intensity_range_tpl = intensity_range | ||
|
||
if np.diff(np.array(intensity_range_tpl)).sum() == 0: |
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 this check overkill? Even if not, I think the error message should at least be improved. But what do we lose if we remove it entirely?
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.
If intensity range spans only a single color, and this color equals the color to be excluded, the recursive call in line 230 will end up in an infinite loop. Therefore this check is important. The error message could be more specific, indeed.
skimage/draw/_random_shapes.py
Outdated
for r in intensity_range_tpl] | ||
colors = np.transpose(colors) | ||
|
||
to_replace = np.array([(exclude == color).all() for color in colors]) |
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.
First convert to a numpy array and then perform array operations?
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.
Yes, would be more efficient.
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.
something like this: to_replace = np.equal(color, exclude).all(axis=1)
skimage/draw/_random_shapes.py
Outdated
if background is None: | ||
background = [255] * num_channels | ||
elif isinstance(background, int): | ||
background = [background] |
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.
[background] * num_channels?
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.
Also, what about the case where the user wants to specify RGB background.
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.
[background] * num_channels
: good idea. My initial idea was to not allow single values for background in multichannel images. But this is more convenient.
I'm not shure if I understand you correctly regarding RGB background. The user can specify background for images of three channels with a tuple of length 3. Or do you think of matplotlib-like color strings as e.g.background='r'
for red background?
No, I was thinking of the 3-tuple case. I missed seeing that that is
already supported.
|
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.
@cmohl2013 very good starting point, thanks! Let's polish the PR a bit. :)
skimage/draw/_random_shapes.py
Outdated
background = [background] | ||
if len(background) != num_channels: | ||
raise ValueError('Nr of background values must match nr of channels') | ||
for intensity in background: |
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.
It seems that operations on background
take too many lines. Let's just cast it to ndarray from the beginning, and use the vectorized ops.
skimage/draw/_random_shapes.py
Outdated
ranges are equal across the channels, and | ||
((min_0, max_0), ... (min_N, max_N)) if they differ. As the function | ||
supports generation of uint8 arrays only, the maximum range is | ||
(0, 255). If None, set to (0, 255) for each channel reserving color of |
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.
reserving color of intensity = 255 for background.
<- this should probably be moreved. The same info is provided 2 lines below.
skimage/draw/_random_shapes.py
Outdated
intensity_range = intensity_range * num_channels | ||
intensity_range_tpl = intensity_range * num_channels | ||
else: | ||
intensity_range_tpl = intensity_range |
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 really a need in changing this block?
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 wanted to keep the original intensity_range value for using in the recursive call in line 230. But I see that this approach is confusing, makes the code less readable. Indeed this can be done outside the block in a more simple way.
skimage/draw/_random_shapes.py
Outdated
else: | ||
intensity_range_tpl = intensity_range | ||
|
||
if np.diff(np.array(intensity_range_tpl)).sum() == 0: |
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.
.sum()
is not a 100% valid check here. Will fail on, for example, ((0, 1), (1, 0), (0, 0))
.
skimage/draw/_random_shapes.py
Outdated
|
||
if np.diff(np.array(intensity_range_tpl)).sum() == 0: | ||
# if intensity range only spans a single color | ||
colors = np.array([np.array(intensity_range_tpl)[:, 0]]) |
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.
To many np.array
constructors here, above, and below. Please, cast intensity_range_tpl
to ndarray as early as possible, and use the vectorized ops.
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.
Sorry to interject, but asarray
might be appropriate to avoid a copy early on.
Also, are you looking for np.newaxis
. you would slice with [:, 0, np.newaxis]
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.
@hmaarrfk thanks for the hint!
skimage/draw/_random_shapes.py
Outdated
elif isinstance(background, int): | ||
background = [background] | ||
if len(background) != num_channels: | ||
raise ValueError('Nr of background values must match nr of channels') |
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.
Nr/nr
abbreviation should be expanded.
def test_generates_white_image_when_intensity_range_255(): | ||
image, labels = random_shapes((128, 128), max_shapes=3, | ||
intensity_range=((255, 255),), | ||
def test_throws_when_intensity_range_255(): |
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.
test_throws_when_intensity_range_255
-> test_throws_when_intensity_range_equals_background
or something. Also, please specify the background value explicitly in the function call below.
random_seed=42) | ||
|
||
|
||
def test_pick_random_colors_within_range(): |
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 could be rewritten using parametrize
decorator.
assert tuple(color) in [(20, 25), (20, 26), (21, 26)] | ||
|
||
|
||
def test_throws_when_intensity_range_equals_excluded_intensities(): |
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.
Erm, this essentially repeats test_throws_when_intensity_range_255
...
assert set(image[:, :, 1].flatten()) == {30, 31} | ||
|
||
|
||
def test_throws_when_backgound_out_of_range(): |
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 function and the one below could be also refactored using parametrize
.
Hello @cmohl2013! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found: There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻 Comment last updated at 2020-11-01 00:48:12 UTC |
I updated the PR according the comments. Should the example in the gallery also be updated with a background argument example? |
|
||
if (np.diff(intensity_range) == 0).all(): | ||
# if intensity range only spans a single color | ||
colors = np.broadcast_to(intensity_range[:, 0], |
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 about switching to .repeat
or .tile
?
skimage/draw/_random_shapes.py
Outdated
background : {tuple of ints, int}, optional | ||
Pixel intensities for background. Values between 0 and 255 are allowed. | ||
For multichannel, a tuple of length num_channels is required. If None, | ||
set to 255 for each channel. |
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.
Does it really make sense to support None
?
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're right, better we do not support None
. Solves also some of your comments below.
skimage/draw/_random_shapes.py
Outdated
elif len(background) != num_channels: | ||
msg = 'Number of background values must match number of channels.' | ||
raise ValueError(msg) | ||
if (background <= 0).any() or (background > 255).any(): |
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.
0 is a valid value.
@@ -171,7 +171,8 @@ def _generate_triangle_mask(point, image, shape, random): | |||
SHAPE_CHOICES = list(SHAPE_GENERATORS.values()) | |||
|
|||
|
|||
def _generate_random_colors(num_colors, num_channels, intensity_range, random): | |||
def _generate_random_colors(num_colors, num_channels, intensity_range, random, | |||
exclude=None): |
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.
The effect of None
should be described in the docstring.
skimage/draw/_random_shapes.py
Outdated
# if intensity range only spans a single color | ||
colors = np.broadcast_to(intensity_range[:, 0], | ||
(num_colors, num_channels)) | ||
msg = ('Shape colors can not be selected.' + |
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.
Move this under the if
loop.
You can remove +
inside the parentheses. Also, add a whitespace after the period (the strings are concatenated).
skimage/draw/_random_shapes.py
Outdated
random = np.random.RandomState(random_seed) | ||
user_shape = shape | ||
image_shape = (image_shape[0], image_shape[1], num_channels) | ||
image = np.ones(image_shape, dtype=np.uint8) * 255 | ||
image = np.ones(image_shape, dtype=np.uint8) * background |
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.
Multiplying by None
is not what you probably want.
skimage/draw/_random_shapes.py
Outdated
if exclude is not None and (colors[0, :] == exclude).all(): | ||
raise ValueError(msg) | ||
return colors | ||
|
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.
else:
skimage/draw/_random_shapes.py
Outdated
|
||
to_replace = (colors == exclude).all(axis=1) | ||
if to_replace.any(): | ||
colors[to_replace, :] = _generate_random_colors(sum(to_replace), |
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 can actually cause stack overflow (highly unlikely, but still). I think the sampling should be implemented in a different way.
@cmohl2013 Thank you for the changes! The current implementation is much better, although, I still see some issues (see the comments above). |
@soupault Thank you again for the review. I'll try to change implementation according your comments. |
…to _generate_random_colors
@soupault |
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.
@cmohl2013 I think, I've found a bug in the current implementation. Once fixed, this PR should be good to go.
else: | ||
return colors | ||
color_candidates = [np.arange(r[0], r[1] + 1) for r in intensity_range] | ||
color_candidates = [np.setdiff1d(c, e) for c, e |
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 think, this might, occasionally, remove some valid colors:
cc = np.array([[1, 2, 3], [4, 5, 6]])
exclude = np.array([1, 4])
cc = [np.setdiff1d(c, e) for c, e in zip(color_candidates, exclude)]
cc -> [array([2, 3]), array([5, 6])]
In this case, [1, 5], [1, 6], [2, 4], [3, 4]
are valid foreground values, but no longer considered.
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.
Definitely, would need a test for this :).
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.
wow, a bug indeed, not so easy to find I guess. Yes I'll add a test and see how to fix this. Thanks!
# if intensity range only spans a single color | ||
colors = np.broadcast_to(intensity_range[:, 0], | ||
(num_colors, num_channels)) | ||
if exclude is not None and (colors[0, :] == exclude).all(): |
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.
Personally, I feel that putting this check before the broadcasting would be more coherent.
exclude : {tuple of ints, int}, optional | ||
A color within the intensity_range that is excluded from | ||
random sampling. For multichannel, a tuple of length num_channels is | ||
required. If `None`, no color is excluded. |
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 should say that you're broadcasting a single value in the multichannel case (effectively).
Co-Authored-By: cmohl2013 <cmohl@yahoo.com>
Description
The new optional background argument defines a custom background color. Before, background was hard coded to 255 for each channel.
The PR also includes a fix for randomly picking colors of foreground objects: Foreground objects are not allowed to have background color any more (i.e. making them invisible).
Checklist
[It's fine to submit PRs which are a work in progress! But before they are merged, all PRs should provide:]
./doc/examples
(new features only)./benchmarks
, if your changes aren't covered by anexisting benchmark
[For detailed information on these and other aspects see scikit-image contribution guidelines]
References
Closes #3083
For reviewers
(Don't remove the checklist below.)
later.
__init__.py
.doc/release/release_dev.rst
.@meeseeksdev backport to v0.14.x