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

这把我C 2021-04-23 14:11:19 5137

摘要:

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

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

img


采集过程实现

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

img

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

img

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

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 97 6 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
这把我C
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区