mirror of
https://github.com/Hopiu/django-imagekit.git
synced 2026-03-17 05:40:25 +00:00
Adds SmartCrop resize processor, with tests.
This commit is contained in:
parent
2dad6fb88d
commit
7ce43309ad
6 changed files with 118 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
BIN
tests/core/assets/Lenna.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
BIN
tests/core/assets/lenna-800x600-white-border.jpg
Normal file
BIN
tests/core/assets/lenna-800x600-white-border.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
BIN
tests/core/assets/lenna-800x600.jpg
Normal file
BIN
tests/core/assets/lenna-800x600.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue