[Image-SIG] Conversion from RGB(A) to P(A)

Ray Pasco pascor at hotpop.com
Mon Nov 15 23:40:58 CET 2004


This isn't a question, but an answer.  After getting frustrated that 
saving a low color count
RGB or RGBA image to a paletted P file would cause color dithering and
loss of any transparency (alpha) info, I wrote a routine that would 
create an "exact
palette" for RGB(A) images that started out with 256 or less colors.  No 
dithering.
Also, if a RGBA image is passed in, all pixels that have alpha values 
less than 255
("active" alpha) will be transformed into a single "transparent" color 
in the P file.  If the input
RGB(A) has more than 256 colors, the default PIL palette/color dithering 
will be used,
but "active" alpha pixels will still become transparent

This isn't optimized or particularly pretty code, but gets the job done.
The attached file calls for various types of PNG RGB(A) images,  so 
substitute
your own that meet the description at the top of the blocks of test code.

Ray Pasco
-------------- next part --------------

import Image
import sys, os
from listpalette import ListPalette


def RgbHistogram (imgRgb, verbose=False):
    hist = None

    # Form a histogram dictionary whose keys are the repr(color)
    if verbose:    print 'RgbHistogram:  Getting datalist ...'
    datalist = list (imgRgb.getdata())
    if verbose:    print '               ... finished'

    dicthist = {}
    xsize, ysize = imgRgb.size
    numcolors = 0
    for i in xrange (xsize * ysize):
        color = datalist [i]
        key = repr (color)

        if dicthist.has_key (key):  # color already exists
            dicthist [key] += 1     # increment the count
        else:                       # make a new key
            dicthist [key] = 1      # instantiate a new entry and init the count
            numcolors += 1

            if numcolors > 256:
                return None         # Error flag:  use PIL default color palette/dithering
            #end if
        #end if
    #end for

    # reform the dictionary into a sorted histogram of the form: (count, (r, g, b))

    hist = []
    for key in dicthist.iterkeys():
        count = dicthist [key]
        color = eval (key)
        hist.append ( (count, color) )
    #end for

    hist.sort()
    hist.reverse()           # make largest counts first

    return hist

#end def RgbHistogram

def Getalphaindex (imgP, maskinv):

    # Find the least used color (palette entry, actually)
    # This will be the color to which transparency will be set when saving the file

    xsize, ysize = imgP.size
    hist = imgP.histogram (maskinv)     # get counts for all colors having non-active alphas

    indexleastused = 255                # arbitrary starting least used palette index
    leastcount = xsize * ysize          # max possible count
    for i in xrange (len (hist)):       # palette size
        if hist [i] < leastcount:
            leastcount = hist [i]       # the count
            indexleastused = i          # the palette index
        #end if

        if hist [i] == 0:    break      # first 0 entry: done
    #end if

    return (indexleastused, leastcount)

#end def Getalphaindex

def Rgb2p (imgRgb, verbose=False):     # image could be a "RGBA"
    verbose = False

    imgP = None
    datalist = None

    size = imgRgb.size
    xsize = size [0]
    ysize = size [1]

    if verbose:
        print
        print 'xsize, ysize =', xsize, ysize
    #end if

    hasalpha = False
    if imgRgb.mode == 'RGBA':
        hasalpha = True                            # for post=processing to create transparency
        if verbose:    print 'Rgb2p:  Input image is RGBA'

        # Create a mask and its inverse
        source = imgRgb.split()
        R, G, B, A = 0, 1, 2, 3         # band indices
        mask    = source [A].point (lambda i: i <  255 and 255)     # = True on active alphas
        maskinv = source [A].point (lambda i: i == 255        )     # = True on inactive alphas
    #end if

    # find the most popular colors, limiting the max number to 256
    # any "excess" colors with be transformed later to the closest palette match

    # create a basic palette and stuff it with the found colors
    palette = []                        # will be [0, 0, 0, ... 255, 255, 255]
    for i in xrange (256):
        palette.append (i); palette.append (i); palette.append (i) 
    #end for

    hist = RgbHistogram (imgRgb, verbose=verbose)

    if hist == None:                        # colors > 256:  use PIL dithered image & palette
        if verbose:    print 'Number of colors > 256:  Using PIL dithered palette.'
        imgP = imgRgb.convert ('P')

        if hasalpha:
            indexleastused, leastcount = Getalphaindex (imgP, maskinv)
            if verbose:
                print '\nHigh color-count image:   least used color index, leastcount =', indexleastused, leastcount
            #end if

            return (imgP, indexleastused, mask)
        else:
            return (imgP)
        #end if
    #end if

    # Make two lists of the colors.
    colors = []
    colorsAndIndices = []
    for i in xrange (len (hist)):
        if hasalpha:
            r, g, b, a = hist [i][1]            # pick off the color tuple
        else:
            r, g, b = hist [i][1]               # pick off the color tuple
        #end if

        palette [i*3 + 0] = r
        palette [i*3 + 1] = g
        palette [i*3 + 2] = b

        colors.append (hist [i][1])                     # [color_tuple, color_tuple, ...]
        colorsAndIndices.append ( (hist[i][1], i) )     # [(color_tuple, palette_index), ...]
    #end for

    imgP = Image.new ('P', size)            # Create a brand new paletted image
    imgP.putpalette (palette)               # Install the palette

    # Rewrite the entire image using new palette's indices.
    if verbose:    print 'Defining the new image using the newly created palette ...'

    # Each pixel gets a palette color index
    if datalist == None:
        datalist = list (imgRgb.getdata())      # xsize*ysize list of color 3-tuples
    #end if

    pxlctr = 0
    colctr = 0
    for yord in xrange (ysize):
        for xord in xrange (xsize):
            pxlcolor = datalist [yord*xsize + xord]     # org image color tuple

            try:
                index = colors.index (pxlcolor)
                paletteindex = colorsAndIndices [index] [1]     # a simple lookup
            except:
                print '###  RGB2P.py:INTERNAL ERROR:   index =', index,
                print  'of color', pxlcolor, 'NOT found in colors =', colors
                sys.exit (1)
            #end try

            imgP.putpixel ((xord, yord), paletteindex)

            # A simple progress indicator:
            pxlctr += 1
            if verbose and ((pxlctr % 1000) == 0):
                valstr = '%8i' % pxlctr
                print valstr,

                colctr += 1
                if colctr >= 8:
                    colctr = 0
                    print
                #end if
            #end if
        #end for xord
    #end for yord
    if verbose:    print

    if hasalpha:
        indexleastused, leastcount = Getalphaindex (imgP, maskinv)
        if verbose:
            print
            print 'Low color-count image:   least used color index, leastcount =', indexleastused, leastcount
        #end if

        return (imgP, indexleastused, mask)
    else:
        return (imgP)
    #end if

