-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Added reduce operation #4251
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
Merged
Merged
Added reduce operation #4251
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
335f543
dummy method
homm 01094a9
naive implementation
homm ab25d9f
good balance
homm e6c3055
more special
homm fba833e
final set of special cases
homm 7e978ae
ImagingReduceCorners
homm 8b6ad4a
tests for supported modes
homm f72d5c0
better branches
homm a241f1e
complete tests for supported modes
homm 008c1c8
L mode support
homm 1d1f3be
unsupported modes
homm d970a39
Special cases:
homm a576b14
F mode support
homm d92c58f
I mode support
homm cc30b1e
Add La mode packing and unpacking
homm 778b5f9
add box parameter
homm b236251
add box parameter to all optimized versions
homm 5838d77
args test
homm 5283141
Merge branch 'master' into reduce
homm b655e81
not square test image
homm e54b9b3
turn on ImagingReduce5x5 special case
homm 5283538
unused import
homm ea9c6e9
Merge branch 'master' into reduce
homm d41f271
Merge branch 'master' into reduce
homm dda5558
Merge branch 'master' into reduce
homm fc02488
wording
homm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,244 @@ | ||
| from PIL import Image, ImageMath, ImageMode | ||
|
|
||
| from .helper import PillowTestCase, convert_to_comparable | ||
|
|
||
|
|
||
| class TestImageReduce(PillowTestCase): | ||
| # There are several internal implementations | ||
| remarkable_factors = [ | ||
| # special implementations | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| 6, | ||
| # 1xN implementation | ||
| (1, 2), | ||
| (1, 3), | ||
| (1, 4), | ||
| (1, 7), | ||
| # Nx1 implementation | ||
| (2, 1), | ||
| (3, 1), | ||
| (4, 1), | ||
| (7, 1), | ||
| # general implementation with different paths | ||
| (4, 6), | ||
| (5, 6), | ||
| (4, 7), | ||
| (5, 7), | ||
| (19, 17), | ||
| ] | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| cls.gradients_image = Image.open("Tests/images/radial_gradients.png") | ||
| cls.gradients_image.load() | ||
|
|
||
| def test_args_factor(self): | ||
| im = Image.new("L", (10, 10)) | ||
|
|
||
| self.assertEqual((4, 4), im.reduce(3).size) | ||
| self.assertEqual((4, 10), im.reduce((3, 1)).size) | ||
| self.assertEqual((10, 4), im.reduce((1, 3)).size) | ||
|
|
||
| with self.assertRaises(ValueError): | ||
| im.reduce(0) | ||
| with self.assertRaises(TypeError): | ||
| im.reduce(2.0) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce((0, 10)) | ||
|
|
||
| def test_args_box(self): | ||
| im = Image.new("L", (10, 10)) | ||
|
|
||
| self.assertEqual((5, 5), im.reduce(2, (0, 0, 10, 10)).size) | ||
| self.assertEqual((1, 1), im.reduce(2, (5, 5, 6, 6)).size) | ||
|
|
||
| with self.assertRaises(TypeError): | ||
| im.reduce(2, "stri") | ||
| with self.assertRaises(TypeError): | ||
| im.reduce(2, 2) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(2, (0, 0, 11, 10)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(2, (0, 0, 10, 11)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(2, (-1, 0, 10, 10)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(2, (0, -1, 10, 10)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(2, (0, 5, 10, 5)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(2, (5, 0, 5, 10)) | ||
|
|
||
| def test_unsupported_modes(self): | ||
| im = Image.new("P", (10, 10)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(3) | ||
|
|
||
| im = Image.new("1", (10, 10)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(3) | ||
|
|
||
| im = Image.new("I;16", (10, 10)) | ||
| with self.assertRaises(ValueError): | ||
| im.reduce(3) | ||
|
|
||
| def get_image(self, mode): | ||
| mode_info = ImageMode.getmode(mode) | ||
| if mode_info.basetype == "L": | ||
| bands = [self.gradients_image] | ||
| for _ in mode_info.bands[1:]: | ||
| # rotate previous image | ||
| band = bands[-1].transpose(Image.ROTATE_90) | ||
| bands.append(band) | ||
| # Correct alpha channel by transforming completely transparent pixels. | ||
| # Low alpha values also emphasize error after alpha multiplication. | ||
| if mode.endswith("A"): | ||
| bands[-1] = bands[-1].point(lambda x: int(85 + x / 1.5)) | ||
| im = Image.merge(mode, bands) | ||
| else: | ||
| assert len(mode_info.bands) == 1 | ||
| im = self.gradients_image.convert(mode) | ||
| # change the height to make a not-square image | ||
| return im.crop((0, 0, im.width, im.height - 5)) | ||
|
|
||
| def compare_reduce_with_box(self, im, factor): | ||
| box = (11, 13, 146, 164) | ||
| reduced = im.reduce(factor, box=box) | ||
| reference = im.crop(box).reduce(factor) | ||
| self.assertEqual(reduced, reference) | ||
|
|
||
| def compare_reduce_with_reference(self, im, factor, average_diff=0.4, max_diff=1): | ||
| """Image.reduce() should look very similar to Image.resize(BOX). | ||
|
|
||
| A reference image is compiled from a large source area | ||
| and possible last column and last row. | ||
| +-----------+ | ||
| |..........c| | ||
| |..........c| | ||
| |..........c| | ||
| |rrrrrrrrrrp| | ||
| +-----------+ | ||
| """ | ||
| reduced = im.reduce(factor) | ||
|
|
||
| if not isinstance(factor, (list, tuple)): | ||
| factor = (factor, factor) | ||
|
|
||
| reference = Image.new(im.mode, reduced.size) | ||
| area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) | ||
| area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) | ||
| area = im.resize(area_size, Image.BOX, area_box) | ||
| reference.paste(area, (0, 0)) | ||
|
|
||
| if area_size[0] < reduced.size[0]: | ||
| self.assertEqual(reduced.size[0] - area_size[0], 1) | ||
| last_column_box = (area_box[2], 0, im.size[0], area_box[3]) | ||
| last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box) | ||
| reference.paste(last_column, (area_size[0], 0)) | ||
|
|
||
| if area_size[1] < reduced.size[1]: | ||
| self.assertEqual(reduced.size[1] - area_size[1], 1) | ||
| last_row_box = (0, area_box[3], area_box[2], im.size[1]) | ||
| last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box) | ||
| reference.paste(last_row, (0, area_size[1])) | ||
|
|
||
| if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: | ||
| last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) | ||
| last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box) | ||
| reference.paste(last_pixel, area_size) | ||
|
|
||
| self.assert_compare_images(reduced, reference, average_diff, max_diff) | ||
|
|
||
| def assert_compare_images(self, a, b, max_average_diff, max_diff=255): | ||
| self.assertEqual(a.mode, b.mode, "got mode %r, expected %r" % (a.mode, b.mode)) | ||
| self.assertEqual(a.size, b.size, "got size %r, expected %r" % (a.size, b.size)) | ||
|
|
||
| a, b = convert_to_comparable(a, b) | ||
|
|
||
| bands = ImageMode.getmode(a.mode).bands | ||
| for band, ach, bch in zip(bands, a.split(), b.split()): | ||
| ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch) | ||
| ch_hist = ch_diff.histogram() | ||
|
|
||
| average_diff = sum(i * num for i, num in enumerate(ch_hist)) / float( | ||
| a.size[0] * a.size[1] | ||
| ) | ||
| self.assertGreaterEqual( | ||
| max_average_diff, | ||
| average_diff, | ||
| ( | ||
| "average pixel value difference {:.4f} > expected {:.4f} " | ||
| "for '{}' band" | ||
| ).format(average_diff, max_average_diff, band), | ||
| ) | ||
|
|
||
| last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] | ||
| self.assertGreaterEqual( | ||
| max_diff, | ||
| last_diff, | ||
| "max pixel value difference {} > expected {} for '{}' band".format( | ||
| last_diff, max_diff, band | ||
| ), | ||
| ) | ||
|
|
||
| def test_mode_L(self): | ||
| im = self.get_image("L") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor) | ||
| self.compare_reduce_with_box(im, factor) | ||
|
|
||
| def test_mode_LA(self): | ||
| im = self.get_image("LA") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor, 0.8, 5) | ||
|
|
||
| # With opaque alpha, an error should be way smaller. | ||
| im.putalpha(Image.new("L", im.size, 255)) | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor) | ||
| self.compare_reduce_with_box(im, factor) | ||
|
|
||
| def test_mode_La(self): | ||
| im = self.get_image("La") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor) | ||
| self.compare_reduce_with_box(im, factor) | ||
|
|
||
| def test_mode_RGB(self): | ||
| im = self.get_image("RGB") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor) | ||
| self.compare_reduce_with_box(im, factor) | ||
|
|
||
| def test_mode_RGBA(self): | ||
| im = self.get_image("RGBA") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor, 0.8, 5) | ||
|
|
||
| # With opaque alpha, an error should be way smaller. | ||
| im.putalpha(Image.new("L", im.size, 255)) | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor) | ||
| self.compare_reduce_with_box(im, factor) | ||
|
|
||
| def test_mode_RGBa(self): | ||
| im = self.get_image("RGBa") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor) | ||
| self.compare_reduce_with_box(im, factor) | ||
|
|
||
| def test_mode_I(self): | ||
| im = self.get_image("I") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor) | ||
| self.compare_reduce_with_box(im, factor) | ||
|
|
||
| def test_mode_F(self): | ||
| im = self.get_image("F") | ||
| for factor in self.remarkable_factors: | ||
| self.compare_reduce_with_reference(im, factor, 0, 0) | ||
| self.compare_reduce_with_box(im, factor) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 name of the operation and argument could be discussed.
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.
@python-pillow/pillow-team any objections to naming?
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.
Well, I can't think of a better name :)
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.
thumbs up for the current naming!