1. Введение

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

Для этого будем рассматривать разности значений между соседями данного пикселя (серые на картинке).

Соседи у пикселя
Соседи у пикселя

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

2. Реализация

Для обработки изображений потребуется модуль pillow (ссылка). Для удобства будем открывать изображение по ссылке. Для этого потребуется модуль urllib (ссылка). Импортируем необходимые модули.

from PIL import Image  # Для обработки изображения
from urllib.request import urlopen  # Для открытия изображения по ссылке

Откроем изображение по ссылки и приступим к обработке.

IMAGE_URL = 'https://avatars.mds.yandex.net/get-zen_doc/1587012/pub_5ccd9b67ffaa2300b352e32a_5ccda2ed02612c00b36f074c/scale_1200'
im_url = urlopen(IMAGE_URL)  # Обработаная ссылка на изображение
with Image.open(im_url) as image:  # Открытие изображения
    image_processed = Image.new('RGB', (image.size[0], image.size[1] * 2))  # Создание нового пустого изображения
    pixels = image.load()  # Получение массива пикселей
    pixels_processed = image_processed.load()

    for x in range(image.size[0]):
        for y in range(image.size[1] * 2):
            if y >= image.size[1]:
                pixels_processed[x, y] = handler_black_n_white(pixels, x, y - image.size[1], painted=True)  # Переписываем пиксели при помощи функции, написанной ранее
            else:
                pixels_processed[x, y] = pixels[x, y]  # Не меняем пиксели

image_processed.show()  # Вывод изображения

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

Введем необходимые функции.

def handler_black_n_white(pixels, i, j, painted=False):
    pixels_nearby = []  # Находим список пикселей которые стоят рядом с данным пикселем
    for di in range(-1, 2):
        for dj in range(-1, 2):
            try:
                pixels_nearby.append(pixels[i + di, j + dj])
            except:
                continue
    
    total_r, total_g, total_b = 0, 0, 0
    for pixel in pixels_nearby:
        current_r, current_g, current_b = pixel
        for other_pixel in pixels_nearby:
            other_r, other_g, other_b = other_pixel  # Выполним обработку
            delta_r = delta_g = delta_b = (abs(other_r - current_r) + abs(other_g - current_g) + abs(other_b - current_b)) / 3
            total_r, total_g, total_b = total_r + delta_r, total_g + delta_g, total_b + delta_b
    n = len(pixels_nearby)
    total_r, total_g, total_b = total_r / n / n, total_g / n / n, total_b / n / n  # Разделим на n **2
    if painted:
        total_r, total_g, total_b = recolor((total_r, total_g, total_b))
    return settle(total_r), settle(total_g), settle(total_b)  # Выведем

На вход этой функции, как понятно из названия, поступают 4 аргумента. Первый - массив с пикселями изображения; второй и третий - координаты пикселя; четвёртый - изменять цвет или нет.

pixels_nearby = []  # Находим список пикселей которые стоят рядом с данным пикселем
for di in range(-1, 2):
  for dj in range(-1, 2):
    try:
      pixels_nearby.append(pixels[i + di, j + dj])
      except:
        continue

Здесь в список pixels_nearby записываются соседи данного пикселя.

total_r, total_g, total_b = 0, 0, 0
for pixel in pixels_nearby:
  current_r, current_g, current_b = pixel
  for other_pixel in pixels_nearby:
    other_r, other_g, other_b = other_pixel  # Выполним обработку
    delta_r = delta_g = delta_b = (abs(other_r - current_r) + abs(other_g - current_g) + abs(other_b - current_b)) / 3
    total_r, total_g, total_b = total_r + delta_r, total_g + delta_g, total_b + delta_b

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

n = len(pixels_nearby)
total_r, total_g, total_b = total_r / n / n, total_g / n / n, total_b / n / n  # Разделим на n **2
if painted:
  total_r, total_g, total_b = recolor((total_r, total_g, total_b))
return settle(total_r), settle(total_g), settle(total_b)  # Выведем

Далее при необходимости пиксели подкрашиваются и возвращаются.

