Играемся с изображениями в Python

  • Tutorial
В этой статье я хотел бы разобрать различные способы преобразования изображений с помощью Python. Для примеров я решил взять несколько наиболее известных. В статье не будет ничего сложного, она ориентированна в основном на новичков.
Картинка для испытаний:


Подготовка

import random
from PIL import Image, ImageDraw #Подключим необходимые библиотеки. 

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



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

Для получения этого преобразования необходимо «усреднить» каждый пиксел.
if (mode == 0):
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			S = (a + b + c) // 3
			draw.point((i, j), (S, S, S))



Сепия


Чтобы получить сепию, нужно посчитать среднее значение и взять какой — нибудь коэффициент.
middle = (R + G + B) / 3
Первое значение пиксела ( R ) = middle + 2 * k
Второе значение пиксела ( G ) = middle + k
Третье значение пиксела ( B ) = middle
if (mode == 1):
	depth = int(input('depth:'))
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			S = (a + b + c) // 3
			a = S + depth * 2
			b = S + depth
			c = S
			if (a > 255):
				a = 255
			if (b > 255):
				b = 255
			if (c > 255):
				c = 255
			draw.point((i, j), (a, b, c))


depth = 30

Негатив

Теперь научимся получать негатив.
Это очень просто, достаточно лишь каждое значение пиксела вычесть из 255.
if (mode == 2):
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			draw.point((i, j), (255 - a, 255 - b, 255 - c))



Добавление шумов

Вот тут совсем всё просто.
Мы будем всегда добавлять к пикселу какое — нибудь рандомное значение. Чем больше разброс этих значений, тем больше шумов.
if (mode == 3):
	factor = int(input('factor:'))
	for i in range(width):
		for j in range(height):
			rand = random.randint(-factor, factor)
			a = pix[i, j][0] + rand
			b = pix[i, j][1] + rand
			c = pix[i, j][2] + rand
			if (a < 0):
				a = 0
			if (b < 0):
				b = 0
			if (c < 0):
				c = 0
			if (a > 255):
				a = 255
			if (b > 255):
				b = 255
			if (c > 255):
				c = 255
			draw.point((i, j), (a, b, c))


factor = 70

Яркость

Для регулирования яркости к каждому пикселу мы будем добавлять определенное значение. Если оно > 0, то картинка становится ярче, иначе темнее.
if (mode == 4):
	factor = int(input('factor:'))
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0] + factor
			b = pix[i, j][1] + factor
			c = pix[i, j][2] + factor
			if (a < 0):
				a = 0
			if (b < 0):
				b = 0
			if (c < 0):
				c = 0
			if (a > 255):
				a = 255
			if (b > 255):
				b = 255
			if (c > 255):
				c = 255
			draw.point((i, j), (a, b, c))


factor = 100

factor = -100

Чёрно — белое изображение

