Come posso convertire qualsiasi immagine in un'immagine con palette a 4 colori usando la libreria di imaging Python?
-
04-07-2019 - |
Domanda
Ho un dispositivo che supporta la grafica a 4 colori (molto simile alla CGA ai vecchi tempi).
Volevo usare PIL per leggere l'immagine e convertirla usando il mio 4- tavolozza dei colori (di rosso, verde, giallo, nero), ma non riesco a capire se sia possibile. Ho trovato alcuni post dell'archivio delle mailing list che sembrano suggerire che altre persone hanno provato a farlo e non ci sono riusciti.
Un semplice esempio di pitone sarebbe molto apprezzato!
Punti bonus se aggiungi qualcosa che converte l'immagine in una stringa di byte in cui ogni byte rappresenta 4 pixel di dati (con ogni due bit che rappresentano un colore da 0 a 3)
Soluzione
Primo: la tua palette di quattro colori (nero, verde, rosso, giallo) ha no componente blu. Quindi, devi accettare che l'immagine di output difficilmente si avvicina all'immagine di input, a meno che non ci sia un componente blu con cui iniziare.
Prova questo codice:
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)
Questo crea un'immagine della palette PNG con solo le prime quattro voci della palette impostate sui colori. Questa immagine di esempio:
diventa ??p>
È banale prendere l'output di image2cga
(produce una sequenza di valori 0-3) e impacchettare ogni quattro valori in un byte.
Se hai bisogno di aiuto su ciò che fa il codice, chiedi e ti spiegherò.
EDIT1: non reinventare la ruota
Certo, risulta che ero troppo entusiasta e & # 8212; come Thomas ha scoperto & # 8212; il metodo Image.quantize può prendere un'immagine della tavolozza come argomento e fare la quantizzazione con risultati molto migliori rispetto al mio metodo ad hoc sopra:
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: impacchetta i pixel in byte
Per "valore aggiunto", ecco il codice che segue per produrre la stringa compressa (4 pixel 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())
Altri suggerimenti
John, ho trovato anche quel primo link, ma non mi ha aiutato direttamente con il problema. Mi ha fatto sembrare più approfondito nel quantizzare.
Me ne sono inventato ieri prima di andare a letto:
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, la tua soluzione sembra funzionare secondo gli stessi principi. Complimenti, avrei dovuto smettere di lavorarci e aspettare la tua risposta. Il mio è un po 'più semplice, anche se sicuramente non più logico del tuo. PIL è complicato da usare. Il tuo spiega cosa sta succedendo per farlo.
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)
Per coloro che non desideravano il dithering per ottenere colori solidi. ho modificato: Converti l'immagine in una tavolozza specifica usando PIL senza dithering con le due soluzioni in questo thread. Anche se questo thread è vecchio, alcuni di noi vogliono tali informazioni. Kudios