Перейти к содержанию

Графика и аудио

Фильтры

Когда-то Instagram превратился из заурядной социальной сети в очень популярный феномен именно из-за удачной реализации встроенных фильтров. Фильтры можно было накладывать на фотографии, которые после этого обычно становились красивыми, похожими на профессиональные.

Фильтры очень широко применяются в киноиндустрии. Сравните цветовую гамму молодежных комедий или современных блокбастеров, например, с классическим «Шерлоком Холмсом».

Иначе говоря, фильтры невероятно востребованы — начиная от самых простых и заканчивая работами с привлечением искусственного интеллекта: например, в проекте Prisma.

Фильтр можно воспринимать как любое преобразование заданного изображения.

Чтобы добиться лучшего эффекта, их можно накладывать последовательно.

В библиотеке PIL реализовано много встроенных фильтров и инструментов (вырезание, изменение размеров и т. д.). Фактически это такой программируемый мини-Photoshop, но мы попытаемся поработать с фильтрами самостоятельно, чтобы поучиться восприятию цветовой палитры и алгоритмизации.

Для начала попробуем превратить изображение в черно-белое.

Черно-белое

Черно-белое изображение содержит только информацию о яркости, но не о цветах. У таких изображений все три компоненты имеют одинаковое значение, поэтому мы можем просто «размазать» суммарную яркость пикселя поровну по трем компонентам.

1
2
3
4
5
for i in range(x):
    for j in range(y):
        r, g, b = pixels[i, j]
        bw = (r + g + b) // 3
        pixels[i, j] = bw, bw, bw

Черно-белый фильтр

Можно сказать, что мы слили содержимое контейнеров R, G, B в одну емкость, а затем разлили обратно, но уже поровну в каждый контейнер. Суммарная яркость пикселя осталась прежней, но информация о цвете не сохранилась. Фотография же стала более «задумчивой». Попробуем поменять местами зеленый и синий каналы:

Смена цветов

1
2
3
4
for i in range(x):
    for j in range(y):
        r, g, b = pixels[i, j]
        pixels[i, j] = r, b, g

Смена цветовых каналов

Негатив

Давайте подумаем над тем, как получить негатив. Если в позитиве белое изображение (255), в негативе должно быть черное (0) и наоборот. То есть для значения х негативом будет 255 — x.

1
2
3
4
for i in range(x):
    for j in range(y):
        r, g, b = pixels[i, j]
        pixels[i, j] = 255 - r, 255 - g, 255 - b

Негатив

Как видим, в негативе можно рассмотреть некоторые детали, которые не видны в позитиве.

Кривые

Во многих редакторах, включая Photoshop, есть инструмент «Кривые» (Curves).

Он позволяет задать функцию, меняющую яркость всего пикселя или отдельной компоненты в зависимости от исходной яркости. Изначально эта функция представляет собой прямую y = x.

В Python можно написать функцию, которая работает как инструмент Curves. Например, мы можем высветлить темные участки в изображении, не трогая светлые. Это очень частая операция: например, когда на снимке светлое небо и очень темное здание, потому что фотоаппарат подстроился под яркость неба.

Кривые — исходное

«Высветлить» означает увеличить значения всех цветовых компонентов на какой-то коэффициент. Важно помнить, что эти значения не могут быть больше 255.

def curve(pixel):
    r, g, b = pixel
    brightness = r + g + b
    if brightness < 60:
        k = 60 / brightness
        return (
            min(255, int(r * k ** 2)),
            min(255, int(g * k ** 2)),
            min(255, int(b * k ** 2))
        )
    else:
        return r, g, b


for i in range(x):
    for j in range(y):
        pixels[i, j] = curve(pixels[i, j])

Кривые — результат

Готовые функции PIL

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

resize — изменение размера

Мы можем изменить размер изображения с помощью функции resize, в которую кортежем передается новый размер изображения. Обратите внимание: все подобные функции не изменяют исходное изображение, а возвращают его измененную копию.

1
2
3
4
5
from PIL import Image

im = Image.open("2.jpg")
im2 = im.resize((100, 100))
im2.save("6.jpg")

Изменение размера

crop — обрезка

С помощью функции crop вырезать прямоугольный кусочек из изображения. В функцию передаются координаты верхнего левого и правого нижнего угла вырезаемого прямоугольника одним кортежем.

1
2
3
4
5
from PIL import Image

im = Image.open("2.jpg")
im2 = im.crop((200, 200, 500, 500))
im2.save("7.jpg")

Обрезка изображения

quantize — квантование цвета

Функция quantizeиспользуется для сокращения цветов в палитре изображения и используется для создания миниатюр для предпросмотра. Принимает на вход число меньшее 256 — количество цветов. Обратите внимание: эта функция также преобразовывает изображение в формат bmp.

