diff --git a/imagekit/processors/crop.py b/imagekit/processors/crop.py index aca43d3..97e3ac0 100644 --- a/imagekit/processors/crop.py +++ b/imagekit/processors/crop.py @@ -1,4 +1,5 @@ from ..lib import Image, ImageChops, ImageDraw, ImageStat +from .utils import histogram_entropy class Side(object): @@ -69,3 +70,70 @@ class TrimBorderColor(object): img = crop(img, bbox, self.sides) return img + + +class SmartCrop(object): + """ + 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): + self.width = width + self.height = 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 diff --git a/imagekit/processors/resize.py b/imagekit/processors/resize.py index 477671d..09d2487 100644 --- a/imagekit/processors/resize.py +++ b/imagekit/processors/resize.py @@ -1,6 +1,6 @@ -import math - from imagekit.lib import Image +from .crop import SmartCrop as _SmartCrop +import warnings class Crop(object): @@ -118,84 +118,9 @@ class Fit(object): 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(object): - """ - 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): - self.width = width - self.height = 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 +class SmartCrop(_SmartCrop): + def __init__(self, *args, **kwargs): + warnings.warn('The SmartCrop processor has been moved to' + ' `imagekit.processors.crop.SmartCrop`, where it belongs.', + DeprecationWarning) + super(SmartCrop, self).__init__(*args, **kwargs) diff --git a/imagekit/processors/utils.py b/imagekit/processors/utils.py new file mode 100644 index 0000000..db244db --- /dev/null +++ b/imagekit/processors/utils.py @@ -0,0 +1,18 @@ +import math +from imagekit.lib import Image + + +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]) diff --git a/tests/core/tests.py b/tests/core/tests.py index 55bc70d..91ca847 100644 --- a/tests/core/tests.py +++ b/tests/core/tests.py @@ -11,7 +11,8 @@ 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, SmartCrop +from imagekit.processors.resize import Crop +from imagekit.processors.crop import SmartCrop class Photo(models.Model):