Чтобы дойти до сложных алгоритмов обработки, стоит проанализировать стандартные схемы, с чего я и предлагаю начать.

Для примеров обработки будет использоваться изображение с различным наборов цветов:

image

Для старта нам потребуется два модуля библиотеки:

from PIL import Image, ImageDraw 

Настроим инструменты для комфортной дальнейшей работы:

image = Image.open('test.jpg')  # Открываем изображение
draw = ImageDraw.Draw(image)  # Создаем инструмент для рисования
width = image.size[0]  # Определяем ширину
height = image.size[1]  # Определяем высоту
pix = image.load()  # Выгружаем значения пикселей

Приступим


Обрабатывать изображения будем в формате RGB. Также PIL поддерживает работу с форматами 1, L, P, RGB, RGBA, CMYK, YCbCr, LAB, HSV, I, F.

Значения пикселя в изображении задаются в формате: (x,y),(red, green, blue), где x,y — координаты, а числовые значения RGB находятся в диапазоне от 0 до 255. То есть работаем с 8-битным изображением.

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


Серый оттенок появляется в случае равенства всех палитр цветов, поэтому нам нужно получить среднее арифметическое значение во всех трёх пунктах:


for x in range(width):
    for y in range(height):
       r = pix[x, y][0] #узнаём значение красного цвета пикселя
       g = pix[x, y][1] #зелёного
       b = pix[x, y][2] #синего
       sr = (r + g + b) // 3 #среднее значение
       draw.point((x, y), (sr, sr, sr)) #рисуем пиксель

image.save("result.jpg", "JPEG") #не забываем сохранить изображение

image
Небольшой нюанс
Человеческий глаз разный спектр цветов воспринимает по-разному. Точной формулы вам никто не даст, так как восприятие цветов так или иначе у всех разное, но если вам интересно, подробнее можно прочитать тут и тут.


Инверсия


Инверсия получается путём вычета из 255 текущего цвета:


for x in range(width):
   for y in range(height):
      r = pix[x, y][0]
      g = pix[x, y][1]
      b = pix[x, y][2]
      draw.point((x, y), (255 - r, 255 - g, 255 - b))

image

Инверсия оттенка серого


Совмещая два предыдущих алгоритма можно написать следующий код:


for x in range(width):
    for y in range(height):
        r = pix[x, y][0]
        g = pix[x, y][1]
        b = pix[x, y][2]
        sr = (r + g + b) // 3
        draw.point((x, y), (255 - sr, 255 - sr, 255 - sr))

image

Выборочная инверсия оттенка серого


Для этого алгоритма нужно определить пороговое значение, которое я возьму за 100:

for x in range(width):
    for y in range(height):
        r = pix[x, y][0]
        g = pix[x, y][1]
        b = pix[x, y][2]
        if (r+g+b)>100: #если сумма значений больше 100 , то используем инверисю
            sr = (r + g + b) // 3
            draw.point((x, y), (255-sr, 255-sr, 255-sr))
        else: #иначе обычный оттенок серого
            sr = (r + g + b) // 3
            draw.point((x, y), (sr, sr, sr))

image

Заключение


В следующих статьях я хотел бы рассказать о том, как более локально подходить к фильтрации изображения, путём разделения его на области, а также показать интересные возможности DFS в алгоритмах обработки изображения