1
2
3
4
5
from PIL import Image

im = Image.open("2.jpg")
im2 = im.quantize(16)
im2.save("8.bmp")

Квантование

transpose — отражение и поворот

В задачах прошлого урока вы пробовали вращать и отражать изображения, манипулируя пикселями напрямую, однако PIL содержит уже готовые реализации данных алгоритмов. Повороты и отражения изображения можно выполнить с помощью функции transpose, в которую передается тип преобразования. Это может быть отражение слева направо, или сверху вниз, или повороты на 90, 180 или 270 градусов.

from PIL import Image

im = Image.open("2.jpg")
im2 = im.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_90)
# Image.FLIP_LEFT_RIGHT,
# Image.FLIP_TOP_BOTTOM,
# Image.ROTATE_90,
# Image.ROTATE_180,
# Image.ROTATE_270
im2.save('9.jpg')

Поворот и отражение

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

GaussianBlur — размытие по Гауссу

Библиотека PIL содержит в себе много любопытного: например, в модуле ImageOps есть встроенные реализации превращения изображения в негатив (функция invert) и в черно-белое изображение (функция grayscale), а в модуле ImageFilter находятся встроенные интересные фильтры изображений, один из которых мы рассмотрим. Этот фильтр называется размытие Гаусса и принимает на вход радиус размытия.

1
2
3
4
5
from PIL import Image, ImageFilter

im = Image.open("2.jpg")
im2 = im.filter(ImageFilter.GaussianBlur(radius=5))
im2.save("10.jpg")

Модуль wave

Фильтры можно накладывать не только на изображения, но и на звуковые файлы. Для манипуляции с «сырыми», необработанными аудиоданными предназначен модуль wave. В ОС Windows такие данные хранятся в файлах с расширением .wav.

«Сырые» аудиоданные представляют собой зависимость амплитуды звукового сигнала от времени. Посмотрите на график этой зависимости. Вы часто видели его в фильмах, репортажах журналистов и т. д.

Вдоль оси абсцисс откладывается время, вдоль оси ординат — амплитуда (интенсивность, громкость) звукового сигнала.

Этот график строится по точкам, причем вместо пар (t, y) хранятся только значения y, а ось абсцисс задана частотой дискретизации (количеством отсчетов в секунду) — как правило, она составляет 44 100 Гц. Такой частоты достаточно, чтобы человек мог прослушать оцифрованный звук и не заметить его отличия от реального.

Форма звуковой волны

Получается, что представление звукового файла для программиста очень простое. Это список целых чисел (положительных и отрицательных) — значений амплитуды звукового сигнала.

В следующем примере мы рассмотрим основные этапы работы со звуковым файлом. Наша программа будет разворачивать звуковой файл в обратную сторону, то есть проигрывать музыкальное произведение задом наперед.

Файл для примера можно скачать по ссылке

import wave
import struct

source = wave.open("in.wav", mode="rb")
dest = wave.open("out.wav", mode="wb")

dest.setparams(source.getparams())
# найдем количество фреймов
frames_count = source.getnframes()

data = struct.unpack("<" + str(frames_count) + "h",
    source.readframes(frames_count))

# собственно, основная строка программы - переворот списка
newdata = data[::-1]

newframes = struct.pack("<" + str(len(newdata)) + "h", *newdata)

# записываем содержимое в преобразованный файл.
dest.writeframes(newframes)
source.close()
dest.close()

Мы открываем два файла: исходный source и формируемый dest.

Одно значение амплитуды в терминах библиотеки wave называется фреймом.

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

Модуль struct позволяет паковать и извлекать несколько значений в бинарные структуры. Это чем-то похоже на структуры в С. В примере со звуковыми волнами значение y хранится как знаковое двухбайтное число. Поэтому извлечение выглядит так странно.

Интересно, что палиндромы в аудиофайлах сохраняют свою «палиндромность» (например, «А роза упала на лапу Азора»).

Что еще можно сделать?

  • Если мы уберем, например, каждый второй фрейм, ускорим воспроизведение вдвое. При этом частота тоже вырастет в два раза (и вы услышите голоса Чипа и Дейла). Кстати, на телевидении и радио часто ускоряют видео и аудио на 5-10%: это незаметно для уха, но позволяет разместить больше рекламы в эфире

  • Если мы увеличим все фреймы в какое-то количество раз, сделаем произведение громче, а если уменьшим — тише.

  • Копируя каждый фрейм два раза, мы замедлим воспроизведение и понизим частоту

  • Если вы заинтересовались обработкой аудиофайлов, обратите свое внимание на библиотеки pydub и librosa.


Задачи

Задача 1

Напишите программу, которая открывает изображение и переводит его в оттенки серого, усредняя значения R, G и B. Сохраните результат в файл result.jpg.

