How do I convert any image to a 4-color paletted image using the Python Imaging Library?

前端 未结 3 920
谎友^
谎友^ 2020-12-08 03:19

I have a device that supports 4-color graphics (much like CGA in the old days).

I wanted to use PIL to read the image and convert it using my 4-color palette (of red

相关标签:
3条回答
  • 2020-12-08 04:06

    First: your four colour palette (black, green, red, yellow) has no blue component. So, you have to accept that your output image will hardly approximate the input image, unless there is no blue component to start with.

    Try this code:

    import Image
    
    def estimate_color(c, bit, c_error):
        c_new= c -  c_error
        if c_new > 127:
            c_bit= bit
            c_error= 255 - c_new
        else:
            c_bit= 0
            c_error= -c_new
        return c_bit, c_error
    
    def image2cga(im):
        "Produce a sequence of CGA pixels from image im"
        im_width= im.size[0]
        for index, (r, g, b) in enumerate(im.getdata()):
            if index % im_width == 0: # start of a line
                r_error= g_error= 0
            r_bit, r_error= estimate_color(r, 1, r_error)
            g_bit, g_error= estimate_color(g, 2, g_error)
            yield r_bit|g_bit
    
    def cvt2cga(imgfn):
        "Convert an RGB image to (K, R, G, Y) CGA image"
        inp_im= Image.open(imgfn) # assume it's RGB
        out_im= Image.new("P", inp_im.size, None)
        out_im.putpalette( (
            0, 0, 0,
            255, 0, 0,
            0, 255, 0,
            255, 255, 0,
        ) )
        out_im.putdata(list(image2cga(inp_im)))
        return out_im
    
    if __name__ == "__main__":
        import sys, os
    
        for imgfn in sys.argv[1:]:
            im= cvt2cga(imgfn)
            dirname, filename= os.path.split(imgfn)
            name, ext= os.path.splitext(filename)
            newpathname= os.path.join(dirname, "cga-%s.png" % name)
            im.save(newpathname)
    

    This creates a PNG palette image with only the first four palette entries set to your colours. This sample image:

    becomes

    It's trivial to take the output of image2cga (yields a sequence of 0-3 values) and pack every four values to a byte.

    If you need help about what the code does, please ask and I will explain.

    EDIT1: Do not reinvent the wheel

    Of course, turns out I was too enthusiastic and —as Thomas discovered— the Image.quantize method can take a palette image as argument and do the quantization with far better results than my ad-hoc method above:

    def cga_quantize(image):
        pal_image= Image.new("P", (1,1))
        pal_image.putpalette( (0,0,0, 0,255,0, 255,0,0, 255,255,0) + (0,0,0)*252)
        return image.convert("RGB").quantize(palette=pal_image)
    

    EDIT1, cont: Pack the pixels into bytes

    For "added value", here follows code to produce the packed string (4 pixels per byte):

    import itertools as it
    
    # setup: create a map with tuples [(0,0,0,0)‥(3,3,3,3)] as keys
    # and values [chr(0)‥chr(255)], because PIL does not yet support
    # 4 colour palette images
    
    TUPLE2CHAR= {}
    
    # Assume (b7, b6) are pixel0, (b5, b4) are pixel1…
    # Call it "big endian"
    
    KEY_BUILDER= [
        (0, 64, 128, 192), # pixel0 value used as index
        (0, 16, 32, 48), # pixel1
        (0, 4, 8, 12), # pixel2
        (0, 1, 2, 3), # pixel3
    ]
    # For "little endian", uncomment the following line
    ## KEY_BUILDER.reverse()
    
    # python2.6 has itertools.product, but for compatibility purposes
    # let's do it verbosely:
    for ix0, px0 in enumerate(KEY_BUILDER[0]):
        for ix1, px1 in enumerate(KEY_BUILDER[1]):
            for ix2, px2 in enumerate(KEY_BUILDER[2]):
                for ix3, px3 in enumerate(KEY_BUILDER[3]):
                    TUPLE2CHAR[ix0,ix1,ix2,ix3]= chr(px0+px1+px2+px3)
    
    # Another helper function, copied almost verbatim from itertools docs
    def grouper(n, iterable, padvalue=None):
        "grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')"
        return it.izip(*[it.chain(iterable, it.repeat(padvalue, n-1))]*n)
    
    # now the functions
    def seq2str(seq):
        """Takes a sequence of [0..3] values and packs them into bytes
        using two bits per value"""
        return ''.join(
            TUPLE2CHAR[four_pixel]
            for four_pixel in grouper(4, seq, 0))
    
    # and the image related function
    # Note that the following function is correct,
    # but is not useful for Windows 16 colour bitmaps,
    # which start at the *bottom* row…
    def image2str(img):
        return seq2str(img.getdata())
    
    0 讨论(0)
  • 2020-12-08 04:07

    John, I found that first link as well, but it didn't directly help me with the problem. It did make me look deeper into quantize though.

    I came up with this yesterday before going to bed:

    import sys
    
    import PIL
    import Image
    
    PALETTE = [
        0,   0,   0,  # black,  00
        0,   255, 0,  # green,  01
        255, 0,   0,  # red,    10
        255, 255, 0,  # yellow, 11
    ] + [0, ] * 252 * 3
    
    # a palette image to use for quant
    pimage = Image.new("P", (1, 1), 0)
    pimage.putpalette(PALETTE)
    
    # open the source image
    image = Image.open(sys.argv[1])
    image = image.convert("RGB")
    
    # quantize it using our palette image
    imagep = image.quantize(palette=pimage)
    
    # save
    imagep.save('/tmp/cga.png')
    

    TZ.TZIOY, your solution seems to work along the same principles. Kudos, I should have stopped working on it and waited for your reply. Mine is a bit simpler, although definately not more logical than yours. PIL is cumbersome to use. Yours explains what's going on to do it.

    0 讨论(0)
  • 2020-12-08 04:19
    import sys
    import PIL
    from PIL import Image
    
    def quantizetopalette(silf, palette, dither=False):
        """Convert an RGB or L mode image to use a given P image's palette."""
    
        silf.load()
    
        # use palette from reference image
        palette.load()
        if palette.mode != "P":
            raise ValueError("bad mode for palette image")
        if silf.mode != "RGB" and silf.mode != "L":
            raise ValueError(
                "only RGB or L mode images can be quantized to a palette"
                )
        im = silf.im.convert("P", 1 if dither else 0, palette.im)
        # the 0 above means turn OFF dithering
        return silf._makeself(im)
    
    if __name__ == "__main__":
        import sys, os
    
    for imgfn in sys.argv[1:]:
        palettedata = [ 0, 0, 0, 0, 255, 0, 255, 0, 0, 255, 255, 0,] 
        palimage = Image.new('P', (16, 16))
        palimage.putpalette(palettedata + [0, ] * 252 * 3)
        oldimage = Image.open(sys.argv[1])
        newimage = quantizetopalette(oldimage, palimage, dither=False)
        dirname, filename= os.path.split(imgfn)
        name, ext= os.path.splitext(filename)
        newpathname= os.path.join(dirname, "cga-%s.png" % name)
        newimage.save(newpathname)
    

    For those that wanted NO dithering to get solid colors. i modded: Convert image to specific palette using PIL without dithering with the two solutions in this thread. Even though this thread is old, some of us want that information. Kudios

    0 讨论(0)
提交回复
热议问题