Skip to content

Commit

Permalink
Move SmartCrop to crop module
Browse files Browse the repository at this point in the history
Addresses one of the issues raised by in @madisvain in #93
  • Loading branch information
matthewwithanm committed Feb 11, 2012
1 parent a041302 commit 09b97ee
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 84 deletions.
68 changes: 68 additions & 0 deletions imagekit/processors/crop.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ..lib import Image, ImageChops, ImageDraw, ImageStat
from .utils import histogram_entropy


class Side(object):
Expand Down Expand Up @@ -69,3 +70,70 @@ def process(self, img):
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
91 changes: 8 additions & 83 deletions imagekit/processors/resize.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import math

from imagekit.lib import Image
from .crop import SmartCrop as _SmartCrop
import warnings


class Crop(object):
Expand Down Expand Up @@ -118,84 +118,9 @@ def process(self, img):
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)
18 changes: 18 additions & 0 deletions imagekit/processors/utils.py
Original file line number Diff line number Diff line change
@@ -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])
3 changes: 2 additions & 1 deletion tests/core/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
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):
Expand Down

0 comments on commit 09b97ee

Please sign in to comment.