Сообщество Engee

Обработка видео — анализ границ объектов

Автор
avatar-yurevyurev
Notebook

Обработка видео — анализ границ объектов

В данной работе рассматривается метод построения энергетических карт для видеопоследовательностей.

Карта энергии, рассчитываемая как евклидова норма градиентов яркости, является ключевым компонентом в алгоритмах компьютерного зрения, таких как seam carving и выделение контуров.

Нами представлена реализация на языке Julia, включающая этап отладки на одиночном статическом кадре и последующую оптимизированную обработку потока видео данных.

Для обеспечения производительности используются techniques предварительного выделения памяти и векторизация операций. Результатом работы является скрипт, генерирующий наглядную анимацию в формате GIF, которая демонстрирует динамическое изменение энергетических карт во времени. Для начала посмотрим исходное видео.

In [ ]:
include("player.jl")
media_player("input.mp4", mode="video")
input.mp4 (1 of 1)

Теперь перейдём к реализации простого примера обработки. Цель этого скрипта, прочитать первый кадр из видеофайла input.mp4, преобразовать его в черно-белое изображение, вычислить карту энергии, визуализировать результат и сохранить его в файл, а также вывести базовую статистику, далее проведём анализ скрипта по пунктам.

Этот скрипт является исследовательским и отладочным, он предназначен для проверки корректности работы алгоритма на одном статическом кадре перед его использованием в более сложных и ресурсоемких задачах.

1. Импорт необходимых библиотек

  • Images.jl: Основной пакет для работы с изображениями (загрузка, сохранение, базовые операции с пикселями).
  • ImageFiltering.jl: Предоставляет функции для фильтрации изображений, такие как imfilter, которая используется для применения ядер свертки (в данном случае, оператора Собела).
  • VideoIO.jl: Пакет для чтения и записи видеофайлов. Здесь используется для открытия видео и извлечения кадров.
In [ ]:
Pkg.add(["Images", "ImageFiltering", "VideoIO"])
In [ ]:
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].
In [ ]:
function calculate_brightness(pixel::AbstractRGB)::Float64
    return 0.299 * red(pixel) + 0.587 * green(pixel) + 0.114 * blue(pixel)
end
Out[0]:
calculate_brightness (generic function with 1 method)

3. Функция вычисления карты энергии, принимает цветное изображение и возвращает его карту энергии.

  • image::AbstractMatrix{<:AbstractRGB}: Входное изображение представляется в виде двумерного массива (матрицы), где каждый элемент — это RGB-пиксель.
  • Ключевой аргумент sobel_kernel: Позволяет передать пользовательские ядра свертки. По умолчанию используются ядра оператора Собела (Kernel.sobel()[1], Kernel.sobel()[2]), которые возвращаются функцией Kernel.sobel() из ImageFiltering.jl.
  • ::Matrix{Float64}: Функция возвращает матрицу чисел Float64, которая и является картой энергии.
  • Формула: energy = sqrt(Gx² + Gy²). Это вычисляет величину (модуль) вектора градиента в каждой точке.
    • Высокое значение энергии означает резкое изменение яркости (край объекта, текстура).
    • Низкое значение энергии означает плавный переход или однородную область (небо, размытый фон).
In [ ]:
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
Out[0]:
calculate_energy (generic function with 1 method)

4. Основная часть скрипта: Обработка видео

  • VideoIO.openvideo("input.mp4"): Открывает видеофайл для чтения.
  • read(video): Считывает следующий кадр из видео. Поскольку видео только что открыто, это будет первый кадр.
  • close(video): Важно закрыть файл видео сразу после чтения необходимых данных, чтобы освободить ресурсы.
  • size(frame): Выводит размерность матрицы кадра (например, (720, 1280) — высота и ширина в пикселях).
  • eltype(frame): Выводит тип элементов матрицы (например, RGB{N0f8} — RGB-пиксель, где каждый канал представлен 8-битным беззнаковым нормализованным числом [0, 1]).
In [ ]:
println("Чтение видео...")
println()
video = VideoIO.openvideo("input.mp4")
frame = read(video)
close(video)
println("Размер кадра: ", size(frame))
println("Тип данных: ", eltype(frame))
Чтение видео...

Размер кадра: (720, 1280)
Тип данных: RGB{N0f8}

