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

draw_random_shapes: add background argument and avoid invisible objects #3216

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

Conversation

cmohl2013
Copy link

@cmohl2013 cmohl2013 commented Jun 21, 2018

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:]

[For detailed information on these and other aspects see scikit-image contribution guidelines]

References

Closes #3083

For reviewers

(Don't remove the checklist below.)

  • Check that the PR title is short, concise, and will make sense 1 year
    later.
  • Check that new functions are imported in corresponding __init__.py.
  • Check that new features, API changes, and deprecations are mentioned in
    doc/release/release_dev.rst.
  • Consider backporting the PR with @meeseeksdev backport to v0.14.x

Copy link
Member

@stefanv stefanv left a 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.

else:
intensity_range_tpl = intensity_range

if np.diff(np.array(intensity_range_tpl)).sum() == 0:
Copy link
Member

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?

Copy link
Author

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.

for r in intensity_range_tpl]
colors = np.transpose(colors)

to_replace = np.array([(exclude == color).all() for color in colors])
Copy link
Member

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?

Copy link
Author

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.

Copy link
Author

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)

if background is None:
background = [255] * num_channels
elif isinstance(background, int):
background = [background]
Copy link
Member

Choose a reason for hiding this comment

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

[background] * num_channels?

Copy link
Member

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.

Copy link
Author

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?

@soupault soupault added the ⏩ type: Enhancement Improve existing features label Jun 21, 2018
@soupault soupault added this to the 0.15 milestone Jun 21, 2018
@soupault soupault self-assigned this Jun 21, 2018
@stefanv
Copy link
Member

stefanv commented Jun 22, 2018 via email

Copy link
Member

@soupault soupault left a 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. :)

background = [background]
if len(background) != num_channels:
raise ValueError('Nr of background values must match nr of channels')
for intensity in background:
Copy link
Member

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.

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
Copy link
Member

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.

intensity_range = intensity_range * num_channels
intensity_range_tpl = intensity_range * num_channels
else:
intensity_range_tpl = intensity_range
Copy link
Member

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?

Copy link
Author

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.

else:
intensity_range_tpl = intensity_range

if np.diff(np.array(intensity_range_tpl)).sum() == 0:
Copy link
Member

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)).


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]])
Copy link
Member

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.

Copy link
Member

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]

Copy link
Author

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!

elif isinstance(background, int):
background = [background]
if len(background) != num_channels:
raise ValueError('Nr of background values must match nr of channels')
Copy link
Member

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():
Copy link
Member

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():
Copy link
Member

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():
Copy link
Member

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():
Copy link
Member

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.

@cmohl2013
Copy link
Author

@stefanv @soupault Thank you for your descent review! I think all the improvements you suggested are clear to me. I'll try to update the PR in the next couple of days.

@pep8speaks
Copy link

pep8speaks commented Jul 10, 2018

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

@cmohl2013
Copy link
Author

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],
Copy link
Member

@soupault soupault Aug 11, 2018

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?

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.
Copy link
Member

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?

Copy link
Author

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.

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():
Copy link
Member

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):
Copy link
Member

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.

# 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.' +
Copy link
Member

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).

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
Copy link
Member

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.

if exclude is not None and (colors[0, :] == exclude).all():
raise ValueError(msg)
return colors

Copy link
Member

Choose a reason for hiding this comment

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

else:


to_replace = (colors == exclude).all(axis=1)
if to_replace.any():
colors[to_replace, :] = _generate_random_colors(sum(to_replace),
Copy link
Member

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.

@soupault
Copy link
Member

@cmohl2013 Thank you for the changes! The current implementation is much better, although, I still see some issues (see the comments above).

@cmohl2013
Copy link
Author

@soupault Thank you again for the review. I'll try to change implementation according your comments.

@cmohl2013
Copy link
Author

@soupault
I implemented changes according to your requests.

Copy link
Member

@soupault soupault left a 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.

skimage/draw/_random_shapes.py Outdated Show resolved Hide resolved
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
Copy link
Member

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.

Copy link
Member

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 :).

Copy link
Author

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():
Copy link
Member

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.
Copy link
Member

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>
@soupault soupault modified the milestones: 0.15, 0.16 Apr 20, 2019
@sciunto sciunto removed this from the 0.16 milestone Mar 8, 2020
Base automatically changed from master to main February 18, 2021 18:23
@scikit-image scikit-image deleted a comment from codecov-io Oct 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add background keyword argument to draw.random_shapes
6 participants