Теперь все пикселы надо разбить на 2 группы: черные и белые.
Для проверки принадлежности к определенной группе мы будем смотреть к чему ближе значение пиксела: к белому цвету или к чёрному.
if (mode == 5):
	factor = int(input('factor:'))
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			S = a + b + c
			if (S > (((255 + factor) // 2) * 3)):
				a, b, c = 255, 255, 255
			else:
				a, b, c = 0, 0, 0
			draw.point((i, j), (a, b, c))


factor = 100

Заключение

Сохраняем результат и удаляем кисть.
image.save("ans.jpg", "JPEG")
del draw


Окончательный код
import random
from PIL import Image, ImageDraw  

mode = int(input('mode:'))  
image = Image.open("temp.jpg")  
draw = ImageDraw.Draw(image)  
width = image.size[0]  
height = image.size[1]  	
pix = image.load() 
if (mode == 0):
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			S = (a + b + c) // 3
			draw.point((i, j), (S, S, S))
if (mode == 1):
	depth = int(input('depth:'))
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			S = (a + b + c) // 3
			a = S + depth * 2
			b = S + depth
			c = S
			if (a > 255):
				a = 255
			if (b > 255):
				b = 255
			if (c > 255):
				c = 255
			draw.point((i, j), (a, b, c))
if (mode == 2):
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			draw.point((i, j), (255 - a, 255 - b, 255 - c))
if (mode == 3):
	factor = int(input('factor:'))
	for i in range(width):
		for j in range(height):
			rand = random.randint(-factor, factor)
			a = pix[i, j][0] + rand
			b = pix[i, j][1] + rand
			c = pix[i, j][2] + rand
			if (a < 0):
				a = 0
			if (b < 0):
				b = 0
			if (c < 0):
				c = 0
			if (a > 255):
				a = 255
			if (b > 255):
				b = 255
			if (c > 255):
				c = 255
			draw.point((i, j), (a, b, c))
if (mode == 4):
	factor = int(input('factor:'))
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0] + factor
			b = pix[i, j][1] + factor
			c = pix[i, j][2] + factor
			if (a < 0):
				a = 0
			if (b < 0):
				b = 0
			if (c < 0):
				c = 0
			if (a > 255):
				a = 255
			if (b > 255):
				b = 255
			if (c > 255):
				c = 255
			draw.point((i, j), (a, b, c))
if (mode == 5):
	factor = int(input('factor:'))
	for i in range(width):
		for j in range(height):
			a = pix[i, j][0]
			b = pix[i, j][1]
			c = pix[i, j][2]
			S = a + b + c
			if (S > (((255 + factor) // 2) * 3)):
				a, b, c = 255, 255, 255
			else:
				a, b, c = 0, 0, 0
			draw.point((i, j), (a, b, c))
image.save("ans.jpg", "JPEG")
del draw

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 48
  • +1
    плашку туториал надо было прилепить.
  • +25
    "Опошлю" и эту статью «магией» «консольного „фотошопа“» :)
    Оттенки серого
    convert input.jpg -colorspace gray gray.jpg

    Получается выразительней. Наверное, потому что используется что-нибудь посложнее среднего значения
    Скрытый текст

    Сепия
    convert input.jpg -sepia-tone 80% sepia.jpg

    Негатив
    convert input.jpg -negate negated.jpg

    Яркость. Положительный параметр делает ярче, отрицательный — темнее
    convert input.jpg -brightness-contrast -20% dark.jpg
    convert input.jpg -brightness-contrast 20% bright.jpg

    Бинаризацию вообще можно многими путями:
    convert input.jpg -monochrome -normalize binarize3.jpg
    convert input.jpg -remap pattern:gray50 binarize2.jpg
    convert input.jpg -colorspace gray  +dither  -colors 2  binarize1.jpg

    Вот результаты:
    Скрытый текст



    Еще можно расчехлить -fx и использовать для всех этих эффектов свои формулы-программы. :)
    • +3
      Это конечно хорошо, но не спортивно, самому же интереснее. Конечно, так удобнее и быстрее, но для обучения, можно и попрактиковаться.
      • +11
        Отлично, можно и спортивно :) Градация серого по вашей формуле:

        convert input.jpg -fx '(r+g+b)/3' gray-fx.jpg

        Или «шум»:

        convert input.jpg -fx 'u+0.5*u*rand()' noise.jpg
      • +2
        Кстати насчёт оттенков серого: автор мог бы всё же поинтересоваться, и узнать, что обычно берется не среднее, а что-то вроде
        (R*30 + G*59 + B*11)/100
        • 0
          ImageMagick это здорово, но на деле PIL быстрее
        • +14
          Оттенки серого считаются вовсе не так.
          • +2
            А остальные эффекты где можно найти? Может какой ресурс есть.
            • +1
              Формула, использованная автором, очень грубая и лобовая — и тем не менее, имеет место и такой вариант тоже.
              Вообще не существует какой-то одной единственно-верной формулы пересчета в grayscale, аналогично для сепии и пр.
              Там если углубляться — черт ногу сломит.
              • +3
                Безусловно, но и углубляться никуда не нужно — эти коэффициенты всего лишь отражают восприятие разных цветов человеком. Оттуда же и растут ноги у цветовых моделей типа YUV.
            • 0
              Спасибо, как раз неспешно в обеденное время прохожу python challenge, там надо как раз по всякому ковырять картинки на предмет зашифрованного содержимого.
              • –1
                Пойду посмотрю, что это.
              • 0
                > Это очень просто, достаточно лишь каждое значение пиксела вычесть из 255.
                > draw.point((i, j), (255 — a, 255 — b, 255 — c))

                Здесь что-то не сходится. Особенно если там по вычету 255, то вычитать 255 не имеет смысла.
                • 0
                  Я вас не понимаю. Что не сходится?
                  • +1
                    Простите, я прочел как из пиксела вычесть 255.
                    Еще раз извиняюсь.
                • 0
                  А насколько это все медленно/быстро работает?
                  • 0
                    Для картинки из примеров приблизительно 3 сек на моём нетбуке.
                    • +1
                      OpenCV в среднем такие операции на картинке такого размера выполняет за 1 мс, без учета IO конечно.
                      • 0
                        Пойду почитаю о нем.
                        • 0
                          Сами посудите, если у вас процессор 2ГГц, получается что на обработку 1 пикселя уходит 30 000 тактов. Как-то многовато
                          • 0
                            Но у меня и нетбук слабенький очень.
                            • 0
                              Это просто задача не для Python. Безопасность, проверки границ и др. очень сильно тормозит работу.
                              • 0
                                Я полностью согласен. Питоновская скорость работы тут не годится для реального применения, но тем не менее получается красивый и лаконичный код, писать это на питоне нужно, я думаю, только для практики.
                                • +1
                                  PIL написан на сях, поэтому PIL достаточно быстр. Быстрее чем ImageMagick, например.
                                  • +1
                                    Но только если работать попиксельно так, как это делается в топике, то думаю в силу вступают проверки массивов Python и всю скорость съедают. Или я не прав?
                                  • +1
                                    Это разве длиннее?
                                    #include <QCoreApplication>
                                    #include <QImage>
                                    
                                    int main(int argc, char *argv[])
                                    {
                                        QCoreApplication a(argc, argv);
                                    
                                        QImage img("test.jpg");
                                        for (int i = 0; i < img.width(); i++) {
                                            for (int j = 0; j < img.height(); j++) {
                                                QRgb rgb = img.pixel(j, i);
                                                int s = (qRed(rgb) + qGreen(rgb) + qBlue(rgb)) / 3;
                                                img.setPixel(j, i, qRgb(s, s, s));
                                            }
                                        }
                                        img.save("outtest.jpg", "JPEG");
                                        return 0;
                                    }
                                    


                                    time ./imagecvt 
                                    
                                    real    0m0.033s
                                    user    0m0.023s
                                    sys     0m0.010s
                                    
                                    • +1
                                      Дело не только а размере кода (хотя код на Питоне из-за особенностей языка всеравно будет короче), а в том, что не нужен процесс компиляции.
                                      Т.е. делаете вы тулсет для работы с изображениями, вдруг требования немного изменились — открыли в notepad скрипт, подправили, сохранили — можно работать дальше.
                                      • 0
                                        Если бы я писал тулсет, я всё равно писал бы на C++. Компиляция не так страшна. Но даже если бы и понадобилось на Python, то написать часть оптимизированную на Си (вроде Python это позволяет, и выше мне подсказывают, что PIL так и сделан), а вызовы дёргать из Python. И тогда реализовать метод с матрицей 5х5 для цветового преобразования изображения.
                                        • 0
                                          Так вот для этого и используют Python+PIL :)
                                          И не нужно писать ничего низкоуровневого — все написано до нас.
                                          • 0
                                            А в PIL есть что-то вроде:
                                            image2 = image.colorTransform([[-1, 0, 0, 0, 1], [0, -1, 0, 0, 1], [0, 0, -1, 0, 1], [0, 0, 0, 1, 0], [0, 0, 0, 0, 1]]);
                                            Для инверсии?
                                            Или
                                            [[0.299, 0.587, 0.114, 0, 0], [0.299, 0.587, 0.114, 0, 0], [0.299, 0.587, 0.114, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 1]]
                                            Для grayscale?
                                            • 0
                                              Image.convert принимает второй аргумент — матрицу. Правда у меня не получилось использовать отрицательные коэффициенты.
                      • +1
                        Ну, PIL быстрее, чем ImageMagick, например
                        • –3
                          Кому нужны говнистые картинки, но быстрее?
                          • 0
                            А почему они говнистее то?
                      • +5
                        Код примера
                        from PIL import Image, ImageDraw, ImageMath
                        
                        def bw1(s):
                            d = s.copy()
                            pix = d.load()
                            draw = ImageDraw.Draw(d)
                            for i in range(s.size[0]):
                                for j in range(s.size[1]):
                                    a = pix[i, j][0]
                                    b = pix[i, j][1]
                                    c = pix[i, j][2]
                                    S = (a + b + c) // 3
                                    draw.point((i, j), (S, S, S))
                            return d
                        
                        def bw2(s):
                            r, g, b = s.split()
                            return ImageMath.eval("convert((r + g + b) / 3, 'L')", r=r, g=g, b=b)
                        
                        def bw3(s):
                            return s.convert('L')
                        
                        s = Image.open('s.jpg')
                        bw1(s).save('bw1.jpg')
                        bw2(s).save('bw2.jpg')
                        bw3(s).save('bw3.jpg')
                        
                        from timeit import timeit
                        print 'Per pixel:', timeit(lambda: bw1(s), number=10)
                        print 'ImageMath:', timeit(lambda: bw2(s), number=10)
                        print 'Image.convert:', timeit(lambda: bw3(s), number=10)
                        

                        $ ./test.py
                        Per pixel: 7.06867003441
                        ImageMath: 0.0504641532898
                        Image.convert: 0.00568413734436

                        Разница более чем в 100 и 1000 раз. Второй вариант идентичен вашему (ну вдруг вам именно это нужно было), третий — правильный. Все остальные преобразования делаются аналогично.
                        • +2
                          Тут просто тупой перебор каждого пикселя, это ахтунг. Если использовать numpy, то думаю скорость возрастет на порядок, а то и на два
                          • 0
                            Особенно если делать более сложные вещи, например, размытие, где нужно минор (подматрицу) постоянно брать.
                        • +1
                          c = max(c, 0)
                          c = min(c, 255)
                          


                          А лучше вынесли бы в отдельную функцию:
                          def limit(value, low=0, high=255):
                              return min(max(value, low), high)
                          
                          • +1
                            Другие алгоритмы:

                            Мой вариант сепии, где картинка получается более насыщенной. У сепии RGB = (112, 66, 20), компоненты с разницей в 46, поэтому я просто складываю и вычитаю на 46.
                            mean = (R + G + B) / 3
                            R = mean + 46 // если больше 255, то = 255
                            G = mean
                            B = mean - 46 // если меньше 0, то = 0
                            



                            Чёрно-белое со случайным порогом (factor). Выглядит лучше, при просмотре издалека.
                            mean = (R + G + B) / 3
                            factor = rand()%255 + 1 // Случайное число от 1 до 256
                            Если mean < factor то
                                result = 0
                            Иначе
                                result = 255
                            R = result
                            G = result
                            B = result
                            



                            Есть ещё интересное создание чёрно-белых изображений с матрицами возмущений, но там уже не так очевидно, лучше почитайте где-нибудь.
                            • 0
                              Когда-то делал сепию для одного проекта, нашёл вот такие коэффициенты:
                              def sepia(im):
                                  if not "RGB" in im.mode: im = im.convert("RGB")
                                  im = im.convert("RGB",(0.393, 0.769, 0.189, 0,
                                                         0.349, 0.686, 0.168, 0,
                                                         0.272, 0.534, 0.131, 0))
                                  
                                  return im
                              
                            • +4
                              Зачем использовать draw, если можно писать напрямую в пиксели аналогично тому, как вы их читаете? Это будет быстрее. В качестве туториала статья никуда не годится. Такое ощущение, что вы освоили цикл пробежки по пикселям и все. PIL позволяет делать куда больше. Позволяет пережимать в различные форматы, увеличивать/уменьшать с различным качеством, вырезать/вставлять куски изображений. Все, что вы сделали можно сделать фильтрами PIL и работать это будет гораздо быстрее.
                              • +1
                                Не понимаю, почему человеку влепили за это минус, он прав!
                                getpixel и putpixel работает несколько быстрее, да и PIL из коробки это умеет делать, причем ресурсоемкие операции написаны на Си.
                              • +3
                                if (mode == 2): 
                                    goto 100
                                
                                • +3
                                  a = pix[i, j][0]
                                  b = pix[i, j][1]
                                  c = pix[i, j][2]

                                  ->

                                  a, b, c = pix[i, j]
                                  • +1
                                    Все хорошо. Вот только это далеко не Pythonic way. Да и вообще с python мало связано. То же можно было на чем угодно продемонстрировать.
                                    • 0
                                      Простите меня за непрофильный вопрос, который, чесслово, мучает со времен школы: правильно ли говорить «играться» и существует ли такое слово? «Биться об стену» и «бить баклуши» понимаю, а вот с «играть с самим собой»… какие-то неприличные синонимы в голову лезут. Ау, Mithgol!
                                      • 0
                                        Сделал на базе вашего интересный эффект:
                                        Добавляем в начало кода строки
                                        image2 = Image.open("test.jpg")
                                        pix = image2.load() #Выгружаем значения пикселей.
                                        

                                        Image2 нужен, в качестве «палитры» оригинальных пикселей, без искажения.

                                        Сам код (добавляем перед сохранением изображения):

                                        if (mode == 6):
                                            mul = int(input('Multiple: '))
                                            abc = float(input('Frequency: '))
                                            freq = abc / 1000
                                            for i in range(width):
                                                for j in range(1,height-1):
                                                    v = round((mul*math.sin(i*freq)))
                                                    m =  v + j -mul
                                                    if (m > (height-1)):
                                                        m = height-1
                                                    if (m < 1):
                                                        m = 1
                                                    a = pix[i,m][0]
                                                    b = pix[i,m][1]
                                                    c = pix[i,m][2]
                                                    draw.point((i,j),(a,b,c))
                                        


                                        Вот, например, стандартная картинка из Windows7, пропущенная через прогу с параметрами mul = 10, freq = 40:
                                        image

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.