ffmpeg+opencv视频裁剪转码摄像头及鼠标事件详解
摘要:
紧接上一篇我们讲了视频批处理过程的具体实现,本篇我们将对摄像头捕获,麦克风和鼠标事件进行讲解。
最近发现很多人问怎么用FFmpeg采集摄像头图像,事实上FFmpeg很早就支持通过DShow获取采集设备(摄像头、麦克风)的数据了,只是网上提供的例子比较少。如果能用FFmpeg实现采集、编码和录制(或推流),那整个实现方案就简化很多,正因为这个原因,我想尝试做一个FFmpeg采集摄像头视频和麦克风音频的程序。经过一个星期的努力,终于做出来了。我打算把开发的心得和经验分享给大家。我分三部分来讲述:首先第一部分介绍如何用FFmpeg的官方工具(ffmpeg.exe)通过命令行来枚举DShow设备和采集摄像头图像,这部分是基础,能够快速让大家熟悉怎么用FFmpeg测试摄像头采集;第二部分介绍我写的采集程序的功能和用法;第三部分讲解各个模块包括采集、编码、封装和录制是如何实现的。
采集过程实现
这个程序叫“AVCapture”,能从视频采集设备(摄像头,采集卡)获取图像,支持图像预览;还可以采集麦克风音频;支持对视频和音频编码,支持录制成文件。该采集程序实现了枚举采集设备,采集控制、显示图像、视频/音频编码和录制的功能,其中输入(Input)、输出(Output)和显示(Paint)这三个模块分别用一个单独的类进行封装:CAVInputStream,CAVOutputStream,CImagePainter。CAVInputStream负责从采集设备获取数据,提供接口获取采集设备的属性,以及提供回调函数把数据传给上层。CAVOutputStream负责对采集的视频和音频流进行编码、封装,保存成一个文件。而CImagePainter则用来显示图像,使用了GDI绘图,把图像显示到主界面的窗口。
AVFormatContext *pFmtCtx = avformat_alloc_context();
AVDictionary* options = NULL;
av_dict_set(&options, "list_devices", "true", 0);
AVInputFormat *iformat = av_find_input_format("dshow");
//printf("Device Info=============\n");
avformat_open_input(&pFmtCtx, "video=dummy", iformat, &options);
首先需要指定采集设备的名称。如果是视频设备类型,则名称以“video=”开头;如果是音频设备类型,则名称以“audio=”开头。调用avformat_open_input接口打开设备,将设备名称作为参数传进去,注意这个设备名称需要转成UTF-8编码。然后调用avformat_find_stream_info获取流的信息,得到视频流或音频流的索引号,之后会频繁用到这个索引号来定位视频和音频的Stream信息。接着,调用avcodec_open2打开视频解码器或音频解码器,实际上,我们可以把设备也看成是一般的文件源,而文件一般采用某种封装格式,要播放出来需要进行解复用,分离成裸流,然后对单独的视频流、音频流进行解码。虽然采集出来的图像或音频都是未编码的,但是按照FFmpeg的常规处理流程,我们需要加上“解码”这个步骤。
m_InputStream.SetVideoCaptureCB(VideoCaptureCallback);
m_InputStream.SetAudioCaptureCB(AudioCaptureCallback);
bool bRet;
bRet = m_InputStream.OpenInputStream(); //初始化采集设备
if(!bRet)
{
MessageBox(_T("打开采集设备失败"), _T("提示"), MB_OK|MB_ICONWARNING);
return 1;
}
int cx, cy, fps;
AVPixelFormat pixel_fmt;
if(m_InputStream.GetVideoInputInfo(cx, cy, fps, pixel_fmt)) //获取视频采集源的信息
{
m_OutputStream.SetVideoCodecProp(AV_CODEC_ID_H264, fps, 500000, 100, cx, cy); //设置视频编码器属性
}
int sample_rate = 0, channels = 0;
AVSampleFormat sample_fmt;
if(m_InputStream.GetAudioInputInfo(sample_fmt, sample_rate, channels)) //获取音频采集源的信息
{
m_OutputStream.SetAudioCodecProp(AV_CODEC_ID_AAC, sample_rate, channels, 32000); //设置音频编码器属性
}
//从Config.INI文件中读取录制文件路径
P_GetProfileString(_T("Client"), "file_path", m_szFilePath, sizeof(m_szFilePath));
bRet = m_OutputStream.OpenOutputStream(m_szFilePath); //设置输出路径
if(!bRet)
{
MessageBox(_T("初始化输出失败"), _T("提示"), MB_OK|MB_ICONWARNING);
return 1;
}
StartCapture函数分别建立了一个读取视频包和读取音频包的线程,两个线程各自独立工作,分别从视频采集设备,音频采集设备获取到数据,然后进行后续的处理。(注意:两个线程同时向一个文件写数据可能会有同步的问题,FFmpeg内部可能没有做多线程安全访问的处理,所以最好在自己线程里加一个锁进行互斥,从而保护临界区的安全)
其中,读取摄像头数据的线程的处理代码如下:
DWORD WINAPI CAVInputStream::CaptureVideoThreadFunc(LPVOID lParam)
{
CAVInputStream * pThis = (CAVInputStream*)lParam;
pThis->ReadVideoPackets();
return 0;
}
int CAVInputStream::ReadVideoPackets()
{
if(dec_pkt == NULL)
{
prepare before decode and encode
dec_pkt = (AVPacket *)av_malloc(sizeof(AVPacket));
}
int encode_video = 1;
int ret;
//start decode and encode
while (encode_video)
{
if (m_exit_thread)
break;
AVFrame * pframe = NULL;
if ((ret = av_read_frame(m_pVidFmtCtx, dec_pkt)) >= 0)
{
pframe = av_frame_alloc();
if (!pframe)
{
ret = AVERROR(ENOMEM);
return ret;
}
int dec_got_frame = 0;
ret = avcodec_decode_video2(m_pVidFmtCtx->streams[dec_pkt->stream_index]->codec, pframe, &dec_got_frame, dec_pkt);
if (ret < 0)
{
av_frame_free(&pframe);
av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
break;
}
if (dec_got_frame)
{
if(m_pVideoCBFunc)
{
CAutoLock lock(&m_WriteLock);
m_pVideoCBFunc(m_pVidFmtCtx->streams[dec_pkt->stream_index], m_pVidFmtCtx->streams[m_videoindex]->codec->pix_fmt, pframe, av_gettime() - m_start_time);
}
av_frame_free(&pframe);
}
else
{
av_frame_free(&pframe);
}
av_free_packet(dec_pkt);
}
else
{
if (ret == AVERROR_EOF)
encode_video = 0;
else
{
ATLTRACE("Could not read video frame\n");
break;
}
}
}
return 0;
}
如果是视频设备类型,则名称以“video=”开头;如果是音频设备类型,则名称以“audio=”开头。调用avformat_open_input接口打开设备,将设备名称作为参数传进去,注意这个设备名称需要转成UTF-8编码。然后调用avformat_find_stream_info获取流的信息,得到视频流或音频流的索引号,之后会频繁用到这个索引号来定位视频和音频的Stream信息。接着,调用avcodec_open2打开视频解码器或音频解码器,实际上,我们可以把设备也看成是一般的文件源,而文件一般采用某种封装格式,要播放出来需要进行解复用,分离成裸流,然后对单独的视频流、音频流进行解码。虽然采集出来的图像或音频都是未编码的,但是按照FFmpeg的常规处理流程,我们需要加上“解码”这个步骤。
鼠标事件
鼠标事件是视频操作最常见的事件行为,在视频区域裁剪,事件拖动过程中就必须有鼠标事件的参与,下面就是对鼠标行为接口的封装,内部参数比较繁杂,具体可以在IDE中尝试模拟整个过程的实现,这里就不做过多阐述。
def get_rect2(im, title='get_rect'):
def redo_click(param):
param['start_x'], param['start_y'], param['end_x'], param['end_y'] = 0, -50, 0, -50,
param['current_x'], param['current_y'], param['delta_x'], param['delta_y'] = 0, 0,0,0
param['is_finish'], param['click_count'] = 0, 0
mouse_value ={}
redo_click(mouse_value)
cv2.namedWindow(title)
cv2.moveWindow(title, 100, 100)
def onMouse(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
param['delta_x'] = x - param['start_x']
param['delta_y'] = y - param['start_y']
if event == cv2.EVENT_MOUSEMOVE and flags == cv2.EVENT_FLAG_LBUTTON:
if abs(x - param['start_x']) < 10 :
param['start_x'] = x
if abs(y - param['start_y']) < 10 :
param['start_y'] = y
if abs(x - param['end_x']) < 10:
param['end_x'] = x
if abs(y - param['end_y']) < 10:
param['end_y'] = y
if x > param['start_x']+100 and x < param['end_x']-100 and y >param['start_y']+100 and y < param['end_y']-100:
w = param['end_x'] - param['start_x']
param['start_x'] = x - param['delta_x']
param['end_x'] = param['start_x'] + w
h = param['end_y'] - param['start_y']
param['start_y'] = y - param['delta_y']
param['end_y'] = param['start_y'] + h
param['current_x'],param['current_y'] = x,y
if event == cv2.EVENT_LBUTTONUP:
if param['click_count'] == 0:
param['start_x'],param['start_y'],param['click_count'] = x,y,1
elif param['click_count'] == 1:
param['click_count'] = 2
elif param['click_count'] >= 2:
if x > param['start_x']+5 and x < param['start_x']+80-5 and y <param['end_y']-5 and y >param['end_y'] - 30+5:
param['is_finish'] = 1
if x > param['end_x'] - 70+5 and x < param['end_x']-5 and y <param['end_y']-5 and y >param['end_y'] - 30+5:
redo_click(param)
if param['click_count'] == 1:
param['end_x'],param['end_y'] = x ,y
if event == cv2.EVENT_RBUTTONDOWN:
redo_click(param)
cv2.setMouseCallback(title, onMouse, mouse_value)
cv2.imshow(title, im)
while mouse_value['is_finish'] == 0:
im_draw = np.copy(im)
cv2.rectangle(im_draw,(mouse_value['start_x'],mouse_value['start_y']),(mouse_value['end_x'],mouse_value['end_y']),(0,1, 1),2)
if mouse_value['click_count'] == 0:
point_str = '(%s,%s)' % (mouse_value['current_x'],mouse_value['current_y'])
cv2.putText(im_draw,point_str,(mouse_value['current_x']+5,mouse_value['current_y']+5), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0,1, 1), 1,100)
if mouse_value['click_count'] == 1:
left_up_str = '(%s,%s)' % (mouse_value['start_x'], mouse_value['start_y'])
w = mouse_value['end_x']-mouse_value['start_x']
h = mouse_value['end_y']-mouse_value['start_y']
cv2.putText(im_draw,left_up_str,(mouse_value['start_x'] - 10, mouse_value['start_y'] - 10),cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 1, 1), 1, 100)
right_down_str = '(%s,%s) size:(%s*%s)' % (mouse_value['end_x'],mouse_value['end_y'],w,h)
cv2.putText(im_draw, right_down_str,(mouse_value['end_x'],mouse_value['end_y']),cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 1, 1), 1, 100)
if mouse_value['click_count'] >= 2:
left_up_str = '(%s,%s)' % (mouse_value['start_x'], mouse_value['start_y'])
w = mouse_value['end_x'] - mouse_value['start_x']
h = mouse_value['end_y'] - mouse_value['start_y']
cv2.putText(im_draw, left_up_str, (mouse_value['start_x'] - 10, mouse_value['start_y'] - 10),cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 1, 1), 1, 100)
right_down_str = '(%s,%s) size:(%s*%s)' % (mouse_value['end_x'], mouse_value['end_y'], w, h)
cv2.putText(im_draw, right_down_str, (mouse_value['end_x'], mouse_value['end_y']), cv2.FONT_HERSHEY_TRIPLEX,0.5, (0, 1, 1), 1, 100)
cv2.rectangle(im_draw, (mouse_value['start_x'], mouse_value['end_y'] - 30),(mouse_value['start_x'] + 80, mouse_value['end_y']), (0, 1, 1), 2)
cv2.putText(im_draw,'Confirm',(mouse_value['start_x']+5,mouse_value['end_y'] - 10),cv2.FONT_HERSHEY_TRIPLEX,0.5,(0,0,1),1,100)
cv2.rectangle(im_draw, (mouse_value['end_x']-70, mouse_value['end_y'] - 30),(mouse_value['end_x'], mouse_value['end_y']), (0, 1, 1), 2)
cv2.putText(im_draw, 'Cancel', (mouse_value['end_x'] - 65, mouse_value['end_y'] - 10),cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 1), 1, 100)
cv2.imshow(title,im_draw)
_ = cv2.waitKey(1)
cv2.destroyWindow(title)
return (mouse_value['start_x'],mouse_value['start_y']),(mouse_value['end_x'],mouse_value['end_y'])
采集开始时,视频和音频数据就会传递给相应的函数去处理,在该程序中,回调函数主要对图像或音频进行编码,然后封装成FFmpeg支持的容器(例如mkv/avi/mpg/ts/mp4)。另外,需要初始化OutputStream的VideoCodec和AudioCodec的属性,在我的程序中,视频编码器是H264,音频编码器用AAC,通过CAVInputStream对象获得输入流的信息之后再赋值给输出流相应的参数。最后调用m_OutputStream对象的OpenOutputStream成员函数打开编码器和录制的容器,其中我们需要传入一个输出文件路径作为参数,这个为录制的文件路径,路径是在Config.ini文件里配置的。如果OpenOutputStream函数返回true,则表示初始化输出流成功。
- 分享
- 举报
-
浏览量:5400次2021-04-23 14:10:42
-
浏览量:1376次2023-04-17 16:03:03
-
浏览量:2095次2020-04-01 10:07:10
-
浏览量:7175次2021-01-28 09:52:52
-
浏览量:15817次2020-09-28 11:08:13
-
浏览量:3442次2020-10-29 17:26:12
-
浏览量:772次2023-06-12 14:35:58
-
浏览量:2030次2017-12-22 21:28:55
-
浏览量:2250次2022-03-05 08:31:20
-
浏览量:4979次2021-04-25 16:34:37
-
2020-12-14 22:33:31
-
浏览量:1823次2024-01-02 22:42:19
-
浏览量:8308次2020-12-13 17:04:33
-
浏览量:1062次2023-12-08 16:23:35
-
浏览量:1256次2023-08-24 13:51:18
-
浏览量:2531次2023-06-28 17:22:40
-
浏览量:1484次2024-02-19 10:16:21
-
浏览量:791次2023-12-13 14:01:46
-
浏览量:1670次2024-05-24 17:53:56
-
广告/SPAM
-
恶意灌水
-
违规内容
-
文不对题
-
重复发帖
这把我C
感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~
举报类型
- 内容涉黄/赌/毒
- 内容侵权/抄袭
- 政治相关
- 涉嫌广告
- 侮辱谩骂
- 其他
详细说明