Как преобразовать любое изображение в 4-цветное палитру изображения с помощью библиотеки изображений Python?

У меня есть устройство, поддерживающее 4-цветную графику (подобно CGA в старые времена).

Я хотел использовать PIL, чтобы прочитать изображение и преобразовать его с помощью моей 4-цветной палитры (красного, зеленого, желтого, черный), но я не могу понять, возможно ли это вообще. Я нашел несколько сообщений в списке рассылки, которые, похоже, предполагают, что другие люди пытались это сделать и не смогли.

Простой пример python будет высоко оценен!

Бонусные точки, если вы добавляете то, что затем преобразует изображение в строку байта, где каждый байт представляет 4 пикселя данных (каждый из двух бит представляет цвет от 0 до 3)

Ответы

Ответ 1

Во-первых: ваша четырехцветная палитра (черный, зеленый, красный, желтый) не имеет синего компонента. Итак, вы должны согласиться с тем, что ваше выходное изображение вряд ли приблизится к входному изображению, если только синий компонент не начнет работать.

Попробуйте этот код:

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

Это создает изображение палитры PNG с только четырьмя четырьмя позициями палитры, заданными для ваших цветов. Это изображение образца:

http://tzotzioy.googlepages.com/new_pic_baby2.jpg

становится

http://tzotzioy.googlepages.com/cga-new_pic_baby2.png

Тривиально взять вывод image2cga (выводит последовательность из 0-3 значений) и упаковать каждые четыре значения в байт.

Если вам нужна помощь в том, что делает код, спросите, и я объясню.

EDIT1: не изобретать велосипед

Конечно, оказывается, я был слишком восторжен, и, как обнаружил Томас, метод Image.quantize может воспринимать изображение палитры как аргумент и делать квантование с гораздо лучшими результатами, чем мой ad-hoc-метод выше:

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, продолжение: установите пиксели в байты

Для "добавленного значения" здесь следует код для создания упакованной строки (4 пикселя на каждый байт):

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 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())

Ответ 2

Джон, я тоже нашел эту первую ссылку, но мне это не помогло. Это заставило меня заглянуть глубже в квантование.

Я придумал это вчера перед сном:

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, ваше решение, похоже, работает по тем же принципам. Kudos, я должен был перестать работать над этим и ждать вашего ответа. Мой немного проще, хотя определенно не более логичен, чем ваш. PIL является громоздким для использования. Твой объясняет, что происходит, чтобы сделать это.

Ответ 4

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

Для тех, кто хотел, чтобы сглаживание не было однородным. я modded: Преобразование изображения в определенную палитру с использованием PIL без сглаживания с двумя решениями в этой теме. Хотя эта ветка устарела, некоторые из нас хотят эту информацию. Kudios