from PIL import Image img = Image.fromarray(obj, mode=None)
obj
- объект с интерфейсом массиваmode=None
- режим для использования при чтении obj
. Если None
, то будет определяться по типу.Image
Функция Image.fromarray()
модуля Pillow
создает изображение в памяти из объекта obj
, экспортирующего интерфейс массива (используя протокол буфера).
Если obj
не является непрерывным, то вызывается метод Image.tobytes()
и используется функция PIL.Image.frombuffer()
.
Объекты библиотек |NumPy
| и Pillow
понимают друг друга без каких либо дополнительных преобразований:
from PIL import Image, ImageGrab import numpy as np import time # 2 секунды на выбор # окна для скриншота time.sleep(2) # создание скриншота выбранного окна img = ImageGrab.grab() # сохраним скриншот img.save("test.jpg") # открываем изображение with Image.open("test.jpg") as img: # преобразуем изображение # `Pillow` в объект `numpy` img_arr = np.asarray(img) # далее обрабатываем изображение `img_arr`, # представленное как массив при помощи `numpy` # ... print(type(img_arr)) # <class 'numpy.ndarray'> print(img_arr.dtype) # uint8 print(img_arr.shape) # (1080, 1920, 3) # преобразуем обратно объект # `numpy` в изображение `Pillow` img = Image.fromarray(img_arr) img.save("test.jpg")
Передача данных изображения в np.asarray()
возвращает 3D ndarray
, форма которого (row, column, color)
, что значит (height_img, width_img, channel_img)
. Другими словами количество строк массива ndarray
означает высоту картинки в пикселях, количество столбцов - ширина картинки в пикселях и последнее значение означает цвет - количество каналов картинки.
Аргумент mode
не будет использоваться для преобразования данных после чтения, но будет использоваться для изменения способа чтения данных:
>>> from PIL import Image >>>import numpy as np >>> arr = np.full((1, 1), 300) >>> img = Image.fromarray(arr, mode="L") >>> img.getpixel((0, 0)) # 44 >>> im = Image.fromarray(arr, mode="RGB") >>> im.getpixel((0, 0)) # (44, 1, 0)
Сделаем скриншот окна/экрана, с которым далее будем работать, но лучше использовать фотографию:
from PIL import ImageGrab import time # 2 секунды на выбор # окна для скриншота time.sleep(2) # создание скриншота выбранного окна img = ImageGrab.grab() # сохраним скриншот img.save('test.jpg')
Можно получить значение пикселя, указав координаты по индексу numpy.ndarray[строка, столбец]
. Обратите внимание, что порядок координат пикселя картинки ху
в массиве numpy.ndarray
инвертируется, т.е. будет ух
. Отсчет пикселей картинки начинается слева сверху, как в Pillow
.
>>> from PIL import Image >>> import numpy as np >>> im = np.array(Image.open('test.jpg')) >>> im.shape # (1080, 1920, 3) # цвет пикселя картинки с # координатами x=150 и y=100 >>> im[100, 150] # array([212, 212, 212], dtype=uint8) >>> type(im[100, 150]) # <class 'numpy.ndarray'>
Приведенный выше пример показывает значение в (y, x) = (100, 150)
, т.е. 100-ю строку и 150-й столбец пикселей. Цвета ndarray
, полученные с помощью Pillow, расположены в порядке RGB
, поэтому результат равен (R, G, B) = (212, 212, 212)
.
Цвета полученного пикселя можно распаковать:
# распаковка цвета на каналы >>> R, G, B = im[100, 150] >>> R, G, B # (212, 212, 212)
Также можно получить эти значения, указав канал.
>>> R = im[100, 150, 0] >>> G = im[100, 150, 1] >>> B = im[100, 150, 2] >>> R, G, B # (212, 212, 212)
Можно изменить цвет RGB на новое значение сразу или изменить его только на одном канале.
# меняем цвета сразу всех каналов >>> im[100, 150] = (0, 50, 100) >>> im[100, 150] # array([ 0, 50, 100], dtype=uint8) # или значение цвета только канала R >>> im[100, 150, 0] = 150 >>> im[100, 150] # array([150, 50, 100], dtype=uint8)
Создадим одноцветные изображения, установив для других каналов значений цвета значение 0, и объедините их по горизонтали с помощью np.concatenate()
. Изображения можно объединять, используя np.hstack()
или np.c_[]
.
from PIL import Image import numpy as np im = np.array(Image.open('test.jpg')) im_R = im.copy() im_R[:, :, (1, 2)] = 0 im_G = im.copy() im_G[:, :, (0, 2)] = 0 im_B = im.copy() im_B[:, :, (0, 1)] = 0 im_RGB = np.concatenate((im_R, im_G, im_B), axis=1) # im_RGB = np.hstack((im_R, im_G, im_B)) # im_RGB = np.c_['1', im_R, im_G, im_B] # обратно в формат pillow pil_img = Image.fromarray(im_RGB) # смотрим что получилось pil_img.save('test_numpy_split_color.jpg')
В numpy
легко вычислять значения пикселей и манипулировать ими.
Вычитая значение пикселя из максимального значения (инвертировать значение пикселя) можно создать негативно-позитивное перевернутое изображение.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im = np.array(img.resize((w//2, h//2))) # инвертировать значение пикселя im_i = 255 - im # смотрим что получилось Image.fromarray(im_i).save('test_numpy_inverse.jpg')
Отсечем дробную часть деления при помощи операции //
(деления без остатка) и снова умножим. Значения пикселей станут дискретными, и следовательно количество цветов может быть уменьшено.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im = np.array(img.resize((w//2, h//2))) # снижаем цветность im_32 = im // 32 * 32 im_128 = im // 128 * 128 # объединяем картинки im_dec = np.concatenate((im, im_32, im_128), axis=1) # смотрим что получилось Image.fromarray(im_dec).save('test_numpy_dec_color.jpg')
Получим черно-белое изображение в соответствии с порогом. В качестве простого примера выполним бинаризацию изображения в градациях серого.
Оператор сравнения для массива numpy.ndarray
возвращает логическое значение ndarray
каждого элемента массива. Так как значение True
считается равным 1, а значение False
- 0, то при умножении на 255 (является максимальным значением uint8
), значение True
становится 255 (белым), а значение False - 0 (черным).
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im_grey = np.array(img.resize((w//2, h//2)).convert('L')) # порог thresh = [100, 125, 150] # сравниваем с порогом и умножаем на 255 im_bin0 = (im_grey > thresh[0]) * im_grey im_bin1 = (im_grey > thresh[1]) * im_grey im_bin2 = (im_grey > thresh[2]) * im_grey # объединяем картинки im_binary = np.concatenate((im_bin0, im_bin1, im_bin2), axis=1) # смотрим что получилось Image.fromarray(np.uint8(im_binary)).save('test_numpy_binary.jpg')
Если умножать логическое значение ndarray
результата сравнения на исходное ndarray
, то значение пикселя True
остается исходным, а значение пикселя False равно 0 (черный).
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im_grey = np.array(img.resize((w//2, h//2)).convert('L')) # порог thresh = [100, 125, 150] # сравниваем с порогом и умножаем на 255 im_bin0 = (im_grey > thresh[0]) * 255 im_bin1 = (im_grey > thresh[1]) * 255 im_bin2 = (im_grey > thresh[2]) * 255 # объединяем картинки im_binary = np.concatenate((im_bin0, im_bin1, im_bin2), axis=1) # смотрим что получилось Image.fromarray(np.uint8(im_binary)).save('test_numpy_binary.jpg')
Применяя различные значения к каждому каналу RGB, можно создать красочное изображение (например сделать сепию). Создадим трехмерный пустой ndarray
с помощью np.empty()
и сохраним результаты умножения каждого цвета (каждого канала) на каждое значение.
Размер size
(высота, ширина), полученный np.shape
, распаковывается с помощью оператора *
и указывается в np.empty()
.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im_grey = np.array(img.resize((w//2, h//2)).convert('L')) im_bool = im_grey > 128 im_dst = np.empty((*im_grey.shape, 3)) r, g, b = 112, 66, 20 # r, g, b = 128, 160, 192 im_dst[:, :, 0] = im_bool * r im_dst[:, :, 1] = im_bool * g im_dst[:, :, 2] = im_bool * b # смотрим что получилось Image.fromarray(np.uint8(im_dst)).save('test_numpy_binarization_color.jpg')
До сих пор обрабатывали изображение в градациях серого, но также возможно обрабатывать цветное изображение с той же идеей, что и в приведенном выше примере. Создадим пустой ndarray
и сохраним каждый результат в каждом канале. Так как оригинал представляет собой цветное изображение (трехмерный массив), то будем использовать np.empty_like()
.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im = np.array(img.resize((w//2, h//2))) thresh = 128 maxval = 255 im_th = np.empty_like(im) for i in range(3): im_th[:, :, i] = (im[:, :, i] > thresh) * maxval # смотрим что получилось Image.fromarray(np.uint8(im_th)).save('test_binarization_from_color.jpg')
В NumPy есть функция, которая вращает ndarray
: называется она np.rot90()
. Эта функция принимает исходный ndarray в качестве первого аргумента, а в качестве второго - количество раз для поворота на 90 градусов.
Когда второй аргумент опущен, то поворот по умолчанию равен 90 градусам против часовой стрелки, а когда второй аргумент равен 2 и 3, поворот равен 180 градусам и 270 градусам против часовой стрелки.
import numpy as np from PIL import Image img = np.array(Image.open('test.jpg')) # крутим и сохраняем Image.fromarray(np.rot90(img)).save('test_rot90.jpg') Image.fromarray(np.rot90(img, 2)).save('test_rot90_180.jpg') Image.fromarray(np.rot90(img, 3)).save('test_rot90_270.jpg')
Функция NumPy, которая переворачивает ndarray
по вертикали и горизонтали, называется np.flip()
. Есть также np.flipud()
, которая переворачивает вертикально (вверх и вниз) и np.fliplr()
, которая переворачивает горизонтально (влево и вправо).
Можно конечно перевернуть ndarray
только по вертикали или горизонтали, указав аргумент np.flip()
, но проще использовать np.flipud()
и np.fliplr()
.
Если необходимо перевернуть ndarray
по вертикали и горизонтали, то нужно использовать np.flip()
.
import numpy as np from PIL import Image img = np.array(Image.open('test.jpg')) # переворачиваем и сохраняем Image.fromarray(np.flipud(img)).save('test_flipud.jpg') Image.fromarray(np.fliplr(img)).save('test_fliplr.jpg') Image.fromarray(np.flip(img, (0, 1))).save('test_flip_ud_lr.jpg')
Можно делать со значениями пикселей все, что угодно, например, умножать, делить, возводить в степень и т. д. При этом не нужно использовать цикл for/in
, потому что все изображение можно рассчитать как есть.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im = np.array(img.resize((w//2, h//2))) # изменяем гамму im_1_22 = 255.0 * (im / 255.0)**(1 / 2.2) im_22 = 255.0 * (im / 255.0)**2.2 # объединяем изображения im_gamma = np.concatenate((im_1_22, im, im_22), axis=1) # из `nympy` в формат `pillow` pil_img = Image.fromarray(np.uint8(im_gamma)) # смотрим что получилось pil_img.save('test_numpy_gamma.jpg')
В результате вычисления тип данных dtype
преобразуется в число с плавающей запятой float
. Обратите внимание, что при сохранении его нужно преобразовать в uint8
.
Указав область срезом, можно обрезать ее до прямоугольника.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: im = np.array(img) # обрезаем срезом im_trim = im[128:628, 128:928] # из `nympy` в формат `pillow` pil_im_trim = Image.fromarray(im_trim) # смотрим что получилось pil_im_trim.save('test_numpy_trim.jpg')
Может быть удобно определить функцию, которая задает левые верхние координаты (x, y), а также ширину и высоту области (width, height), подлежащей обрезке.
def trim_img(array, x, y, width, height): return array[y:y + height, x:x+width] im_trim2 = trim_img(im, 128, 192, 256, 128) print(im_trim2.shape) # (128, 256, 3) im_trim3 = trim_img(im, 128, 192, 512, 128) print(im_trim3.shape) # (128, 384, 3)
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im = np.array(img.resize((w//2, h//2))) print(im.shape) # делим изображение с помощью среза im_0 = im[:, :400] im_1 = im[:, 400:] # смотрим что получилось Image.fromarray(im_0).save('test_numpy_split_0.jpg') Image.fromarray(im_0).save('test_numpy_split_1.jpg')
Также возможно разделить изображение по горизонтали с помощью функции np.hsplit()
- разбивает ndarray
по горизонтали. Если для второго аргумента указано целочисленное значение, ndarray
делится поровну.
Когда в качестве второго аргумента np.hsplit()
или np.vsplit()
(делит по вертикали) указывается целочисленное значение, может возникнуть ошибка, если его нельзя разделить поровну. Функция np.array_split()
соответствующим образом регулирует размер и разбивает его.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im = np.array(img.resize((w//2, h//2))) im_0, im_1, im_2 = np.array_split(im, 3, axis=1) print(im_0.shape) print(im_1.shape) print(im_2.shape) # смотрим что получилось Image.fromarray(im_0).save('test_numpy_split_0.jpg') Image.fromarray(im_1).save('test_numpy_split_1.jpg') Image.fromarray(im_2).save('test_numpy_split_2.jpg') # (540, 320, 3) # (540, 320, 3) # (540, 320, 3)
Если в качестве второго аргумента перечисленных выше функций указан список, то ndarray
разделяется в позиции указанных значений.
import numpy as np from PIL import Image with Image.open('test.jpg') as img: w, h = img.size im = np.array(img.resize((w//2, h//2))) im_0, im_1, im_2 = np.hsplit(im, [200, 500]) print(im_0.shape) print(im_1.shape) print(im_2.shape) # смотрим что получилось Image.fromarray(im_0).save('test_numpy_split_0.jpg') Image.fromarray(im_1).save('test_numpy_split_1.jpg') Image.fromarray(im_2).save('test_numpy_split_2.jpg') # (540, 200, 3) # (540, 300, 3) # (540, 460, 3)
Используя срезы, один прямоугольник массива можно заменить другим прямоугольником массива. Используя такое поведение, часть изображения или все изображение целиком можно вставить в другое изображение.
import numpy as np from PIL import Image with Image.open('test.jpg') as img1, Image.open('test.jpg') as img2: w, h = img1.size dst = np.array(img1) // 2 src = np.array(img2.resize((w//2, h//2))) # вставляем в начало массива dst[0:src.shape[0], 0:src.shape[1]] = src # смотрим что получилось Image.fromarray(dst).save('test_numpy_paste.jpg')
Обратите внимание, что произойдет ошибка, если размер области, указанной в левой части, отличается от размера области, указанной в правой части.
import numpy as np from PIL import Image with Image.open('test.jpg') as img1, Image.open('test.jpg') as img2: w, h = img2.size dst = np.array(img1) // 4 src = np.array(img2.resize((w//2, h//2))) # вычисляем область вставки h = dst.shape[0] // 2 - src.shape[0] // 2 w = dst.shape[1] // 2 - src.shape[1] // 2 # вставляем ... dst[h:h+src.shape[0], w:w+src.shape[1]] = src # смотрим что получилось Image.fromarray(dst).save('test_numpy_paste.jpg')
С помощью операции для каждого элемента массива (пикселя) два изображения могут быть смешаны по альфа-каналу или скомпонованы на основе изображения маски.
Так как операция ndarray
и скалярного значения является операцией значения каждого элемента и скалярного значения, альфа-смешение можно рассчитать следующим образом.
Пример "водяного знака":
import numpy as np from PIL import Image, ImageFont, ImageDraw # создадим "водяной знак" src2 = Image.new("RGB", (500, 300)) fnt = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf", size=100) d = ImageDraw.Draw(src2) d.multiline_text((50, 100), "PYTHON", font=fnt, fill=(255, 255, 255)) # открываем скриншот src1 = np.array(Image.open('test.jpg')) # изменяем размер "водяного знака" до размеров скриншота src2 = np.array(src2.resize(src1.shape[1::-1], Image.BILINEAR)) # смешиваем dst_float = src1 + src2 * -0.15 print(dst_float.dtype) # float64 # для Pillow нужен тип `uint8` dst = dst_float.astype(np.uint8) # что получилось сохраняем Image.fromarray(dst).save('test_alpha_blend.jpg')
Наложение маски на изображение также легко выполняется с помощью операций с массивами NumPy. Арифметические операции с массивами одинаковой формы - это операции для каждого пикселя в одной и той же позиции.
Изображение в оттенках серого, считываемое как uint8, имеет 0 для черного и 255 для белого. При делении этого значения на 255 черный становится 0.0, а белый - 1.0, а при умножении этого значения на исходное изображение остается только белая часть 1.0, и может быть реализована обработка маски.
import numpy as np from PIL import Image, ImageFont, ImageDraw # создадим маску mask = Image.new("RGB", (500, 300)) fnt = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf", size=100) d = ImageDraw.Draw(mask) d.multiline_text((50, 100), "PYTHON", font=fnt, fill=(255, 255, 255)) mask.save('mask.jpg') # открываем картинку src = np.array(Image.open('test.jpg')) # открываем изображение mask = np.asarray(Image.open('mask.jpg') # и изменяем размер до размера картинки .resize(src.shape[1::-1], Image.BILINEAR)) # сведения print(mask.dtype, mask.min(), mask.max()) # uint8 0 255 mask = mask / 255 print(mask.dtype, mask.min(), mask.max()) # float64 0.0 1.0 # накладываем маску dst_float = src * mask # для Pillow нужен тип `uint8` dst = dst_float.astype(np.uint8) # что получилось сохраняем Image.fromarray(dst.astype(np.uint8)).save('test_image_mask.jpg')
В этом примере, если использовать выражение dst = src * mask / 255
, то сначала вычисляется src * mask
как uint8
и значение округляется, а затем делится на 255, что не является ожидаемым результатом. Здесь нужно учитывать порядок выполнения действий и это нормально: dst = src * (mask / 255)
или dst = mask / 255 * src
.
Если не хотите учитывать порядок, то можно привести все массивы к float64, а затем работать. Ошибок может быть меньше.
Предупреждение. Будьте осторожны, если изображение маски представляет собой изображение в градациях серого и 2D ndarray (без каналов), а умножение
src * mask
выполняется как есть, то возникает ошибка.... tmp_mask = Image.open('mask.jpg').convert('L') mask = np.array(tmp_mask.resize(src.shape[1::-1], Image.BILINEAR)) # маска без цветовых каналов print(mask.shape) # (225, 400) mask = mask / 255 dst = src * mask # ValueError: operands could not be broadcast # together with shapes (225,400,3) (225,400)