mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-26 09:14:27 +03:00
commit
5f69035865
BIN
Tests/images/radial_gradients.png
Normal file
BIN
Tests/images/radial_gradients.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 208 KiB |
244
Tests/test_image_reduce.py
Normal file
244
Tests/test_image_reduce.py
Normal file
|
@ -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)
|
1
setup.py
1
setup.py
|
@ -54,6 +54,7 @@ _LIB_IMAGING = (
|
|||
"Access",
|
||||
"AlphaComposite",
|
||||
"Resample",
|
||||
"Reduce",
|
||||
"Bands",
|
||||
"BcnDecode",
|
||||
"BitDecode",
|
||||
|
|
|
@ -1777,9 +1777,9 @@ class Image:
|
|||
If the image has mode "1" or "P", it is
|
||||
always set to :py:attr:`PIL.Image.NEAREST`.
|
||||
See: :ref:`concept-filters`.
|
||||
:param box: An optional 4-tuple of floats giving the region
|
||||
of the source image which should be scaled.
|
||||
The values should be within (0, 0, width, height) rectangle.
|
||||
:param box: An optional 4-tuple of floats providing
|
||||
the source image region to be scaled.
|
||||
The values must be within (0, 0, width, height) rectangle.
|
||||
If omitted or None, the entire source is used.
|
||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||
"""
|
||||
|
@ -1824,6 +1824,39 @@ class Image:
|
|||
|
||||
return self._new(self.im.resize(size, resample, box))
|
||||
|
||||
def reduce(self, factor, box=None):
|
||||
"""
|
||||
Returns a copy of the image reduced by `factor` times.
|
||||
If the size of the image is not dividable by the `factor`,
|
||||
the resulting size will be rounded up.
|
||||
|
||||
:param factor: A greater than 0 integer or tuple of two integers
|
||||
for width and height separately.
|
||||
:param box: An optional 4-tuple of ints providing
|
||||
the source image region to be reduced.
|
||||
The values must be within (0, 0, width, height) rectangle.
|
||||
If omitted or None, the entire source is used.
|
||||
"""
|
||||
if not isinstance(factor, (list, tuple)):
|
||||
factor = (factor, factor)
|
||||
|
||||
if box is None:
|
||||
box = (0, 0) + self.size
|
||||
else:
|
||||
box = tuple(box)
|
||||
|
||||
if factor == (1, 1) and box == (0, 0) + self.size:
|
||||
return self.copy()
|
||||
|
||||
if self.mode in ["LA", "RGBA"]:
|
||||
im = self.convert(self.mode[:-1] + "a")
|
||||
im = im.reduce(factor, box)
|
||||
return im.convert(self.mode)
|
||||
|
||||
self.load()
|
||||
|
||||
return self._new(self.im.reduce(factor, box))
|
||||
|
||||
def rotate(
|
||||
self,
|
||||
angle,
|
||||
|
|
|
@ -1787,6 +1787,51 @@ _resize(ImagingObject* self, PyObject* args)
|
|||
return PyImagingNew(imOut);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
_reduce(ImagingObject* self, PyObject* args)
|
||||
{
|
||||
Imaging imIn;
|
||||
Imaging imOut;
|
||||
|
||||
int xscale, yscale;
|
||||
int box[4] = {0, 0, 0, 0};
|
||||
|
||||
imIn = self->image;
|
||||
box[2] = imIn->xsize;
|
||||
box[3] = imIn->ysize;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "(ii)|(iiii)", &xscale, &yscale,
|
||||
&box[0], &box[1], &box[2], &box[3]))
|
||||
return NULL;
|
||||
|
||||
if (xscale < 1 || yscale < 1) {
|
||||
return ImagingError_ValueError("scale must be > 0");
|
||||
}
|
||||
|
||||
if (box[0] < 0 || box[1] < 0) {
|
||||
return ImagingError_ValueError("box offset can't be negative");
|
||||
}
|
||||
|
||||
if (box[2] > imIn->xsize || box[3] > imIn->ysize) {
|
||||
return ImagingError_ValueError("box can't exceed original image size");
|
||||
}
|
||||
|
||||
if (box[2] <= box[0] || box[3] <= box[1]) {
|
||||
return ImagingError_ValueError("box can't be empty");
|
||||
}
|
||||
|
||||
if (xscale == 1 && yscale == 1) {
|
||||
imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]);
|
||||
} else {
|
||||
// Change box format: (left, top, width, height)
|
||||
box[2] -= box[0];
|
||||
box[3] -= box[1];
|
||||
imOut = ImagingReduce(imIn, xscale, yscale, box);
|
||||
}
|
||||
|
||||
return PyImagingNew(imOut);
|
||||
}
|
||||
|
||||
|
||||
#define IS_RGB(mode)\
|
||||
(!strcmp(mode, "RGB") || !strcmp(mode, "RGBA") || !strcmp(mode, "RGBX"))
|
||||
|
@ -1801,7 +1846,7 @@ im_setmode(ImagingObject* self, PyObject* args)
|
|||
char* mode;
|
||||
Py_ssize_t modelen;
|
||||
if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen))
|
||||
return NULL;
|
||||
return NULL;
|
||||
|
||||
im = self->image;
|
||||
|
||||
|
@ -3235,6 +3280,7 @@ static struct PyMethodDef methods[] = {
|
|||
{"rankfilter", (PyCFunction)_rankfilter, 1},
|
||||
#endif
|
||||
{"resize", (PyCFunction)_resize, 1},
|
||||
{"reduce", (PyCFunction)_reduce, 1},
|
||||
{"transpose", (PyCFunction)_transpose, 1},
|
||||
{"transform2", (PyCFunction)_transform2, 1},
|
||||
|
||||
|
|
|
@ -313,6 +313,7 @@ extern Imaging ImagingRotate270(Imaging imOut, Imaging imIn);
|
|||
extern Imaging ImagingTranspose(Imaging imOut, Imaging imIn);
|
||||
extern Imaging ImagingTransverse(Imaging imOut, Imaging imIn);
|
||||
extern Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]);
|
||||
extern Imaging ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]);
|
||||
extern Imaging ImagingTransform(
|
||||
Imaging imOut, Imaging imIn, int method, int x0, int y0, int x1, int y1,
|
||||
double *a, int filter, int fill);
|
||||
|
|
1438
src/libImaging/Reduce.c
Normal file
1438
src/libImaging/Reduce.c
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user