[Image-SIG] Fixes for PIL enhancers
Robb Shecter
rs@onsitetech.com
Mon, 06 Jan 2003 15:40:16 -0800
This is a multi-part message in MIME format.
--------------090100070507030306020803
Content-Type: text/plain; charset=us-ascii; format=flowed
Content-Transfer-Encoding: 7bit
Dinu Gherman wrote:
> ...I just found that ImageEnhance.Brightness(img).enhance(f) would not
> intensify
> an image for f > 1, but only dim it down for 0 <= f < 1...
Yep! That's what got me started. Here's what I've come up with. It's
working in a production app, too. It has a lot features for
introspection and meta-data in order to be able to support flexible use
like this:
http://wiki.wxpython.org/index.cgi/RobbShecter?action=AttachFile&do=view&target=image-editor.jpg
I also have dialogs, for example, that automatically build themselves
with appropriately created sliders for all Enhancers that accept an
integer parameter.
Any suggestions (or more Enhancer implementations!) are appreciated.
Robb
--------------090100070507030306020803
Content-Type: text/plain;
name="Enhance.py"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="Enhance.py"
#
# ui.image.Enhance
#
import Image, ImageFilter, ImageChops
class ScaleTranslator(object):
"""
A class that translates linearly between two scales. It works
by treating the start points as a 2D point and the end points
as a 2D point. It then solves the equation for the line that
connects these points (y = mx + b). This line, then, becomes
the mapping from one scale to another; every (x,y) pair along
it is effectively a mapping from (oldscale,newscale).
Here's how this class is used: say you have a wxSlider which
gives values from 1 to 25. But you want these converted to
work with a library that expects values between -1 and 1:
>>> st = ScaleTranslator(1, 25, -1, 1)
To find the translated equivalent of a number like 6, you'd do:
>>> st.translate(6)
-0.58333333333333326
"""
def __init__(self, scale1Start, scale1End, scale2Start, scale2End):
"""
Instantiate a ScaleTranslator that converts from scale 1 to
scale 2.
"""
# Solve the equation by calculating m and b.
x1 = scale1Start
y1 = scale2Start
x2 = scale1End
y2 = scale2End
self.__m = (float(y2) - y1) / (x2 - x1)
self.__b = y1 - (self.__m * x1)
def translate(self, aNumber):
""" Return the equivalent of aNumber in the new scale. Do it
by calculating (y = mx + b), where aNumber is x."""
return self.__m * aNumber + self.__b
class Enhancer(object):
"""
An abstract base class for PIL image enhancers.
"""
__scale = ScaleTranslator(-100,100,-1,1)
def __init__(self, pilImage):
self.__mode = pilImage.mode
if self.__mode != 'RGB':
self.__image = pilImage.convert('RGB')
else:
self.__image = pilImage
self.__lastValue = 0
self.__less = None
self.__more = None
#
# Public API
#
def getValue(self):
"""
Return the enhancement value of the image produced
by the last call to enhance().
"""
return self.__lastValue
def enhance(self, value):
"""
value is in the range [-100,100].
"""
newImage = self.__enhance(value)
# Commented out, because this conversion is not
# done very well.
#if newImage.mode != self.__mode:
# newImage = newImage.convert(self.__mode)
return newImage
#
# Metadata API
#
def getInfo():
"""
Return information about the currently imported
enhancers. This returns a dict of dicts, initially
keyed on the human-readable name for each enhancer.
"""
if not hasattr(Enhancer, 'metadata'):
subclasses = Enhancer.__subclasses__()
info = {}
for aClass in subclasses:
info[aClass.getName()] = {'class': aClass,
'domain': aClass.getDomain()}
Enhancer.metadata = info
return Enhancer.metadata
def getEnhancers(domain):
"""
Return a list of enhancers that accept values in the
given domain. This will match domains specified either
as a general or specific domain. (More docs coming.)
"""
info = Enhancer.getInfo()
if len(domain) == 1:
l = filter(lambda i: i[1]['domain'][0] == domain[0], info.items())
else:
l = filter(lambda i: i[1]['domain'] == domain, info.items())
return dict(l)
getInfo = staticmethod(getInfo)
getEnhancers = staticmethod(getEnhancers)
def __enhance(self, value):
from util.Math import limitToRange
self.__lastValue = value
# 1. See if the rawEnhance protocol has
# been implemented:
if hasattr(self, 'rawEnhance'):
return self.rawEnhance(self.__image, value)
# 2. Continue on with the more complex protocol:
domain = self.__class__.domain
if domain[0] == int:
min = domain[1][0]
max = domain[1][1]
if value != limitToRange(value, min, max):
raise ValueError('enhancement value not within [%d, %d]' % (min, max))
if value == 0 and (min == -1 * max): # Zero means no change
return self.__image
# To do: Make customized scale translator if necessary.
alpha = self.__class__.__scale.translate(value)
if alpha < 0:
alpha *= -1
if self.__less is None: # Lazily instantiate
self.__less = self.makeDecreasingDegenerate(self.__image)
degenerate = self.__less
else:
if self.__more is None: # Lazily instantiate
self.__more = self.makeIncreasingDegenerate(self.__image)
degenerate = self.__more
return Image.blend(self.__image, degenerate, alpha)
#
# Class methods
#
domain = (int, (-100,100))
def getDomain(myClass):
"""
Return a tuple containing the type of the value that's
required, plus a way of specifying its legal values.
"""
return myClass.domain
def getName(myClass):
"""
Return my human-readable name.
"""
return myClass.name
getDomain = classmethod(getDomain)
getName = classmethod(getName)
#
# Template methods to be implemented by subclasses
#
def makeDecreasingDegenerate(self, aPilImage):
"""
Implemented by subclasses to provide an image
that REDUCES the effect when mixed with the
original.
"""
raise NotImplementedError('Enhancer is an abstract class')
def makeIncreasingDegenerate(self, aPilImage):
"""
Implemented by subclasses to provide an image
that INCREASES the effect when mixed with the
original.
"""
raise NotImplementedError('Enhancer is an abstract class')
#
# Support some standard protocols.
#
def Destroy(self):
self.__less = None
self.__more = None
class ResizeEnhancer(Enhancer):
name = 'Resize Image'
domain = (tuple,)
def rawEnhance(self, image, newSize):
return image.resize(newSize, Image.ANTIALIAS)
class ShrinkToFitEnhancer(Enhancer):
name = 'Shrink To Fit'
domain = (tuple,)
def rawEnhance(self, image, newSize):
# Image.thumbnail() makes changes in place:
newImage = image.copy()
newImage.thumbnail(newSize, Image.ANTIALIAS)
return newImage
class CanvasSizeEnhancer(Enhancer):
# To do: implement for reducing the canvas size.
name = 'Resize Canvas'
domain = (tuple,)
def rawEnhance(self, image, (newSize, bgColor)):
w, h = newSize
W, H = image.size
if w<W or h<H:
print 'CANT MAKE A CANVAS SMALLER'
else:
newImage = Image.new(image.mode, newSize, bgColor)
x = (w - W) / 2
y = (h - H) / 2
newImage.paste(image, (x,y))
return newImage
## class RotateEnhancer(Enhancer):
## """
## Rotate the given number of degrees to the right.
## """
## name = 'Rotate'
## def getDomain(self):
## return ( (int, (-180, 180)) )
## def rawEnhance(self, image, degrees):
## return image.rotate(degrees, Image.BICUBIC)
class DiscreteRotateEnhancer(Enhancer):
"""
Rotate the given number of degrees to
the right.
"""
name = 'Rotate'
domain = ('discrete',
(('Left', 90),
('Left', 180),
('Left', 270),
('Right', 90),
('Right', 180),
('Right', 270)))
def rawEnhance(self, image, direction):
degrees = 360 - direction[1]
if direction[0] == 'Left':
degrees = 360 - degrees
param={90: Image.ROTATE_90,
180: Image.ROTATE_180,
270: Image.ROTATE_270}[degrees]
return image.transpose(param)
class CropEnhancer(Enhancer):
name = 'Crop'
domain = (tuple,)
def rawEnhance(self, image, box):
return image.crop(box)
class ContrastEnhancer(Enhancer):
"""
An enhancer that increases and descreases contrast
in a PIL image.
"""
name = 'Contrast'
def makeDecreasingDegenerate(self, image):
# Make an image where each pixel has a colour
# which is the average grey for the image.
histogram = image.convert("L").histogram()
sum = reduce(lambda a,b: a+b, histogram)
weightedSum = 0
index = 0
for count in histogram:
weightedSum += (count * index)
index += 1
mean = weightedSum / float(sum)
return Image.new("L", image.size, mean).convert(image.mode)
def makeIncreasingDegenerate(self, image):
# Make an image where each color value is shoved to
# the extreme; either 0 or 255.
return image.point(lambda x: x > 128 and 255)
class BrightnessEnhancer(Enhancer):
"""
An enhancer that changes the brightness level of
a PIL image.
"""
name = 'Brightness'
def makeDecreasingDegenerate(self, image):
return Image.new(image.mode, image.size, (0,0,0))
def makeIncreasingDegenerate(self, image):
return Image.new(image.mode, image.size, (255,255,255))
class SharpnessEnhancer(Enhancer):
name = 'Sharpness'
def makeDecreasingDegenerate(self, image):
return image.filter(ImageFilter.SMOOTH_MORE)
def makeIncreasingDegenerate(self, image):
return image.filter(ImageFilter.SHARPEN)
class ColorEnhancer(Enhancer):
name = 'Color Saturation'
def makeDecreasingDegenerate(self, image):
return image.convert("L").convert(image.mode)
def makeIncreasingDegenerate(self, image):
#
# The formula for my algorithm is:
# Overcolored = Image - (alpha * Grey) / (1 - alpha)
#
alpha = 0.7
grey = self.makeDecreasingDegenerate(image)
alpha_g = grey.point(lambda x: x * alpha)
i_minus_alpha_g = ImageChops.subtract(image, alpha_g)
overcolored = i_minus_alpha_g.point(lambda x: x / (1-alpha))
return overcolored
if __name__ == '__main__':
from pprint import *
desc = """This module has classes that make up a framework for
manipulating PIL (Python Image Library) images. It could be
changed, though, to work with any images. These are the current
enhancer types that are available:"""
pprint (desc)
pprint (Enhancer.getInfo())
--------------090100070507030306020803--