5. Вычисление, нормализация и сохранение карты энергии.
@time: Макрос, который измеряет время выполнения и объем выделенной памяти для выражения calculate_energy(frame). Полезно для профилирования.

In [ ]:
println("Вычисление карты энергии...")
@time energy_result = calculate_energy(frame)
Вычисление карты энергии...
  3.525561 seconds (3.27 M allocations: 231.483 MiB, 2.55% gc time, 211.54% compilation time: 8% of which was recompilation)
Out[0]:
720×1280 Matrix{Float64}:
 0.00207973   0.00346621  0.00249952  0.00285831  …  0.00196078  0.00196078
 0.00346621   0.00346621  0.00353485  0.00421683     0.00196078  0.00196078
 0.00249952   0.00353485  0.00346621  0.00438445     0.00196078  0.00196078
 0.00285831   0.00421683  0.00438445  0.00485269     0.00196078  0.00196078
 0.00373322   0.00415945  0.00421683  0.00490196     0.00196078  0.00196078
 0.00346621   0.00346621  0.00421683  0.00485269  …  0.00196078  0.00196078
 0.00249952   0.00353485  0.00421683  0.00438445     0.00196078  0.00196078
 0.00168833   0.00239457  0.00324651  0.00415548     0.00265611  0.0024771
 0.00168833   0.00307968  0.00445745  0.0051481      0.00385631  0.00248562
 0.00651902   0.00809465  0.00874961  0.00809465     0.00421683  0.00249952
 0.00997693   0.0105391   0.00990548  0.00747741  …  0.00415945  0.00499904
 0.00749628   0.00756774  0.00500523  0.00253093     0.00554594  0.00438445
 0.00690355   0.00562718  0.00345571  0.00304781     0.00485269  0.00285831
 ⋮                                                ⋱              
 0.00639137   0.00970538  0.00970538  0.008504       0.00155014  0.000693242
 0.00438445   0.00825837  0.00837142  0.00853233     0.00155014  0.00196078
 0.00310027   0.00649016  0.00618004  0.00804052  …  0.00207973  0.00196078
 0.00179171   0.00457896  0.00437281  0.00641239     0.00196078  1.4259e-16
 0.0030243    0.0032631   0.00334111  0.00496933     0.00219222  0.00155014
 0.00588235   0.00499904  0.00285831  0.00412068     0.00285831  0.00353485
 0.00588235   0.00499904  0.00310027  0.00367745     0.00373322  0.00353485
 0.00541439   0.00504688  0.00438445  0.00353485  …  0.00438445  0.00219222
 0.0055891    0.0055891   0.00421683  0.00249952     0.00404226  0.000693242
 0.00438445   0.00554594  0.00485269  0.00249952     0.00392157  0.00138648
 0.00285831   0.00490196  0.00504688  0.00207973     0.00404226  0.00285831
 0.000693242  0.00404226  0.00541439  0.00196078     0.00404226  0.00249952

Нормализация: Исходная карта энергии energy_result имеет произвольный диапазон значений. Чтобы корректно сохранить ее как изображение (где значения пикселей должны быть в диапазоне [0, 1] для Float или [0, 255] для целых чисел), мы делим каждый элемент на максимальное значение в матрице. Это масштабирует все значения в интервал [0.0, 1.0].

