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

Я использую мультиспектральную камеру для сбора данных. Один в ближнем инфракрасном диапазоне, а другой - красочный. Не две камеры, а одна камера может одновременно получать два разных вида изображений. Есть несколько функций API, которые я мог бы использовать, например J_Image_OpenStream. Две части основных кодов показаны ниже. Один используется для открытия двух потоков (на самом деле они находятся в одном образце, и я должен их использовать, но я не слишком четко понимаю их значение) и установки путей сохранения двух файлов avi и начала сбора.

 // Open stream
 retval0 = J_Image_OpenStream(m_hCam[0], 0, reinterpret_cast<J_IMG_CALLBACK_OBJECT>(this), reinterpret_cast<J_IMG_CALLBACK_FUNCTION>(&COpenCVSample1Dlg::StreamCBFunc0), &m_hThread[0], (ViewSize0.cx*ViewSize0.cy*bpp0)/8);
if (retval0 != J_ST_SUCCESS) {
    AfxMessageBox(CString("Could not open stream0!"), MB_OK | MB_ICONEXCLAMATION);
    return;
}
TRACE("Opening stream0 succeeded\n");
retval1 = J_Image_OpenStream(m_hCam[1], 0, reinterpret_cast<J_IMG_CALLBACK_OBJECT>(this), reinterpret_cast<J_IMG_CALLBACK_FUNCTION>(&COpenCVSample1Dlg::StreamCBFunc1), &m_hThread[1], (ViewSize1.cx*ViewSize1.cy*bpp1)/8);
if (retval1 != J_ST_SUCCESS) {
    AfxMessageBox(CString("Could not open stream1!"), MB_OK | MB_ICONEXCLAMATION);
    return;
}
TRACE("Opening stream1 succeeded\n");

const char *filename0 = "C:\\Users\\shenyang\\Desktop\\test0.avi"; 
const char *filename1 = "C:\\Users\\shenyang\\Desktop\\test1.avi";
int fps = 10; //frame per second
int codec = -1;//choose the compression method

writer0 = cvCreateVideoWriter(filename0, codec, fps, CvSize(1296,966), 1);
writer1 = cvCreateVideoWriter(filename1, codec, fps, CvSize(1296,964), 1);

// Start Acquision
retval0 = J_Camera_ExecuteCommand(m_hCam[0], NODE_NAME_ACQSTART);
retval1 = J_Camera_ExecuteCommand(m_hCam[1], NODE_NAME_ACQSTART);


// Create two OpenCV named Windows used for displaying "BGR" and "INFRARED" images
cvNamedWindow("BGR");
cvNamedWindow("INFRARED");

Еще одна - две потоковые функции, они очень похожи.

void COpenCVSample1Dlg::StreamCBFunc0(J_tIMAGE_INFO * pAqImageInfo)
{
if (m_pImg0 == NULL)
{
    // Create the Image:
    // We assume this is a 8-bit monochrome image in this sample
    m_pImg0 = cvCreateImage(cvSize(pAqImageInfo->iSizeX, pAqImageInfo->iSizeY), IPL_DEPTH_8U, 1);
}

// Copy the data from the Acquisition engine image buffer into the OpenCV Image obejct
memcpy(m_pImg0->imageData, pAqImageInfo->pImageBuffer, m_pImg0->imageSize);

// Display in the "BGR" window
cvShowImage("INFRARED", m_pImg0);

frame0 = m_pImg0;
cvWriteFrame(writer0, frame0);

}

void COpenCVSample1Dlg::StreamCBFunc1(J_tIMAGE_INFO * pAqImageInfo)
{
if (m_pImg1 == NULL)
{
    // Create the Image:
    // We assume this is a 8-bit monochrome image in this sample
    m_pImg1 = cvCreateImage(cvSize(pAqImageInfo->iSizeX, pAqImageInfo->iSizeY), IPL_DEPTH_8U, 1);
}

// Copy the data from the Acquisition engine image buffer into the OpenCV Image obejct
memcpy(m_pImg1->imageData, pAqImageInfo->pImageBuffer, m_pImg1->imageSize);

// Display in the "BGR" window
cvShowImage("BGR", m_pImg1);

frame1 = m_pImg1;
cvWriteFrame(writer1, frame1);
}

Вопрос в том, не сохраняю ли я avi файлы, как

/*writer0 = cvCreateVideoWriter(filename0, codec, fps, CvSize(1296,966), 1);
writer1 = cvCreateVideoWriter(filename1, codec, fps, CvSize(1296,964), 1);*/
//cvWriteFrame(writer0, frame0);
//cvWriteFrame(writer0, frame0);

В двух окнах отображения изображения захватываются одинаково, что означает, что они синхронны. Но если мне нужно записать данные в файлы avi из-за разного размера двух типов изображений и их большого размера, оказывается, что это влияет на скорость сбора данных двумя камерами, и снимки не синхронны. Но я не мог создать такой огромный буфер для хранения всех данных в памяти, а устройство ввода-вывода работает довольно медленно. Что я должен делать? Огромное спасибо.

некоторые переменные класса:

 public:
FACTORY_HANDLE  m_hFactory;             // Factory Handle
CAM_HANDLE      m_hCam[MAX_CAMERAS];    // Camera Handles
THRD_HANDLE     m_hThread[MAX_CAMERAS]; // Stream handles
char            m_sCameraId[MAX_CAMERAS][J_CAMERA_ID_SIZE]; // Camera IDs

IplImage        *m_pImg0 = NULL;        // OpenCV Images
IplImage        *m_pImg1 = NULL;        // OpenCV Images

