Adds SmartCrop resize processor, with tests.

This commit is contained in:
FI$H 2000 2011-11-09 16:23:04 -05:00 committed by Matthew Tretter
parent 2dad6fb88d
commit 7ce43309ad
6 changed files with 118 additions and 10 deletions

View file

@ -1,11 +1,12 @@
# Required PIL classes may or may not be available from the root namespace
# depending on the installation method used.
try:
from PIL import Image, ImageColor, ImageEnhance, ImageFile, ImageFilter
from PIL import Image, ImageColor, ImageChops, ImageEnhance, ImageFile, ImageFilter
except ImportError:
try:
import Image
import ImageColor
import ImageChops
import ImageEnhance
import ImageFile
import ImageFilter

View file

@ -1,5 +1,6 @@
from imagekit.lib import Image
import math
from imagekit.lib import Image
class _Resize(object):
width = None
@ -121,3 +122,87 @@ class Fit(_Resize):
return img
img = img.resize(new_dimensions, Image.ANTIALIAS)
return img
def histogram_entropy(im):
"""
Calculate the entropy of an images' histogram. Used for "smart cropping" in easy-thumbnails;
see: https://raw.github.com/SmileyChris/easy-thumbnails/master/easy_thumbnails/utils.py
"""
if not isinstance(im, Image.Image):
return 0 # Fall back to a constant entropy.
histogram = im.histogram()
hist_ceil = float(sum(histogram))
histonorm = [histocol / hist_ceil for histocol in histogram]
return -sum([p * math.log(p, 2) for p in histonorm if p != 0])
class SmartCrop(_Resize):
"""
Crop an image 'smartly' -- based on smart crop implementation from easy-thumbnails:
https://github.com/SmileyChris/easy-thumbnails/blob/master/easy_thumbnails/processors.py#L193
Smart cropping whittles away the parts of the image with the least entropy.
"""
def __init__(self, width=None, height=None):
super(SmartCrop, self).__init__(width, height)
def compare_entropy(self, start_slice, end_slice, slice, difference):
"""
Calculate the entropy of two slices (from the start and end of an axis),
returning a tuple containing the amount that should be added to the start
and removed from the end of the axis.
"""
start_entropy = histogram_entropy(start_slice)
end_entropy = histogram_entropy(end_slice)
if end_entropy and abs(start_entropy / end_entropy - 1) < 0.01:
# Less than 1% difference, remove from both sides.
if difference >= slice * 2:
return slice, slice
half_slice = slice // 2
return half_slice, slice - half_slice
if start_entropy > end_entropy:
return 0, slice
else:
return slice, 0
def process(self, img):
source_x, source_y = img.size
diff_x = int(source_x - min(source_x, self.width))
diff_y = int(source_y - min(source_y, self.height))
left = top = 0
right, bottom = source_x, source_y
while diff_x:
slice = min(diff_x, max(diff_x // 5, 10))
start = img.crop((left, 0, left + slice, source_y))
end = img.crop((right - slice, 0, right, source_y))
add, remove = self.compare_entropy(start, end, slice, diff_x)
left += add
right -= remove
diff_x = diff_x - add - remove
while diff_y:
slice = min(diff_y, max(diff_y // 5, 10))
start = img.crop((0, top, source_x, top + slice))
end = img.crop((0, bottom - slice, source_x, bottom))
add, remove = self.compare_entropy(start, end, slice, diff_y)
top += add
bottom -= remove
diff_y = diff_y - add - remove
box = (left, top, right, bottom)
img = img.crop(box)
return img

BIN
tests/core/assets/Lenna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View file

@ -9,13 +9,17 @@ from imagekit import utils
from imagekit.lib import Image
from imagekit.models import ImageSpec
from imagekit.processors import Adjust
from imagekit.processors.resize import Crop
from imagekit.processors.resize import Crop, SmartCrop
class Photo(models.Model):
original_image = models.ImageField(upload_to='photos')
thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), Crop(50, 50)],
image_field='original_image', format='JPEG', quality=90)
smartcropped_thumbnail = ImageSpec([Adjust(contrast=1.2, sharpness=1.1), SmartCrop(50, 50)],
image_field='original_image', format='JPEG', quality=90)
class IKTest(TestCase):
@ -24,32 +28,50 @@ class IKTest(TestCase):
Image.new('RGB', (800, 600)).save(tmp, 'JPEG')
tmp.seek(0)
return tmp
def generate_lenna(self):
"""
See also:
http://en.wikipedia.org/wiki/Lenna
http://sipi.usc.edu/database/database.php?volume=misc&image=12
"""
tmp = tempfile.TemporaryFile()
lennapath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets', 'lenna-800x600-white-border.jpg')
with open(lennapath, "r+b") as lennafile:
Image.open(lennafile).save(tmp, 'JPEG')
tmp.seek(0)
return tmp
def setUp(self):
self.photo = Photo()
img = self.generate_image()
img = self.generate_lenna()
file = ContentFile(img.read())
self.photo.original_image = file
self.photo.original_image.save('test.jpeg', file)
self.photo.save()
img.close()
def test_save_image(self):
photo = Photo.objects.get(id=self.photo.id)
self.assertTrue(os.path.isfile(photo.original_image.path))
def test_setup(self):
self.assertEqual(self.photo.original_image.width, 800)
self.assertEqual(self.photo.original_image.height, 600)
def test_thumbnail_creation(self):
photo = Photo.objects.get(id=self.photo.id)
self.assertTrue(os.path.isfile(photo.thumbnail.file.name))
def test_thumbnail_size(self):
""" Explicit and smart-cropped thumbnail size """
self.assertEqual(self.photo.thumbnail.width, 50)
self.assertEqual(self.photo.thumbnail.height, 50)
self.assertEqual(self.photo.smartcropped_thumbnail.width, 50)
self.assertEqual(self.photo.smartcropped_thumbnail.height, 50)
def test_thumbnail_source_file(self):
self.assertEqual(
self.photo.thumbnail.source_file, self.photo.original_image)