按照以往的做法,视频录制推荐FFMPEG、GStreamer等效率更高的方式,OpenCV在视频方面就显得不是那么专业了,但由于其较高的封装性和使用方法简单,小伙伴们有时候更愿意拿OpenCV去做一些专业度要求不高的简单录制。
OpenCV工程中给出了录制普通摄像头的示例代码,如下:
/**
@file videowriter_basic.cpp
@brief A very basic sample for using VideoWriter and VideoCapture
@author PkLab.net
@date Aug 24, 2016
*/
#include
#include
#include
#include
#include
using namespace cv;
using namespace std;
int main(int, char**)
{
Mat src;
// use default camera as video source
VideoCapture cap(0);
// check if we succeeded
if (!cap.isOpened()) {
cerr << "ERROR! Unable to open camera\n";
return -1;
}
// get one frame from camera to know frame size and type
cap >> src;
// check if we succeeded
if (src.empty()) {
cerr << "ERROR! blank frame grabbed\n";
return -1;
}
bool isColor = (src.type() == CV_8UC3);
//--- INITIALIZE VIDEOWRITER
VideoWriter writer;
int codec = VideoWriter::fourcc('M', 'J', 'P', 'G'); // select desired codec (must be available at runtime)
double fps = 25.0; // framerate of the created video stream
string filename = "./live.avi"; // name of the output video file
writer.open(filename, codec, fps, src.size(), isColor);
// check if we succeeded
if (!writer.isOpened()) {
cerr << "Could not open the output video file for write\n";
return -1;
}
//--- GRAB AND WRITE LOOP
cout << "Writing videofile: " << filename << endl
<< "Press any key to terminate" << endl;
for (;;)
{
// check if we succeeded
if (!cap.read(src)) {
cerr << "ERROR! blank frame grabbed\n";
break;
}
// encode the frame into the videofile stream
writer.write(src);
// show live and wait for a key with timeout long enough to show images
imshow("Live", src);
if (waitKey(5) >= 0)
break;
}
// the videofile will be closed and released automatically in VideoWriter destructor
return 0;
}
上面的注释已经非常清晰了,这里就不再做过多解释。如果只是简单的录制,我们把上例中VideoCapture定义的内容替换成工业相机相关的接口即可,但在实际工程中,录制只是一个辅助功能,主体功能是图像处理,例如检测、识别、分类等等。因此,从工业相机获取的图像一方面需要送给图像处理算法去做处理,另一方面要交给录制模块去做录制,甚至更多时候,还需要拿出一份拷贝去做展示,等等。也就是说,从工业相机获取到的一帧图像,需要根据具体的需求有多份拷贝和分流。因此,我们的程序实现起来,会比上面的示例程序略复杂一点。
最常用的方法是设置一个图像Buffer,将从相机获取的图像放到Buffer中,不同的模块(线程)都从该Buffer取图像。这就需要将获取图像的功能也独立成一个线程,相机图像的获取、处理、录制、显示等多个功能同步进行。本篇我们使用的是Basler工业相机,相机接口已经封装好,下面示例中是在接口的基础上来做调用。
#include
#include
#include
#include
#include
#include
#include
#include
#include "opencv/cv.h"
#include "opencv/cxcore.h"
#include "camera_devices_gige.h" /* 工业相机接口头文件 */
using namespace cv;
using namespace std;
typedef class CameraDevices_GigE CameraDevices;
/* 定义一个最多包含10帧图像的Buffer,下面的ImgBufSize用来限制Buffer大小 */
list imageBuffer;
const int ImgBufSize = 10;
/* 互斥量,用来保护不同线程对图像Buffer的操作 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/* 工业相机图像抓取的参数,作为线程的输入 */
struct GrabParam
{
CameraDevices *CD;
int fps;
};
/* 计算时间间隔的函数,用来控制图像获取和编码的时间间隔 */
// Calculate time interval between current and t_start
int64_t get_time_interval(struct timeval t_start)
{
struct timeval t_curr;
gettimeofday(&t_curr, NULL);
return (t_curr.tv_sec * 1000) + (t_curr.tv_usec / 1000) - ((t_start.tv_sec * 1000) + (t_start.tv_usec / 1000));
}
/* 定义从工业相机抓取图像的线程,抓取间隔与帧率相关 */
// Image grabing thread, camera 0
void *grabImage2Buffer(void *arg)
{
GrabParam *grab_param;
grab_param = (struct GrabParam *)arg;
CameraDevices *CD = grab_param->CD;
int fps = grab_param->fps;
struct timeval t_start;
int ret, cnt = 0;
Mat Img;
int grab_intval = 1000 / fps; // ms, fps = 25, enc_interval = 40ms
gettimeofday(&t_start, NULL);
while(1)
{
gettimeofday(&t_start, NULL); // Update t_start every time before grab an image
ret = CD->Grab_cvImage(0, Img); //相机类方法,抓取一帧图像
if(ret < 0)
{
cout << "Grab image from camera failed, will abort!" << endl;
exit(EXIT_FAILURE);
}
/* lock */
if(pthread_mutex_lock(&mutex) != 0)
{
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
/* 图像Buffer未满时,直接将图像数据放入Buffer */
if(imageBuffer.size() < ImgBufSize)
{
imageBuffer.push_back(Img);
}
/* 若Buffer已满,则先将最老的图像数据清除,再将新的图像数据压入 */
else
{
imageBuffer.pop_front();
imageBuffer.push_back(Img);
}
/* Unlock */
if(pthread_mutex_unlock(&mutex) != 0)
{
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
/* 判断当前时间是否已到采集时间,若未到,则等待 */
int64_t intval = get_time_interval(t_start); // current time and last grab time interval
if(intval < grab_intval)
{
usleep((grab_intval - intval) * 1000); // usecond
}
}
cout << "Thread grabImage2Buffer exit" << endl;
return ((void*)0);
}
/* 定义函数:从图像Buffer中取出一帧数据 */
void get_one_image_from_buffer(Mat &Img)
{
Mat Img_tmp;
while(1)
{
if(!imageBuffer.empty())
{
if(pthread_mutex_lock(&mutex) != 0)
{
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
/* Get the front image for video recording */
Img_tmp = imageBuffer.front();
Img_tmp.copyTo(Img);
/* Delete the front image */
imageBuffer.pop_front();
if(pthread_mutex_unlock(&mutex) != 0)
{
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
break;
}
}
}
/* The main function */
int main()
{
/* 定义相机实例、初始化并设置必要的参数 */
CameraDevices camera_devices;
bool is_cam_ok = camera_devices.initCameras();
if(!is_cam_ok)
{
exit(EXIT_FAILURE);
}
for(int i = 0; i < camera_devices.deviceNum; i++)
{
camera_devices.setTriggerMode(i, 0, 1000000);
}
/* 从相机获取一帧图像,判断相机是否工作正常,并提供后续编码的参数,如图像宽高 */
Mat Img;
if(camera_devices.Grab_cvImage(0, Img) < 0)
{
cout << "Grab image from camera failed, will abort!" << endl;
exit(EXIT_FAILURE);
}
/* 相机抓取线程的输入参数 */
GrabParam gparam;
gparam.CD = &camera_devices;
gparam.fps = 25; //FRAME_RATE;
int fps = gparam.fps; /* 编码帧率与图像抓取帧率一致 */
/* 创建从工业相机抓取图像线程 */
/* Create an image grabbing thread */
pthread_t thread_grab;
pthread_create(&thread_grab, NULL, grabImage2Buffer, (void*)&gparam);
/* video record */
/* 使用VideoWriter类进行录制,首先创建输出文件 */
/* Output file creation */
VideoWriter vRec;
vRec.open("out.mp4", CV_FOURCC('D','I','V','X'), fps, Size(Img.cols, Img.rows));
if(!vRec.isOpened())
{
cout << "VideoWriter open failed! \n" << endl;
exit(EXIT_FAILURE);
}
struct timeval t_start;
int ret, cnt = 0;
int enc_intval = 1000 / fps; // ms, fps = 25, enc_interval = 40ms
/* After output file is created, get images and write to the file */
while(1)
{
/* Record a recording time of one frame */
gettimeofday(&t_start, NULL);
/* 调用从Buffer读取图像函数,获取一帧图像 */
get_one_image_from_buffer(Img);
/* 将获取到的一帧图像数据编码并写入输出文件 */
if(!Img.empty())
{
vRec.write(Img); /* Write one frame to the output file */
}
/* 通过时间控制帧率 */
int64_t intval = get_time_interval(t_start); // current time and last grab time interval
if(intval < enc_intval)
{
usleep((enc_intval - intval) * 1000); // usecond
}
/* 录制停止 */
if(waitKey(5) == 'q')
{
break;
}
}
pthread_join(thread_grab, NULL);
}
以上代码编译通过后,运行,可能出现如下错误,
就需要检查你的OpenCV编译时是否使能了FFMPEG/GStreamer等编码依赖项,如果未使能,需要安装FFMPEG/GStreamer(我因为对FFMPEG更熟悉一些,首选FFMPEG),并重编OpenCV,在cmake时加入编译参数 -D WITH_FFMPEG=ON,详见之前的文章:在CentOS系统上安装OpenCV-3
当然,你也可以选择自己喜欢或者熟悉的编码框架,感兴趣的同学可以看一下VideoWriter的定义,如下代码中,通过在VideoWriter(const String& filename, int apiPreference, int fourcc, double fps, Size frameSize, bool isColor = true)或open(const String& filename, int apiPreference, int fourcc, double fps, Size frameSize, bool isColor = true);中指定apiPreference来启用相应的后台编码框架。
class CV_EXPORTS_W VideoWriter
{
public:
/** @brief Default constructors
The constructors/functions initialize video writers.
- On Linux FFMPEG is used to write videos;
- On Windows FFMPEG or VFW is used;
- On MacOSX QTKit is used.
*/
CV_WRAP VideoWriter();
/** @overload
@param filename Name of the output video file.
@param fourcc 4-character code of codec used to compress the frames. For example,
VideoWriter::fourcc('P','I','M','1') is a MPEG-1 codec, VideoWriter::fourcc('M','J','P','G') is a
motion-jpeg codec etc. List of codes can be obtained at [Video Codecs by
FOURCC](http://www.fourcc.org/codecs.php) page. FFMPEG backend with MP4 container natively uses
other values as fourcc code: see [ObjectType](http://www.mp4ra.org/codecs.html),
so you may receive a warning message from OpenCV about fourcc code conversion.
@param fps Framerate of the created video stream.
@param frameSize Size of the video frames.
@param isColor If it is not zero, the encoder will expect and encode color frames, otherwise it
will work with grayscale frames (the flag is currently supported on Windows only).
@b Tips:
- With some backends `fourcc=-1` pops up the codec selection dialog from the system.
- To save image sequence use a proper filename (eg. `img_%02d.jpg`) and `fourcc=0`
OR `fps=0`. Use uncompressed image format (eg. `img_%02d.BMP`) to save raw frames.
- Most codecs are lossy. If you want lossless video file you need to use a lossless codecs
(eg. FFMPEG FFV1, Huffman HFYU, Lagarith LAGS, etc...)
- If FFMPEG is enabled, using `codec=0; fps=0;` you can create an uncompressed (raw) video file.
*/
CV_WRAP VideoWriter(const String& filename, int fourcc, double fps,
Size frameSize, bool isColor = true);
/** @overload
The `apiPreference` parameter allows to specify API backends to use. Can be used to enforce a specific reader implementation
if multiple are available: e.g. cv::CAP_FFMPEG or cv::CAP_GSTREAMER.
*/
CV_WRAP VideoWriter(const String& filename, int apiPreference, int fourcc, double fps,
Size frameSize, bool isColor = true);
/** @brief Default destructor
The method first calls VideoWriter::release to close the already opened file.
*/
virtual ~VideoWriter();
/** @brief Initializes or reinitializes video writer.
The method opens video writer. Parameters are the same as in the constructor
VideoWriter::VideoWriter.
@return `true` if video writer has been successfully initialized
The method first calls VideoWriter::release to close the already opened file.
*/
CV_WRAP virtual bool open(const String& filename, int fourcc, double fps,
Size frameSize, bool isColor = true);
/** @overload
*/
CV_WRAP bool open(const String& filename, int apiPreference, int fourcc, double fps,
Size frameSize, bool isColor = true);
/** @brief Returns true if video writer has been successfully initialized.
*/
CV_WRAP virtual bool isOpened() const;
/** @brief Closes the video writer.
The method is automatically called by subsequent VideoWriter::open and by the VideoWriter
destructor.
*/
CV_WRAP virtual void release();
/** @brief Stream operator to write the next video frame.
@sa write
*/
virtual VideoWriter& operator << (const Mat& image);
/** @brief Writes the next video frame
@param image The written frame. In general, color images are expected in BGR format.
The function/method writes the specified image to video file. It must have the same size as has
been specified when opening the video writer.
*/
CV_WRAP virtual void write(const Mat& image);
/** @brief Sets a property in the VideoWriter.
@param propId Property identifier from cv::VideoWriterProperties (eg. cv::VIDEOWRITER_PROP_QUALITY)
or one of @ref videoio_flags_others
@param value Value of the property.
@return `true` if the property is supported by the backend used by the VideoWriter instance.
*/
CV_WRAP virtual bool set(int propId, double value);
/** @brief Returns the specified VideoWriter property
@param propId Property identifier from cv::VideoWriterProperties (eg. cv::VIDEOWRITER_PROP_QUALITY)
or one of @ref videoio_flags_others
@return Value for the specified property. Value 0 is returned when querying a property that is
not supported by the backend used by the VideoWriter instance.
*/
CV_WRAP virtual double get(int propId) const;
/** @brief Concatenates 4 chars to a fourcc code
@return a fourcc code
This static method constructs the fourcc code of the codec to be used in the constructor
VideoWriter::VideoWriter or VideoWriter::open.
*/
CV_WRAP static int fourcc(char c1, char c2, char c3, char c4);
protected:
Ptr writer;
Ptr iwriter;
static Ptr create(const String& filename, int fourcc, double fps,
Size frameSize, bool isColor = true);
};