CvVideoWriter* writer0;
IplImage *frame0;
CvVideoWriter* writer1;
IplImage *frame1;

BOOL OpenFactoryAndCamera();
void CloseFactoryAndCamera();
void StreamCBFunc0(J_tIMAGE_INFO * pAqImageInfo);
void StreamCBFunc1(J_tIMAGE_INFO * pAqImageInfo);
void InitializeControls();
void EnableControls(BOOL bIsCameraReady, BOOL bIsImageAcquiring);

person Panfeng Li    schedule 10.05.2016    source источник
comment
Сохранение (и фактически любая обработка) должно выполняться в другом потоке, чем захват, то есть поток, ответственный за захват изображений с камеры, не должен иметь никаких других обязанностей, просто захватите кадр и поместите его в некоторую очередь. . Вы упомянули, что устройство ввода-вывода довольно медленное - действительно ли оно достаточно быстрое, чтобы обрабатывать общую скорость передачи данных?   -  person Dan Mašek    schedule 10.05.2016
comment
Я имею в виду, что время записи данных на диск намного больше, чем в память. Да, я знаю, что сохранение повлияет на скорость захвата. Вы имеете в виду, что я должен создать очередь или что-то еще, чтобы я мог сначала поместить в нее данные, а затем использовать другую функцию для записи данных из очереди на диск?   -  person Panfeng Li    schedule 10.05.2016
comment
Искренне спасибо, попробую.   -  person Panfeng Li    schedule 10.05.2016
comment
Да, синхронизированная очередь. Вы уже сделали копию из буфера захвата в изображения OpenCV, поэтому просто вставьте их в очередь. Возможно, вы действительно захотите начать использовать C ++ API (например, cv :: VideoWriter, cv :: Mat и т. Д.), А не устаревший C api, который вы используете в настоящее время. Это сделает захват независимым от сохранения. Периодически проверяйте размер очереди, чтобы избежать нехватки памяти. Кроме того, у вас, кажется, есть int codec = -1; в качестве кода FOURCC для записи видео. Какая цель? Какой именно кодек выбирает?   -  person Dan Mašek    schedule 10.05.2016
comment
Кроме того, удалите cvShowImage из цикла захвата (это тоже занимает заметное время) и добавьте несколько time к вашему коду, чтобы вы лучше понимали, что и насколько замедляет его. Я посмотрю, смогу ли я написать вам простой пример того, как это можно сделать.   -  person Dan Mašek    schedule 10.05.2016
comment
Хорошо, я, наконец, оставлю код imshow. Кодек - это параметр, используемый в операционной системе Windows, что означает, что вы можете вручную выбрать системные кодеки.   -  person Panfeng Li    schedule 11.05.2016
comment
Да, тем временем я понял, что -1 стоит вместо CV_FOURCC_PROMPT.   -  person Dan Mašek    schedule 11.05.2016


Ответы (1)


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

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

Ниже приводится простой пример, показывающий, как это можно сделать. Поскольку у меня только одна камера, а не такая, как у вас, я просто буду использовать веб-камеру и дублировать кадры, но общий принцип применим и к вашему сценарию.


Образец кода

Вначале у нас есть некоторые из них:

#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
comment
Еще раз спасибо! Однажды я попытался изменить свои коды. Из-за моей плохой привычки кодировать, например, использовать пространство имен std, поэтому очередь переменных, определенная очередью std :: vector ‹frame_queue› (2), вызовет у меня неопределенность. Теперь я понимаю важность использования std ::, но не пространства имен. - person Panfeng Li; 12.05.2016
comment
Но есть еще одна проблема. Используя MAX_FRAME_COUNT, мы можем контролировать использование памяти, однако, если нам нужно сделать запись видео в реальном времени, бывает, что памяти недостаточно для хранения всех данных изображения, и система остановит его до реального конца. . - person Panfeng Li; 12.05.2016
comment
Значит, запись или кодирование все еще слишком медленные, чтобы успевать даже с наименее требовательным кодеком? (Я предполагаю, что это не тот случай, когда у вас заканчивается место на диске). Полагаю, нет шансов добавить память или использовать более производительную систему? Дайте угадаю, это эта камера? Два 8-битных 1-канальных изображения (цвет Байера и ИК) с разрешением 1296x966 и 31 кадром в секунду - ›1.2MiB * 2 * 31 Hz = 74.4 MiB/s. Сможет ли ваше хранилище справиться с такой устойчивой скоростью? Сделайте тест и измерьте его. И вместо VideoWriter попробуйте просто выгружать необработанные кадры в двоичный файл, который должен иметь наименьшие накладные расходы. - person Dan Mašek; 12.05.2016
comment
Ех, это точно такая же камера. Я изменил размер изображения до 640 * 480, чтобы сэкономить время. Я попробую двоичный файл и добавлю немного виртуальной памяти. Это так мило с вашей стороны, что вы мне очень помогли ^ _ ^ - person Panfeng Li; 12.05.2016
comment
Вы также можете установить ROI на камере, если вам не нужно все изображение, и изменить частоту кадров на более низкую фиксированную частоту. Binning тоже может помочь, изменив размер за вас, если он доступен. Будьте осторожны при изменении размера изображения Байера вручную - если вы просто измените размер изображения одного канала, вы испортить информацию о цвете. Вам нужно будет либо отбросить информацию о цвете путем интерполяции в оттенки серого, либо сделать его трехканальным изображением, но это не очень поможет с точки зрения требований к пространству. - person Dan Mašek; 12.05.2016
comment
Хорошо, я понимаю. Я замечу изменение размера. - person Panfeng Li; 12.05.2016