在将帧保存到磁盘时防止丢帧

Prevent frame dropping while saving frames to disk

本文关键字:保存 磁盘      更新时间:2023-10-16

我正在尝试编写C++代码,将传入的视频帧保存到磁盘。异步到达的帧由生产者线程推送到队列中。帧由使用者线程从队列中弹出。生产者和消费者的相互排斥是使用互斥锁完成的。但是,我仍然注意到帧被丢弃。丢弃的帧(可能)对应于生产者尝试将当前帧推送到队列但由于使用者持有锁而无法这样做的情况。有什么建议吗?我基本上不希望制片人等待。等待消费者对我来说是可以的。

EDIT-0 :不涉及锁定的替代想法。这行得通吗?

  1. 制作人最初排队 n 秒的视频。 n可以是帧速率的一小部分倍数。
  2. 只要队列包含 >= n 秒的视频,使用者就会逐帧取消排队并保存到磁盘。
  3. 视频完成后,队列将刷新到磁盘。

编辑-1:帧到达~15 fps。

编辑-2:代码概述:

主驱动程序代码

 // Main function
 void LVD::DumpFrame(const IplImage *frame)
 {
     // Copies frame into internal buffer.
     // buffer object is a wrapper around OpenCV's IplImage
     Initialize(frame);
     // (Producer thread) -- Pushes buffer onto queue 
     // Thread locks queue, pushes buffer onto queue, unlocks queue and dies
     PushBufferOntoQueue();
     // (Consumer thread) -- Pop off queue and save to disk
     // Thread locks queue, pops it, unlocks queue,
     // saves popped buffer to disk and dies                
     DumpQueue();
     ++m_frame_id;
}
void LVD::Initialize(const IplImage *frame)
{
    if(NULL == m_buffer) // first iteration 
         m_buffer = new ImageBuffer(frame);         
    else    
         m_buffer->Copy(frame); 
}

制作人

void LVD::PushBufferOntoQueue()
{   
     m_queingThread = ::CreateThread( NULL, 0, ThreadFuncPushImageBufferOntoQueue, this, 0, &m_dwThreadID);
}
 DWORD WINAPI LVD::ThreadFuncPushImageBufferOntoQueue(void *arg)
 {
     LVD* videoDumper = reinterpret_cast<LVD*>(arg);
     LocalLock ll( &videoDumper->m_que_lock, 60*1000 ); 
     videoDumper->m_frameQue.push(*(videoDumper->m_buffer));
     ll.Unlock();   
     return 0;
 }

消费者

void LVD::DumpQueue()
{   
    m_dumpingThread = ::CreateThread( NULL, 0, ThreadFuncDumpFrames, this, 0, &m_dwThreadID);       
}
 DWORD WINAPI LVD::ThreadFuncDumpFrames(void *arg)
 {
        LVD* videoDumper = reinterpret_cast<LVD*>(arg);
        LocalLock ll( &videoDumper->m_que_lock, 60*1000 );
        if(videoDumper->m_frameQue.size() > 0 )
        {
           videoDumper->m_save_frame=videoDumper->m_frameQue.front();
           videoDumper->m_frameQue.pop();
        }
        ll.Unlock();    
    stringstream ss;
    ss << videoDumper->m_saveDir.c_str() << "\";
    ss << videoDumper->m_startTime.c_str() << "\";     
    ss << setfill('0') << setw(6) << videoDumper->m_frame_id;
    ss << ".png";       
    videoDumper->m_save_frame.SaveImage(ss.str().c_str());
    return 0;

}

注意:

(1) 我不能使用 C++11。因此,Herb Sutter的DDJ文章不是一种选择。

(2)我发现了对无限单一生产者-消费者队列的引用。但是,作者指出排队(添加帧)可能不是没有等待的。

(3)我还找到了liblfds,一个C库,但不确定它是否能达到我的目的。

队列

不可能是问题。 视频帧以 16 毫秒的间隔到达,最坏的情况是。 队列只需要存储指向帧的指针。 以线程安全的方式添加/删除一个永远不会超过一微秒。

您需要寻找另一种解释和解决方案。 视频确实永远存在消防水带问题。 磁盘驱动器通常不够快,无法跟上未压缩的视频流。 因此,如果您的消费者无法跟上生产者的步伐,那么就会有一些东西。 使用丢帧时,当您(正确)阻止队列无限制地增长时,可能出现的结果。

请务必考虑对视频进行编码。 提供实时 MPEG 和 AVC 编码器。 在他们压缩流后,您应该没有问题跟上磁盘。

循环缓冲区绝对是一个不错的选择。如果你让它使用 2^n 大小,你也可以使用这个技巧来更新指针:

inline int update_index(int x) 
{
   return (x + 1) & (size-1);
}

这样,就不需要使用昂贵的比较(和相应的跳转)或除法(任何处理器中最昂贵的整数操作 - 不包括"填充/复制大块内存"类型的操作)。

在处理视频(或一般图形)时,必须进行"缓冲区管理"。通常,这是跟踪"帧缓冲区"状态并避免复制不必要的内容的情况。

典型的方法是分配 2 或 3 个视频缓冲区(或帧缓冲区,或您所说的)。缓冲区可以由生产者或使用者拥有。转让只是所有权。因此,当视频驱动程序发出"此缓冲区已满"的信号时,所有权现在属于消费者,消费者将读取缓冲区并将其存储到磁盘 [或其他任何内容]。存储完成后,缓冲区被返回("释放"),以便生产者可以重用它。将数据从缓冲区中复制出来是昂贵的 [需要时间],因此除非绝对必要,否则您不想这样做。