Python PIL.Image.convert not replacing color with the closest palette.
这是一个后续问题:从不使用抖动的情况下使用PIL将图像转换为特定调色板
我也想创建一个脚本,该脚本可以将图像转换为一组特定的颜色而不会发生抖动。
我已经实施了变通的"自定义量化"功能,作为对问题的答案。除了一个大问题外,大多数脚本都能正常工作。
浅绿色RGB(130,190,40)被浅棕色RGB(166、141、95)取代。 (请参见鬃毛左上方的浅绿色。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | from PIL import Image def customConvert(silf, palette, dither=False): ''' Convert an RGB or L mode image to use a given P image's palette. PIL.Image.quantize() forces dither = 1. This custom quantize function will force it to 0. https://stackoverflow.com/questions/29433243/convert-image-to-specific-palette-using-pil-without-dithering ''' silf.load() # use palette from reference image made below palette.load() im = silf.im.convert("P", 0, palette.im) # the 0 above means turn OFF dithering making solid colors return silf._new(im) palette = [ 0,0,0, 0,0,255, 15,29,15, 26,141,52, 41,41,41, 65,105,225, 85,11,18, 128,0,128, 135,206,236, 144,238,144, 159,30,81, 165,42,42, 166,141,95, 169,169,169, 173,216,230, 211,211,211, 230,208,122, 245,245,220, 247,214,193, 255,0,0, 255,165,0, 255,192,203, 255,255,0, 255,255,255 ] + [0,] * 232 * 3 # a palette image to use for quant paletteImage = Image.new('P', (1, 1), 0) paletteImage.putpalette(palette) # open the source image imageOrginal = Image.open('lion.png').convert('RGB') # convert it using our palette image imageCustomConvert = customConvert(imageOrginal, paletteImage, dither=False).convert('RGB') |
CIE76 Delta-E:
当前:RGB(130,190,40)-> RGB(166,141,95)= 57.5522
预期:RGB(130,190,40)-> RGB(144,238,144)= 31.5623
有人可以解释我是否写错了代码,也没有建议如何使它正常工作。

如果速度是问题,ImageMagick可以更快地完成此操作。它已安装在大多数Linux发行版上,并且可用于macOS和Windows。
基本上,您将创建一个名为
1 | magick lion.png +dither -quantize Lab -remap map.png result.png |
运行时间不到0.3秒。如果您想从Python做到这一点,则可以像这样进行炮击:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | #!/usr/bin/env python3 import subprocess import numpy as np from PIL import Image palette = [ 0,0,0, 0,0,255, 15,29,15, 26,141,52, 41,41,41, 65,105,225, 85,11,18, 128,0,128, 135,206,236, 144,238,144, 159,30,81, 165,42,42, 166,141,95, 169,169,169, 173,216,230, 211,211,211, 230,208,122, 245,245,220, 247,214,193, 255,0,0, 255,165,0, 255,192,203, 255,255,0, 255,255,255 ] + [0,] * 232 * 3 # Write"map.png" that is a 24x1 pixel image with one pixel for each colour entries = 24 resnp = np.arange(entries,dtype=np.uint8).reshape(24,1) resim = Image.fromarray(resnp, mode='P') resim.putpalette(palette) resim.save('map.png') # Use Imagemagick to remap to palette saved above in 'map.png' # magick lion.png +dither -quantize Lab -remap map.png result.png subprocess.run(['magick', 'lion.png', '+dither', '-quantize', 'Lab', '-remap', 'map.png', 'result.png']) |
我尝试为每个像素计算CIE76 Delta-E函数以获得最接近的颜色。 Python不是我最好的语言,因此您可能想问另一个问题,以使代码按照您的预期工作,从而优化代码。
我基本上将输入图像和调色板转换为Lab色彩空间,然后计算从每个像素到每个调色板条目的平方的CIE76 Delta-E值,并取最接近的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | #!/usr/bin/env python3 import numpy as np from PIL import Image from skimage import color def CIE76DeltaE2(Lab1,Lab2): """Returns the square of the CIE76 Delta-E colour distance between 2 lab colours""" return (Lab2[0]-Lab1[0])*(Lab2[0]-Lab1[0]) + (Lab2[1]-Lab1[1])*(Lab2[1]-Lab1[1]) + (Lab2[2]-Lab1[2])*(Lab2[2]-Lab1[2]) def NearestPaletteIndex(Lab,palLab): """Return index of entry in palette that is nearest the given colour""" NearestIndex = 0 NearestDist = CIE76DeltaE2(Lab,palLab[0,0]) for e in range(1,palLab.shape[0]): dist = CIE76DeltaE2(Lab,palLab[e,0]) if dist < NearestDist: NearestDist = dist NearestIndex = e return NearestIndex palette = [ 0,0,0, 0,0,255, 15,29,15, 26,141,52, 41,41,41, 65,105,225, 85,11,18, 128,0,128, 135,206,236, 144,238,144, 159,30,81, 165,42,42, 166,141,95, 169,169,169, 173,216,230, 211,211,211, 230,208,122, 245,245,220, 247,214,193, 255,0,0, 255,165,0, 255,192,203, 255,255,0, 255,255,255 ] + [0,] * 232 * 3 # Load the source image as numpy array and convert to Lab colorspace imnp = np.array(Image.open('lion.png').convert('RGB')) imLab = color.rgb2lab(imnp) h,w = imLab.shape[:2] # Load palette as numpy array, truncate unused palette entries, and convert to Lab colourspace palnp = np.array(palette,dtype=np.uint8).reshape(256,1,3)[:24,:] palLab = color.rgb2lab(palnp) # Make numpy array for output image resnp = np.empty((h,w), dtype=np.uint8) # Iterate over pixels, replacing each with the nearest palette entry for y in range(0, h): for x in range(0, w): resnp[y, x] = NearestPaletteIndex(imLab[y,x], palLab) # Create output image from indices, whack a palette in and save resim = Image.fromarray(resnp, mode='P') resim.putpalette(palette) resim.save('result.png') |
我明白了:
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #!/usr/bin/env python3 import numpy as np from PIL import Image from skimage import color from scipy.spatial.distance import cdist palette = [ 0,0,0, 0,0,255, 15,29,15, 26,141,52, 41,41,41, 65,105,225, 85,11,18, 128,0,128, 135,206,236, 144,238,144, 159,30,81, 165,42,42, 166,141,95, 169,169,169, 173,216,230, 211,211,211, 230,208,122, 245,245,220, 247,214,193, 255,0,0, 255,165,0, 255,192,203, 255,255,0, 255,255,255 ] + [0,] * 232 * 3 # Load the source image as numpy array and convert to Lab colorspace imnp = np.array(Image.open('lion.png').convert('RGB')) h,w = imnp.shape[:2] imLab = color.rgb2lab(imnp).reshape((h*w,3)) # Load palette as numpy array, truncate unused palette entries, and convert to Lab colourspace palnp = np.array(palette,dtype=np.uint8).reshape(256,1,3)[:24,:] palLab = color.rgb2lab(palnp).reshape(24,3) # Make numpy array for output image resnp = np.empty(h*w, dtype=np.uint8) # Iterate over pixels, replacing each with the nearest palette entry x = 0 for L in imLab: resnp[x] = cdist(palLab, L.reshape(1,3), metric='seuclidean').argmin() x = x +1 # Create output image from indices, whack the palette in and save resim = Image.fromarray(resnp.reshape(h,w), mode='P') resim.putpalette(palette) resim.save('result.png') |