Комментарии (21)


  1. andreymal
    09.05.2019 00:29

    PIL на Python

    Всё же речь не о PIL, а о Pillow. Оригинальная библиотека PIL никогда не была внутри модуля PIL, это самодеятельность Pillow. Оригинальный PIL не разрабатывается с 2011-го и не существует для Python 3.


    PIL работает с изображениями в формате RGB.

    Pillow поддерживает режимы 1, L, P, RGB, RGBA, CMYK, YCbCr, LAB, HSV, I, F.


    нам нужно получить среднее арифметическое значение

    Это не единственный и при этом не самый лучший алгоритм обесцвечивания


    r = pix[x, y][0]
    g = pix[x, y][1]
    b = pix[x, y][2]

    r, g, b = pix[x, y] — даже такая мелочь ускоряет код на 20%


    1. Yunow Автор
      09.05.2019 23:54

      Спасибо. Статью немного дописал. Код писал с акцентом на понимание, а не на оптимизацию.


  1. samodum
    09.05.2019 03:59

    Правильная формула для получения серого цвета:
    Y = 0.299 R + 0.587 G + 0.114 B


    1. Neusser
      09.05.2019 11:30
      +2

      А правильная функция:

      grayscale_image = image.convert("L")


      1. UnrealQW
        09.05.2019 16:06
        +1

        Или image.convert(«LA») — для альфа-канала.


    1. Yunow Автор
      10.05.2019 00:11

      Да, в интернете много формул с акцентом на то, что цвета человеческий глаз воспринимает иначе и соответственно алгоритмы совершенно другие. Спасибо за замечание, статью дополнил, но для общего понимания, я считаю, приведённого алгоритма достаточно.


    1. masai
      10.05.2019 01:59

      Правильная формула для получения серого цвета

      Это не совсем так. Почитайте, например, статью «Об относительной яркости, или насколько живучим бывает легаси».


      1. samodum
        11.05.2019 13:23

        хорошее замечание


  1. shybovycha
    09.05.2019 08:01
    +3

    Вот бы вы еще написали что за PIL, о каких "более сложных алгоритмах обработки (изображений?)" идет речь и зачем вот это все надо.


  1. shybovycha
    09.05.2019 08:10

    Небольшой код ревью: у вас очень много одинакового кода в примерах, не было бы проще вынести отдельную функцию вроде handle(x, y, [ r, g, b]) и в каждом параграфе определить конкретную реализацию?


    def handlePixel(( x, y ), [ r, g, b ]):
      pass
    
    for x in range(width):
      for y in range(height):
        draw.point((x, y), handlePixel(( x, y ), pix[x, y]))
    
    # позже в статье
    def handlePixel(( x, y ), [ r, g, b ]):
      sr = (r + g + b) // 3
      return ( 255 - sr, 255 - sr, 255 - sr )

    Ну и я бы использовал кортежы везде (вместо массива [r,g,b]), но эт такое


    1. qpy
      10.05.2019 00:37
      +1

      зачем у вас ( x, y ) в

      def handlePixel(( x, y ), [ r, g, b ])
      ?

      у меня отлично так работает, проверено лично в PyCharm:

      def handlePixel(r, g, b):
          sr = (r + g + b) //3
          return (255 - sr, 255 - sr, 255 - sr )
      
      for x in range(width):
        for y in range(height):
            r,g,b=pix[x, y]
            draw.point((x, y), handlePixel(r,g,b))


      1. shybovycha
        10.05.2019 01:26
        -1

        во-первых, странно что вам понадобилось запускать код в PyCharm чтобы это увидеть


        во-вторых, да, можно и без этого обойтись — координаты не используются ни в одном куске кода. но как знать — может в какой-то момент и понадобится нечто большее? ;) например, для тех же фильтров пригодится окно изображения — размеры окна, координаты куда писать и данные пикселей окна. но принцип YAGNI говорит что не надо этот параметр функции добавлять =)


        def handlePixel([ r, g, b ])

        вполне себе заработает. передавать цвет пикселя массивом — банально проще:


        # не нужно деструктурировать в три переменных здесь
        draw.point((x, y), handlePixel(pix[x, y])


        1. qpy
          10.05.2019 10:55
          +1

          более того, вы в определении функции передаете параметры в таком виде

          def handlePixel([ r, g, b ])

          def handlePixel(( x, y ), [ r, g, b ])

          (smiling)


          1. shybovycha
            10.05.2019 13:30

            не понял что вас смущает — pix[x, y] — это массив из трех элементов, я их деструктуририрую в определении функции, вроде ничего необычного. а координаты я передал в виде кортежа — просто так (на самом деле я думал что можно будет рисовать прямо в этой функции).


            1. masai
              10.05.2019 22:16

              Запись со списком в заголовке определения функции — это синтаксическая ошибка.


              Например, код


              def f([a, b, c]):
                  return a + b + c

              просто упадёт с ошибкой парсинга.


              Кстати, pix[x, y] — это не массив, а кортеж, если быть точным. Массивы могут быть многомерными и обычно ассоциируются с массивами Numpy или стандартным array.array. Здесь же функция просто возвращает три значения. И [r, g, b] — это тоже не массив, а список.


              1. shybovycha
                11.05.2019 08:08

                таки да, ~переоценил~ перепутал я питон с жаваскриптом, деструктурирование в параметрах функций отсутствует.


  1. Ktulhy
    09.05.2019 08:14
    +6

    Снова люди, вчера изучившие питон, пытаются научить ему других. А потом все думают, что питон "медленный" и "непонятный"


  1. NotThatEasy
    10.05.2019 00:38
    +1

    Девочка няшная.
    Насчёт PIL – не разбираюсь, однако же, не торопитесь закидывать помидорами. Имел опыт с OpenCV под плюсы и Пайтон и могу сообщить, что поток выполнения может проводить большУю часть времени в алгоритмах обработки, а не в отрисовке даже в плюсах.
    Вывод 1 – большинство либ для рисования предоставляют подобный функционал не на одном языке.
    Вывод 2 – язык Пайтон, к сожалению, не лучший для обработки изображений с большим количеством точек, как и для других нужд, где желательна (ну например) возможность обработки видео без потери кадров в секунду.


    1. masai
      10.05.2019 02:05

      Согласен, но для склейки вызовов библиотечных функций он очень даже ничего. Понятно, что если обрабатывать изображение попиксельно в питоновских циклах, ничего путного из этого не выйдет.


  1. assembled
    10.05.2019 00:38
    +2

    Фи, питон. Лучше бы что-нибудь поинтереснее выбрал:load 'media/imagekit'
    load 'primitives'
    mean =: +/ % #
    round =: floor f. @ +&0.5
    gray =: round f. @ #~&3"0 @ mean f. rows
    negative =: 255&-
    negative_gray =: negative f. @ gray f.

    image =: read_image 'input.jpg'
    (negative_gray image) write_image 'output.jpg'
    view_image 'output.jpg'


  1. iroln
    11.05.2019 02:03

    Откройте для себя scikit-image и/или OpenCV и не занимайтесь ерундой. Писать на Python циклы с попиксельным обходом изображений — это бесперспективно и бессмысленно.


    Pillow неплохая библиотека, но не для image processing. Хотя в ней несомненно есть некоторые функции обработки изображений. Кстати, рекомендую почитать.