Open CV. Основы работы с видео

Вводная

Содержание курса по Open CV:
  1. Основы работы с изображениями
  2. Основы работы с видео
  3. Инструменты визуализации

Из предыдущей статьи мы знаем, что изображение воспринимается Open CV как матрица пикселей. Видео же в этой библиотеке представляется как набор последовательных изображений (кадров), прогоняющихся в цикле, на каждой итерации которого единичный текущий кадр представляется обычным OpenCV изображением. Однако, есть свои нюансы, с которыми сейчас познакомимся.

В этой части курса научимся:
  1. открывать видео из файловой системы,
  2. записывать видео с веб-камеры
  3. и с CSI-камеры Raspberry Pi.

Начнем с того, что перейдем в виртуальное окружение, в котором установлен OpenCV:
source ~/.profile && workon cv



1. Чтение видео из файловой системы

Видео файл для примера можно использовать любой имеющийся, переименовав его в “video.mp4”, или взять из архива.
import cv2
cap = cv2.VideoCapture('video.mp4')
while(cap.isOpened()):
 ret, frame = cap.read()

 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

 cv2.imshow('frame',gray)
 if cv2.waitKey(1) & 0xFF == ord('q'):
 break
cap.release()
cv2.destroyAllWindows()

Отступы. В отличие от Ruby в Python нет закрывающих операторов  для функций, циклов и условий. Вместо них используется единообразие отступов. Отступ - это 4 символа пробела от начала строки (так принято) или один символ табуляции (Tab) (так удобней). Разберем код по порядку:
cap = cv2.VideoCapture('video.mp4')

Создаем экземпляр класса VideoCapture() и присваиваем его переменной cap. VideoCapture() принимает один аргумент - это путь к файлу (относительный или абсолютный) или целое число (индекс подключенной камеры). Захват с камеры рассмотрим ниже.

Цикл WHILE

Объявляется цикл WHILE, его аргументом должна быть функция, возвращающая булевое значение (True или False). В нашем случае функция isOpened() объекта класса VideoCapture() каждый раз при обращении будет возвращать True пока видео не дойдет до конца.

После объявления цикла (функции/метода или условия) в Python ставится символ “:”, а следующие строки тела объявленного цикла (функции/метода или условия) пишутся с +1 отступом - в нашем случает это будет 0 + 1 = 1 отступ.

Выход из цикла WHILE происходит при передаче ему в качестве аргумента False или командой break внутри тела цикла.

Функция cap.read() класса VideoCapture() возвращает два объекта:
  1. булевое значение (True или False), в случае отсутствия ошибок при загрузке текущего кадра - True. Запишем это в переменную ret
  2. сам текущий прочитанный кадр из видео. Запишем его в переменную frame. В принципе для вывода и сохранения единичного изображения уже можно использовать переменную frame - она содержит полноразмерный цветной кадр, однако, на практике в машинном зрении прочитанный кадр сперва конвертируют в нужное цветовое представление
Функция cvtColor() конвертирует изображение в нужное цветовое представление. Принимает аргументами сам объект изображения и имя представления, в нашем случае - это черно-белое для уменьшения ресурсозатрат при выводе прочитанного видео на экран. Для справки, список всех доступных цветовых представлений можно вывести скриптом (на скриншоте только часть списка):
import cv2
flags = [i for i in dir(cv2) if i.startswith('COLOR_')]
print(flags)
Функция cv2.imshow() нам знакома из предыдущего урока - выводит единичное изображение (объект Open CV) на экран в отдельном окне.

Условие

Условие IF. Ранее мы использовали функцию waitKey() с аргументом “0” для ожидания нажатия клавиши клавиатуры. Если же передать этой функции аргумент “1”, то она не будет тормозить скрипт на себе ожидая нажатия, а просто выполнит проверку и при наличии нажатия вернет индекс клавиши, а скрипт продолжит выполняться дальше.

Также как и для цикла WHILE, для условия аргументом должна служить функция, возвращающая булевое значение. Сравнив индекс нажатой клавиши с индексом клавиши “q” мы получим булевое значение - они будут или равны или нет (True или False)

Мы до сих пор находимся в теле цикла WHILE и объявляем условие начиная строку с одного отступа.
Чтоб при нажатии клавиши “q” завершить цикл в теле условия IF мы вызываем команду break, которая прервет цикл WHILE, и скрипт продолжит выполняться дальше.
break находится внутри условия, значит его отступ нужно увеличить на 1 относительно строки объявления его условия: 1 + 1 = 2 отступа.