Тест 1:

Ввод:

python solution.py image.jpg
Вывод:
Сохранено: result.jpg

Тест 2:

Ввод:

python solution.py photo.png
Вывод:
Сохранено: result.jpg


Задача 2

Напишите программу, которая создаёт негатив изображения (каждый пиксель заменяется на 255 - значение по каждому каналу). Сохраните результат.

Тест 1:

Ввод:

python solution.py image.jpg
Вывод:
Сохранено: negative.jpg

Тест 2:

Ввод:

python solution.py photo.png
Вывод:
Сохранено: negative.jpg


Задача 3

Напишите программу, которая меняет местами красный и синий каналы каждого пикселя изображения. Сохраните результат в swapped.jpg.

Тест 1:

Ввод:

python solution.py image.jpg
Вывод:
Сохранено: swapped.jpg

Тест 2:

Ввод:

python solution.py sunset.jpg
Вывод:
Сохранено: swapped.jpg


Задача 4

Напишите функцию make_thumbnail(filename, max_size), которая открывает изображение и уменьшает его так, чтобы большая сторона не превышала max_size пикселей (сохраняя пропорции). Сохраните результат в thumb.jpg.

Тест 1:

Ввод:

make_thumbnail("photo.jpg", 100)
Вывод:
Сохранено: thumb.jpg (100x75)

Тест 2:

Ввод:

make_thumbnail("photo.jpg", 200)
Вывод:
Сохранено: thumb.jpg (200x150)


Задача 5

Напишите программу, которая вырезает центральный квадратный фрагмент изображения со стороной, равной меньшей стороне изображения. Сохраните результат в cropped.jpg.

Тест 1:

Ввод:

python solution.py image.jpg
Вывод:
Исходный размер: 800x600
Сохранено: cropped.jpg (600x600)

Тест 2:

Ввод:

python solution.py photo.jpg
Вывод:
Исходный размер: 1200x900
Сохранено: cropped.jpg (900x900)


Задача 6

Напишите функцию apply_blur(filename, radius), которая применяет размытие по Гауссу с указанным радиусом и сохраняет результат в blurred.jpg.

Тест 1:

Ввод:

apply_blur("photo.jpg", 3)
Вывод:
Применено размытие с радиусом 3. Сохранено: blurred.jpg

Тест 2:

Ввод:

apply_blur("photo.jpg", 10)
Вывод:
Применено размытие с радиусом 10. Сохранено: blurred.jpg


Задача 7

Напишите программу, которая делает изображение «ярче»: увеличивает каждый цветовой канал каждого пикселя на 50, не превышая значения 255.

Тест 1:

Ввод:

python solution.py dark.jpg
Вывод:
Сохранено: bright.jpg

Тест 2:

Ввод:

python solution.py photo.jpg
Вывод:
Сохранено: bright.jpg


Задача 8

Напишите функцию count_frames(filename), которая открывает WAV-файл и возвращает количество кадров, частоту дискретизации и длительность (в секундах, округлённую до 2 знаков).

Тест 1:

Ввод:

count_frames("audio.wav")
Вывод:
Кадров: 44100
Частота: 44100 Гц
Длительность: 1.0 с

Тест 2:

Ввод:

count_frames("long.wav")
Вывод:
Кадров: 220500
Частота: 44100 Гц
Длительность: 5.0 с


Задача 9

Напишите программу, которая изменяет громкость WAV-файла в factor раз (аргумент задаётся в коде). Значения амплитуды должны оставаться в диапазоне от −32767 до 32767.

Тест 1:

Ввод:

## factor = 2.0
python solution.py in.wav
Вывод:
Громкость увеличена в 2.0 раз. Сохранено: out.wav

Тест 2:

Ввод:

## factor = 0.5
python solution.py in.wav
Вывод:
Громкость уменьшена в 2.0 раз. Сохранено: out.wav


Задача 10

Напишите программу, которая применяет к изображению последовательность из трёх фильтров: сначала переводит в чёрно-белое, затем создаёт негатив, и наконец применяет размытие по Гауссу с радиусом 2. Каждый промежуточный результат сохраняется в отдельный файл (step1.jpg, step2.jpg, step3.jpg).

Тест 1:

Ввод:

python solution.py image.jpg
Вывод:
Шаг 1 (ч/б): сохранено step1.jpg
Шаг 2 (негатив): сохранено step2.jpg
Шаг 3 (размытие): сохранено step3.jpg

Тест 2:

Ввод:

python solution.py photo.png
Вывод:
Шаг 1 (ч/б): сохранено step1.jpg
Шаг 2 (негатив): сохранено step2.jpg
Шаг 3 (размытие): сохранено step3.jpg