В функции handler_black_n_white использовались следующие функции.

def settle(n):
    return int(max(0, min(255, n)))

settle используется для того, чтобы пиксель имел допустимые значения.

def recolor(t):
    r, g, b = t
    max_r, max_g, max_b = 255, 0, 255  # Фиолетовый
    min_r, min_g, min_b = 0, 0, 0  # Черный
    return settle(min_r + r * (max_r - min_r) / 256), settle(min_g + g * (max_g - min_g) / 256), settle(min_b + b * (max_b - min_b) / 256)

recolor нужна для подкраски пикселей.

По аналогии с handler_black_n_white можно построить следующую функцию.

def handler_color(pixels, i, j):
    pixels_nearby = []  # Находим список пикселей которые стоят рядом с данным пикселем
    for di in range(-1, 2):
        for dj in range(-1, 2):
            try:
                pixels_nearby.append(pixels[i + di, j + dj])
            except:
                continue
    
    total_r, total_g, total_b = 0, 0, 0
    for pixel in pixels_nearby:
        current_r, current_g, current_b = pixel
        for other_pixel in pixels_nearby:
            other_r, other_g, other_b = other_pixel  # Выполним обработку
            delta_r, delta_g, delta_b = abs(other_r ** 2 - current_r ** 2) ** 0.5, abs(other_g ** 2 - current_g ** 2) ** 0.5, abs(other_b ** 2 - current_b ** 2) ** 0.5
            total_r, total_g, total_b = total_r + delta_r, total_g + delta_g, total_b + delta_b
    n = len(pixels_nearby)
    total_r, total_g, total_b = total_r / n / n, total_g / n / n, total_b / n / n  # Разделим на n **2
    return settle(total_r), settle(total_g), settle(total_b)  # Выведем

Из названия понятно, что пиксели, которые вернёт эта функция, будут цветными, поэтому recolor, не требуется.

handler_color отличается от hander_black_n_white только 15 строкой. Здесь считается корень из разности квадратов, а не просто модуль разности, как в hander_black_n_white. В следствии чего значения пикселей будут на порядок больше и можно будет различать изменения цвета.

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

3. Код

Вот итоговый код.

from PIL import Image  # Для обработки изображения
from urllib.request import urlopen  # Для открытия изображения по ссылке

IMAGE_URL = 'https://avatars.mds.yandex.net/get-zen_doc/1587012/pub_5ccd9b67ffaa2300b352e32a_5ccda2ed02612c00b36f074c/scale_1200'
im_url = urlopen(IMAGE_URL)  # Обработаная ссылка на изображение

def settle(n):
    return int(max(0, min(255, n)))

def handler_color(pixels, i, j):
    pixels_nearby = []  # Находим список пикселей которые стоят рядом с данным пикселем
    for di in range(-1, 2):
        for dj in range(-1, 2):
            try:
                pixels_nearby.append(pixels[i + di, j + dj])
            except:
                continue
    
    total_r, total_g, total_b = 0, 0, 0
    for pixel in pixels_nearby:
        current_r, current_g, current_b = pixel
        for other_pixel in pixels_nearby:
            other_r, other_g, other_b = other_pixel  # Выполним обработку
            delta_r, delta_g, delta_b = abs(other_r ** 2 - current_r ** 2) ** 0.5, abs(other_g ** 2 - current_g ** 2) ** 0.5, abs(other_b ** 2 - current_b ** 2) ** 0.5
            total_r, total_g, total_b = total_r + delta_r, total_g + delta_g, total_b + delta_b
    n = len(pixels_nearby)
    total_r, total_g, total_b = total_r / n / n, total_g / n / n, total_b / n / n  # Разделим на n **2
    return settle(total_r), settle(total_g), settle(total_b)  # Выведем

def recolor(t):
    r, g, b = t
    max_r, max_g, max_b = 255, 0, 255  # Фиолетовый
    min_r, min_g, min_b = 0, 0, 0  # Черный
    return settle(min_r + r * (max_r - min_r) / 256), settle(min_g + g * (max_g - min_g) / 256), settle(min_b + b * (max_b - min_b) / 256)

