Сообщить об ошибке.

Функция Image.fromarray() модуля Pillow в Python

Создает изображение из объекта массива

Синтаксис:

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)

Примеры обработки изображений с помощью NumPy.

Сделаем скриншот окна/экрана, с которым далее будем работать, но лучше использовать фотографию:

from PIL import ImageGrab
import time

# 2 секунды на выбор 
# окна для скриншота
time.sleep(2)
# создание скриншота выбранного окна
img = ImageGrab.grab()
# сохраним скриншот
img.save('test.jpg')

Получение/установка пикселей изображения с помощью NumPy.

Можно получить значение пикселя, указав координаты по индексу 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.

В 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')

Снижение цвета с помощью 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_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.

Получим черно-белое изображение в соответствии с порогом. В качестве простого примера выполним бинаризацию изображения в градациях серого.

Оператор сравнения для массива 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')

Получаем цветное изображение с помощью NumPy.

Применяя различные значения к каждому каналу 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.

В 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)

Вставка одного изображения в другое с помощью NumPy.

Используя срезы, один прямоугольник массива можно заменить другим прямоугольником массива. Используя такое поведение, часть изображения или все изображение целиком можно вставить в другое изображение.

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)