In [ ]:
energy_normalized = energy_result ./ maximum(energy_result)
Out[0]:
720×1280 Matrix{Float64}:
 0.00790444  0.0131741   0.00949995  …  7.786e-16   0.00745238  0.00745238
 0.0131741   0.0131741   0.013435       7.786e-16   0.00745238  0.00745238
 0.00949995  0.013435    0.0131741      7.786e-16   0.00745238  0.00745238
 0.0108636   0.0160269   0.016664       7.786e-16   0.00745238  0.00745238
 0.0141889   0.0158089   0.0160269      7.786e-16   0.00745238  0.00745238
 0.0131741   0.0131741   0.0160269   …  7.786e-16   0.00745238  0.00745238
 0.00949995  0.013435    0.0160269      7.786e-16   0.00745238  0.00745238
 0.00641684  0.00910106  0.012339       0.00187029  0.0100951   0.00941476
 0.00641684  0.011705    0.0169415      0.00941476  0.0146567   0.00944714
 0.0247769   0.0307654   0.0332547      0.0186309   0.0160269   0.00949995
 0.0379194   0.0400562   0.0376479   …  0.0191817   0.0158089   0.0189999
 0.0284912   0.0287628   0.0190234      0.0210785   0.0210785   0.016664
 0.0262384   0.0213873   0.0131342      0.0210785   0.0184437   0.0108636
 ⋮                                   ⋱                          
 0.0242918   0.0368874   0.0368874      0.013435    0.00589162  0.00263481
 0.016664    0.0313877   0.0318174      0.0117832   0.00589162  0.00745238
 0.0117832   0.0246672   0.0234886   …  0.0131741   0.00790444  0.00745238
 0.00680979  0.0174033   0.0166198      0.0131741   0.00745238  5.41943e-16
 0.0114945   0.0124021   0.0126986      0.0108636   0.00833201  0.00589162
 0.0223571   0.0189999   0.0108636      0.00833201  0.0108636   0.013435
 0.0223571   0.0189999   0.0117832      0.00949995  0.0141889   0.013435
 0.0205785   0.0191817   0.016664    …  0.0141889   0.016664    0.00833201
 0.0212425   0.0212425   0.0160269      0.0153635   0.0153635   0.00263481
 0.016664    0.0210785   0.0184437      0.0117832   0.0149048   0.00526963
 0.0108636   0.0186309   0.0191817      0.00790444  0.0153635   0.0108636
 0.00263481  0.0153635   0.0205785      0.00589162  0.0153635   0.00949995

save: Функция из пакета Images.jl для сохранения изображения. Она по умолчанию понимает, что матрица чисел с плавающей запятой в диапазоне [0,1] должна быть сохранена как grayscale-изображение.

In [ ]:
save("first_frame_energy_improved.png", energy_normalized)

load...display: Загружает только что сохраненное изображение и отображает его прямо в среде выполнения для просмотра результата.

In [ ]:
display(load("first_frame_energy_improved.png"))
No description has been provided for this image

6. Вывод статистики
maximum/minimum: Показывают фактический диапазон значений на карте энергии до нормализации. Это помогает понять динамический диапазон данных. Минимальное значение, близкое к нулю (8.13e-17), ожидаемо для однородных областей, а максимальное (~0.26) указывает на силу самых контрастных границ.

In [ ]:
println("Максимальное значение энергии: ", maximum(energy_result))
println("Минимальное значение энергии: ", minimum(energy_result))
Максимальное значение энергии: 0.26310858855863645
Минимальное значение энергии: 8.139066063954426e-17

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

Принцип работы основан на предварительном выделении памяти для буферов (gray_buffer, energy_buffer, frame_matrix), что исключает повторное создание массивов для каждого кадра и значительно ускоряет вычисления. Функция calculate_energy_fast! использует векторизованные операции (@.) и типы с одинарной точностью (Float32) для оптимизации производительности, а также работает с переданными извне буферами, избегая затратного выделения памяти.

Основной цикл последовательно считывает кадры, вычисляет для каждого карту энергии, создает heatmap с предустановленными настройками визуализации и добавляет кадр в анимацию. В результате формируется GIF-анимация, наглядно демонстрирующая динамику изменения энергетических карт во времени для первых 40 кадров видео, что предоставляет качественно новый уровень анализа по сравнению с статичным изображением одного кадра.

In [ ]:
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()
Создаю GIF...
Обработано 20 из 40 кадров
Обработано 40 из 40 кадров
[ Info: Saved animation to /user/my_projects/Demo/Work/track_contours/energy_animation.gif
No description has been provided for this image
Анимация сохранена как 'energy_animation.gif'
132.895229 seconds (134.93 M allocations: 9.237 GiB, 4.35% gc time, 18.41% compilation time: 12% of which was recompilation)

Вывод

Данные примеры представляют собой комплексное учебное пособие по современной обработке изображений и видео, раскрывая несколько ключевых аспектов программирования в научной среде.
Данный пример разбирает такие вопросы, как:

  1. Методология отладки: Начинать с верификации алгоритма на малых данных перед масштабированием
  2. Оптимизация вычислений: Использование предварительной аллокации, правильных типов данных (Float32), векторизации
  3. Работа с памятью: Избегание ненужных выделений памяти в циклах
  4. Создание воспроизводимых результатов: Экспорт и визуализация данных на всех этапах обработки

Эти примеры демонстрируют полный цикл научного программирования - от исследовательского до оптимизированного кода, что делает их ценным материалом для освоения современных методов обработки мультимедийных данных.