Также следует заметить, что в данном случае условие сработает тогда и только тогда, когда нажат символ “q”, а не “Q” или “й” или “Й”.

Завершив цикл WHILE, продолжаем писать скрипт, начиная строку с такого отступа, который использовался до цикла. В нашем случае - с нулевым. cap.release() освобождает оперативную память, занятую переменной cap.
Функция cv2.destroyAllWindows() уже известна (закрывает все открытые в скрипте окна).

Отладка

Убедимся, что путь к видеофайлу указан верно и запустим скрипт. Просмотрев черно белое видео увидим в терминале исключение (Exception) - в программе в принципе не может быть ошибок, ведь она выполняется по скрипту строка за строкой. То что мы наблюдаем называется исключением так как в скрипте не было инструкций как программе действовать при этом условии, и интерпретатор приказал ей завершиться и вывести нам тревожное сообщение.
Избавимся от этого исключения - дадим нашей программе инструкции как действовать в наблюдаемом случае: для этого разберемся что произошло.
Из сообщения видим, что исключение вылезло на строке
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

взглянув на которую, понимаем, что от одной итерации к другой в цикле здесь меняется лишь переменная frame, которую однажды не смогла нормально воспринять функция cvtColor(). Также можно приметить, что эксепшен (исключение) произошло в конце видео.
Вспомним, что строкой выше объявлялась переменная булевая ret, возвращающая состояние захваченного кадра. Выведем ее в терминал на каждой итерации цикла:
Запустим скрипт:Действительно, последний кадр был прочтен неверно.
Чтоб обойти этот эксепшен, добавим в условие, вызывающее обрыв цикла (break) слагаемое  ret == False через оператор or (или) и перенесем условие выше до вызова функции cvtColor():
Проверим:Скрипт завершился без исключений.
Здесь мы научились загружать видео из файловой системы и распознавать его библиотекой Open CV в качестве рабочего объекта.
Файл примера: read_video_file.py



2. Запись видео с USB-вебкамеры

Основное предназначение Open CV - работа с потоковыми данными в реальном времени. Разберемся как получить видеопоток с USB-вебкамеры и сохранить его в файловой системе.

Определение индекса устройства

Индекс веб-камеры (если их несколько) указывается аргументом в функции захвата VideoCapture(), если же камера одна или тем более встроенная в ноутбуке - однозначно имеет индекс 0, но убедиться можно следующим образом:
В Линуксе в системной папке /dev (devices - устройства) у каждой подключенной веб-камеры имеется свой файл вида video*, где вместо “*” 0, 1, 2 и т.д. Удобно получить список можно терминал командой
ls /dev/ | grep video
В нашем случае обнаружилось два файла: это встроенная в ноутбук и USB-камера. Проверить какой камере какой индекс соответствует можно следующей командой записав видео с одной из камер:
ffmpeg -f v4l2 -framerate 25 -video_size 640x480 -i /dev/video0 output.mkv

аргументом -i указывается путь к файлу устройства (вебкамеры), последним аргументом указывается путь к создаваемой видеозаписи.
Просмотрев запись, можно понять индекс нужной камеры.

Используем написанный ранее скрипт этого урока заменив аргумент при создании экземпляра класса VideoCapture, с пути к файлу на индекс вебкамеры:
cap = cv2.VideoCapture(0)

Запустив скрипт, получим тот же результат, однако изображение, выводящееся на экран, будет с веб-камеры и с ее разрешением.
Полный скрипт:
import cv2
cap = cv2.VideoCapture(0)
while(cap.isOpened()):
ret, frame = cap.read()
if cv2.waitKey(1) & 0xFF == ord('q') or ret == False:
 break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
cv2.imshow('frame', gray)
cap.release()
cv2.destroyAllWindows()


Запись видео в файл

Для добавления возможности сохранения захваченного видео в файл дополним скрипт выделенными строками (также для простоты кода можно избавиться от операции конвертирования цвета в черно-белый):
import cv2
cap = cv2.VideoCapture(0)
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('captured.avi',fourcc, 20.0, (640,480))
while(cap.isOpened()):
 ret, frame = cap.read()
 if cv2.waitKey(1) & 0xFF == ord('q') or ret == False:
 break
 cv2.imshow('frame', frame)
 out.write(frame)
out.release()
cap.release()
cv2.destroyAllWindows()

Проясним новые изменения:
  • в начале мы создаем объект для записи видео,
  • потом внутри цикла на каждой итерации производим запись нового кадра в файл captured.avi,
  • а в конце выгружаем из памяти объект записи видео.

