Пишем простейшие фильтры изображений на Python

Дело было вечером, делать было нечего. Стало мне вдруг интересно, как реализованы простейшие фильтры изображений: оттенки серого, яркость, контрастность, сепия и т.п. Вооружившись Гуглом и Питоном решил разобраться потратить вечер с пользой и хоть немного разобраться в теме.

Эта статья не претендует на исчерпывающее руководство или хоть какую-нибудь интеллектуальную ценность. Какие-то алгоритмы я взял с Хабра, какие-то с Википедии, какие-то пришлось переписывать на Питон с других языков. Все они доступны в сети и ничего особо нового здесь вы не найдете.

Исходный код и изображения из статьи вы можете найти здесь.

Вот над таким изображением мы будем издеваться в статье:

Исходное изображение

Для работы с изображением мы будем использовать Питоновскую библиотеку PIL. Из нее нам понадобится только один класс:

from PIL import Image

Изменения яркости

Начнем с самого простого - изменения яркости. Если мы хотим увеличить яркость изображения, то значение каждого канала мы увеличиваем на определенный коэффициент. Если хотим затемнить - уменьшаем. Для каждого канала используется одинаковый коэффициент. Если итоговое значение канала больше 255 или меньше 0, то исправляем их.

def bright(source_name, result_name, brightness):
    source = Image.open(source_name)
    result = Image.new('RGB', source.size)
    for x in range(source.size[0]):
        for y in range(source.size[1]):
            r, g, b = source.getpixel((x, y))

            red = int(r * brightness)
            red = min(255, max(0, red))

            green = int(g * brightness)
            green = min(255, max(0, green))

            blue = int(b * brightness)
            blue = min(255, max(0, blue))

            result.putpixel((x, y), (red, green, blue))
    result.save(result_name, "JPEG")

Если brightness больше 1, то изображение осветляется. Если brightness меньше 1, то изображение затемняется. Вот как будет выглядеть исходное изображение при brightness = 0.5 и brightness = 1.5 соответственно:

Яркость 0.5 Яркость 1.5

Негатив

Еще один элементарный фильтр - это негатив. Для этого нам нужно инвертировать значение каждого канала. Просто вычитаем значение канала из 255. Здесь результирующее значение не может выйти за допустимые границы, поэтому проверка не нужна.

def negative(source_name, result_name):
    source = Image.open(source_name)
    result = Image.new('RGB', source.size)
    for x in range(source.size[0]):
        for y in range(source.size[1]):
            r, g, b = source.getpixel((x, y))
            result.putpixel((x, y), (255 - r, 255 - g, 255 - b))
    result.save(result_name, "JPEG")

Вот результат обработки исходного изображения этим фильтром:

Негатив

Черно-белое изображение

Изображение рисуется только двумя цветами: черным и белым. Поэтому нам надо разделить все пиксели на две группы: темные и светлые. Для этого мы суммируем значения всех каналов и сравниваем его в пороговым значением. Если сумма больше этого значения, то пиксель будет рисоваться черным цветом, иначе - белым. Существует несколько способов расчета порогового значения. Мне понравился этот: separator = 255 / brightness / 2 * 3. Где brightness - значение, указанное пользователем. Чем оно больше, тем больше пикселей будет окрашено в белый.

def white_black(source_name, result_name, brightness):
    source = Image.open(source_name)
    result = Image.new('RGB', source.size)
    separator = 255 / brightness / 2 * 3
    for x in range(source.size[0]):
        for y in range(source.size[1]):
            r, g, b = source.getpixel((x, y))
            total = r + g + b
            if total > separator:
                result.putpixel((x, y), (255, 255, 255))
            else:
                result.putpixel((x, y), (0, 0, 0))
    result.save(result_name, "JPEG")

Исходное изображение при brightness = 0.8 и brightness = 1.2 соответственно:

Уровень черного 0.8 Уровень черного 1.2

Оттенки серого

Если говорить грубо, то для наложения этого фильтра надо закрасить пиксель усредненным значением всех его каналов gray = (red + green + blue) /3. Эта формула будет работать, но даст не самый лучший результат. Дело в том, что человеческий глаз по разному воспринимает яркость для разных цветов. Из-за этого, например, красный цвет будет оказывать большее влияние на результат.Нужно учитывать это если мы хотим добиться более приятного глазу результата.