def handler_black_n_white(pixels, i, j, painted=False):
    pixels_nearby = []  # Находим список пикселей которые стоят рядом с данным пикселем
    for di in range(-1, 2):
        for dj in range(-1, 2):
            try:
                pixels_nearby.append(pixels[i + di, j + dj])
            except:
                continue
    
    total_r, total_g, total_b = 0, 0, 0
    for pixel in pixels_nearby:
        current_r, current_g, current_b = pixel
        for other_pixel in pixels_nearby:
            other_r, other_g, other_b = other_pixel  # Выполним обработку
            delta_r = delta_g = delta_b = (abs(other_r - current_r) + abs(other_g - current_g) + abs(other_b - current_b)) / 3
            total_r, total_g, total_b = total_r + delta_r, total_g + delta_g, total_b + delta_b
    n = len(pixels_nearby)
    total_r, total_g, total_b = total_r / n / n, total_g / n / n, total_b / n / n  # Разделим на n **2
    if painted:
        total_r, total_g, total_b = recolor((total_r, total_g, total_b))
    return settle(total_r), settle(total_g), settle(total_b)  # Выведем

with Image.open(im_url) as image:  # Открытие изображения
    image_processed = Image.new('RGB', (image.size[0], image.size[1] * 2))  # Создание нового пустого изображения
    pixels = image.load()  # Получение массива пикселей
    pixels_processed = image_processed.load()

    for x in range(image.size[0]):
        for y in range(image.size[1] * 2):
            if y >= image.size[1]:
                pixels_processed[x, y] = handler_black_n_white(pixels, x, y - image.size[1], True)  # Переписываем пиксели при помощи функции, написанной ранее
            else:
                pixels_processed[x, y] = pixels[x, y]  # Не меняем пиксели

image_processed.show()  # Вывод изображения

Или ссылка на colab.

4. Итоги

Обработанная картинка
Обработанная картинка

Нам удалось выполнить поставленную задачу. Картинка успешно обрабатывается и выводится. На данном этапе обработка занимает довольно много времени. Стоит подумать над способами оптимизации алгоритма.

Подумайте, где можно применить этот фильтр.

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


  1. lgorSL
    02.12.2021 13:26
    +5

    Для перекладывания пикселей лучше использовать numpy. Можно будет писать код

    img[1: -1, :] = np.max(img[0: -2, :], img[1: -1, :], img[2: 0, :])

    и работать с целыми областями изображений. Под капотом вызовется код на си и тормозить не будет.


  1. Coppermine
    02.12.2021 14:14
    +2

    Было интересно, но какова финальная цель (ха-ха, кто бы говорил)? Не проще выполнить свёртку силами того же pillow с каким-нибудь дифференциальным оператором, например Лапласа или Собеля?

    src = Image.open(r"/path/to/image")
    src = src.convert("L")
    dst = src.filter(ImageFilter.Kernel((3, 3), (0, 1, 0, 1, -4, 1, 0, 1, 0), 1, 1))


    1. yajohn
      02.12.2021 17:00
      +1

      Не проще. Для этого надо документацию читать.


  1. yajohn
    02.12.2021 17:03
    +1

    Вообще говоря, если в Питоне вам приходится писать цикл по элементам массива (в особенности если это пиксели изображения) или по DataFrame - то с вероятностью 99% вы делаете что-то радикально неверно. И ваш код - яркий тому пример.


  1. mst_72
    03.12.2021 00:03
    -1

    А OpenCV - не вариант?


    1. Brotherofken
      03.12.2021 00:43
      +2

      Очевидно, что в условиях лабораторки было не использовать библиотеки. Без сарказма.

      Поставил плюсик автору авансом для мотивации в развитии.


      1. yajohn
        03.12.2021 11:53
        +1

        Питон хорош именно как клей между библиотеками. Использование OpenCV или хотя бы numpy тут более чем осмысленно как с технической, так и с педагогической точки зрения.