#end def Rgb2p

#====================================================================

if __name__ == '__main__':

    if 1:           # low color, no active alpha ;squares inside squares
        imgRgb = Image.new ('RGB', (100, 100))
        imgRgb.paste ( (185, 85, 0), (0, 0,   100, 100) )       # red
        imgRgb.paste ( (0, 185, 0), (15, 15,   85, 85) )        # green
        imgRgb.paste ( (0, 85, 185), (30,  30,    70, 70) )     # blue
        #imgRgb.save ('THREECOLOR.RGB.png')

        imgP = Rgb2p (imgRgb, verbose=True)     # I know only a 1-tuple will be returned
        imgP.save ('THREECOLOR.P.png')
    #end if

    if 1:           # high color count, no active alpha, large spatial gradients
        ifilename = 'AURORA.RGB.png'
        ofilename = 'AURORA.P.png'

        imgRgb = Image.open (ifilename)
        imgP = Rgb2p (imgRgb, verbose=True)     # I know only a 1-tuple will be returned
        imgP.save (ofilename)
    #end if

    if 1:           # high color count, no active alpha, small spatial gradients
        ifilename = 'LORI.jpg'
        ofilename = 'LORI.P.png'

        imgRgb = Image.open (ifilename)
        imgP = Rgb2p (imgRgb, verbose=True)     # I know only a 1-tuple will be returned
        imgP.save (ofilename)
    #end if

    if 1:           # high color count, active & varying alpha, small spatial gradients
        ifilename = 'TRANS.RGB.png'
        ofilename = 'TRANS.PA.png'

        imgRgba = Image.open (ifilename)
        imgP = Rgb2p (imgRgba, verbose=True)    # I know a 3-tuple will be returned

        if len (imgP) == 3:
            imgP, indexleastused, mask = imgP
            print
            print 'An alpha-tized paletted image has been returned for image [%s]' % ifilename

            xsize, ysize = imgP.size
            imgP.paste (indexleastused, (0, 0, xsize, ysize), mask)
            imgP.save (ofilename, transparency=indexleastused)
        #end if
    #end if

    if 1:           # low color, active alpha ;squares inside squares
        imgRgba = Image.new ('RGBA', (100, 100))
        imgRgba.paste ( (185,  85,   0, 255), (0, 0,   100, 100) )       # red
        imgRgba.paste ( (0,   185,   0, 255), (15, 15,   85, 85) )       # green
        imgRgba.paste ( (0,    85, 185,   0), (30,  30,    70, 70) )     # blue
        #imgRgba.save ('THREECOLOR.RGBA.png')

        imgP = Rgb2p (imgRgba, verbose=True)    # I know a 3-tuple will be returned

        if len (imgP) == 3:
            imgP, indexleastused, mask = imgP
            print
            print 'An alpha-tized paletted image has been returned for image [%s]' % 'THREECOLOR.RGBA.png'

            xsize, ysize = imgP.size
            imgP.paste (indexleastused, (0, 0, xsize, ysize), mask)
            imgP.save ('THREECOLOR.PA.png', transparency=indexleastused)
        #end if
    #end if

#end if



More information about the Image-SIG mailing list