1

这是一个后续问题:Convert image to specific palette using PIL without dithering

我也想创建一个脚本,可以将图像转换为特定的一组颜色而无需抖动。

我已经实现了作为问题答案给出的变通“自定义量化”功能。除了 1 个大问题外,大多数脚本都运行良好。

浅绿色 RGB(130,190,40) 被浅棕色 RGB(166, 141, 95) 取代。(见鬃毛左上角的浅绿色。)

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 三角洲-E:

当前:RGB(130,190,40) --> RGB(166, 141, 95) = 57.5522

预期:RGB(130,190,40) --> RGB(144,238,144) = 31.5623


有人可以解释我是否错误地编写了代码或建议如何让它工作。

原始图像 自定义转换

4

2 回答 2

3

如果速度是问题, ImageMagick可以更快地做到这一点。它安装在大多数 Linux 发行版上,可用于 macOS 和 Windows。

基本上,您将创建一个 24x1 图像,称为"map.png",在您的调色板中每种颜色都有一个像素,并告诉ImageMagick将您的狮子图像重新映射到颜色空间中的该颜色图Lab而不进行抖动。因此,终端/命令提示符中的命令将是:

magick lion.png +dither -quantize Lab -remap map.png result.png

运行时间不到 0.3 秒。如果你想从 Python 中做到这一点,你可以这样写:

#!/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'])

在此处输入图像描述

于 2018-11-26T14:51:15.940 回答
1

我尝试计算每个像素的 CIE76 Delta-E 函数以获得最接近的颜色。Python 不是我最好的语言,所以你可能想问另一个问题来优化代码,如果它按你期望的那样工作。

我基本上将输入图像和调色板转换为 Lab 颜色空间,然后计算从每个像素到每个调色板条目的 CIE76 Delta-E 值平方并取最近的一个。

#!/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')

我明白了:

在此处输入图像描述


scipy.spatial.distance使用'scdist()函数似乎更快更简洁:

#!/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')
于 2018-11-26T13:26:10.093 回答