[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--