Правильный подход к записи видео без выпадения кадров состоит в том, чтобы изолировать две задачи (получение кадра и сериализация кадра), чтобы они не влияли друг на друга (в частности, чтобы колебания в сериализации не отнимали время от захвата кадров. , что должно происходить без задержек, чтобы предотвратить потерю кадров).
Это может быть достигнуто путем делегирования сериализации (кодирования кадров и записи их в видеофайл) отдельным потокам и использования какой-то синхронизированной очереди для подачи данных в рабочие потоки.
Ниже приводится простой пример, показывающий, как это можно сделать. Поскольку у меня только одна камера, а не такая, как у вас, я просто буду использовать веб-камеру и дублировать кадры, но общий принцип применим и к вашему сценарию.
Образец кода
Вначале у нас есть некоторые из них:
#include <opencv2/opencv.hpp>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
// ============================================================================
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::microseconds;
// ============================================================================
Синхронизированная очередь
Первым шагом является определение нашей синхронизированной очереди, которую мы будем использовать для связи с рабочими потоками, которые записывают видео.
Нам необходимы следующие основные функции:
- Поместите новые изображения в очередь
- Выталкивайте изображения из очереди, ожидая, когда она опустеет.
- Возможность отменить все ожидающие всплывающие сообщения, когда мы закончим.
Мы используем std::queue
для хранения _ 3_ экземпляров и _ 4_ для синхронизации. std::condition_variable
используется для уведомления потребителя, когда изображение было вставлено в очередь ( или установлен флаг отмены), а простой логический флаг используется для уведомления об отмене.
Наконец, мы используем пустой struct cancelled
как исключение, выброшенное из pop()
, поэтому мы можем полностью завершить рабочий процесс, отменив очередь.
// ============================================================================
class frame_queue
{
public:
struct cancelled {};
public:
frame_queue();
void push(cv::Mat const& image);
cv::Mat pop();
void cancel();
private:
std::queue<cv::Mat> queue_;
std::mutex mutex_;
std::condition_variable cond_;
bool cancelled_;
};
// ----------------------------------------------------------------------------
frame_queue::frame_queue()
: cancelled_(false)
{
}
// ----------------------------------------------------------------------------
void frame_queue::cancel()
{
std::unique_lock<std::mutex> mlock(mutex_);
cancelled_ = true;
cond_.notify_all();
}
// ----------------------------------------------------------------------------
void frame_queue::push(cv::Mat const& image)
{
std::unique_lock<std::mutex> mlock(mutex_);
queue_.push(image);
cond_.notify_one();
}
// ----------------------------------------------------------------------------
cv::Mat frame_queue::pop()
{
std::unique_lock<std::mutex> mlock(mutex_);
while (queue_.empty()) {
if (cancelled_) {
throw cancelled();
}
cond_.wait(mlock);
if (cancelled_) {
throw cancelled();
}
}
cv::Mat image(queue_.front());
queue_.pop();
return image;
}
// ============================================================================
Хранитель
Следующим шагом является определение простого storage_worker
, который будет отвечать за извлечение кадров из синхронизированной очереди и кодирование их в видеофайл до тех пор, пока очередь не будет отменена.
Я добавил простую синхронизацию, чтобы у нас было некоторое представление о том, сколько времени тратится на кодирование кадров, а также простую запись в консоль, чтобы у нас было некоторое представление о том, что происходит в программе.
// ============================================================================
class storage_worker
{
public:
storage_worker(frame_queue& queue
, int32_t id
, std::string const& file_name
, int32_t fourcc
, double fps
, cv::Size frame_size
, bool is_color = true);
void run();
double total_time_ms() const { return total_time_ / 1000.0; }
private:
frame_queue& queue_;
int32_t id_;
std::string file_name_;
int32_t fourcc_;
double fps_;
cv::Size frame_size_;
bool is_color_;
double total_time_;
};
// ----------------------------------------------------------------------------
storage_worker::storage_worker(frame_queue& queue
, int32_t id
, std::string const& file_name
, int32_t fourcc
, double fps
, cv::Size frame_size
, bool is_color)
: queue_(queue)
, id_(id)
, file_name_(file_name)
, fourcc_(fourcc)
, fps_(fps)
, frame_size_(frame_size)
, is_color_(is_color)
, total_time_(0.0)
{
}
// ----------------------------------------------------------------------------
void storage_worker::run()
{
cv::VideoWriter writer(file_name_, fourcc_, fps_, frame_size_, is_color_);
try {
int32_t frame_count(0);
for (;;) {
cv::Mat image(queue_.pop());
if (!image.empty()) {
high_resolution_clock::time_point t1(high_resolution_clock::now());
++frame_count;
writer.write(image);
high_resolution_clock::time_point t2(high_resolution_clock::now());
double dt_us(static_cast<double>(duration_cast<microseconds>(t2 - t1).count()));
total_time_ += dt_us;
std::cout << "Worker " << id_ << " stored image #" << frame_count
<< " in " << (dt_us / 1000.0) << " ms" << std::endl;
}
}
} catch (frame_queue::cancelled& /*e*/) {
// Nothing more to process, we're done
std::cout << "Queue " << id_ << " cancelled, worker finished." << std::endl;
}
}
// ============================================================================
Обработка
Наконец, мы можем собрать все это вместе.
Начнем с инициализации и настройки нашего видеоисточника. Затем мы создаем два frame_queue
экземпляра, по одному для каждого потока изображений. Мы следуем этому, создавая два экземпляра storage_worker
, по одному для каждой очереди. Чтобы было интересно, я установил для каждого свой кодек.
Следующим шагом является создание и запуск рабочих потоков, которые будут выполнять метод run()
каждого storage_worker
. Когда наши потребители готовы, мы можем начать захватывать кадры с камеры и передавать их frame_queue
экземплярам. Как упоминалось выше, у меня только один источник, поэтому я вставляю копии одного и того же кадра в обе очереди.
NB: мне нужно использовать clone()
метод cv::Mat
, чтобы сделать глубокую копию, иначе я бы вставлял ссылки на единственный буфер, который OpenCV VideoCapture
использует по соображениям производительности. Это означало бы, что рабочие потоки будут получать ссылки на это единственное изображение, и не будет никакой синхронизации для доступа к этому общему буферу изображений. Вы должны убедиться, что этого не происходит и в вашем сценарии.
После того, как мы прочитали соответствующее количество кадров (вы можете реализовать любое другое условие остановки, какое пожелаете), мы отменяем рабочие очереди и ждем завершения рабочих потоков.
Наконец, мы пишем некоторую статистику о времени, необходимом для различных задач.
// ============================================================================
int main()
{
// The video source -- for me this is a webcam, you use your specific camera API instead
// I only have one camera, so I will just duplicate the frames to simulate your scenario
cv::VideoCapture capture(0);
// Let's make it decent sized, since my camera defaults to 640x480
capture.set(CV_CAP_PROP_FRAME_WIDTH, 1920);
capture.set(CV_CAP_PROP_FRAME_HEIGHT, 1080);
capture.set(CV_CAP_PROP_FPS, 20.0);
// And fetch the actual values, so we can create our video correctly
int32_t frame_width(static_cast<int32_t>(capture.get(CV_CAP_PROP_FRAME_WIDTH)));
int32_t frame_height(static_cast<int32_t>(capture.get(CV_CAP_PROP_FRAME_HEIGHT)));
double video_fps(std::max(10.0, capture.get(CV_CAP_PROP_FPS))); // Some default in case it's 0
std::cout << "Capturing images (" << frame_width << "x" << frame_height
<< ") at " << video_fps << " FPS." << std::endl;
// The synchronized queues, one per video source/storage worker pair
std::vector<frame_queue> queue(2);
// Let's create our storage workers -- let's have two, to simulate your scenario
// and to keep it interesting, have each one write a different format
std::vector <storage_worker> storage;
storage.emplace_back(std::ref(queue[0]), 0
, std::string("foo_0.avi")
, CV_FOURCC('I', 'Y', 'U', 'V')
, video_fps
, cv::Size(frame_width, frame_height)
, true);
storage.emplace_back(std::ref(queue[1]), 1
, std::string("foo_1.avi")
, CV_FOURCC('D', 'I', 'V', 'X')
, video_fps
, cv::Size(frame_width, frame_height)
, true);
// And start the worker threads for each storage worker
std::vector<std::thread> storage_thread;
for (auto& s : storage) {
storage_thread.emplace_back(&storage_worker::run, &s);
}
// Now the main capture loop
int32_t const MAX_FRAME_COUNT(10);
double total_read_time(0.0);
int32_t frame_count(0);
for (; frame_count < MAX_FRAME_COUNT; ++frame_count) {
high_resolution_clock::time_point t1(high_resolution_clock::now());
// Try to read a frame
cv::Mat image;
if (!capture.read(image)) {
std::cerr << "Failed to capture image.\n";
break;
}
// Insert a copy into all queues
for (auto& q : queue) {
q.push(image.clone());
}
high_resolution_clock::time_point t2(high_resolution_clock::now());
double dt_us(static_cast<double>(duration_cast<microseconds>(t2 - t1).count()));
total_read_time += dt_us;
std::cout << "Captured image #" << frame_count << " in "
<< (dt_us / 1000.0) << " ms" << std::endl;
}
// We're done reading, cancel all the queues
for (auto& q : queue) {
q.cancel();
}
// And join all the worker threads, waiting for them to finish
for (auto& st : storage_thread) {
st.join();
}
if (frame_count == 0) {
std::cerr << "No frames captured.\n";
return -1;
}
// Report the timings
total_read_time /= 1000.0;
double total_write_time_a(storage[0].total_time_ms());
double total_write_time_b(storage[1].total_time_ms());
std::cout << "Completed processing " << frame_count << " images:\n"
<< " average capture time = " << (total_read_time / frame_count) << " ms\n"
<< " average write time A = " << (total_write_time_a / frame_count) << " ms\n"
<< " average write time B = " << (total_write_time_b / frame_count) << " ms\n";
return 0;
}
// ============================================================================
Консольный вывод
Запустив этот небольшой пример, мы получаем следующий вывод журнала в консоли, а также два видеофайла на диске.
NB: поскольку на самом деле кодирование происходило намного быстрее, чем захват, я добавил немного ожидания в storage_worker, чтобы лучше показать разделение.
Capturing images (1920x1080) at 20 FPS.
Captured image #0 in 111.009 ms
Captured image #1 in 67.066 ms
Worker 0 stored image #1 in 94.087 ms
Captured image #2 in 62.059 ms
Worker 1 stored image #1 in 193.186 ms
Captured image #3 in 60.059 ms
Worker 0 stored image #2 in 100.097 ms
Captured image #4 in 78.075 ms
Worker 0 stored image #3 in 87.085 ms
Captured image #5 in 62.061 ms
Worker 0 stored image #4 in 95.092 ms
Worker 1 stored image #2 in 193.187 ms
Captured image #6 in 75.074 ms
Worker 0 stored image #5 in 95.093 ms
Captured image #7 in 63.061 ms
Captured image #8 in 64.061 ms
Worker 0 stored image #6 in 102.098 ms
Worker 1 stored image #3 in 201.195 ms
Captured image #9 in 76.074 ms
Worker 0 stored image #7 in 90.089 ms
Worker 0 stored image #8 in 91.087 ms
Worker 1 stored image #4 in 185.18 ms
Worker 0 stored image #9 in 82.08 ms
Worker 0 stored image #10 in 94.092 ms
Queue 0 cancelled, worker finished.
Worker 1 stored image #5 in 179.174 ms
Worker 1 stored image #6 in 106.102 ms
Worker 1 stored image #7 in 105.104 ms
Worker 1 stored image #8 in 103.101 ms
Worker 1 stored image #9 in 104.102 ms
Worker 1 stored image #10 in 104.1 ms
Queue 1 cancelled, worker finished.
Completed processing 10 images:
average capture time = 71.8599 ms
average write time A = 93.09 ms
average write time B = 147.443 ms
average write time B = 176.673 ms
Возможные улучшения
В настоящее время нет защиты от переполнения очереди в ситуации, когда сериализация просто не успевает за скоростью, с которой камера генерирует новые изображения. Установите верхний предел для размера очереди и проверьте производителя, прежде чем нажимать фрейм. Вам нужно будет решить, как именно вы хотите справиться с этой ситуацией.
person
Dan Mašek
schedule
10.05.2016
int codec = -1;
в качестве кода FOURCC для записи видео. Какая цель? Какой именно кодек выбирает? - person Dan Mašek   schedule 10.05.2016cvShowImage
из цикла захвата (это тоже занимает заметное время) и добавьте несколько time к вашему коду, чтобы вы лучше понимали, что и насколько замедляет его. Я посмотрю, смогу ли я написать вам простой пример того, как это можно сделать. - person Dan Mašek   schedule 10.05.2016CV_FOURCC_PROMPT
. - person Dan Mašek   schedule 11.05.2016