Подробнее об объекте записи

В прошлом уроке для сохранения отдельных изображений в файловую систему мы просто использовали функцию cv2.imwrite() указывая ей лишь путь к файлу, однако для видео-файла существует куда больше параметров (кодек, фреймрейт, разрешение).

Следующая строка
fourcc = cv2.VideoWriter_fourcc(*'XVID')
просто возвращает идентификатор кодека XVID,чтоб передать его аргументом в функцию создания экземпляра класса VideoWriter:
out = cv2.VideoWriter('captured.avi',fourcc, 20.0, (640,480))

Третьим аргументом устанавливаем количество кадров в секунду, а четвертым - размер кадра.
На Линуксе выбор кодеков осуществляется следующим образом:
  • кодек XVID является золотой серединой в плане производительности и качества видео
  • MJPG дает лучшее качество
  • а X264 кодирует видео с минимальным размером файла
Проверим запуск скрипта - запись будет идти до завершения программы пользователем.
Файл примера: capture_video_file.py



3. Захват видео с CSI-камеры Raspberry Pi

Инициализируем камеру как и в предыдущем уроке, однако сейчас укажем разрешение для облегчения процесса (иначе будет использоваться максимально возможное разрешение модуля камеры):
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import cv2
camera = PiCamera()
camera.resolution = (640, 480)
camera.framerate = 24
raw = PiRGBArray(camera, size=(640, 480))
time.sleep(0.1) # allow the camera to warmup

Объект класса VideoCapture теперь нам не требуется так как захват видео идет не с веб-камеры, так что для записи в файл инициализируем только VideoWriter:
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('captured.avi',fourcc, 20.0, (640,480))

В библиотеке picamera предусмотрено получение кадров с камеры в режиме захвата видео через цикл FOR:
for frame in camera.capture_continuous(raw, format="bgr", use_video_port=True):

при помощи функции capture_continuous, которая аналогична функции capture из предыдущего урока, однако capture_continuous возвращает объекты изображений не единично, а последовательно кадр за кадром до тех пор, пока не будет прервана. Рассмотрим ее аргументы:
  • если передать в нее аргумент “output” (или просто единичным аргументом без названия) с именем файла, то каждый кадр будет сохранен в файл с указанным паттерном. Например capture_continuous('image{counter:02d}.jpg'), таким образом можно удобно программировать таймлапс в Python-коде, но для захвата видео это не требуется. Подробнее см. руководство по библиотеке PiCamera
  • нам же надо сохранить кадр не в файл, а в переменную, так что первым аргументом указываем raw - экземпляр класса PiRGBArray
  • format="bgr"  - формат BGR, кодирующий каждый пиксель 24 битами
  • use_video_port=True Модуль CSI-камеры для Raspberry Pi имеет два разных порта для вывода изображения: видео-порт более производителен, но качество изображения при этом не на высоте (для его включения передаем True); фото-порт всегда задействован по умолчанию и работает намного медленнее, зато производит изображения высокого качества.

Внутри цикла FOR каждый кадр записывается в итерационную переменную frame, применим к ней функцию array, чтобы сконвертировать в массив, понятный для OpenCV и выведем на экран:
image = frame.array
cv2.imshow("Frame", image)

Передадим image в объект видео-файла out для записи очередного кадра:
out.write(image)

В конце итерации цикла функции capture_continuous необходимо очищать массив PiRGBArray, вызывая на его объекте функцию truncate(0):
raw.truncate(0) # clear the stream

Знакомое условие завершения цикла:
if cv2.waitKey(1) & 0xFF == ord("q"):
break

Как обычно в конце программы очищаем память и закрываем все окна:
camera.close()
out.release()
cv2.destroyAllWindows()

Весь скрипт полностью:
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import cv2
camera = PiCamera()
camera.resolution = (640, 480)
camera.framerate = 24
raw = PiRGBArray(camera, size=(640, 480))
time.sleep(0.1) # allow the camera to warmup
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('captured.avi',fourcc, 20.0, (640,480))
for frame in camera.capture_continuous(raw, format="bgr", use_video_port=True):
 image = frame.array
 cv2.imshow("Frame", image)
 out.write(image)
 raw.truncate(0) # clear the stream
 if cv2.waitKey(1) & 0xFF == ord("q"):
 break
camera.close()
out.release()
cv2.destroyAllWindows()

Запустим скрипт:
python3 raspberry_capture.py

Изображение с камеры будет выводится в отдельное окно, при этом видео записываться в файл captured.avi.
Файл примера: raspberry_capture.py
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.