Более быстрая реализация для квантования изображения с существующей палитрой?

Я использую Python 3.6 для выполнения основных операций с изображениями через подушку. В настоящее время я пытаюсь взять 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() - медленная операция
  • Многократное вычисление функции расстояния для каждого пикселя является затратным с точки зрения вычислений

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

Я заметил, что Pillow имеет встроенную функцию 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)

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

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


person Fawfulcopter    schedule 11.06.2018    source источник
comment
Однако Related использует scipy.   -  person Paul Panzer    schedule 11.06.2018
comment
Просто с помощью ImageMagick в командной строке magick input.png +dither -remap palette.png result.png. Также тривиально можно распараллелить с помощью GNU Parallel parallel magick {} +dither -remap palette.png {} ::: *.png   -  person Mark Setchell    schedule 11.06.2018
comment
Вы можете ускорить свой код, удалив sqrt(), поскольку a ‹b, если a ^ 2‹ b ^ 2   -  person Mark Setchell    schedule 11.06.2018
comment
Вам следует настроить структуру пространственной индексации для своей цветовой карты. Это сделает поиск намного быстрее. Ищите деревья R *, октодеревья или kd-деревья.   -  person Cris Luengo    schedule 11.06.2018
comment
@MarkSetchell Это должна быть чистая функция Python, вызываемая из части более крупной программы Python. Инструменты командной строки здесь не применимы, если чего-то не хватает.   -  person Fawfulcopter    schedule 11.06.2018
comment
Именно поэтому я поставил это как комментарий. Иногда люди просто хотят выполнить свою работу, и им все равно, как долго они смогут двигаться дальше. У респондентов мало указаний на то, полностью ли опрашиваемые настроены на определенный язык / технологию или они не знают об альтернативных возможностях. Итак, я прокомментировал, и вы можете проигнорировать это, если это не то, что вы ищете.   -  person Mark Setchell    schedule 11.06.2018
comment
Если вы найдете в этом ответе слова Создать буфер для выходных пикселей, stackoverflow.com/a/50551755/2836621 вы можете увидеть, как я избежал медленного putpixel(), используя массив numpy для выходных пикселей и просто преобразовав его в изображение в самом конце.   -  person Mark Setchell    schedule 13.06.2018


Ответы (1)


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
person Ole Tange    schedule 17.06.2018