1. Введение
В процессе этой статьи будет разработан фильтр для изображения, который позволит показать контуры фигур на изображение.
Для этого будем рассматривать разности значений между соседями данного пикселя (серые на картинке).
Если пиксель находится на границе какой-либо фигуры, то одни соседи будут иметь значение , а другие соседи - . Отсюда, чем ближе пиксель к границе, тем больше . Тем самым, пиксели возле границы будут иметь большее значение, то есть ярче.
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)
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))
yajohn
02.12.2021 17:03+1Вообще говоря, если в Питоне вам приходится писать цикл по элементам массива (в особенности если это пиксели изображения) или по DataFrame - то с вероятностью 99% вы делаете что-то радикально неверно. И ваш код - яркий тому пример.
mst_72
03.12.2021 00:03-1А OpenCV - не вариант?
Brotherofken
03.12.2021 00:43+2Очевидно, что в условиях лабораторки было не использовать библиотеки. Без сарказма.
Поставил плюсик автору авансом для мотивации в развитии.
yajohn
03.12.2021 11:53+1Питон хорош именно как клей между библиотеками. Использование OpenCV или хотя бы numpy тут более чем осмысленно как с технической, так и с педагогической точки зрения.
lgorSL
Для перекладывания пикселей лучше использовать numpy. Можно будет писать код
и работать с целыми областями изображений. Под капотом вызовется код на си и тормозить не будет.