Open CV. Основы работы с видео
Вводная
Содержание курса по Open CV:- Основы работы с изображениями
- Основы работы с видео
- Инструменты визуализации
Из предыдущей статьи мы знаем, что изображение воспринимается Open CV как матрица пикселей. Видео же в этой библиотеке представляется как набор последовательных изображений (кадров), прогоняющихся в цикле, на каждой итерации которого единичный текущий кадр представляется обычным OpenCV изображением. Однако, есть свои нюансы, с которыми сейчас познакомимся.
В этой части курса научимся:
- открывать видео из файловой системы,
- записывать видео с веб-камеры
- и с 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() возвращает два объекта:
- булевое значение (True или False), в случае отсутствия ошибок при загрузке текущего кадра - True. Запишем это в переменную ret
- сам текущий прочитанный кадр из видео. Запишем его в переменную frame. В принципе для вывода и сохранения единичного изображения уже можно использовать переменную frame - она содержит полноразмерный цветной кадр, однако, на практике в машинном зрении прочитанный кадр сперва конвертируют в нужное цветовое представление
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