Обработка видео — анализ границ объектов
Обработка видео — анализ границ объектов
В данной работе рассматривается метод построения энергетических карт для видеопоследовательностей.
Карта энергии, рассчитываемая как евклидова норма градиентов яркости, является ключевым компонентом в алгоритмах компьютерного зрения, таких как seam carving и выделение контуров.
Нами представлена реализация на языке Julia, включающая этап отладки на одиночном статическом кадре и последующую оптимизированную обработку потока видео данных.
Для обеспечения производительности используются techniques предварительного выделения памяти и векторизация операций. Результатом работы является скрипт, генерирующий наглядную анимацию в формате GIF, которая демонстрирует динамическое изменение энергетических карт во времени. Для начала посмотрим исходное видео.
include("player.jl")
media_player("input.mp4", mode="video")
Теперь перейдём к реализации простого примера обработки. Цель этого скрипта, прочитать первый кадр из видеофайла input.mp4
, преобразовать его в черно-белое изображение, вычислить карту энергии, визуализировать результат и сохранить его в файл, а также вывести базовую статистику, далее проведём анализ скрипта по пунктам.
Этот скрипт является исследовательским и отладочным, он предназначен для проверки корректности работы алгоритма на одном статическом кадре перед его использованием в более сложных и ресурсоемких задачах.
1. Импорт необходимых библиотек
Images.jl
: Основной пакет для работы с изображениями (загрузка, сохранение, базовые операции с пикселями).ImageFiltering.jl
: Предоставляет функции для фильтрации изображений, такие какimfilter
, которая используется для применения ядер свертки (в данном случае, оператора Собела).VideoIO.jl
: Пакет для чтения и записи видеофайлов. Здесь используется для открытия видео и извлечения кадров.
Pkg.add(["Images", "ImageFiltering", "VideoIO"])
using Images, ImageFiltering, VideoIO
2. Функция преобразования RGB в Grayscale, эта функция использует стандартные коэффициенты восприятия яркости человеческим глазом, цель функции преобразовать один пиксель цветного изображения (RGB) в значение его яркости (оттенок серого).
pixel::AbstractRGB
: Аргумент функции. Аннотация типа::AbstractRGB
указывает, что функция ожидает на вход объект, представляющий RGB-пиксель.::Float64
: Аннотация возвращаемого типа указывает, что функция вернет число с плавающей запятой двойной точности.- Формула (
0.299 * R + 0.587 * G + 0.114 * B
): Это стандартные коэффициенты (ITU-R BT.601), используемые для преобразования цветного изображения в grayscale. Они учитывают различную чувствительность человеческого глаза к разным цветам (зеленый воспринимается как самый яркий, синий — как самый темный). red(pixel)
,green(pixel)
,blue(pixel)
: Функции из пакетаImages.jl
, которые извлекают соответствующий цветовой канал из пикселя. Возвращаемые значения обычно нормализованы в диапазон[0, 1]
.
function calculate_brightness(pixel::AbstractRGB)::Float64
return 0.299 * red(pixel) + 0.587 * green(pixel) + 0.114 * blue(pixel)
end
3. Функция вычисления карты энергии, принимает цветное изображение и возвращает его карту энергии.
image::AbstractMatrix{<:AbstractRGB}
: Входное изображение представляется в виде двумерного массива (матрицы), где каждый элемент — это RGB-пиксель.- Ключевой аргумент
sobel_kernel
: Позволяет передать пользовательские ядра свертки. По умолчанию используются ядра оператора Собела(Kernel.sobel()[1], Kernel.sobel()[2])
, которые возвращаются функциейKernel.sobel()
изImageFiltering.jl
. ::Matrix{Float64}
: Функция возвращает матрицу чиселFloat64
, которая и является картой энергии.- Формула:
energy = sqrt(Gx² + Gy²)
. Это вычисляет величину (модуль) вектора градиента в каждой точке.- Высокое значение энергии означает резкое изменение яркости (край объекта, текстура).
- Низкое значение энергии означает плавный переход или однородную область (небо, размытый фон).
function calculate_energy(image::AbstractMatrix{<:AbstractRGB};
sobel_kernel::Tuple=(Kernel.sobel()[1], Kernel.sobel()[2]))::Matrix{Float64}
# Конвертируем в grayscale
gray_image = calculate_brightness.(image)
# Вычисляем градиенты по осям X и Y
gradient_x = imfilter(gray_image, sobel_kernel[1])
gradient_y = imfilter(gray_image, sobel_kernel[2])
# Вычисляем энергию как евклидову норму градиентов
energy_map = sqrt.(gradient_x.^2 + gradient_y.^2)
return energy_map
end
4. Основная часть скрипта: Обработка видео
VideoIO.openvideo("input.mp4")
: Открывает видеофайл для чтения.read(video)
: Считывает следующий кадр из видео. Поскольку видео только что открыто, это будет первый кадр.close(video)
: Важно закрыть файл видео сразу после чтения необходимых данных, чтобы освободить ресурсы.size(frame)
: Выводит размерность матрицы кадра (например,(720, 1280)
— высота и ширина в пикселях).eltype(frame)
: Выводит тип элементов матрицы (например,RGB{N0f8}
— RGB-пиксель, где каждый канал представлен 8-битным беззнаковым нормализованным числом[0, 1]
).
println("Чтение видео...")
println()
video = VideoIO.openvideo("input.mp4")
frame = read(video)
close(video)
println("Размер кадра: ", size(frame))
println("Тип данных: ", eltype(frame))
5. Вычисление, нормализация и сохранение карты энергии.
@time
: Макрос, который измеряет время выполнения и объем выделенной памяти для выражения calculate_energy(frame)
. Полезно для профилирования.
println("Вычисление карты энергии...")
@time energy_result = calculate_energy(frame)
Нормализация: Исходная карта энергии energy_result
имеет произвольный диапазон значений. Чтобы корректно сохранить ее как изображение (где значения пикселей должны быть в диапазоне [0, 1]
для Float
или [0, 255]
для целых чисел), мы делим каждый элемент на максимальное значение в матрице. Это масштабирует все значения в интервал [0.0, 1.0]
.
energy_normalized = energy_result ./ maximum(energy_result)
save
: Функция из пакета Images.jl
для сохранения изображения. Она по умолчанию понимает, что матрица чисел с плавающей запятой в диапазоне [0,1]
должна быть сохранена как grayscale-изображение.
save("first_frame_energy_improved.png", energy_normalized)
load
...display
: Загружает только что сохраненное изображение и отображает его прямо в среде выполнения для просмотра результата.
display(load("first_frame_energy_improved.png"))
6. Вывод статистики
maximum
/minimum
: Показывают фактический диапазон значений на карте энергии до нормализации. Это помогает понять динамический диапазон данных. Минимальное значение, близкое к нулю (8.13e-17
), ожидаемо для однородных областей, а максимальное (~0.26
) указывает на силу самых контрастных границ.
println("Максимальное значение энергии: ", maximum(energy_result))
println("Минимальное значение энергии: ", minimum(energy_result))
Далее рассмотрим доработанный алгоритм, его код представляет собой оптимизированную версию для пакетной обработки видео, кардинально отличающуюся от предыдущего скрипта, который анализировал только один кадр. Ключевое отличие заключается в переходе от единичного вычисления к потоковой обработке множества кадров с созданием анимированной визуализации.
Принцип работы основан на предварительном выделении памяти для буферов (gray_buffer
, energy_buffer
, frame_matrix
), что исключает повторное создание массивов для каждого кадра и значительно ускоряет вычисления. Функция calculate_energy_fast!
использует векторизованные операции (@.
) и типы с одинарной точностью (Float32
) для оптимизации производительности, а также работает с переданными извне буферами, избегая затратного выделения памяти.
Основной цикл последовательно считывает кадры, вычисляет для каждого карту энергии, создает heatmap с предустановленными настройками визуализации и добавляет кадр в анимацию. В результате формируется GIF-анимация, наглядно демонстрирующая динамику изменения энергетических карт во времени для первых 40 кадров видео, что предоставляет качественно новый уровень анализа по сравнению с статичным изображением одного кадра.
using Images, ImageFiltering, VideoIO
function calculate_energy_fast!(output::Matrix{Float32}, image::AbstractMatrix{<:AbstractRGB},
gray_buffer::Matrix{Float32}, sobel_x, sobel_y)
@. gray_buffer = 0.299f0 * red.(image) + 0.587f0 * green.(image) + 0.114f0 * blue.(image)
gradient_x = imfilter(gray_buffer, sobel_x)
gradient_y = imfilter(gray_buffer, sobel_y)
@. output = sqrt(gradient_x^2 + gradient_y^2)
return output
end
function process_video_energy()
video = VideoIO.openvideo("input.mp4")
frame_count = counttotalframes(video)
anim = Animation()
sobel_x, sobel_y = Kernel.sobel()
first_frame = read(video)
seek(video, 1)
frame_matrix = collect(first_frame)
h, w = size(frame_matrix)
gray_buffer = Matrix{Float32}(undef, h, w)
energy_buffer = Matrix{Float32}(undef, h, w)
heatmap_settings = (color = :viridis, aspect_ratio = :equal, axis = false, colorbar = false, size = (600, 400), dpi = 80)
total_frames = min(40, frame_count)
println("Создаю GIF...")
gif_output = "energy_animation.gif"
for i in 1:total_frames
frame = read(video)
copyto!(frame_matrix, frame)
calculate_energy_fast!(energy_buffer, frame_matrix, gray_buffer, sobel_x, sobel_y)
plt = heatmap(reverse(energy_buffer); heatmap_settings...)
Plots.frame(anim)
if i % 20 == 0
println("Обработано $i из $total_frames кадров")
end
end
close(video)
display(gif(anim, gif_output, fps=10))
println("Анимация сохранена как '$gif_output'")
end
@time process_video_energy()
Вывод
Данные примеры представляют собой комплексное учебное пособие по современной обработке изображений и видео, раскрывая несколько ключевых аспектов программирования в научной среде.
Данный пример разбирает такие вопросы, как:
- Методология отладки: Начинать с верификации алгоритма на малых данных перед масштабированием
- Оптимизация вычислений: Использование предварительной аллокации, правильных типов данных (Float32), векторизации
- Работа с памятью: Избегание ненужных выделений памяти в циклах
- Создание воспроизводимых результатов: Экспорт и визуализация данных на всех этапах обработки
Эти примеры демонстрируют полный цикл научного программирования - от исследовательского до оптимизированного кода, что делает их ценным материалом для освоения современных методов обработки мультимедийных данных.