Результаты: в YouTube Shorts, в TikTok
Мотивация
Так получилось, что когда-то с командой мы начали проект https://web.gomusic.to/, это приложение которое умеет распознавать музыку и делать универсальную ссылку сразу на все платформы, так же оно умеет принимать ссылки на все платформы и конвертировать ваш трек или плейлист куда вам нужно. Так же важно понимать, что кейс очень специфический, мы добавили функцию, по которой ты копируешь ссылку из TikTok и автоматически мы распознаем музыку из него и ты можешь сохранить трек где хочешь. Мы целились в то, что сможем генерировать однотипные видео с разной музыкой и будем попадать в ситуацию, когда человеку будет нравится песня и он тут же решит проблему через наше приложение. Во-первых, очевидно, что просто делать однотипные ролики никак не поможет, но и выдавливать из себя креативы каждый день тоже не получится, поэтому это показалось оптимальной гипотезой: универсальная оболочка для других видео или простых креативов и разная музыка для каждого видео чтобы попадать в разную целевую аудиторию. Огромную работу проделал AlexLiam за что ему огромное спасибо, я лишь периодически приносил лопату и совсем изредка копал.
Предисловие
В этой статье вы узнаете, как был написан скрипт для монтажа видеороликов, с какими проблемами мы столкнулись во время разработки. Мы постараемся показать и рассказать о том, как мы их решали, а также покажем результаты нашей работы. Упрощали и объясняли как могли, если у вас будут замечания или недопонимания по каким-то аспектам — пишите в комментарии, мы все поправим, задача в том, чтобы простыми словами объяснить как все устроенно.
Вступление
Все началось с идеи создать рекламный видеоролик, используя After Effects или Premier Pro. Нами был выделен достаточный бюджет. Были попытки работать с монтажерами, но 10 из 10 монтажеров оказались с нюансами. У кто-то был слишком загружен рабочими проектами, и они не захотели браться еще за нашу задачу, кто-то просто не приходил на встречу, кто-то говорил, что все сделает, потом говорил что это слишком сложно.
Сама идея была в том, что монтажер сделает набросок и монтаж ролика, а поверх этого будет работать скрипт, который будет заменять определенные отрезки видео и просто перезапускать компиляцию проекта. Это был бы идеальный вариант, который можно использовать множество раз и получать разные результаты.
Но поскольку мы не монтажеры, то было решено работать с теми технологиями, в которых мы лучше разбираемся. А точнее, с кодом. Хотя, изначально, было понятно, что мы лишаем себя самого главного для такой работы — гибкости и дешевых эксперементов за счет существующей программы для монтажа. Однако, радует, что даже несмотря на то, что многие видео редакторы умеют работать с хромакеем, наша функция по замене экрана телефона часто работает быстрее, эффективнее и не требует никакой настройки.
Начало работы
Сперва мы решили посмотреть, есть ли уже готовые решения для нашей задачи. Мы приступили к поиску способов, которыми можно решить данную задачу и насобирали разных подходов и библиотек.
Рассмотрев различные технологии, мы сначала решили использовать маски в OpenCV.
OpenCV (Open Source Computer Vision Library) — это библиотека компьютерного зрения с открытым исходным кодом, предназначенная для анализа, классификации и обработки изображений.
Данная библиотека предоставляет обширный набор инструментов для обработки изображений и видео, а также поддерживает большое количество алгоритмов и высокопроизводительных операций. Её применяют в различных сферах, например в разработке систем видеонаблюдения, робототехнике, медицине и других областях.
Цветовые маски в OpenCV
Самый простой вариант — это работа с каждым отдельным кадром видеоролика.
Это кадры из нашего видео, мысль простая, на каждом кадре удалим зеленый фон и каждый кадр без зеленого фона запишем в новое видео. Cначала необходимо конвертировать форматы HEX и RGB в понятный и удобный для OpenCV формат — HSV.
Перед этим давайте немного разберёмся, что такое форматы RGB, HEX и HSV:
RGB — это цветовая модель, в которой цвета формируются из сочетания трёх компонентов: красного (R), зелёного (G) и синего (B). RGB позволяет получить 16,7 миллиона цветов. Эта модель в том или ином виде используется во всех телефонах, мониторах и фотоаппаратах.
HEX (Hexadecimal) — это обозначение цвета в шестнадцатеричной системе счисления.
Шестнадцатеричный цвет задаётся следующим образом: #RRGGBB. RR (красный), GG (зелёный) и BB (синий) являются шестнадцатеричными целыми числами от 00 до FF, которые задают интенсивность цвета. Проще говоря, это тот же RGB в шестнадцатеричной системе счисления. Для тех, кто хочет ознакомиться с доступными цветами в формате HEX, можно воспользоваться этим сайтом.
HSV (Hue, Saturation, Value) — это цветовая модель, которая представляет цветовое пространство в виде трёх компонентов: тона, насыщенности и яркости.
Параметры HSV:
Тон (Hue): измеряется в углах от 0 до 360 градусов или в дробных значениях от 0 до 1.
Насыщенность (Saturation): чем больше этот параметр, тем «чище» цвет. Чем ближе значение к нулю, тем ближе цвет к нейтральному серому.
Значение (Value): представляет интенсивность выбранного цвета. Его значение выражается в процентах от 0 до 100:
0 — чёрный,
100 — самый яркий, максимально раскрывающий цвет.
Теперь, немного ознакомившись с форматами, попробуем вырезать конкретно наш зеленый цвет, а точнее диапазон зеленых цветов, с нашего кадра:
На картинке справа можно заметить множество “точек”, которые являются световыми отражениями зелёного хромакея. Эти “повреждённые участки” создают шум, который значительно портит конечный результат.
Также видно, что на заднем фоне присутствуют “синие квадратики”. Они предназначены для использования монтажёром в качестве контрольных точек, но в нашем случае только мешают “достать девушку” с зелёного фона.
В конечном итоге мы получили следующий результат:
Результат кадра, положили на "бумагу".
Поэкспериментировав с различными диапазонами зелёного, мы пришли к выводу, что этот метод достаточно нестабилен, хотя в некоторых ситуациях он может работать гораздо эффективнее. Например, при замене “синего экрана” телефона данный метод справился идеально.
Из-за обилия шума мы начали искать решение в области “удаления фона на видео и фотографиях с людьми”. В результате было принято решение использовать обученные модели для выполнения этой задачи.
Robust Video Matting
Во время поисков нейронных сетей мы наткнулись на замечательную технологию — Robust Video Matting (RVM) (https://github.com/PeterL1n/RobustVideoMatting), которая использует заранее обученную модель для обработки видеороликов.
RVM — это нейронная сеть, предназначенная для решения задачи видеоматтинга, то есть отделения переднего плана от фона на видеозаписях.
Благодаря наличию временной памяти RVM эффективно устраняет такие проблемы, как мерцание и несоответствия, которые часто возникают в видеопоследовательностях.
Архитектура RVM построена на основе кодера-декодера.
Кодер извлекает иерархические признаки из входных кадров.
Recurrent Decoder (рекуррентный декодер) позволяет модели сохранять информацию о предыдущих кадрах, что обеспечивает согласованность результатов во времени.
Что такое кодер-декодер?
Рассмотрим эти компоненты более подробно:
Кодер: извлекает важные признаки (особенности) из каждого кадра. Например, определяет границы объектов, текстуры и другие ключевые детали.
Рекуррентный декодер: на основе извлечённых признаков создаёт маттинг-карту (т.е. маску, которая указывает, какие пиксели принадлежат переднему плану, а какие — фону).
Robust принимает на вход изображение высокого разрешения, после чего снижает разрешение (Downsample) этого изображеия, чтобы оптимизировать дальнейшие вычисления. Затем уменьшенное изображение передается в энкодер (Encoder Blk), который извлекает (features) признаки изображения:
Низкоуровневые: текстуры, границы и контуры объектов:
Текстуры: Мелкие повторяющиеся узоры на поверхности, такие как шероховатость, гладкость или полосы.
Границы: Линии, где яркость или цвет резко меняются, отделяя одну область от другой.
Контуры объектов: Линии или кривые, которые описывают форму объекта.
Для получения данных этого уровня, мы сначала должны пройти этапы Conv, BathNorm и ReLU, подробнее о них поговорит позже, а сейчас рассмотрим визуально:
Среднеуровневые: комбинация текстур и структур, помогающая выделить форму и сегментацию объектов.
Комбинация текстур и форм: Разные поверхности и очертания собранные в одно целое, чтобы показать, например, крышу дома с черепицей.
Форма объектов: Как выглядит объект в общих чертах.
Сегментация объектов: Способ разделить картинку на части, например, где заканчивается стол и начинается кружка.
И наконец - высокоуровневые признаки:
Высокоуровневые: форма объекта в целом и объект в контексте фона.
Форма объекта в целом: Простая идея о том, что это за объект, например, «это машина».
Объект в контексте фона: Как объект связан с тем, что вокруг него.
Все эти признаки извлекаются на разных уровнях разрешения: 1/2, 1/4, 1/8 и 1/16 от исходного размера.
После этого применяется метод LR-ASPP (Low-Resolution Atrous Spatial Pyramid Pooling), который позволяет учитывать информацию из разных масштабов изображения, объединяя детали и контекст для более точного результата.
Давайте рассмотрим главные блоки диаграммы:
Bottleneck Block
В архитектуре Robust Video Matting рекуррентный декодер использует рекуррентный декодер использует ConvGRU.
ConvGRU — это механизм, который помогает нейросети принимать решения о том:
Что нового из входных данных стоит запомнить.
Что из прошлого опыта можно забыть или сохранить
ConvGRU работает так:
Рассмотрим формулы немного подробнее:
-
z(t) - Это вычисление обновляющего коэффициента. Он решает, насколько сильно новое значение должно влиять на текущее состояние. В этой формуле:
x(t) - текущее входное значение.
h(t-1) - предыдущее состояние.
w(zx) и w(zh) - веса, позволяющие “настроить” модель, чему она будет больше верить, входному значению или предыдущему состоянию.
b(z) - смещение, улучшает точность работы.
σ — сигмоида, функция, которая "сжимает" значение в диапазон от 0 до 1
То есть, если z(t)=0, то модель забывает предыдущее состояние, чем ближе значение к единице, тем более подробно сохраняется старое состояние
r(t) - Это коэффициент сброса. Он определяет, какие части старого состояния стоит "сбросить" или игнорировать при добавлении новой информации. Остальные переменные те же.
-
o(t) - Это новая информация, которая будет добавлена в "память" системы.
(r(t)⊙h(t−1)) — "сброс" некоторых частей старого состояния (где r(t) решает, что оставить, а что обнулить).
tanh — функция активации, которая "сжимает" данные в диапазон от -1 до 1.
-
h(t) - Обновленное состояние.
(z(t)⊙h(t−1)) — старое состояние "взвешивается" обновляющим коэффициентом z(t) (то есть сохраняется только его часть).
(1−zt)⊙(ot) — новая информация "взвешивается" тем, что не было сохранено из прошлого.
Особенности ConvGRU в RVM: ConvGRU работает только с половиной каналов, разделяя их на две части, которые затем объединяются (concatenate). После ConvGRU применяется билинейное масштабирование (bilinear upsampling) с коэффициентом 2X.
Расмотрим результат работы ConvGRU визуально:
Билинейное масштабирование — это метод, который используется для качественного масштабирования изображений. В этом методе цвет пикселя - взвешенная сумма ближайших четырех пикселей исходного изображение при увеличении, из-за чего изображение получается более гладким.
Благодаря такой архитектуре Robust Video Matting, эффективно объединяет старую и новую информацию, решая, сколько из прошлых данных использовать вместе с новыми входными данными.
Upsampling block
Данный блок отвечает за увеличение размера изображения и восстанавливает его до исходного разрешения. Он используется на уровнях масштаба 1/8, 1/4 и 1/2.
Главная задача данного блока - восстановить исходное разрешение изображения, сохраняя важные признаки и подготовить данные для следующих слоев.
Рассмотрим его подробнее.
Соединение данных на разных масштабах
Для каждого масштаба в процессе работы Upsampling Block происходит соединение трёх типов данных:
Результат предыдущего блока, то есть билинейно увеличенное изображение, которое восстанавливает информацию и помогает улучшить качество восстанавливаемого изображения.
Карта признаков: края объектов, текстуры, формы и контуры, а также другие признаки.
Исходное изображение, уменьшенное с использованием Average pooling.
Average pooling - это один из типов пулинга в сверточных нейронных сетях. Он возвращает среднее всех значений из части изображения, покрываемой фильтром. Этот тип пулинга, позволяет уменьшить изображение, при этом сохранив важную информацию (про типы пуллинга можно почитать тут этой небольшой статье: https://biswas-sagar97.medium.com/pooling-in-neural-networks-c783d9c3a35c)
Свертка и нормализация
После соединения данных на разных масштабах, выполняется свертка, которая объединяет признаки и уменьшает количество каналов, чтобы избежать перегрузки сети. Свертка позволяет более эффективно обрабатывать данные, выделяя важные особенности, а уменьшение числа каналов снижает вычислительные расходы и предотвращает излишнюю сложность модели.
После свертки выполняется стабилизация Batch Normalization.
Batch Normalization — это метод, используемый для ускорения и повышения стабильности обучения искусственных нейронных сетей за счет нормализации входов слоев путем перецентровки и перемасштабирования.
Рассмотрим основные этапы Batch Normalization:
Перецентровка: вычитание среднего значения из каждого входного значения, чтобы привести его к среднему значению 0.
Перемасштабирование: деление на стандартное отклонение, чтобы данные имели дисперсию, равную 1.
Данный метод, приводит все данные, на каждом слое, к стандартному виду. То есть, каждый слой будет иметь данные одного вида, благодаря чему будет проще их обработать. Кроме того, помимо ускорения обучения модели, также избегаются проблемы с градиентами, таких как их исчезновение или взрыв, особенно при глубоком обучении.
Далее активируется ReLU.
ReLU — это нелинейная функция активации, широко используемая в глубоком обучении. Она помогает принимать решения, на основе которых будет обучаться и делать предсказания сеть.
ReLU — простая функция, которая работает следующим образом:
Если входное значение x больше 0, функция возвращает x.
Если входное значение x меньше или равно 0, функция возвращает 0.
Проще говоря, ReLU пропускает положительные данные, а отрицательные данные заменяет на ноль. Это позволяет сети сосредоточиться на активных сигналах, что ускоряет и улучшает процесс обучения.
Полученные данные передаюстя в механиз ConvGRU, после чего вновь происходит билинейное увеличение и так до тех пор, пока не отработают все масштабы.
Output Block
Данный блок является заключительным. Результаты работы данного блока передаются пользователю.
Его основная задача — уточнение и проекция финальных результатов, которые используются для выполнения ключевых задач: предсказания альфа-канала, переднего плана и сегментации изображения.
Рассмотрим, как работает блок output:
На вход принимаются результаты предыдущего блока. Далее происходят уже описанные выше этапы свёртки (Conv), Batch Normalization и ReLU.
После прохождения двух итераций всех этих этапов мы получаем три результата:
1-канальный alpha для прозрачности,
3-канальный foreground для изображения переднего плана,
1-канальный segmentation для обучения сегментации.
В окончательный этап DGF будут переданы: альфа-канал, 3-канальный foreground для изображения переднего плана, исходное изображение, а также скрытые признаки.
Скрытые признаки — это промежуточные представления данных, сформированные на выходе Output Block.
Они содержат:
пространственную информацию о контуре объектов и их текстурах,
высокоуровневые признаки (например, семантическую информацию о переднем плане и фоне),
детали, которые помогают восстанавливать точные границы объектов и прозрачность.
DGF (Deep Guided Filter) — это модуль, используемый для улучшения высококачественного вывода (high-resolution output) альфа-канала и переднего плана, особенно при обработке видео высокого разрешения (например, 4K или HD).
DGF использует высокодетализированный входной кадр в качестве направляющего сигнала для уточнения низкодетализированных выходов из блока Output Block. Таким образом, низкокачественные предсказания масштабируются до высокого разрешения, при этом учитываются мелкие детали, текстуры и границы из исходного изображения. То есть исходное изображение здесь будет являться некой «подсказкой», чтобы улучшить результаты работы блока Output. DGF как бы «подгоняет» низкокачественные предсказания к исходному изображению, добавляя мелкие детали, текстуры и точные границы объектов.
В итоге мы получаем 1-канальный alpha для прозрачности, а также 3-канальный foreground для изображения переднего плана, с чем и работаем в дальнейшем.
Рассмотрим их на примере:
Промежуточные этапы обработки переднего плана
Для тех, кто хочет почитать подробнее о Robust Video Matting, вот публикация на английском языке:
https://www.researchgate.net/publication/354157557_Robust_High-Resolution_Video_Matting_with_Temporal_Guidance
Данная модель отлично подошла для удаления зелёного фона с ролика, благодаря чему удалось покадрово получить передний план без фона. Однако перед этим было необходимо сделать задний фон более подходящим, увеличив его насыщенность, и избавиться от элементов, которые могут мешать при распознавании фона. В нашем случае это были точки на заднем фоне ролика.
Подготовка ролика к работе с RVM
Для решения данной проблемы была использована библиотека OpenCV.
В нашем случае нас интересует именно работа с изображениями, а в частности обнаружение и отслеживание объектов. За поиск объектов в OpenCV отвечает функция findContours, позволяющая находить похожие объекты на кадре, исходя из различных параметров.
В начале нам необходимо получить само изображение из ролика. Для этого мы берём ролик и проходим по каждому его кадру, после чего передаём его в функцию, где выполняем все необходимые действия для обработки кадра, а затем записываем результат в виде видео.
import cv2
def replace(original_video_path, output_video_path, lower_green, upper_green):
video = cv2.VideoCapture(original_video_path)
frame_width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(video.get(cv2.CAP_PROP_FPS))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
output_video = cv2.VideoWriter(output_video_path, fourcc, fps, (frame_width, frame_height))
while True:
ret, frame = video.read()
if not ret:
break
processed_frame = process_frame(frame, lower_green, upper_green)
output_video.write(processed_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
video.release()
output_video.release()
cv2.destroyAllWindows()
На следующем этапе мы преобразуем кадр в HSV-пространство для работы с цветами, после чего создаём маску для выделения определённого диапазона цветов. Используя бинарное изображение маски, выполняем поиск контуров с помощью функции findContours, получая их координаты на кадре.
После этого упрощаем форму каждого контура с помощью аппроксимации, выделяя только ключевые точки контура, и ограничиваем область определения для сокращения избыточных данных. Затем немного расширяем область контура и ограничиваем область поиска. На заключительном этапе проверяем, находится ли контур на зелёном фоне, что позволяет отфильтровать ненужные области.
Дальнейшим этапом мы закрашиваем все контуры, расположенные на зелёном фоне, чтобы удалить нежелательные элементы, и увеличиваем насыщенность кадра для улучшения его визуального восприятия.
import cv2
import numpy as np
def increase_saturation(frame, saturation_scale=2):
hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv_frame)
s = cv2.multiply(s, saturation_scale)
s = np.clip(s, 0, 255).astype(np.uint8)
hsv_adjusted = cv2.merge([h, s, v])
return cv2.cvtColor(hsv_adjusted, cv2.COLOR_HSV2BGR)
def is_green_background(hsv_area, green_lower, green_upper, green_threshold=0.3):
background_mask = cv2.inRange(hsv_area, green_lower, green_upper)
green_pixels = cv2.countNonZero(background_mask)
total_pixels = hsv_area.shape[0] * hsv_area.shape[1]
return (green_pixels / total_pixels) > green_threshold
def process_frame(frame, green_lower, green_upper, radius=20):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, np.array([80, 0, 140]), np.array([130, 100, 180]))
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
square_contours = []
for cnt in contours:
epsilon = 0.02 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
x, y, w, h = cv2.boundingRect(cnt)
x_start, y_start = max(0, x - radius), max(0, y - radius)
x_end, y_end = min(hsv.shape[1], x + w + radius), min(hsv.shape[0], y + h + radius)
background_area = hsv[y_start:y_end, x_start:x_end]
if is_green_background(background_area, green_lower, green_upper):
square_contours.append(approx)
for contour in square_contours:
cv2.drawContours(frame, [contour], -1, (110, 141, 116), thickness=cv2.FILLED) # Draw filled green contours
cv2.drawContours(frame, [contour], -1, (110, 141, 116), thickness=10, lineType=cv2.LINE_AA) # Outline contours
return increase_saturation(frame)
Для лучшего восприятия визуализируем это:
Работа с RVM в коде
После подготовки ролика к работе с RVM, запускается скрипт, для получения переднего плана ролика. Ознакомиться с доступными параметрами для запуска можно на странице RVM на GitHub.В нашем случае мы остановились на покадровой обработке ролика. Для этого в RVM был передан параметр, который указывает на то, что нам необходима png_sequence — набора кадров ролика без заднего фона.
Перед запуском убедитесь, что установлена необходимая модель. Скачать её можно по ссылке:
https://github.com/PeterL1n/RobustVideoMatting?tab=readme-ov-file#download
from RobustVideoMatting.inference import convert_video
from RobustVideoMatting.model import MattingNetwork
robust_model_type = "resnet50" # Тип установленной модели. resnet50 либо mobilenetv3
robust_model_path = "rvm_resnet50.pth" # Путь к установленной модели.
model = MattingNetwork(robust_model_type).eval().cpu() # Модель также может работать с GPU: MattingNetwork(args.robust_model).eval().cuda()
model.load_state_dict(torch.load(robust_model_path)) # Загрузка модели
convert_video(
model=model,
input_source=replace_output_video_path,
# Путь к видеоролику с зеленым фоном
output_type='png_sequence',
output_composition=output_composition_path,
# Путь по которому будет сохранена png_sequence
output_video_mbps=4,
downsample_ratio=None,
seq_chunk=24
)
В результате обработки мы получаем покадровое изображение переднего плана ролика.
Давайте взглянем на промежуточный результат:
Вот с этим уже можно работать.
Обработка фонового видео
На данном этапе у нас уже есть всё, чтобы полноценно заменить фон в ролике. Следующий шаг — написать скрипт, который будет обрабатывать каждый кадр и заменять на нём фон.
Для этого нам нужно пройти по всем кадрам, полученным из RVM, и выполнить их обработку. Но прежде чем приступить к этой задаче, важно определить параметры результирующего ролика.
Особенно нас интересует FPS.
FPS (frames per second) — это количество кадров, сменяющих друг друга за одну секунду. Проще говоря, это количество изображений, которые отображаются за секунду времени. Чем выше FPS, тем плавнее воспроизводится видео.
Поскольку мы собираем новый ролик, объединяя кадры из разных источников, необходимо синхронизировать FPS с основным роликом. Если этого не сделать, то полученный ролик будет смотреться неприятно из-за рассинхронизации скорости смены кадров.
Как можно заметить, без синхронизации видео на заднем фоне воспроизводится замедленно.
Следующий важный шаг — сохранение правильных пропорций роликов. Для этого нам необходимо узнать соотношение сторон исходного видео и изменить его для ролика с фоном, если они различны, чтобы картинка не была растянута или сжата.
Работает это так:
Вычисляется центр исходного видео.
Видео масштабируется до заданного размера, сохраняя пропорции.
Лишние части обрезаются, чтобы соответствовать целевым пропорциям.
Создаётся размытый фон из оригинального видео.
Основное видео центрируется на фоне.
Если указана длительность, видео зацикливается или обрезается до нужной длины.
Объединённое видео сохраняется в заданном формате.
Давайте рассмотрим реализацию кода:
import cv2
import moviepy.editor as mp
from moviepy.video.io.VideoFileClip import VideoFileClip
# Узнаем параметры исходного ролика
video = cv2.VideoCapture(original_video_path)
frame_width, frame_height = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)), int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(video.get(cv2.CAP_PROP_FPS))
# Функция изменения пропорций видео
def create_resized_video(full_background_video_path, full_background_video_dir, fps, target_width, target_height, duration=None, no_resize=False):
temp_video_path = os.path.join(full_background_video_dir)
source_video = full_background_video_path
source_video_without_extension = os.path.splitext(os.path.basename(source_video))[0]
temp_video = os.path.join(temp_video_path, f'{source_video_without_extension}_temp.mp4')
if no_resize:
# Использование VideoFileClip в контекстном менеджере для автоматического закрытия ресурсов
with VideoFileClip(source_video) as video_clip:
video_clip.write_videofile(temp_video, codec="libx264", fps=fps)
return temp_video
if not os.path.exists(temp_video_path):
os.makedirs(temp_video_path)
# Создаём временное видео, если оно ещё не существует
if not os.path.exists(temp_video):
# Загружаем основное видео
video_clip = mp.VideoFileClip(source_video).set_fps(fps)
# Целевое соотношение сторон
target_aspect_ratio = target_width / target_height
video_aspect_ratio = video_clip.w / video_clip.h
# Изменяем размер и обрезаем, центрируя видео
if video_aspect_ratio > target_aspect_ratio:
new_width = int(target_aspect_ratio * video_clip.h)
video_clip = video_clip.crop(x_center=video_clip.w // 2, width=new_width)
else:
new_height = int(video_clip.w / target_aspect_ratio)
video_clip = video_clip.crop(y_center=video_clip.h // 2, height=new_height)
# Изменяем размер видео, чтобы оно точно соответствовало целевому размеру
video_clip = video_clip.resize((target_width, target_height))
# Создаём размытую фонограмму
background_clip = video_clip.fl_image(lambda frame: apply_blur(frame, blur_radius=10))
# Если указана продолжительность, корректируем длительность видео
if duration:
# Если продолжительность видео меньше, зацикливаем его для достижения нужной длительности
if video_clip.duration < duration:
loops_needed = int(duration // video_clip.duration) + 1
video_clip = mp.concatenate_videoclips([video_clip] * loops_needed)
video_clip = video_clip.subclip(0, duration)
else:
# Если длительность не указана, оставляем исходную длительность
duration = video_clip.duration
# Корректируем длительность фона, чтобы она совпадала с видео
if background_clip.duration < duration:
loops_needed = int(duration // background_clip.duration) + 1
background_clip = mp.concatenate_videoclips([background_clip] * loops_needed)
background_clip = background_clip.subclip(0, duration)
else:
background_clip = background_clip.subclip(0, duration)
# Объединяем фон и видео, устанавливая точную длительность для финального ролика
final_video = mp.CompositeVideoClip(
[background_clip, video_clip.set_position("center")],
size=(target_width, target_height)
).set_duration(duration)
# Сохраняем временный видеоролик
final_video.write_videofile(temp_video, codec='libx264', fps=fps)
return temp_video
Благодаря этому, мы сохраняем правильные пропорции ролика заднего фона.
Замена фона в ролике
Теперь у нас есть не только нарезанный на кадры ролик, а также синхронизированный по разрешению и FPS фоновый ролик. Следующий этап — наложение фонового видео на задний план.
Как уже упоминалось ранее, процесс заключается в покадровой обработке: мы поочерёдно берём кадры из основного ролика и накладываем на них кадры фонового видео.
Для этого на каждом шаге обработки необходимо использовать как кадр главного ролика, так и соответствующий кадр фонового видео:
for frame in frame_iterator:
ret_bg, background_frame = background_video.read()
if not ret_bg:
background_video.set(cv2.CAP_PROP_POS_FRAMES, 0)
ret_bg, background_frame = background_video.read()
processed_frame = replace_phone_screen_png(
frame, background_frame, background_phone_frame,
required_frames_for_one_second, frame_width, frame_height
)
Далее, в функции replace_phone_screen_png будет происходить вся обработка кадра. Полный код можно посмотреть на нашем гитхаб (https://github.com/gomusic/editor).
А сейчас, давайте посмотрим визуально, что происходит с роликом.
В начале мы получаем кадр с передним планом. Далее мы конвертируем его в HSV формат, а также создаем маску для синего цвета. Вырезаем маску с исходного кадра и ищем контуры маски. Если маска найдена, то мы вставляем на ее место один из роликов фона, что был передан, но перед этим нам необходимо избавится от точек внутри маски, после мы инвертируем маску и вставляем на ее место ролик с фоном.
На этом этапе мы завершили вставку ролика на место синего фона. Теперь переходим к работе с главным фоном.
Сначала извлекаем кадр главного фона. Берем кадр переднего плана, удаляем зелёные блики и устраняем остаточные зелёные оттенки. После этого послойно накладываем кадры друг на друга, чтобы получить финальное изображение.
Удаление зеленых оттенков
Как уже упоминалось ранее, перед получением итогового наложения кадров, мы сначала удаляем зеленые оттенки. Это нужно, чтобы устранить блики, которые были описаны в начале статьи, — те самые "чёрные точки", которые видны здесь. Вы можете подумать, что это не имеет значения, ведь Robust уже избавил нас от этой проблемы, но это не совсем так. Несмотря на отличную работу Robust, при наложении кадра на фон могут всё ещё оставаться зелёные участки, которые бросаются в глаза. Именно от них мы и избавляемся на этом этапе.
Как же работает удаление зеленых бликов? Всё довольно просто. Сначала мы получаем кадр в формате HSV, затем создаём маску для зелёных оттенков. На основе этой маски уменьшаем насыщенность зелёного цвета, после чего увеличиваем насыщенность остальных областей кадра.
Зуммирование на телефон
С наложением фонов мы разобрались, в итоге получается уже неплохой результат, но ролику чего-то не хватает. Для большей динамичности мы решили добавить зуммирование экрана телефона (синего фона), это сделает ролик более интересным.
Теперь давайте рассмотрим, как работает процесс зуммирования.
Из предыдущего пункта нам известно, что на каждом кадре происходит поиск маски для замены синего фона. Зуммирование работает следующим образом: когда синий фон остаётся в кадре более 1 секунды, начинается процесс зуммирования на объект.
Сначала увеличиваем масштаб, который отвечает за увеличение кадра. Затем находим центр координат синего фона, к которому будем производить зуммирование, и вычисляем новые размеры кадра. После этого, на основе этих новых координат, обрезаем края кадра и вырезаем необходимую область. Масштабируем эту часть обратно до исходных размеров и возвращаем как зуммированный кадр. Если зуммированная область становится слишком маленькой, мы возвращаем фоновый кадр, благодаря чему плавно переходим на видео телефона.
import cv2
import numpy as np
# Функция для применения зума к центру фона
def apply_zoom_to_center(main_frame, background_rect, background_frame, phone_frame, frame_width, frame_height, zoom_scale):
global global_editor_config
h, w = main_frame.shape[:2] # Высота и ширина основного кадра
rect_x, rect_y, rect_w, rect_h = background_rect # Позиция и размеры области фона
# Обеспечиваем, чтобы зум был сосредоточен на центре фонового кадра
center_x = rect_x + rect_w // 2
center_y = rect_y + rect_h // 2
# Рассчитываем новый размер зумированного кадра
new_w = int(w / zoom_scale)
new_h = int(h / zoom_scale)
new_bg_w = int(rect_w * zoom_scale)
new_bg_h = int(rect_h * zoom_scale)
# Проверяем, достаточно ли зумированный фон для покрытия обнаруженной области
if new_bg_w >= frame_width or new_bg_h >= frame_height:
# global_editor_config.start_phone_video = True
return phone_frame
# Рассчитываем координаты для обрезки зумированного кадра
x1 = max(0, center_x - new_w // 2)
y1 = max(0, center_y - new_h // 2)
x2 = min(w, center_x + new_w // 2)
y2 = min(h, center_y + new_h // 2)
cropped_frame = main_frame[y1:y2, x1:x2]
# Изменяем размер обратно к оригинальному (эффект зума)
zoomed_frame = cv2.resize(cropped_frame, (w, h))
return zoomed_frame
Поиск элементов
Отлично! Теперь у нас не только накладываются два фона, но и происходит зуммирование.
Однако, поскольку мы планируем использовать обработанный ролик для рекламы, нужно добавить ещё несколько важных шагов. Одним из них является поиск элементов на кадре.
Для этого было решено использовать поиск шаблонов из OpenCV, но тут все не так просто. Обычный поиск шаблонов в нашем случае работает крайне нестабильно. Разбираясь с тем, почему так происходит, мы пришли к выводу, что в ролике слишком много шумов, которые могут мешать его определению. Если с кнопкой "поделиться", изображённой в виде стрелки, этот метод работает довольно хорошо, то при попытке найти кнопку "копировать ссылку" в TikTok результат крайне неудовлетворительный.
Давайте подробнее рассмотрим, в чём заключается проблема.
Как можно заметить на втором фото, шумов на кадре достаточно много. Чтобы улучшить стабильность работы, нам нужно уменьшить их количество.
Для этого воспользуемся методами, встроенными в OpenCV, и рассмотрим, как они работают:
cv2.blur() — размывает изображение, благодаря чему оно становится более гладким и мягким. Этот метод работает так: определяется небольшая область, в которой вычисляется средний цвет всех пикселей, и центральный пиксель заменяется на этот средний цвет.
cv2.GaussianBlur() — этот метод также размывает изображение, но делает это более естественно. Вместо простого усреднения цветов, он придаёт больший "вес" пикселям, расположенным ближе к центру области. Пиксели внутри области взвешиваются по Гауссовому распределению, что означает, что центральные пиксели оказывают большее влияние, чем те, что находятся на периферии. Это придаёт размытому изображению мягкий и естественный вид.
cv2.bilateralFilter() — этот метод сохраняет резкие края изображения, в отличие от cv2.blur() и cv2.GaussianBlur(), которые размывают всё равномерно. Фильтр учитывает как расстояние между пикселями, так и разницу в их яркости. Пиксели с похожими значениями яркости (например, часть одного объекта) размываются, в то время как пиксели с сильно отличающейся яркостью (например, край объекта) остаются чёткими.
-
cv2.FastNIMeansDenoisingColored() — этот метод анализирует каждую область изображения и удаляет шумы, не размывая сами объекты, то есть сохраняя структуру изображения и мелкие детали на нём.
Кроме того, у данного метода в OpenCV есть несколько реализаций для разных случаев:
Рассмотрев данные методы и их результаты, мы решили использовать метод cv2.FastNIMeansDenoisingColored(), посколько он показался нам самым лучшим для нашей ситуации
Однако устранения шумов было недостаточно. Как уже упоминалось, если с определением кнопки "поделиться", которая выглядит как стрелка и имеет белый цвет, всё работает относительно стабильно, то с определением кнопки "копировать ссылку" возникают проблемы.
Очень часто эта кнопка обнаруживалась не там, где ожидалось.
Машинное зрение определяло, что эти места наиболее подходят под шаблон кнопки, так как не было привязки к цвету, что являлось серьёзной проблемой.
Чтобы избавиться от этой проблемы, было решено использовать не только поиск шаблона, но и поиск похожих по цвету областей.
Этот подход позволил нам определить места, где может находиться кнопка, что значительно сузило область поиска шаблона. То есть, изначально мы находим все области с указанным цветом, а затем, проходя по этим областям, ищем наиболее подходящую под шаблон.
Благодаря такому подходу, определение элементов похожих на кнопку “копировать ссылку” стало более стабильным.
Зуммирование к элементу
С поиском элементов на кадре мы разобрались, но как обратить внимание пользователя на них? Чтобы сделать видео более динамичным и увлекательным, было решено использовать эффект зуммирования в сочетании с затемнением, выделяя элементы на кадре.
Для этого нам нужно было знать координаты элемента, его размеры, а также плавно накладывать маску с затемнением поверх кадра, при этом не затрагивая найденный элемент.
Мы использовали подход с вычислением центра найденной области элемента, после чего вычисляли радиус вокруг этого центра, чтобы понять, какую область нам нужно оставлять не затемнённой.
Затем, применяя квадратичную функцию сглаживания, мы создаём плавный эффект зуммирования:
Изначально мы использовали обычное постепенное увеличение коэффициента зуммирования, но результат был слишком резким и неприятным для восприятия. Попробовав разные варианты, мы решили остановиться на том, который оказался более плавным и приятным. В этом подходе мы начинаем зуммировать кадр быстрее, а затем замедляемся, что делает зуммирование гораздо более комфортным для просмотра.
Чтобы элемент не затемнялся вместе с кадром, при создании маски в радиусе, который мы уже вычислили, мы оставляем "белую область", на которую не накладывается маска, и кадр в этой области остаётся неизменным.
Субтитры, звук и ускорение
На данном этапе, мы уже сделали все что было необходимо, осталось совсем ничего. Для полноценного ролика нам нужен еще звук, а для рекламного — не помешали бы субтитры, возможное ускорение некоторых сцен и добавление финального кадра с анимацией.
Прежде всего, для реализации данной задачи нам было необходимо найти подходящие библиотеки. Для добавления субтитров использовалась библиотека whisperx.
whisperx (https://github.com/m-bain/whisperX?ysclid=m4mqn1l9jj431306904904) — это продвинутая библиотека на Python, предназначенная для распознавания речи и задач обработки естественного языка. Она поддерживает несколько языков, в том числе и русский, что было очень важно для нас.
Рассмотрим результат наложения субтитров:
Для озвучивания текста использовалась обученная модель silero (https://github.com/snakers4/silero-models?ysclid=m4mqmlx7sr686186275), которая позволила нам озвучить текст. Эта модель поддерживает множество языков и предоставляет широкий выбор спикеров для озвучивания.
Голоса для озвучки можно послушать тут:
https://soundcloud.com/alexander-veysov/sets/silero-tts-samples-00 - для русского языка
https://oobabooga.github.io/silero-samples/ - для английского
Чтобы объединить все элементы и ускорить процесс, был использован FFMPEG.
FFMPEG (https://github.com/FFmpeg/FFmpeg?ysclid=m4mqquzhj1571813231) - это набор библиотек и утилит для обработки видео, аудио и графических файлов. Он предоставляет широкий функционал для работы с видео и является основой многих современных инструментов для обработки, сжатия и редактирования видео, в том числе тех, что мы использовали в этой статье.
Рассмотрим процесс обработки ролика:
Заключение
Наконец, можно с уверенностью сказать, что работа завершена! На её выполнение ушло не один месяц и множество усилий. За это время мы столкнулись с множеством проблем, которые решали различными способами. Мы постарались описать все этапы как можно понятнее в этой статье, и надеемся, что она будет полезна и интересна для читателей ☺.
В итоге мы получили отличный результат для проекта, который начался с небольшой идеи — сделать рекламный ролик, используя те навыки, которыми мы обладаем. Мы прошли немалый путь, но результат того стоил!
Все желающие могут ознакомиться с нашим кодом на GitHub по ссылке:
https://github.com/gomusic/editor
А вот и результат, который мы получили:
В YouTube Shorts, в TikTok
Было бы правильнее, показать результаты охватов аудитории и просмотров, но это отдельная война в которой все очень привязано к алгоритмам конкретной платформы, пока видно, что на абсолютно не расскурченные аккаунты на видео можно получить от 400 до 800 просмотров, в зависимости от платформы. Важно понимать, что тот же TikTok не будет долгое время показывать контент пользователям не из вашей геолокации. Поэтому, продолжаем эксперементировать.
Спасибо за внимание, надемся, это было вам полезно!
Tomasina
Видео на YT недоступно. Другие ролики YT показывает нормально.
cucusenok Автор
Исправлено, спасибо