Для решения этой проблемы мы будем умножать значение каждого канала на определенный коэффициент. Для каждого канала свой. Есть несколько разных вариантов этих значений. На мой взгляд такая формула наиболее удачна: gray = int(r * 0.2126 + g * 0.7152 + b * 0.0722).

def gray_scale(source_name, result_name):
    source = Image.open(source_name)
    result = Image.new('RGB', source.size)
    for x in range(source.size[0]):
        for y in range(source.size[1]):
            r, g, b = source.getpixel((x, y))
            gray = int(r * 0.2126 + g * 0.7152 + b * 0.0722)
            result.putpixel((x, y), (gray, gray, gray))
    result.save(result_name, "JPEG")

И результат применения этого фильтра:

Оттенки серого

Сепия

Для того чтобы представить исходное изображение в сепии, надо усреднить значение всех каналов, а так же "сдвинуть" цвет каждого канала в сторону цвета Сепия (#704214). Так же, как и в оттенках серого, здесь тоже существуют разные наборы коэффициентов сдвигов. Я не заметил между ними особой разницы.

def sepia(source_name, result_name):
    source = Image.open(source_name)
    result = Image.new('RGB', source.size)
    for x in range(source.size[0]):
        for y in range(source.size[1]):
            r, g, b = source.getpixel((x, y))
            red = int(r * 0.393 + g * 0.769 + b * 0.189)
            green = int(r * 0.349 + g * 0.686 + b * 0.168)
            blue = int(r * 0.272 + g * 0.534 + b * 0.131)
            result.putpixel((x, y), (red, green, blue))
    result.save(result_name, "JPEG")

Исходное изображение в сепии:

Сепия

Контрастность

Последний из рассмотренных фильтров - изменение контрастности изображения. Его алгоритм сложнее и состоит из нескольких этапов.

Сначала мы считаем усредненное значение яркости для всего изображения.

avg = 0
for x in range(source.size[0]):
    for y in range(source.size[1]):
        r, g, b = source.getpixel((x, y))
        avg += r * 0.299 + g * 0.587 + b * 0.114
avg /= source.size[0] * source.size[1]

После этого для каждого канала каждого пикселя мы находим отклонение от усредненного значения, найденного на предыдущем шаге, и увеличиваем/уменьшаем его на заданный пользователем коэффициент. Этот шаг можно оптимизировать. Т.к. значение каналов лежит в пределах 0 - 255, то мы можем рассчитать новые значения для этого диапазона. А затем просто брать для каждого канала заранее рассчитанное значение.

palette = []
for i in range(256):
    temp = int(avg + coefficient * (i - avg))
    if temp < 0:
        temp = 0
    elif temp > 255:
        temp = 255
    palette.append(temp)

Обрабатываем изображение:

for x in range(source.size[0]):
    for y in range(source.size[1]):
        r, g, b = source.getpixel((x, y))
        result.putpixel((x, y), (palette[r], palette[g], palette[b]))

В итоге наш фильтр выглядит так:

def contrast(source_name, result_name, coefficient):
    source = Image.open(source_name)
    result = Image.new('RGB', source.size)

    avg = 0
    for x in range(source.size[0]):
        for y in range(source.size[1]):
            r, g, b = source.getpixel((x, y))
            avg += r * 0.299 + g * 0.587 + b * 0.114
    avg /= source.size[0] * source.size[1]

    palette = []
    for i in range(256):
        temp = int(avg + coefficient * (i - avg))
        if temp < 0:
            temp = 0
        elif temp > 255:
            temp = 255
        palette.append(temp)

    for x in range(source.size[0]):
        for y in range(source.size[1]):
            r, g, b = source.getpixel((x, y))
            result.putpixel((x, y), (palette[r], palette[g], palette[b]))

    result.save(result_name, "JPEG")

И контрастное изображение с coefficient = 2 вот так:

Контраст

Заключение

В этой статье рассматривались только самые простые фильтры изображений. Я не ставил целью показать что-то крутое. Мне просто было интересно узнать, как они работают, и попробовать написать их самому. Возможно это будет интересно кому-то еще.

Коментарии

Используйте Markdown

Спасибо за коментарий!
Ваше сообщение будет доступно после проверки.

#1 Равиль

В чём заключается смещение канала в сторону какого-то цвета? (Цитата: "сдвинуть цвет каждого канала в сторону цвета Сепия") Есть ли общая формула для смещения к определённому цвету? Статья очень пригодилась!)

#0 Елена

Спасибо за обзор! Действительно всё просто, если не углубляться в подбор коэффициентов )