Более быстрая реализация для квантования изображения с существующей палитрой? - PullRequest
0 голосов
/ 11 июня 2018

Я использую Python 3.6 для выполнения основных манипуляций с изображениями с помощью Pillow.В настоящее время я пытаюсь взять 32-битные изображения PNG (RGBA) произвольных цветовых композиций и размеров и квантовать их в известную палитру из 16 цветов.Оптимально, этот метод квантования должен иметь возможность оставлять полностью прозрачные (A = 0) пиксели в одиночку, при этом все полупрозрачные пиксели должны быть полностью непрозрачными (A = 255).Я уже разработал рабочий код, который выполняет это, но мне интересно, может ли он быть неэффективным:

import math
from PIL import Image

# a list of 16 RGBA tuples
palette = [
    (0, 0, 0, 255),
    # ...
    ]

with Image.open('some_image.png').convert('RGBA') as img:
    for py in range(img.height):
        for px in range(img.width):
            pix = img.getpixel((px, py))

            if pix[3] == 0:  # Ignore fully transparent pixels
                continue

            # Perform exhaustive search for closest Euclidean distance
            dist = 450
            best_fit = (0, 0, 0, 0)
            for c in palette:
                if pix[:3] == c:  # If pixel matches exactly, break
                    best_fit = c
                    break
                tmp = sqrt(pow(pix[0]-c[0], 2) + pow(pix[1]-c[1], 2) + pow(pix[2]-c[2], 2))
                if tmp < dist:
                    dist = tmp
                    best_fit = c
            img.putpixel((px, py), best_fit + (255,))
    img.save('quantized.png')

Я думаю о двух основных недостатках этого кода:

  • Image.putpixel()медленная операция
  • Вычисление функции расстояния несколько раз на пиксель неэффективно в вычислительном отношении

Есть ли более быстрый метод для этого?

Я заметил, что у Подушки есть встроенная функция Image.quantize(), которая, кажется, делает именно то, что я хочу.Но поскольку он закодирован, он вызывает сглаживание в результате, что я не хочу.Это было поднято в другом вопросе StackOverflow .Ответ на этот вопрос состоял в том, чтобы просто извлечь внутренний код Pillow и настроить контрольную переменную для дизеринга, которую я тестировал, но я обнаружил, что Pillow искажает палитру, которую я ей предоставляю, и последовательно выдает изображение, где квантованные цвета значительно темнее, чем они.должно быть.

Image.point() - это дразнящий метод, но он работает только для каждого цветового канала индивидуально, где для квантования цвета требуется работа со всеми каналами как набором.Было бы неплохо иметь возможность объединить все каналы в один канал с 32-битными целочисленными значениями, что кажется тем, что сделал бы плохо документированный режим "I", но если бы язапустите img.convert('I'), я получу полностью полутоновый результат, уничтожив все цвета.

Кажется, что альтернативным методом является использование NumPy и прямое изменение изображения.Я пытался создать таблицу поиска значений RGB, но трехмерное индексирование синтаксиса NumPy сводит меня с ума.В идеале я хотел бы, чтобы какой-то код работал следующим образом:

img_arr = numpy.array(img)

# Find all unique colors
unique_colors = numpy.unique(arr, axis=0)

# Generate lookup table
colormap = numpy.empty(unique_colors.shape)
for i, c in enumerate(unique_colors):
    dist = 450
    best_fit = None
    for pc in palette:
        tmp = sqrt(pow(c[0] - pc[0], 2) + pow(c[1] - pc[1], 2) + pow(c[2] - pc[2], 2))
        if tmp < dist:
            dist = tmp
            best_fit = pc
    colormap[i] = best_fit

# Hypothetical pseudocode I can't seem to write out
for iy in range(arr.size):
for ix in range(arr[0].size):
    if arr[iy, ix, 3] == 0: # Skip transparent
        continue
    index = # Find index of matching color in unique_colors, somehow
    arr[iy, ix] = colormap[index]

В этом гипотетическом примере я отмечаю, что numpy.unique() - еще одна медленная операция, поскольку она сортирует выходные данные.Поскольку мне кажется, что я не могу закончить код так, как я хочу, я так и не смог проверить, быстрее ли этот метод.

Я также рассмотрел попытку сгладить ось RGBA путем преобразования значений в32-разрядное целое число и желание создать одномерную таблицу поиска с более простым индексом:

def shift(a):
    return a[0] << 24 | a[1] << 16 | a[2] << 8 | a[3]

img_arr = numpy.apply_along_axis(shift, 1, img_arr)

Но сама эта операция показалась заметно медленной.

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

1 Ответ

0 голосов
/ 17 июня 2018

for следует избегать циклов для скорости.

Я думаю, что вы должны создать тензор, подобный:

d2[x,y,color_index,rgb] = distance_squared

, где rgb = 0..2 (0 = r, 1= g, 2 = b).

Затем вычислите расстояние:

d[x,y,color_index] =
  sqrt(sum(rgb,d2))

Затем выберите color_index с минимальным расстоянием:

c[x,y] = min_index(color_index, d)

Наконец замените альфа какнеобходимо:

alpha = ceil(orig_image.alpha)
img = c,alpha
...