Фильтры играют важную роль в обработке изображений и сигналов, позволяя улучшить качество изображения, удалить шум, выделить интересующие области и многое другое. Существует несколько типов фильтров, используемых в цифровой обработке сигналов и компьютерном зрении. В этой статье мы рассмотрим точечные фильтры и изучим принципы их работы.
Для начала импортируем модули для дальнейшей работы с изображениями.
import math
import random
from PIL import Image, ImageChops
math
- модуль математических функций и констант.random
- модуль для работы с случайными числами.PIL.Image
иPIL.ImageChops
- модули для работы с изображениями в формате PIL.
Определим класс Filters
, который будет содержать несколько свойств и методов для работы с изображениями и фильтрами.
class Filters:
def __init__(self, image):
self.image = image
self.pixels = image.load()
self.width, self.height = image.size
self.pixels = list(image.getdata())
self.new_pixels = []
self.newimg = Image.new("RGB", (self.width, self.height), "white")
В конструкторе
__init__()
происходит инициализация объекта класса Filters. Он принимает изображение в качестве аргумента и сохраняет его в свойствеself.image
.Метод
load()
используется для загрузки пикселей изображения и сохранения их в свойствеself.pixels
в виде двумерного массива.Свойства
self.width
иself.height
хранят ширину и высоту изображения соответственно.Пиксели изображения сохраняются в виде списка при помощи метода
list(image.getdata())
и присваиваются свойствуself.pixels
.Создается пустой список
self.new_pixels
, который будет использоваться для хранения обработанных пикселей.Создается новое изображение
self.newimg
с помощьюImage.new()
. Оно имеет такие же размеры, как и исходное изображение, и начальный цвет «white».
Теперь перейдём к реализации самих фильтров. Выберем фотографию, чтобы увидеть как работают фильтры и какой результат у нас получится.
Точечные фильтры
Точечные фильтры, также известные как фильтры одного пикселя, применяются к каждому пикселю изображения независимо от других пикселей. Они обычно используются для изменения яркости, контрастности и цветовых характеристик изображения. Рассмотрим алгоритмы обработки изображений с помощью точечных фильтров.
Первым делом создадим класс SpotFilter
, который будет наследовать все свойства и методы класса Filters
. И сразу же добавим в него первый фильтр.
Inversion
class SpotFilters(Filters):
def inversion(self):
for pixel in self.pixels:
self.new_pixels.append((255 - pixel[0],
255 - pixel[1],
255 - pixel[2]))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Inversion.jpg', 'JPEG')
В цикле
for
каждый пиксель изself.pixels
обрабатывается и добавляется в списокself.new_pixels
. Обработка пикселя выполняется путем вычитания значений каждого цветового канала (R, G, B) из 255, что приводит к инверсии цвета. Новые пиксели сохраняются в виде кортежей (R, G, B) вself.new_pixels
.После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Inversion.jpg'
) и формат файла ('JPEG'
).
Grayscale
def grayscale(self):
for pixel in self.pixels:
self.new_pixels.append((int((pixel[0] + pixel[1] + pixel[2]) / 3),
int((pixel[0] + pixel[1] + pixel[2]) / 3),
int((pixel[0] + pixel[1] + pixel[2]) / 3)))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Grayscale.jpg', 'JPEG')
В цикле
for
каждый пиксель изself.pixels
обрабатывается и добавляется в списокself.new_pixels
. Обработка пикселя выполняется путем вычисления среднего значения цветовых каналов (R, G, B) и присваивания этого значения каждому цветовому каналу нового пикселя. Таким образом, все цветовые каналы нового пикселя будут иметь одно и то же значение, что создаст оттенок серого.После цикла
for
, методputdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Grayscale.jpg'
) и формат файла ('JPEG'
).
Black and white
def blackwhite(self):
for pixel in self.pixels:
if (pixel[0] + pixel[1] + pixel[2]) / 3 > 128:
self.new_pixels.append((255, 255, 255))
else:
self.new_pixels.append((0, 0, 0))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Black_white.jpg', 'JPEG')
В цикле
for
каждый пиксель изself.pixels
обрабатывается.Вычисляется среднее значение цветовых каналов (R, G, B) пикселя путем деления суммы значений на 3.
Если среднее значение больше 128 (половины максимального значения цветового канала), то пиксель считается более светлым и добавляется в список
self.new_pixels
с цветом(255, 255, 255)
(белый цвет).Если среднее значение меньше или равно 128, то пиксель считается более темным и добавляется в список
self.new_pixels
с цветом(0, 0, 0)
(черный цвет).После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Black_white.jpg'
) и формат файла ('JPEG'
).
Если кратко, то мы преобразовываем изображение в черно-белое (бинарное) изображение, где пиксели со средним значением яркости больше 128 становятся белыми, а пиксели со значением меньше или равным 128 становятся черными.
Sepia
def sepia(self):
for pixel in self.pixels:
self.new_pixels.append((int((pixel[0] * 0.393) + (pixel[1] * 0.769) + (pixel[2] * 0.189)),
int((pixel[0] * 0.349) + (pixel[1] * 0.686) + (pixel[2] * 0.168)),
int((pixel[0] * 0.272) + (pixel[1] * 0.534) + (pixel[2] * 0.131))))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Sepia.jpg', 'JPEG')
В цикле
for
каждый пиксель изself.pixels
обрабатывается.Для каждого пикселя вычисляются новые значения цветовых каналов (R, G, B) с помощью заданных коэффициентов для каждого канала. Коэффициенты используются для создания эффекта сепии. Вычисленные значения округляются до целых чисел с помощью
int()
.Вычисленные значения цветовых каналов добавляются в список
self.new_pixels
.После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Sepia.jpg'
) и формат файла ('JPEG'
).
Значения коэффициентов, используемых для преобразования цветов изображения в сепию, выбираются на основе исторических соображений и эстетических предпочтений. Эти значения позволяют приблизительно имитировать эффект старых фотографий, создавая характерные оттенки и теплую атмосферу.
В данном коде используются следующие значения коэффициентов:
Для красного канала (R): 0.393, 0.349, 0.272.
Для зеленого канала (G): 0.769, 0.686, 0.534.
Для синего канала (B): 0.189, 0.168, 0.131.
Эти значения выбраны на основе экспериментов и эмпирического опыта, чтобы достичь характерных оттенков сепии. Коэффициенты определяют вклад каждого цветового канала в формирование нового значения для каждого канала. Чем больше коэффициент для определенного канала, тем больший вклад этого канала в итоговое значение цвета.
Brightness
def brightness(self, value=50):
for pixel in self.pixels:
self.new_pixels.append((int(pixel[0] + value),
int(pixel[1] + value),
int(pixel[2] + value)))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Brightness.jpg', 'JPEG')
Метод
brightness()
принимает необязательный аргументvalue
, который по умолчанию равен 50. Значениеvalue
указывает на величину изменения яркости изображения.В цикле
for
каждый пиксель изself.pixels
обрабатывается.Для каждого цветового канала (R, G, B) каждого пикселя производится сдвиг на значение
value
. Это осуществляется путем прибавления значенияvalue
к текущему значению цветового канала.Вычисленные значения цветовых каналов добавляются в список
self.new_pixels
.После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Brightness.jpg'
) и формат файла ('JPEG'
).
Тут мы изменяем яркость изображения путем сдвига значений цветовых каналов каждого пикселя на указанное значение value
. Положительное значение value
увеличит яркость изображения, а отрицательное значение уменьшит яркость.
Сontrast
def contrast(self, value=2):
for pixel in self.pixels:
self.new_pixels.append((int(pixel[0] * value),
int(pixel[1] * value),
int(pixel[2] * value)))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Contrast.jpg', 'JPEG')
Метод
contrast()
принимает необязательный аргументvalue
, который по умолчанию равен 2. Значениеvalue
указывает на множитель изменения контрастности изображения.В цикле
for
каждый пиксель изself.pixels
обрабатывается.Для каждого цветового канала (R, G, B) каждого пикселя производится умножение на значение
value
. Это осуществляется путем умножения текущего значения цветового канала наvalue
.Вычисленные значения цветовых каналов добавляются в список
self.new_pixels
.После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Contrast.jpg'
) и формат файла ('JPEG'
).
Изменения контрастности изображения достигаются путем масштабирования значений цветовых каналов каждого пикселя на указанное значение value
. Значение value
определяет множитель контрастности, где значение больше 1 увеличивает контрастность, а значение меньше 1 уменьшает контрастность.
Gamma
def gamma(self, value=2):
for pixel in self.pixels:
self.new_pixels.append((int(pixel[0] ** value),
int(pixel[1] ** value),
int(pixel[2] ** value)))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Gamma.jpg', 'JPEG')
Метод
gamma()
принимает необязательный аргументvalue
, который по умолчанию равен 2. Значениеvalue
указывает на значение гаммы для коррекции изображения.В цикле
for
каждый пиксель изself.pixels
обрабатывается.Для каждого цветового канала (R, G, B) каждого пикселя производится возведение в степень
value
. Это осуществляется путем возведения текущего значения цветового канала в степеньvalue
.Вычисленные значения цветовых каналов добавляются в список
self.new_pixels
.После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Gamma.jpg'
) и формат файла ('JPEG'
).
Этот код реализует фильтр гамма-коррекции изображения, который изменяет яркость и контрастность изображения путем возведения значений цветовых каналов каждого пикселя в указанную степень value
. Значение value
определяет, насколько сильно будет изменяться яркость и контрастность изображения.
Blackout
Метод
blackout()
принимает необязательный аргументvalue
, который по умолчанию равен 5. Значениеvalue
указывает на величину затемнения изображения.В цикле
for
каждый пиксель изself.pixels
обрабатывается.Для каждого цветового канала (R, G, B) каждого пикселя производится деление на значение
value
. Это осуществляется путем деления текущего значения цветового канала наvalue
.Вычисленные значения цветовых каналов добавляются в список
self.new_pixels
.После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Blackout.jpg'
) и формат файла ('JPEG'
).
Реализовываем фильтр затемнения изображения путем уменьшения яркости каждого пикселя на указанную величину value
. Чем больше значение value
, тем сильнее будет затемнение изображения.
Red
def green(self):
for pixel in self.pixels:
self.new_pixels.append((0, pixel[1], 0))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Green.jpg', 'JPEG')
В цикле
for
каждый пиксель изself.pixels
обрабатывается.Для каждого пикселя создается новый пиксель с составляющей красного цвета
pixel[0]
, а остальные составляющие устанавливаются в ноль (R=pixel[0], G=0, B=0). Таким образом, все пиксели получают красный оттенок, где значение красного цвета остается неизменным, а остальные цветовые каналы устанавливаются в ноль.Сформированный новый пиксель добавляется в список
self.new_pixels
.После завершения цикла, метод
putdata()
вызывается для нового изображенияself.newimg
. Он принимает списокself.new_pixels
в качестве аргумента и устанавливает пиксели нового изображения.Затем метод
save()
вызывается для сохранения нового изображения на диск. Он принимает два аргумента: путь и имя файла для сохранения ('Results/Spot_filters/Red.jpg'
) и формат файла ('JPEG'
).
В фильтре красного оттенка изображения все пиксели получают красный цвет, а остальные цветовые каналы устанавливаются в ноль. В зелёном и синем фильтрах используется тот же способ, только меняются значения (R=0, G=pixel[1], B=0) и (R=0, G=0, B=pixel[2]).
Green
def green(self):
for pixel in self.pixels:
self.new_pixels.append((0, pixel[1], 0))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Green.jpg', 'JPEG')
Blue
def blue(self):
for pixel in self.pixels:
self.new_pixels.append((0, 0, pixel[2]))
self.newimg.putdata(self.new_pixels)
self.newimg.save('Results/Spot_filters/Blue.jpg', 'JPEG')
Надеюсь, что данная статья была полезна. В следующий раз разберем матричные фильтры, основанные на матрицах коэффициентов, называемых ядрами, и математическую морфологию, которая основана на теории множеств и применяется для анализа и обработки двумерных изображений :)
Комментарии (3)
imageman
16.05.2023 08:49-1А что же вы не сказали про скорость обработки? Про требования к памяти? Возьмите картинку с мобильного телефона (сколько там сейчас мегапикселей? 12-16?) замерьте скорость обработки и сколько памяти откушало, дополните статью.
Выбранный вами подход будет крайне не оптимальный. Мало того что вы активно используете питоновские циклы (питон довольно медленный), так вы еще всё складываете в list. Может стоит посмотреть в сторону NumPy?
Videoman
Вроде фильтры совсем базовые и простые но есть одно замечание и один совет.
Замечание - gamma у вас работает неправильно. В нем граничные значения динамического диапазона [0, 255] (или [0.0, 1.0], если удобно) должны переходить сами в себя. Поэтому лучше сначала отнормировать значение яркости к [0.0, 1.0], а потом уже возводит в степень, затем обратно.
Совет - так как фильтры точечные и каждый канал обрабатывается независимо, лучше считать их в таблице для всех значений динамического диапазона, [0, 255] например, и уже потом брать оттуда. Во-первых, это сделает скорость любого фильтра независимой от размера изображения и от сложности самого фильтра. Во-вторых, можно применять сразу несколько фильтров к таблице, а потом обсчитывать уже каналы изображения по ней. Это еще увеличит скорость обработки.
kath_ml Автор
Хорошо, спасибо за совет, учту!
Я только начала этим заниматься, т.к. была лаб. работа в университете на это тему, решила осветить пока работу точечных фильтров (самое простое и базовое), а так ещё есть фильтры , основанные на матрицах, и мат. морфология, что работают чуть посложнее :)