Merge pull request #994 from homm/add-transpose

Add transpose and cache aware rotation
This commit is contained in:
wiredfool 2014-11-07 13:58:18 -08:00
commit cfbe49f124
5 changed files with 275 additions and 126 deletions

View File

@ -150,6 +150,7 @@ FLIP_TOP_BOTTOM = 1
ROTATE_90 = 2
ROTATE_180 = 3
ROTATE_270 = 4
TRANSPOSE = 5
# transforms
AFFINE = 0
@ -1920,13 +1921,13 @@ class Image:
:param method: One of :py:attr:`PIL.Image.FLIP_LEFT_RIGHT`,
:py:attr:`PIL.Image.FLIP_TOP_BOTTOM`, :py:attr:`PIL.Image.ROTATE_90`,
:py:attr:`PIL.Image.ROTATE_180`, or :py:attr:`PIL.Image.ROTATE_270`.
:py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270` or
:py:attr:`PIL.Image.TRANSPOSE`.
:returns: Returns a flipped or rotated copy of this image.
"""
self.load()
im = self.im.transpose(method)
return self._new(im)
return self._new(self.im.transpose(method))
def effect_spread(self, distance):
"""

View File

@ -1,30 +1,114 @@
from helper import unittest, PillowTestCase, hopper
from PIL import Image
FLIP_LEFT_RIGHT = Image.FLIP_LEFT_RIGHT
FLIP_TOP_BOTTOM = Image.FLIP_TOP_BOTTOM
ROTATE_90 = Image.ROTATE_90
ROTATE_180 = Image.ROTATE_180
ROTATE_270 = Image.ROTATE_270
from PIL.Image import (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90, ROTATE_180,
ROTATE_270, TRANSPOSE)
class TestImageTranspose(PillowTestCase):
def test_sanity(self):
hopper = {
'L': hopper('L').crop((0, 0, 121, 127)).copy(),
'RGB': hopper('RGB').crop((0, 0, 121, 127)).copy(),
}
im = hopper()
def test_flip_left_right(self):
def transpose(mode):
im = self.hopper[mode]
out = im.transpose(FLIP_LEFT_RIGHT)
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size)
im.transpose(FLIP_LEFT_RIGHT)
im.transpose(FLIP_TOP_BOTTOM)
x, y = im.size
self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, 1)))
self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, y-2)))
im.transpose(ROTATE_90)
im.transpose(ROTATE_180)
im.transpose(ROTATE_270)
for mode in ("L", "RGB"):
transpose(mode)
def test_flip_top_bottom(self):
def transpose(mode):
im = self.hopper[mode]
out = im.transpose(FLIP_TOP_BOTTOM)
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size)
x, y = im.size
self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, y-2)))
self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((x-2, y-2)))
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1)))
for mode in ("L", "RGB"):
transpose(mode)
def test_rotate_90(self):
def transpose(mode):
im = self.hopper[mode]
out = im.transpose(ROTATE_90)
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size[::-1])
x, y = im.size
self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, x-2)))
self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, x-2)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1)))
for mode in ("L", "RGB"):
transpose(mode)
def test_rotate_180(self):
def transpose(mode):
im = self.hopper[mode]
out = im.transpose(ROTATE_180)
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size)
x, y = im.size
self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, y-2)))
self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, y-2)))
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1)))
for mode in ("L", "RGB"):
transpose(mode)
def test_rotate_270(self):
def transpose(mode):
im = self.hopper[mode]
out = im.transpose(ROTATE_270)
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size[::-1])
x, y = im.size
self.assertEqual(im.getpixel((1, 1)), out.getpixel((y-2, 1)))
self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((y-2, x-2)))
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2)))
for mode in ("L", "RGB"):
transpose(mode)
def test_transpose(self):
def transpose(mode):
im = self.hopper[mode]
out = im.transpose(TRANSPOSE)
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size[::-1])
x, y = im.size
self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, x-2)))
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2)))
for mode in ("L", "RGB"):
transpose(mode)
def test_roundtrip(self):
im = hopper()
im = self.hopper['L']
def transpose(first, second):
return im.transpose(first).transpose(second)
@ -33,12 +117,13 @@ class TestImageTranspose(PillowTestCase):
im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT))
self.assert_image_equal(
im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM))
self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270))
self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180))
self.assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM))
self.assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT))
if __name__ == '__main__':
unittest.main()
# End of file

View File

@ -1750,6 +1750,7 @@ _transpose(ImagingObject* self, PyObject* args)
break;
case 2: /* rotate 90 */
case 4: /* rotate 270 */
case 5: /* transpose */
imOut = ImagingNew(imIn->mode, imIn->ysize, imIn->xsize);
break;
default:
@ -1774,6 +1775,9 @@ _transpose(ImagingObject* self, PyObject* args)
case 4:
(void) ImagingRotate270(imOut, imIn);
break;
case 5:
(void) ImagingTranspose(imOut, imIn);
break;
}
return PyImagingNew(imOut);

View File

@ -30,6 +30,13 @@
/* Undef if you don't need resampling filters */
#define WITH_FILTERS
/* For large images rotation is an inefficient operation in terms of CPU cache.
One row in the source image affects each column in destination.
Rotating in chunks that fit in the cache can speed up rotation
8x on a modern CPU. A chunk size of 128 requires only 65k and is large enough
that the overhead from the extra loops are not apparent. */
#define ROTATE_CHUNK 128
#define COORD(v) ((v) < 0.0 ? -1 : ((int)(v)))
#define FLOOR(v) ((v) < 0.0 ? ((int)floor(v)) : ((int)(v)))
@ -98,7 +105,7 @@ Imaging
ImagingRotate90(Imaging imOut, Imaging imIn)
{
ImagingSectionCookie cookie;
int x, y, xr;
int x, y, xx, yy, xr, xxsize, yysize;
if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0)
return (Imaging) ImagingError_ModeError();
@ -107,11 +114,18 @@ ImagingRotate90(Imaging imOut, Imaging imIn)
ImagingCopyInfo(imOut, imIn);
#define ROTATE_90(image)\
for (y = 0; y < imIn->ysize; y++) {\
xr = imIn->xsize-1;\
for (x = 0; x < imIn->xsize; x++, xr--)\
imOut->image[xr][y] = imIn->image[y][x];\
#define ROTATE_90(image) \
for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \
for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \
yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \
xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \
for (yy = y; yy < yysize; yy++) { \
xr = imIn->xsize - 1 - x; \
for (xx = x; xx < xxsize; xx++, xr--) { \
imOut->image[xr][yy] = imIn->image[yy][xx]; \
} \
} \
} \
}
ImagingSectionEnter(&cookie);
@ -127,6 +141,43 @@ ImagingRotate90(Imaging imOut, Imaging imIn)
}
Imaging
ImagingTranspose(Imaging imOut, Imaging imIn)
{
ImagingSectionCookie cookie;
int x, y, xx, yy, xxsize, yysize;
if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0)
return (Imaging) ImagingError_ModeError();
if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize)
return (Imaging) ImagingError_Mismatch();
#define TRANSPOSE(image) \
for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \
for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \
yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \
xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \
for (yy = y; yy < yysize; yy++) { \
for (xx = x; xx < xxsize; xx++) { \
imOut->image[xx][yy] = imIn->image[yy][xx]; \
} \
} \
} \
}
ImagingCopyInfo(imOut, imIn);
ImagingSectionEnter(&cookie);
if (imIn->image8)
TRANSPOSE(image8)
else
TRANSPOSE(image32)
ImagingSectionLeave(&cookie);
return imOut;
}
Imaging
ImagingRotate180(Imaging imOut, Imaging imIn)
{
@ -166,7 +217,7 @@ Imaging
ImagingRotate270(Imaging imOut, Imaging imIn)
{
ImagingSectionCookie cookie;
int x, y, yr;
int x, y, xx, yy, yr, xxsize, yysize;
if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0)
return (Imaging) ImagingError_ModeError();
@ -175,12 +226,19 @@ ImagingRotate270(Imaging imOut, Imaging imIn)
ImagingCopyInfo(imOut, imIn);
yr = imIn->ysize - 1;
#define ROTATE_270(image)\
for (y = 0; y < imIn->ysize; y++, yr--)\
for (x = 0; x < imIn->xsize; x++)\
imOut->image[x][y] = imIn->image[yr][x];
#define ROTATE_270(image) \
for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \
for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \
yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \
xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \
yr = imIn->ysize - 1 - y; \
for (yy = y; yy < yysize; yy++, yr--) { \
for (xx = x; xx < xxsize; xx++) { \
imOut->image[xx][yr] = imIn->image[yy][xx]; \
} \
} \
} \
}
ImagingSectionEnter(&cookie);

View File

@ -292,6 +292,7 @@ extern Imaging ImagingRotate90(Imaging imOut, Imaging imIn);
extern Imaging ImagingRotate180(Imaging imOut, Imaging imIn);
extern Imaging ImagingRotate270(Imaging imOut, Imaging imIn);
extern Imaging ImagingStretch(Imaging imOut, Imaging imIn, int filter);
extern Imaging ImagingTranspose(Imaging imOut, Imaging imIn);
extern Imaging ImagingTransformPerspective(
Imaging imOut, Imaging imIn, int x0, int y0, int x1, int y1,
double a[8], int filter, int fill);