首页 > 技术知识 > 正文

摘要:

紧接上一篇我们讲了视频批处理过程的具体实现,本篇我们将对摄像头捕获,麦克风和鼠标事件进行讲解。

最近发现很多人问怎么用FFmpeg采集摄像头图像,事实上FFmpeg很早就支持通过DShow获取采集设备(摄像头、麦克风)的数据了,只是网上提供的例子比较少。如果能用FFmpeg实现采集、编码和录制(或推流),那整个实现方案就简化很多,正因为这个原因,我想尝试做一个FFmpeg采集摄像头视频和麦克风音频的程序。经过一个星期的努力,终于做出来了。我打算把开发的心得和经验分享给大家。我分三部分来讲述:首先第一部分介绍如何用FFmpeg的官方工具(ffmpeg.exe)通过命令行来枚举DShow设备和采集摄像头图像,这部分是基础,能够快速让大家熟悉怎么用FFmpeg测试摄像头采集;第二部分介绍我写的采集程序的功能和用法;第三部分讲解各个模块包括采集、编码、封装和录制是如何实现的。

ffmpeg+opencv视频裁剪转码摄像头及鼠标事件详解

采集过程实现

这个程序叫“AVCapture”,能从视频采集设备(摄像头,采集卡)获取图像,支持图像预览;还可以采集麦克风音频;支持对视频和音频编码,支持录制成文件。该采集程序实现了枚举采集设备,采集控制、显示图像、视频/音频编码和录制的功能,其中输入(Input)、输出(Output)和显示(Paint)这三个模块分别用一个单独的类进行封装:CAVInputStream,CAVOutputStream,CImagePainter。CAVInputStream负责从采集设备获取数据,提供接口获取采集设备的属性,以及提供回调函数把数据传给上层。CAVOutputStream负责对采集的视频和音频流进行编码、封装,保存成一个文件。而CImagePainter则用来显示图像,使用了GDI绘图,把图像显示到主界面的窗口。

ffmpeg+opencv视频裁剪转码摄像头及鼠标事件详解1

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内部可能没有做多线程安全访问的处理,所以最好在自己线程里加一个锁进行互斥,从而保护临界区的安全)

ffmpeg+opencv视频裁剪转码摄像头及鼠标事件详解2

其中,读取摄像头数据的线程的处理代码如下: 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,则表示初始化输出流成功。

猜你喜欢