第3章 CHAPTER 3 FFmpeg二次开发采集 并预览本地摄像头 5min FFmpeg中有一个和多媒体设备交互的类库,即Libavdevice,使用这个库可以读取计算机(或者其他设备)的多媒体设备中的数据,或者将数据输出到指定的多媒体设备上,也可以使用Qt或SDL将捕获到的摄像头数据显示到窗口中。 3.1FFmpeg的命令行方式处理摄像头 使用ffmpeg hide_banner devices命令可以查看本机支持的输入/输出设备,以Windows和Linux为例。显示的信息中D表示支持解码,可以作为输入; E表示支持编码,可以作为输出,具体信息如下: //chapter3/help-others.txt Devices: D = Demuxing supported E = Muxing supported --- D dshow DirectShow capture D lavfi Libavfilter virtual input device E sdl,sdl2SDL2 output device D vfwcap VfW video capture Devices: D. = Demuxing supported E = Muxing supported -- DE alsa ALSA audio output E caca caca (color ASCII art) output device DE fbdev Linux framebuffer D iec61883libiec61883 (new DV1394) A/V input device D jack JACK Audio Connection Kit D kmsgrabKMS screen capture D lavfi Libavfilter virtual input device D libcdio D libdc1394dc1394 v.2 A/V grab D openal OpenAL audio capture device E opengl OpenGL output DE oss OSS (Open Sound System) playback DE pulse Pulse audio output E sdl,sdl2SDL2 output device DE sndio sndio audio playback DE video4linux2,v4l2 Video4Linux2 output device E vout_rpiRpi (mmal) video output device D x11grabX11 screen capture, using XCB E xv XV (XVideo) output device Windows平台下使用vfwcap的效果比使用dshow的效果要差一些,可以通过ffmpeg h demuxer=dshow 命令查看支持的操作参数,支持查看设备列表、选项列表,以及可以设置设备输出的视频分辨率、帧率等,具体的输出信息如下: //chapter3/help-others.txt Demuxer dshow [DirectShow capture]: dshow indev AVOptions: -video_size<image_size> .D....... set video size given a string such as 640x480 or hd720. -pixel_format <pix_fmt>.D....... set video pixel format (default none) -framerate<string>.D....... set video frame rate -sample_rate<int>.D....... set audio sample rate (from 0 to INT_MAX) (default 0) -sample_size<int>.D....... set audio sample size (from 0 to 16) (default 0) -channels<int>.D....... set number of audio channels, such as 1 or 2 (from 0 to INT_MAX) (default 0) -audio_buffer_size <int>.D....... set audio device buffer latency size in milliseconds (default is the device's default) (from 0 to INT_MAX) (default 0) -list_devices<boolean>.D....... list available devices (default false) -list_options<boolean>.D....... list available options for specified device (default false) -video_device_number<int>.D....... set video device number for devices with same name (starts at 0) (from 0 to INT_MAX) (default 0) -audio_device_number <int>.D....... set audio device number for devices with same name (starts at 0) (from 0 to INT_MAX) (default 0) -crossbar_video_input_pin_number <int>.D....... set video input pin number for crossbar device (from -1 to INT_MAX) (default -1) -crossbar_audio_input_pin_number <int>.D....... set audio input pin number for crossbar device (from -1 to INT_MAX) (default -1) -show_video_device_dialog <boolean>.D....... display property dialog for video capture device (default false) -show_audio_device_dialog <boolean>.D....... display property dialog for audio capture device (default false) -show_video_crossbar_connection_dialog <boolean>.D....... display property dialog for crossbar connecting pins filter on video device (default false) -show_audio_crossbar_connection_dialog <boolean>.D....... display property dialog for crossbar connecting pins filter on audio device (default false) -show_analog_tv_tuner_dialog <boolean>.D....... display property dialog for analog tuner filter (default false) -show_analog_tv_tuner_audio_dialog <boolean>.D....... display property dialog for analog tuner audio filter (default false) -audio_device_load <string>.D....... load audio capture filter device (and properties) from file -audio_device_save <string>.D....... save audio capture filter device (and properties) to file -video_device_load <string>.D....... load video capture filter device (and properties) from file -video_device_save <string>.D....... save video capture filter device (and properties) to file Windows平台下查询支持的所有设备列表,命令如下: ffmpeg -list_devices true -f dshow -i dummy 命令执行后,笔者本地的输出结果如图31所示(注: 有可能出现中文乱码的情况)。列表所显示的设备名称很重要,因为输入时需要使用f dshow i video="设备名"的方式。 图31Windows平台列举设备列表 获取摄像头数据后,可以保存为本地文件或者将实时流发送到流媒体服务器。例如从摄像头读取数据并编码为H.264,最后保存成mycamera.flv的命令如下: ffmpeg -f dshow -i video="Lenovo EasyCamera" -vcodec libx264 mycamera001.flv 使用ffplay.exe可以直接播放摄像头的数据,命令如下: ffplay -f dshow -i video="Lenovo EasyCamera" 如果设备名称正确,则会直接打开本机的摄像头,效果如图32所示。 图32FFplay播放摄像头 可以查看摄像头的流信息,执行效果如图33所示,具体命令如下: ffmpeg -hide_banner -f dshow -i "video=Lenovo EasyCamera" 图33查看摄像头的流信息 查询本机dshow设备、查看USB 2.0摄像头的信息,只能输出yuyv422压缩编码数据(可以理解为rawvideo编码器,像素格式为yuyv422),经过解码后获得yuyv422编码的图像数据,所以,在这种情况下可以不解码。 Linux下大多使用video4linux2/v4l2设备,通过 ffmpeg h demuxer=v4l2 命令可以查看相关的操作参数,输出信息如下: //chapter3/help-others.txt Demuxer video4linux2,v4l2 [Video4Linux2 device grab]: V4L2 indev AVOptions: -standard<string>.D....... set TV standard, used only by analog frame grabber -channel<int>.D....... set TV channel, used only by frame grabber (from -1 to INT_MAX) (default -1) -video_size<image_size>.D....... set frame size -pixel_format<string>.D....... set preferred pixel format -input_format<string>.D....... set preferred pixel format (for raw video) or codec name -framerate<string>.D....... set frame rate -list_formats<int>.D....... list available formats and exit (from 0 to INT_MAX) (default 0) all.D....... show all available formats raw.D....... show only non-compressed formats compressed.D....... show only compressed formats -list_standards<int>.D....... list supported standards and exit (from 0 to 1) (default 0) all.D....... show all supported standards -timestamps<int>.D....... set type of timestamps for grabbed frames (from 0 to 2) (default default) default.D....... use timestamps from the kernel abs.D....... use absolute timestamps (wall clock) mono2abs .D....... force conversion from monotonic to absolute timestamps -ts<int>.D....... set type of timestamps for grabbed frames (from 0 to 2) (default default) default.D....... use timestamps from the kernel abs.D....... use absolute timestamps (wall clock) mono2abs.D....... force conversion from monotonic to absolute timestamps -use_libv4l2<boolean>.D....... use libv4l2 (v4l-utils) conversion functions (default false) 当前机器上挂载了CSI接口的相机,可以查看其支持的格式,执行效果如图34所示,该相机支持多种非压缩编码格式,例如JFIF JPEG、MotionJPEG、H.264等,具体命令如下: ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0 图34Linux查看摄像头支持的格式 Linux平台下读取摄像头并编码为H.264的命令如下: #ffmpeg -f dshow -i video="USB2.0 PC CAMERA" -vcodec libx264 xxxx.h264 ffmpeg -f v4l2 -i video=/dev/video0 -vcodec libx264 xxxx.h264 在实时流推送中如果需要提高libx264的编码速度,则可以添加preset:v ultrafast 和 tune:v zerolatency两个选项。 Windows平台下使用gdigrab设备可以录制桌面屏幕,可以查看gdigrab支持的选项,命令如下: ffmpeg -h demuxer=gdigrab 该命令的输出信息如下: //chapter3/help-others.txt Demuxer gdigrab [GDI API Windows frame grabber]: GDIgrab indev AVOptions: -draw_mouse<int>.D....... draw the mouse pointer (from 0 to 1) (default 1) -show_region<int>.D....... draw border around capture area (from 0 to 1) (default 0) -framerate<video_rate>.D....... set video frame rate (default "ntsc") -video_size<image_size>.D....... set video frame size -offset_x<int>.D....... capture area x offset (from INT_MIN to INT_MAX) (default 0) -offset_y<int>.D....... capture area y offset (from INT_MIN to INT_MAX) (default 0) 录屏并编码为H.264的命令如下: ffmpeg -f gdigrab -i desktop -vcodec h264 xxxx.h264 录屏并显示鼠标,从屏幕左上角(100,200)的640×480区域录屏,帧率为25的命令如下: ffmpeg -f gdigrab -draw_mouse -framerate 25 -offset_x 100 -offset_y 200 \ -video_size 640x480 -i desktop out1.mpg Linux平台下与之类似,使用 x11grab 设备,可以查看支持的选项,命令如下: $ ffmpeg -h demuxer=x11grab 该命令的输出信息如下: //chapter3/help-others.txt Demuxer x11grab [X11 screen capture, using XCB]: xcbgrab indev AVOptions: -x<int>.D...... Initial x coordinate. (from 0 to INT_MAX) (default 0) -y<int>.D...... Initial y coordinate. (from 0 to INT_MAX) (default 0) -grab_x<int>.D...... Initial x coordinate. (from 0 to INT_MAX) (default 0) -grab_y<int>.D...... Initial y coordinate. (from 0 to INT_MAX) (default 0) -video_size<string> .D...... A string describing frame size, such as 640x480 or hd720. (default "vga") -framerate<string>.D...... (default "ntsc") -draw_mouse<int>.D...... Draw the mouse pointer. (from 0 to 1) (default 1) -follow_mouse<int>.D...... Move the grabbing region when the mouse pointer reaches within specified amount of pixels to the edge of region. (from -1 to INT_MAX) (default 0) centered.D...... Keep the mouse pointer at the center of grabbing region when following. -show_region<int>.D...... Show the grabbing region. (from 0 to 1) (default 0) -region_border<int>.D...... Set the region border thickness. (from 1 to 128) (default 3) Linux平台下录屏并捕获鼠标的命令如下: ffmpeg -f x11grab -draw_mouse -framerate 25 - x 100 -y 200 \ -video_size 640x480 -i :0.0 out3.mpg 3.2FFmpeg的SDK方式读取本地摄像头 使用FFmpeg采集并预览本地摄像头的流程如图35所示。 图35FFmpeg采集并预览摄像头 使用Libavdevice时需要包含头文件,代码如下: #include "libavdevice/avdevice.h" 然后在程序中需要注册Libavdevice,代码如下: avdevice_register_all(); 接下来就可以使用Libavdevice的功能了,使用Libavdevice读取数据和直接打开视频文件比较类似。因为系统的设备会被FFmpeg当成一种输入的格式(AVInputFormat)。使用FFmpeg的API可以根据指定参数打开输入流,并返回输入封装的上下文AVFormatContext,函数的代码如下: //chapter3/code3.2.txt int avformat_open_input(AVFormatContext **ps, const char *url,//打开的地址,文件或设备等 AVInputFormat *fmt,//指定输入格式,若为空,则自动检测 AVDictionary **options);//指定解复用器的私有选项 通常,打开一个文件或者直播流的代码如下: //chapter3/code3.2.txt AVFormatContext *input_fmt_ctx = NULL; const char* file_path = "test.mp4"; //也可以打开网络直播流 //const char* file_path = "rtmp://192.168.1.100:1935/live/test"; avformat_open_input(&input_fmt_ctx, file_path, NULL, NULL); 使用Libavdevice时,唯一的不同在于首先需要查找用于输入的设备,例如可以使用av_find_input_format()函数来查找音视频设备。在Windows平台上使用vfw设备作为输入设备,然后在URL中指定打开第0个设备(在笔者的计算机上为摄像头设备),代码如下: //chapter3/code3.2.txt AVFormatContext *pFormatCtx = avformat_alloc_context(); AVInputFormat *ifmt = av_find_input_format("vfwcap"); avformat_open_input(&pFormatCtx, 0, ifmt,NULL); 在Windows平台上除了可以使用vfw设备作为输入设备之外,还可以使用DirectShow作为输入设备,代码如下: //chapter3/code3.2.txt AVFormatContext *pFormatCtx = avformat_alloc_context(); AVInputFormat *ifmt = av_find_input_format("dshow"); avformat_open_input(&pFormatCtx,"video=Integrated Camera",ifmt,NULL) ; 在Linux平台上可以使用v4l2设备作为输入设备,代码如下: //chapter3/code3.2.txt //查找设备前要先调用avdevice_register_all函数 AVInputFormat *in_fmt = in_fmt = av_find_input_format("video4linux2"); if (in_fmt == NULL) { printf("can't find_input_format\n"); return ; } AVFormatContext *fmt_ctx = NULL; if (avformat_open_input(&fmt_ctx, "/dev/video0", in_fmt, NULL) < 0) { printf("can't open_input_file\n"); return ; } 另外,使用选项av_dict_set(&options, "f", "v4l2", 0)和指定参数ifmt的效果相同。通常在Linux平台下可以不设置,默认为支持v4l2且自动识别,而在Windows平台下的vfwp、dshow需要明确指定,代码如下: AVInputFormat *ifmt = av_find_input_format("v4l2");//加快探测流的速度 avformat_open_input(&fmt_ctx, "/dev/video0", ifmt, NULL); 上述代码等效于下面的代码: AVDictionary *options = NULL; av_dict_set(&options, "f", "v4l2", 0); avformat_open_input(&fmt_ctx,"/dev/video0",NULL,&options); 如果需要指定输入格式,则可以通过AVOption设置,并且参数不一样,描述信息如下: -pixel_format <string> .D.... set preferred pixel format -input_format <string> .D.... set preferred pixel format (for raw video) or codec name 当选择像素格式时,一定是非压缩的原始数据,两个参数均可,代码如下: av_dict_set(&options, "pixel_format", "rgb24", 0); av_dict_set(&options, "input_format", "rgb24", 0); //同上 当选择压缩编码格式,必须只能使用input_format,代码如下: av_dict_set(&options, "input_format", "h264", 0); av_dict_set(&options, "input_format", "mjpeg", 0); 可以调用avformat_open_input()函数来打开摄像头设备,通过这个函数的参数指定打开的设备路径“/dev/video0”,使用的驱动“video4linux2”,将相应的格式pix_fmt指定为yuyv422,以及将分辨率指定为640×480,代码如下: //chapter3/code3.2.txt AVFormatContext *fmt_ctx = NULL; AVDictionary *options = NULL; char *devicename = "/dev/video0"; avdevice_register_all(); AVInputFormat *iformat = av_find_input_format("video4linux2"); av_dict_set(&options,"video_size","640x480",0); av_dict_set(&options,"pixel_format","yuyv422", 0); avformat_open_input(&fmt_ctx, devicename, iformat, &options); avformat_close_input(&fmt_ctx); 可以调用av_read_frame()函数来读取一帧YUV数据,代码如下: //chapter3/code3.2.txt int ret = 0; AVPacket pkt; while((ret = av_read_frame(fmt_ctx, &pkt)) == 0) { av_log(NULL, AV_LOG_INFO, "packet size is %d(%p)\n", pkt.size, pkt.data); av_packet_unref(&pkt);//释放包 } 可以调用fwrite()函数将读取到的YUV数据保存到文件中,代码如下: //chapter3/code3.2.txt char *out = "out.yuv"; FILE *outfile = fopen(out, "wb+"); fwrite(pkt.data, 1, pkt.size, outfile); //614400 fflush(outfile); fclose(outfile); 打开本地摄像头,循环读取帧数据并存储到文件中的完整代码如下: //chapter3/record-video.c #include <stdio.h> #include "libavutil/avutil.h" #include "libavdevice/avdevice.h" #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" //打开摄像头 static AVFormatContext* open_dev(){ int ret = 0; char errors[1024] = {0, }; //上下文环境 AVFormatContext *fmt_ctx = NULL; AVDictionary *options = NULL; //设备名称 char *devicename = "/dev/video0"; //注册Libavdevice库 avdevice_register_all(); //查找设备 AVInputFormat *iformat = av_find_input_format("video4linux2"); av_dict_set(&options,"video_size","640x480",0);//分辨率 av_dict_set(&options,"pixel_format","yuyv422", 0);//YUV帧格式 //打开设备 if((ret = avformat_open_input(&fmt_ctx, devicename, iformat, &options)) < 0 ){ av_strerror(ret, errors, 1024); fprintf(stderr, "Failed to open audio device, [%d]%s\n", ret, errors); return NULL; } return fmt_ctx; } //读取并录制摄像头数据 void rec_video() { int ret = 0; AVFormatContext *fmt_ctx = NULL; int count = 0; //packet:数据包 AVPacket pkt; //设置日志级别 av_log_set_level(AV_LOG_Debug); //create file:创建本地文件 char *out = "out.yuv"; FILE *outfile = fopen(out, "wb+"); //打开设备 fmt_ctx = open_dev(); //从摄像头中读取YUV帧数据 while((ret = av_read_frame(fmt_ctx, &pkt)) == 0 && count++ < 100) { av_log(NULL, AV_LOG_INFO, "packet size is %d(%p)\n", pkt.size, pkt.data); fwrite(pkt.data, 1, pkt.size, outfile); //写入本地文件 fflush(outfile); av_packet_unref(&pkt); //release pkt:释放AVPacket } __ERROR: if(outfile){ //关闭文件 fclose(outfile); } //关闭设备并释放上下文环境 if(fmt_ctx) { avformat_close_input(&fmt_ctx); } av_log(NULL, AV_LOG_Debug, "finish!\n"); return; } int main(int argc, char *argv[]) { rec_video(); return 0; } 在Linux系统中使用编译该文件的命令如下: gcc record_video.c -lavformat -lavutil -lavdevice -lavcodec -o record_video 运行该程序会打开摄像头并循环读取帧,然后存储到本地文件out.yuv中,命令如下: ./record_video 使用FFplay可以播放生成的YUV文件,注意指定分辨率和YUV格式,命令如下: ffplay -s 640x480 -pix_fmt yuyv422 out.yuv 3.3FFmpeg+SDL2读取并显示本地摄像头 使用FFmpeg读取摄像头数据之后,可以调用SDL2库来渲染。 3.3.1SDL2简介 SDL2使用GNU通用公共许可证为授权方式,即动态链接(Dynamic Link)其库并不需要开放本身的源代码。虽然SDL时常被比喻为“跨平台的DirectX”,但事实上SDL被定位成以精简的方式来完成基础的功能,大幅度简化了控制图像、声音、输入/输出等工作所需的代码。但更高级的绘图功能或是音效功能则需搭配OpenGL和OpenAL等API来完成。另外,它本身也没有方便创建图形用户界面的函数。SDL2在结构上是将不 同操作系统的库包装成相同的函数,例如SDL在Windows平台上是DirectX的再包装,而在使用X11的平台上(包括Linux)则是调用Xlib库来输出图像。虽然SDL2本身是使用C语言写成的,但是它几乎可以被所有的编程语言所使用,例如C++、Perl、Python和Pascal等,甚至是Euphoria、Pliant这类较不流行的编程语言也都可行。SDL2库分为Video、Audio、CDROM、Joystick和Timer等若干子系统,除此之外,还有一些单独的官方扩充函数库。这些库由官方网站提供,并包含在官方文档中,它们共同组成了SDL的“标准库”。SDL的整体结构如图36所示。 图36SDL库的层次结构 3.3.2VS 2015搭建SDL2开发环境 本节将介绍如何在VS 2015下配置SDL2.0.8开发库的详细步骤。 1. 下载SDL2 进入SDL2官网,链接网址为https://github.com/libsdlorg/SDL/releases/。选择SDL2的Development Libraries中的SDL2devel2.0.12VC.zip(链接网址为https://github.com/libsdlorg/SDL/releases/tag/release2.0.12),如图37所示。下载并解压以供其他程序调用,在项目配置中可以使用SDL库的相对路径。 图37SDL库的下载网址 2. VS 2015项目配置 (1) 打开VS 2015,新建Win32控制台项目,将项目命名为SDLtest1,然后单击右下方的“确定”按钮,如图38所示。 图38新建VS 2015的控制台项目 (2) 右击项目名称(SDLtest1),在弹出的菜单中单击“属性”按钮,然后在弹出的属性页中配置包含目录和库目录,注意笔者这里使用SDL2库的相对路径,选择的平台为Win32,如图39所示。 图39配置VS 2015项目的包含目录和库目录 (3) 在项目SDLtest1属性页中选择“链接器”下的“输入”,编辑右侧的“附加依赖项”,在附加依赖项中添加SDL2.lib和SDL2main.lib(注意中间以英文分号分隔),然后单击右下方的“确定”按钮,如图310所示。 图310配置VS 2015项目的附加依赖项 3. 测试案例 项目配置成功后,可以调用SDL_Init()函数来测试是否配置成功,代码如下: //chapter3/SDLtest1/SDLtest1.cpp //SDLtest.cpp : 定义控制台应用程序的入口点 #include "stdafx.h" #include <iostream> #define SDL_MAIN_HANDLED //如果没有此宏,则会报错 #include <SDL.h> int main(){ if (SDL_Init(SDL_INIT_VIDEO) != 0){ std::cout << "SDL_Init Error: " << SDL_GetError() << std::endl; return 1; } else{ std::cout << "SDL_Init OK " << std::endl; } SDL_Quit(); return 0; } 需要注意这个宏语句(#define SDL_MAIN_HANDLED),如果没有定义这个宏,则会报错(并且要放到SDL.h之前),错误信息如下: 无法解析的外部符号main,该符号在函数"int cdecl invoke_main(void)" (?invoke_main@@YAHXZ) 中被引用 这是因为在SDL库的内部重新定义了main,因此main()函数需要写成如下形式: int main(int argc,char* argv[]) 而添加 #define SDL_MAIN_HANDLED 这个宏之后,即使main()函数的参数列表为空,也不会报错。 编译并运行该程序会提示找不到SDL2.dll,如图311所示。将SDL2devel2.0.12VC\lib\x86目录下的SDL2.dll复制到SDLtest1.exe同目录下,如图312所示。重新编译并运行该程序,若不报错,则表示配置成功,如图313所示。 图311运行时找不到SDL2.dll文件 图312复制SDL2.dll文件 图313SDL2库配置成功 3.3.3Qt 5.9平台搭建SDL2开发环境 笔者本地的Qt版本为5.9.8,配置SDL2开发环境的具体步骤如下。 (1) 下载SDL2的mingw版本,文件名为SDL2devel2.0.12mingw.tar.gz,链接网址为https://github.com/libsdlorg/SDL/releases/tag/release2.0.12。 (2) 打开Qt Creator,新建Qt Console Application类型的项目,单击右下方的Choose 按钮,如图314所示。 图314新建Qt控制台项目 (3) 在Project Location页面输入项目名称(SDLQtDemo1)和路径,如图315所示。 图315输入Qt项目名称和路径 (4) 在Kit Selection页面选中Desktop Qt 5.9.8 MinGW 32bit,然后单击右下方的“下一步”按钮,如图316所示。 注意: 读者也可以选择其他的编译套件,但不同的编译套件对应着不同的SDL2开发包,例如MinGW 32位编译套件对应SDL2devel2.0.12mingw.tar.gz,并且运行时需要对应32位的动态链接库。 图316选择MinGW 32位编译套件 (5) 解压SDL2devel2.0.12mingw.tar.gz后有两个重要的子目录,如图317所示。i686w64mingw32对应的是32位的开发库,x86_64w64mingw32对应的是64位的开发库。 图317解压SDL2devel2.0.12mingw.tar.gz (6) 配置Qt项目(SDLQtDemo1),打开SDLQtDemo1.pro配置文件,如图318所示,代码如下: //chapter3/SDLQtDemo1/SDLQtDemo1.pro.txt INCLUDEPATH+=../../SDL2-devel-2.0.12-mingw/i686-w64-mingw32/include/SDL2/ LIBS += -L../../SDL2-devel-2.0.12-mingw/i686-w64-mingw32/lib/ -lSDL2 -lSDL2main 图318修改Qt的项目配置文件 需要注意的是这里使用的是相对路径,如图319所示。 图319SDL2的相对路径 (7) 修改main.cpp,注释掉原来的源码,新增代码如下: //chapter3/SDLQtDemo1/main.cpp #include <iostream> #define SDL_MAIN_HANDLED //如果没有此宏,则会报错 #include <SDL.h> int main(){ if (SDL_Init(SDL_INIT_VIDEO) != 0){ std::cout << "SDL_Init Error: " << SDL_GetError() << std::endl; return 1; } else{ std::cout << "SDL_Init OK " << std::endl; } SDL_Quit(); return 0; } (8) 编译并运行该项目,输出的错误信息如下: /.../F5Codes/chapter6/build-SDLQtDemo1-Desktop_Qt_5_9_8_MinGW_32位-Debug/Debug/SDLQtDemo1.exe exited with code -1073741515 这是因为SDLQtDemo1.exe程序运行时找不到SDL2.dll动态链接库。将SDL2devel2.0.12mingw\i686w64mingw32\bin目录下的SDL2.dll文件复制到chapter6\buildSDLQtDemo1Desktop_Qt_5_9_8_MinGW_32bitDebug\debug目录下。重新编译并运行该项目会输出 SDL_Init OK,如图320所示。 图320成功配置并运行SDL2项目 3.3.4Linux平台搭建SDL2开发环境 笔者本地环境为Ubuntu 18.04,安装并配置SDL2的具体步骤如下。 (1) 安装依赖项,命令如下: //chapter3/help-others.txt sudo apt-get update && sudo apt-get -y install \ autoconf automake build-essential cmake \ git-core pkg-config texinfo wget yasm zlib1g-dev (2) 安装SDL2库(只包含.so动态链接库),命令如下: sudo apt-get install libsdl2-2.0 libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-gfx-dev (3) 检验是否安装成功,命令如下: sdl2-config --exec-prefix --version -cflag 需要注意的是此处安装的SDL2库是没有头文件的,只包含系统运行时需要依赖的动态链接库(.so),而在实际开发过程中没有头文件是不行的,所以需要自己编译SDL2并且安装。 (4) 下载并解压SDL2库的源码SDL2devel2.0.12.tar.gz,具体的下载网址为https://github.com/libsdlorg/SDL/releases/tag/release2.0.12。 (5) 编译并安装SDL2,命令如下: //chapter3/help-others.txt #解压下载的文件,然后进入SDL2解压目录 #配置configure的可执行命令 sudo chmod +x configure #配置configure的参数命令 //chapter6/other-help.txt ./configure --enable-static --enable-shared #编译: make #安装 make install (6) 查看SDL2是否安装成功,命令如下: //chapter3/help-others.txt #在/usr/local/lib下面查看是否存在libSDL2.a ls /usr/local/lib #在/usr/local/include下面查看是否存在SDL2文件夹 ls /usr/local/include (7) 配置LD_LIBRARY_PATH环境变量,命令如下: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib 3.3.5SDL2播放YUV视频文件 SDL2的核心对象主要包括窗口(SDL_Window)、表面(SDL_Surface)、渲染器(SDL_Renderer)、纹理(SDL_Texture)和事件(SDL_Event)等。使用SDL2进行渲染的基本流程如图321所示,具体步骤如下。 图321SDL2渲染流程及API (1) 创建窗口。 (2) 创建渲染器。 (3) 清空缓冲区。 (4) 绘制要显示的内容。 (5) 最终将缓冲区内容渲染到Window窗口上。 使用SDL2播放YUV视频文件完全遵循上述SDL_Renderer的渲染流程,而YUV视频文件不能直接渲染,需要循环读取视频帧,然后将帧数据更新到纹理上进行渲染。 1. SDL2播放YUV视频文件的流程 使用SDL2播放YUV视频文件的函数调用步骤及相关API,代码如下: //chapter3/SDLQtDemo1/main.cpp /* SDL2播放YUV视频文件,函数调用步骤如下 * * [初始化SDL2库] * SDL_Init(): 初始化SDL2 * SDL_CreateWindow(): 创建窗口(Window) * SDL_CreateRenderer(): 基于窗口创建渲染器(Render) * SDL_CreateTexture(): 创建纹理(Texture) * * [循环渲染数据] * SDL_UpdateTexture(): 设置纹理的数据 * SDL_RenderCopy(): 纹理复制给渲染器 * SDL_RenderPresent(): 显示 * SDL_DestroyTexture(texture); * * [释放资源] * SDL_DestroyTexture(texture): 销毁纹理 * SDL_DestroyRenderer(render): 销毁渲染器 * SDL_DestroyWindow(win): 销毁窗口 * SDL_Quit(): 释放SDL2库 */ 2. 使用SDL2开发YUV视频播放器的完整案例 先介绍该案例程序中用到几个的重要变量类型,SDL_Window就是使用SDL时弹出的那个窗口; SDL_Texture用于显示YUV数据,一个SDL_Texture对应一帧YUV数据(案例中提供的YUV视频格式为YUV420p); SDL_Renderer用于将SDL_Texture渲染至SDL_Window; SDL_Rect用于确定SDL_Texture显示的位置。为了简单起见,程序中定义了几个全局变量,变量g_bpp代表1个视频像素占用的位数,例如1个YUV420P格式的视频像素占用12位; 变量g_pixel_w和g_pixel_h代表视频的宽和高,在本案例中提供的测试视频(ande10_yuv420p_352x288.yuv)的宽和高分别为352和288; 变量g_screen_w和g_screen_h代表屏幕的宽和高,在本案例中被初始化为400和300,程序运行中可以通过拖曳窗口的右下角来改变窗口的大小; 变量g_buffer_YUV420p是一字节数组,用于存储1帧YUV420p的视频数据,在播放视频的过程中会循环调用SDL_UpdateTexture()函数以将该数组中存储的视频数据更新到纹理(SDL_Texture)中。refresh_video_SDL2()函数用于定时刷新,在本案例中通过SDL_CreateThread()函数创建了一条线程,将线程的入口函数指定数为refresh_video_SDL2()函数,固定的刷新周期为40ms。本案例的代码如下: 注意: 本案例的完整工程及代码可参考chapter3/SDLQtDemo1工程,代码位于main2.cpp文件中。 //chapter3/SDLQtDemo1/main2.cpp #define SDL_MAIN_HANDLED//如果没有此宏,则会报错,并且要放到SDL.h之前 #include <iostream> #include <SDL.h> #include <vector> using namespace std; //刷新事件 #define REFRESH_EVENT (SDL_USEREVENT + 1) int g_thread_exit = 0; const int g_bpp = 12; //YUV420p,1像素占用的位数 const int g_pixel_w = 352,g_pixel_h = 288;//在本案例中YUV420p视频的宽和高 int g_screen_w = 400, g_screen_h = 300; //1帧视频占用的字节数 unsigned char g_buffer_YUV420p[g_pixel_w * g_pixel_w * g_bpp / 8]; //增加画面刷新机制 int refresh_video_SDL2(void *opaque){ while (g_thread_exit == 0) { SDL_Event event; event.type = REFRESH_EVENT; SDL_PushEvent(&event); SDL_Delay(40); } return 0; } int TestYUVPlayer001( ){ if(SDL_Init(SDL_INIT_VIDEO)) { printf( "Could not initialize SDL - %s\n", SDL_GetError()); return -1; } SDL_Window *screen; //SDL 2.0 对多窗口的支持 screen = SDL_CreateWindow("SDL2-YUVPlayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, g_screen_w, g_screen_h,SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE); if(!screen) { printf("SDL: could not create window - exiting:%s\n",SDL_GetError()); return -1; } //创建渲染器 SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0); //创建纹理:格式为YUV420p,宽和高为352x288 SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer,SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, g_pixel_w, g_pixel_h); FILE *fpYUV420p = NULL; //打开YUV420p视频文件 fpYUV420p = fopen("./ande10_yuv420p_352x288.yuv", "rb+"); if(fpYUV420p == NULL){ printf("cannot open this file\n"); return -1; } SDL_Rect sdlRect; SDL_Thread *refresh_thread =//创建独立线程,用于定时刷新 SDL_CreateThread(refresh_video_SDL2,NULL,NULL); SDL_Event event; while(1){ SDL_WaitEvent(&event);//等待事件 if(event.type == REFRESH_EVENT){ fread(g_buffer_YUV420p, 1, g_pixel_w * g_pixel_h * g_bpp / 8, fpYUV420p); //将1帧YUV420p的数据更新到纹理中 SDL_UpdateTexture(sdlTexture,NULL,g_buffer_YUV420p,g_pixel_w); //重新定义窗口大小 sdlRect.x = 0; sdlRect.y = 0; sdlRect.w = g_screen_w; sdlRect.h = g_screen_h; //Render:渲染三部曲 SDL_RenderClear( sdlRenderer ); SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect); SDL_RenderPresent( sdlRenderer ); //注意这里不再需要延迟,因为有独立的线程来刷新 //SDL_Delay(40); //休眠40ms //if(feof(fpYUV420p) != 0 )break;//如果遇到文件尾,则自动退出循环 }else if(event.type==SDL_WINDOWEVENT){ //If Resize:以拖曳方式更改窗口大小 SDL_GetWindowSize(screen,&g_screen_w,&g_screen_h); }else if(event.type == SDL_QUIT){//退出事件 break;//如果关闭事件,则退出循环 } } g_thread_exit = 1;//如果跳出循环,则将退出标志量修改为1 //释放资源 if (sdlTexture){ SDL_DestroyTexture(sdlTexture); sdlTexture = nullptr; } if (sdlRenderer){ SDL_DestroyRenderer(sdlRenderer); sdlRenderer = nullptr; } if (screen){ SDL_DestroyWindow(screen); screen = nullptr; } if (fpYUV420p){ fclose(fpYUV420p); fpYUV420p = nullptr; } SDL_Quit(); return 0; } 图322SDL2播放YUV420p视频 编译并运行该程序,将ande10_yuv420p_352x288.yuv这两个音频文件复制到buildSDLQtDemo1Desktop_Qt_5_9_8_MinGW_32bitDebug目录下,可以通过拖曳改变窗口大小,效果如图322所示。 3. SDL2画面刷新机制 实现SDL2的事件与渲染机制之后,增加画面刷新机制就可以作为一个播放器使用了。在上述案例中,通过while循环执行SDL_RenderPresent(renderer)就可以令视频逐帧播放了,但还是需要一个独立的刷新机制。这是因为在一个循环中,重复执行一个函数的效果通常不是周期性的,因为每次加载和处理的数据所消耗的时间是不固定的,因此单纯地在一个循环中使用SDL_RenderPresent(renderer)会令视频播放产生帧率跳动的情况,因此需要引入一个定期刷新机制,令视频的播放有一个固定的帧率。通常使用多线程的方式进行画面刷新管理,主线程进入主循环中等待(SDL_WaitEvent)事件,画面刷新线程在一段时间后发送(SDL_PushEvent)画面刷新事件,主线程收到画面刷新事件后进行画面刷新操作。 画面刷新线程定期构造一个REFRESH_EVENT事件,然后调用SDL_PushEvent()函数将事件发送出来,代码如下: //chapter3/help-others.txt #define REFRESH_EVENT (SDL_USEREVENT + 1) int g_thread_exit = 0; int refresh_video_SDL2(void *opaque){ while (g_thread_exit == 0) { SDL_Event event; event.type = REFRESH_EVENT; SDL_PushEvent(&event); SDL_Delay(40); } return 0; } 该函数只有两部分内容,第一部分是发送画面刷新事件,也就是发信号以通知主线程来干活; 另一部分是延时,使用一个定时器,保证自己是定期来通知主线程的。首先定义一个“刷新事件”,代码如下: #define REFRESH_EVENT (SDL_USEREVENT + 1)//请求画面刷新事件 SDL_USEREVENT是自定义类型的SDL事件,不属于系统事件,可以由用户自定义,这里通过宏定义便于后续引用,然后调用SDL_PushEvent()函数将事件发送出来,代码如下: SDL_PushEvent(&event);//发送画面刷新事件 SDL_PushEvent()是SDL2.0之后引入的函数,该函数能够将事件放入SDL2的事件队列中,当它从事件队列中被取出时,被接收事件的函数识别,并采取相应操作。也就是说在刷新操作中,使用刷新线程不断地将“刷新事件”放到SDL2的事件队列,在主线程中读取SDL2的事件队列里的事件,当发现事件是“刷新事件”时就进行刷新操作。 主线程(main()函数)的主要工作是首先初始化所有的组件和变量,包括SDL2的窗口、渲染器和纹理等,然后进入一个大循环,同时读取事件队列里的事件,如果是刷新事件,则进行渲染相关工作,进行画面刷新。随后需要创建一个缓冲区,每次渲染时都是先从视频文件里读一帧,这一帧先存到缓冲区再交给渲染器去渲染。这个缓冲区的大小应该和视频文件的每帧大小是相同的,这也意味着需要提前计算该视频文件类型的每帧大小,所以需要提前计算好YUV格式的视频帧的大小,例如在本案例中视频文件的格式为YUV420p,宽和高为352×288。主循环其实就是在不断地读取事件队列里的事件,每读取到一个事件,就进行判断,根据该事件的类型采取不同的操作。当收到需要刷新画面的事件后,开始进行读数据帧并渲染的操作,代码如下: //chapter3/help-others.txt while(1){ SDL_WaitEvent(&event); //等待事件 if(event.type == REFRESH_EVENT){ fread(g_buffer_YUV420p,1,g_pixel_w*g_pixel_h*g_bpp/8, fpYUV420p); //将1帧YUV420p的数据更新到纹理中 SDL_UpdateTexture( sdlTexture,NULL,g_buffer_YUV420p,g_pixel_w); //渲染三部曲 SDL_RenderClear( sdlRenderer ); SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect); SDL_RenderPresent( sdlRenderer ); if(feof(fpYUV420p) != 0 )break;//如果遇到文件尾,则自动退出循环 }else if(event.type==SDL_WINDOWEVENT){ //重定义窗口大小 SDL_GetWindowSize(screen,&g_screen_w,&g_screen_h); }else if(event.type == SDL_QUIT){ break; } } 3.3.6使用FFmpeg+SDL2读取本地摄像头并渲染 使用FFmpeg可以打开本地摄像头并循环读取视频帧数据,然后可以调用SDL2对视频帧进行渲染。打开Qt Creator,创建一个基于Widget的Qt Widgets Application项目(项目名称为FFmpegSDL2QtMonitor),如图323所示。 图323新建Qt的Widgets项目 双击widget.ui界面文件,界面中使用QVerticalLayout进行布局,然后往该界面中拖曳一个QLabel和两个QPushButton(它们的文本分别为Stop Camera和Start Camera),如图324所示。 图324设计widget.ui界面 右击QPushButton按钮,在弹出的菜单中选择“转到槽”,分别为这两个QPushButton按钮添加clicked()槽函数,如图325所示。 图325为QPushButton添加槽函数 1. 封装一个类QtFFmpegCamera用于操作FFmpeg 新增一个类QtFFmpegCamera用于操作FFmpeg,为了响应Qt的信号槽机制,将该类的父类设置为QObject。为了方便使用,需要创建一个Play()和SetStopped()等公共成员函数,并且新增AVPicture、AVFormatContext、AVCodecContext、AVFrame、SwsContext和AVPacket 等类型的私有成员函数。该类的头文件为qtffmpegcamera.h,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.h #ifndef QTFFMPEGCAMERA_H #define QTFFMPEGCAMERA_H //必须加以下内容,否则编译不能通过,为了兼容C和C99标准 #ifndef INT64_C #define INT64_C #define UINT64_C #endif //引入FFmpeg和SDL2的头文件 #include <iostream> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <libavdevice/avdevice.h> #include <libavfilter/avfilter.h> #include <libavutil/imgutils.h> #include <SDL.h> #include <SDL_main.h> }; #undef main using namespace std; #include <QObject> #include <QMutex> #include <QImage> class QtFFmpegCamera : public QObject { Q_OBJECT public: explicit QtFFmpegCamera(QObject *parent = nullptr); void Play(); int GetVideoWidth() const { return this->videoWidth; } int GetVideoHeight() const {return this->videoHeight; } int GetVideoStreamIndex() const { return this->videoStreamIndex; } QString GetVideoURL() const {return this->videoURL; } void SetVideoURL(QString url){this->videoURL = url; } void SetStopped(int st){this->stopped = st;} private: AVPicture pAVPicture; AVFormatContext *pAVFormatCtx; AVCodecContext *pAVCodecContext; AVFrame *pAVFrame; SwsContext * pSwsContext;//将YUYV422 转换为YUV420p SwsContext * pSwsContext2;//将YUV420p 转换为 RGB888 (RGB24) AVPacket pAVPacket; QMutexmutex; intvideoWidth; intvideoHeight; intvideoStreamIndex; QStringvideoURL; intstopped; signals: void GetImage(const QImage &image); public slots: }; #endif //QTFFMPEGCAMERA_H 在该类的构造函数QtFFmpegCamera::QtFFmpegCamera(QObject *parent)中对成员变量进行初始化,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp QtFFmpegCamera::QtFFmpegCamera(QObject *parent) : QObject(parent) { stopped = 0; pAVFormatCtx = NULL; pAVCodecContext = NULL; pSwsContext = NULL; pSwsContext2 = NULL; pAVFrame = NULL; } 在该类的成员函数QtFFmpegCamera::Play()中初始化FFmpeg和SDL2,使用FFmpeg打开本地摄像头并循环读取视频帧数据,然后调用SDL2进行渲染,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp void QtFFmpegCamera::Play(){ avformat_network_init();//初始化FFmpeg的网络库 pAVFormatCtx = avformat_alloc_context();//分配格式化上下文环境 pAVFormatCtx->probesize = 10000 * 1024; pAVFormatCtx->duration = 10 * AV_TIME_BASE; //1. 打开本地摄像头 OpenLocalCamera(pAVFormatCtx, true); printf("---------File Information:输出格式信息---------\n"); av_dump_format(pAVFormatCtx, 0, NULL, 0); //2.寻找视频流信息 if (avformat_find_stream_info(pAVFormatCtx, NULL) < 0) { printf("Couldn't find stream information.\n"); return ; } //打开视频以获取视频流,设置视频默认索引值 int videoindex = -1; for (int i = 0; i < pAVFormatCtx->nb_streams; i++) { if (pAVFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { videoStreamIndex = videoindex = i; //break; } } //如果没有找到视频的索引,则说明没有视频流 if (videoindex == -1) { printf("Didn't find a video stream.\n"); return ; } //3. 打开解码器 //AVCodecContext 为解码上下文结构体 //avcodec_alloc_context3 为解码分配函数 //avcodec_parameters_to_context 为参数格式转换 //avcodec_find_decoder(codec_ID) 用于查找解码器 //avcodec_open2 用于打开解码器 //分配解码器上下文 pAVCodecContext = avcodec_alloc_context3(NULL); //获取解码器上下文信息 if (avcodec_parameters_to_context(pAVCodecContext, pAVFormatCtx->streams[videoindex]->codecpar) < 0) { cout << "Copy stream failed!" << endl; return ; } //查找解码器//codec_id=13 //AV_CODEC_ID_RAWVIDEO: 13 //AV_CODEC_ID_H264: 27 printf("codec_id=%d\n", pAVCodecContext->codec_id); AVCodec *pCodec = avcodec_find_decoder(pAVCodecContext->codec_id); if (pCodec == NULL) { printf("Codec not found.\n"); return ; } //打开解码器 if (avcodec_open2(pAVCodecContext, pCodec, NULL) < 0) { printf("Could not open codec.\n"); return ; } //4. 格式转换 //(1)sdl: yuyv422--->yuv420p:SDL2渲染需要使用YUV420p格式 //(2)Qt : yuv420p--->rgb24:Qt渲染需要使用RGB24格式 //对图形进行裁剪以便于显示得更好 pSwsContext = sws_getContext( pAVCodecContext->width, pAVCodecContext->height, pAVCodecContext->pix_fmt,pAVCodecContext->width, pAVCodecContext->height, AV_PIX_FMT_YUV420P,SWS_BICUBIC, NULL, NULL, NULL); pSwsContext2 = sws_getContext( pAVCodecContext->width, pAVCodecContext->height, AV_PIX_FMT_YUV420P,pAVCodecContext->width, pAVCodecContext->height, AV_PIX_FMT_RGB24,SWS_BICUBIC, NULL, NULL, NULL); if (NULL == pSwsContext) { cout << "Get swscale context failed!" << endl; return ; } //获取视频流的分辨率大小 //pAVCodecContext = pAVFormatContext->streams[videoStreamIndex]->codec; videoWidth = pAVCodecContext->width; //视频宽度 videoHeight = pAVCodecContext->height;//视频高度 avpicture_alloc(&pAVPicture, AV_PIX_FMT_RGB24, videoWidth, videoHeight); //以视频格式及分辨率来分配内存 //5. SDL2.0:初始化SDL2的库 if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){ printf("Could not initialize SDL - %s\n", SDL_GetError()); return; } //SDL2的多窗口支持 int screen_w = pAVCodecContext->width; int screen_h = pAVCodecContext->height; SDL_Window* screen = SDL_CreateWindow("FFmpegPlayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h, SDL_WINDOW_OPENGL); if (!screen){ printf("SDL: could not create window - exiting:%s\n", SDL_GetError()); return ; } //创建渲染器 SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0); //创建纹理 //IYUV: Y + U + V (3 planes):3个平面 //YV12: Y + V + U (3 planes):3个平面 SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pAVCodecContext->width, pAVCodecContext->height); SDL_Rect sdlRect; sdlRect.x = 0; sdlRect.y = 0; sdlRect.w = screen_w; sdlRect.h = screen_h; //创建渲染刷新线程:thread.sdl SDL_Thread *video_tid = SDL_CreateThread(sfp_refresh_thread, NULL, NULL); //6. 使用FFmpeg解封装并解码 AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket)); AVFrame *pFrame = av_frame_alloc(); AVFrame *pFrameYUV420p = av_frame_alloc(); uint8_t *out_buffer = (uint8_t *)av_malloc(av_image_get_buffer_size( AV_PIX_FMT_YUV420p,pAVCodecContext->width,pAVCodecContext->height, 1)); av_image_fill_arrays( pFrameYUV420p->data, pFrameYUV420p->linesize, out_buffer,AV_PIX_FMT_YUV420P,pAVCodecContext->width, AVCodecContext->height, 1); //一帧一帧地读取视频:事件循环 SDL_Event event; for (;;) { if(this->stopped){ //检测是否需要停止 thread_exit = 1; break; } //等待事件:Wait SDL_WaitEvent(&event); if (event.type == SFM_REFRESH_EVENT) { //----------读取视频包-------------------- if (av_read_frame(pAVFormatCtx, packet) >= 0) //解封装 { if (packet->stream_index == videoindex) { //解码 : YUYV422--->YUV420p decode(pAVCodecContext, packet, pFrame, pFrameYUV420p, pSwsContext); //格式转换: YUV420p-->RGB24 mutex.lock(); sws_scale(pSwsContext2, (const uint8_t* const *)pFrameYUV420p->data, pFrameYUV420p->linesize, 0,videoHeight, pAVPicture.data, pAVPicture.linesize); //Qt发送获取一帧图像信号:注意下面两行代码使用的是Qt的渲染机制 //QImage image(pAVPicture.data[0],videoWidth,videoHeight,QImage::Format_RGB888); //emit GetImage(image); mutex.unlock(); //SDL2:渲染视频帧 SDL_UpdateTexture(sdlTexture, NULL, pFrameYUV420p->data[0], pFrameYUV420p->linesize[0]); SDL_RenderClear(sdlRenderer); SDL_RenderCopy(sdlRenderer, sdlTexture, &sdlRect, &sdlRect); SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, NULL); SDL_RenderPresent(sdlRenderer); } av_packet_unref(packet); } else { //退出线程 thread_exit = 1; } } else if (event.type == SDL_KEYDOWN) { qDebug() << "keywdown"; //暂停 if (event.key.keysym.sym == SDLK_SPACE) thread_pause = !thread_pause; else if (event.key.keysym.sym == SDLK_ESCAPE){ thread_exit = 1; qDebug() << SDLK_ESCAPE; } } else if (event.type == SDL_QUIT) { thread_exit = 1; } else if (event.type == SFM_BREAK_EVENT) { break; } } sws_freeContext(pSwsContext); SDL_Quit(); av_frame_free(&pFrameYUV420p); av_frame_free(&pFrame); avcodec_close(pAVCodecContext); avformat_close_input(&pAVFormatCtx); } 在qtffmpegcamera.cpp文件中有一个OpenLocalCamera()静态函数,它的功能是调用FFmpeg来打开本地摄像头,代码是通用的,可以兼容Windows和Linux平台,但传递的参数略有区别。该函数的完整代码如下: //chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp /*********************** * 打开本地摄像头 ***********************/ static int OpenLocalCamera(AVFormatContext *pFormatCtx, bool isUseDshow = false) { avdevice_register_all();//注册Libavdevice库 #ifdef _WIN32 if (isUseDshow) {//使用DShow方式打开摄像头 AVInputFormat *ifmt = av_find_input_format("dshow"); //设置视频设备的名称 //if (avformat_open_input(&pFormatCtx, "video=Lenovo EasyCamera", ifmt, NULL) != 0) if (avformat_open_input(&pFormatCtx, "video=HP TrueVision HD Camera", ifmt, NULL) != 0) { printf("Couldn't open input stream.(无法打开输入流)\n"); return -1; } } else {//使用VFW方式打开摄像头 AVInputFormat *ifmt = av_find_input_format("vfwcap"); if (avformat_open_input(&pFormatCtx, "0", ifmt, NULL) != 0) { printf("Couldn't open input stream.(无法打开输入流)\n"); return -1; } } #endif //Linux平台 #ifdef linux AVInputFormat *ifmt = av_find_input_format("video4linux2"); if (avformat_open_input(&pFormatCtx, "/dev/video0", ifmt, NULL) != 0) { printf("Couldn't open input stream.(无法打开输入流)\n"); return -1; } #endif return 0; } 注意: 在Windows平台下avformat_open_input(&pFormatCtx, "video=HP TrueVision HD Camera", ifmt, NULL)函数中的参数需要修改为读者本地的摄像头名称,例如笔者这里的摄像头名称为HP TrueVision HD Camera。 在qtffmpegcamera.cpp文件中有一个sfp_refresh_thread()静态函数,它的功能是手工创建SDL的刷新事件,以此来实时刷新视频帧,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp //Refresh Event #define SFM_REFRESH_EVENT (SDL_USEREVENT + 1) #define SFM_BREAK_EVENT (SDL_USEREVENT + 2) static int thread_exit = 0; static int thread_pause = 0; static int sfp_refresh_thread(void *opaque) { thread_exit = 0; thread_pause = 0; while (!thread_exit){ if (!thread_pause) {//判断是否暂停 //手工创建SDL刷新事件 SDL_Event event; event.type = SFM_REFRESH_EVENT; SDL_PushEvent(&event); } SDL_Delay(5); } thread_exit = 0; thread_pause = 0; //Break:退出事件 SDL_Event event; event.type = SFM_BREAK_EVENT; SDL_PushEvent(&event); return 0; } 2. 新增1个Qt的线程类来调用FFmpeg 因为读取视频帧需要使用while循环,从界面上单击Start Camera按钮之后,如果直接进入while循环,就会导致界面僵死,所以需要开启一个独立的线程来源源不断地读取视频帧并解码,然后将解码出来的YUV帧数据送给SDL2进行渲染。可以使用Qt的QThread类来封装一个线程类,头文件代码如下: //chapter3/FFmpegSDL2QtMonitor/qtcamerathread.h #ifndef QTCAMERATHREAD_H #define QTCAMERATHREAD_H #include <QThread> #include "qtffmpegcamera.h" class QtCameraThread : public QThread{ Q_OBJECT public: explicit QtCameraThread(QObject *parent = nullptr); void run(); void setffmpeg(QtFFmpegCamera *f){ffmpeg=f;} private: QtFFmpegCamera * ffmpeg; signals: public slots: }; #endif //QTCAMERATHREAD_H QtCameraThread类的基类是QThread,在该类中有一个QtFFmpegCamera类型的私有成员变量,用于操作FFmpeg,然后需要重写QThread的run()虚函数,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp #include "qtcamerathread.h" QtCameraThread::QtCameraThread(QObject *parent) : QThread(parent) { } void QtCameraThread::run() { ffmpeg->Play(); } 由此可见,在run()虚函数中主要调用QtFFmpegCamera类的Play()函数,先打开本地摄像头,然后循环读取视频帧并通过SDL2进行渲染。 3. 通过Qt的界面按钮开启或停止摄像头 在Start Camera按钮的Widget::on_pushButton_OpenCamera_clicked()槽函数中开启线程即可,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp void Widget::on_pushButton_OpenCamera_clicked(){ qDebug() << "clicked\n"; //修改按钮状态 ui->pushButton_stop->setEnabled(true); ui->pushButton_OpenCamera->setEnabled(false); objFmpg.SetStopped(0); //objFmpg.Play();//注意:不要直接调用FFmpeg封装类的Play()函数,否则会导致界面僵死 //通过线程方式来开启FFmpeg封装类的Play()函数 QtCameraThread *rtsp = new QtCameraThread(this); rtsp->setffmpeg(&objFmpg); rtsp->start(); } 在Stop Camera按钮的Widget::on_pushButton_stop_clicked()槽函数中修改播放状态即可,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp void Widget::on_pushButton_stop_clicked(){ objFmpg.SetStopped(1);//将播放状态设置为 Stopped即可 //修改按钮的状态 ui->pushButton_stop->setEnabled(false ); ui->pushButton_OpenCamera->setEnabled( true); } 编译并运行该程序,发现使用SDL2渲染时会弹出一个单独的窗口,如图326所示。 图326SDL2弹出的窗口 4. 将SDL2弹出的窗口嵌入Qt的QLabel中 可以将SDL2弹出的窗口嵌入Qt的QLabel中,将SDL_CreateWindow()函数替换为SDL_CreateWindowFrom()函数即可,代码如下: //chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp //SDL_Window* screen = SDL_CreateWindow("FFmpegPlayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h, SDL_WINDOW_OPENGL); //将SDL2窗口嵌入Qt子窗口的方法 SDL_Window* screen = SDL_CreateWindowFrom( m_MainWidget ); 重新编译并运行该程序,SDL2的窗口已经被嵌入Qt的QLabel标签中,如图327所示。 图327将SDL2弹出的窗口嵌入Qt窗口中 3.4FFmpeg+Qt读取并显示本地摄像头 信号与槽(Signal & Slot)是Qt编程的基础,也是Qt的一大创新。因为有了信号与槽的编程机制,在Qt中处理界面各个组件的交互操作时会变得更加直观和简单。信号槽是Qt框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,例如按钮检测到自己被单击了一下,它就会发出一个信号(Signal)。以这种方式发出信号类似广播。如果有对象对这个信号感兴趣,则可以使用连接(Connect)函数将想要处理的信号和自己的一个槽函数(Slot)绑定,以此来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。信号与槽机制是Qt GUI编程的基础,使用信号与槽机制可以比较容易地将信号与响应代码关联起来。 3.4.1信号 信号(Signal)就是在特定情况下被发射的事件,例如下压式按钮(PushButton)最常见的信号就是鼠标单击时发射的clicked()信号,而一个组合下拉列表(ComboBox)最常见的信号是选择的列表项变化时发射的CurrentIndexChanged()信号。GUI 程序设计的主要内容就是对界面上各组件的信号进行响应,只需知道什么情况下发射哪些信号,合理地去响应和处理这些信号就可以了。信号是一个特殊的成员函数声明,返回值的类型为void,只能声明而不能通过定义实现。信号必须用signals关键字声明,访问属性为protected,只能通过emit关键字调用(发射信号)。当某个信号对其客户或所有者发生的内部状态发生改变时,信号被一个对象发射。只有定义过这个信号的类及其派生类能够发射这个信号。当一个信号被发射时,与其相关联的槽将被立刻执行,就像一个正常的函数调用一样。信号槽机制完全独立于任何GUI事件循环。只有当所有的槽返回以后发射函数(emit)才返回。如果存在多个槽与某个信号相关联,则当这个信号被发射时,这些槽将会一个接一个地执行,但执行的顺序将会是随机的,不能人为地指定哪个先执行、哪个后执行。信号的声明是在头文件中进行的,Qt的signals关键字用于指出进入了信号声明区,随后即可声明自己的信号,代码如下: signals: void mycustomsignals(); signals是QT的关键字,而非C/C++的关键字。信号可以重载,但信号却没有函数体定义,并且信号的返回类型都是void,不要指望能从信号返回什么有用信息。信号由MOC自动产生,不应该在.cpp文件中实现。 3.4.2槽 槽(Slot)就是对信号响应的函数,即槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何部分(public、private 或 protected),可以具有任何参数,也可以被直接调用。槽函数与一般函数的不同点在于: 槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。槽也能够声明为虚函数。槽的声明也是在头文件中进行的,代码如下: public slots: void setValue(int value); 只有QObject的子类才能自定义槽,定义槽的类必须在类声明的最开始处使用Q_OBJECT,类中声明槽需要使用slots关键字,槽与所处理的信号在函数签名上必须一致。 3.4.3信号与槽的关联 信号与槽关联是用 QObject::connect() 函数实现的,其代码如下: //chapter3/qt-help-apis.txt //QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot())); bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection ); connect()函数是QObject 类的一个静态函数,而 QObject 是所有 Qt 类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为如下形式。 connect(sender, SIGNAL(signal()), receiver, SLOT(slot())); 其中,sender 是发射信号的对象的名称,signal() 是信号名称。信号可以看作特殊的函数,需要带圆括号,有参数时还需要指明参数。receiver 是接收信号的对象名称,slot() 是槽函数的名称,需要带圆括号,有参数时还需要指明参数。SIGNAL 和 SLOT 是Qt 的宏,用于指明信号和槽,并将它们的参数转换为相应的字符串。一段简单的代码如下: QObject::connect(btnClose, SIGNAL(clicked()), Widget, SLOT(close())); 这行代码的作用就是将 btnClose按钮的clicked()信号与窗体(Widget)的槽函数close()相关联,当单击btnClose按钮(界面上的Close按钮)时,就会执行Widget的close()槽函数。 当信号与槽没有必要继续保持关联时,可以使用disconnect函数来断开连接,代码如下: bool QObject::disconnect (const QObject * sender, const char * signal, const QObject * receiver, const char *method); disconnect()函数用于断开发射者中的信号与接收者中的槽函数之间的关联。在disconnect()函数中0可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数,但是发射者sender不能为0,其他3个参数的值可以等于0。以下3种情况需要使用disconnect()函数断开信号与槽的关联。 (1) 断开与某个对象相关联的任何对象,代码如下: disconnect(sender, 0, 0, 0); sender->disconnect(); (2) 断开与某个特定信号的任何关联,代码如下: disconnect(sender, SIGNAL(mySignal()), 0, 0); sender->disconnect(SIGNAL(mySignal())); (3) 断开两个对象之间的关联,代码如下: disconnect(sender, 0, receiver, 0); sender->disconnect(receiver); 3.4.4信号与槽的注意事项 Qt利用信号与槽(Signal/Slot)机制取代传统的回调函数机制(callback)进行对象之间的沟通。当操作事件发生时,对象会提交一个信号(Signal),而槽(Slot)则是一个函数接收特定信号并且运行槽本身设置的动作。信号与槽之间需要通过QObject的静态方法connect()函数连接。信号在任何运行点上皆可发射,甚至可以在槽里再发射另一个信号,信号与槽的链接不限定为一对一的链接,一个信号可以链接到多个槽或者多个信号链接到同一个槽,甚至信号也可连接到信号。以往的callback缺乏类型安全,在调用处理函数时,无法确定是传递正确型态的参数,但信号和其接收的槽之间传递的数据型态必须相匹配,否则编译器会发出警告。信号和槽可接收任何数量、任何形态的参数,所以信号与槽机制是完全类型安全。信号与槽机制也确保了低耦合性,发送信号的类并不知道可被哪个槽接收,也就是说一个信号可以调用所有可用的槽。此机制会确保当“连接”信号和槽时,槽会接收信号的参数并且正确运行。关于信号与槽的使用,需要注意以下规则。 (1) 一个信号可以连接多个槽,代码如下: connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(addFun(int)); connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(updateStatus(int)); 当一个对象spinNum的数值发生变化时,所在窗体有两个槽函数进行响应,一个addFun()函数用于计算,另一个updateStatus()函数用于更新状态。当一个信号与多个槽函数关联时,槽函数按照建立连接时的按顺序依次执行。当信号和槽函数带有参数时,在connect()函数里要写明参数的类型,但可以不写参数名称。 (2) 多个信号可以连接同一个槽,例如将3个选择颜色的RadioButton的clicked()信号关联到相同的一个自定义槽函数setTextFontColor(),代码如下: //chapter3/qt-help-apis.txt connect(ui->rBtnBlue,SIGNAL(clicked()),this,SLOT(setTextFontColor())); connect(ui->rBtnRed,SIGNAL(clicked()),this,SLOT(setTextFontColor())); connect(ui->rBtnBlack,SIGNAL(clicked()),this,SLOT(setTextFontColor())); 当任何一个 RadioButton 被单击时,都会执行 setTextFontColor() 槽函数。 (3) 一个信号可以连接另外一个信号,代码如下: connect(spinNum, SIGNAL(valueChanged(int)), this, SIGNAL (refreshInfo(int)); 当一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。 (4) 在严格的情况下,信号与槽的参数的个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,则会出现编译错误或运行错误。 (5) 在使用信号与槽的类中,必须在类的定义中加入宏 Q_OBJECT。 (6) 当一个信号被发射时,与其关联的槽函数通常会被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。 3.4.5元对象工具 元对象编译器(Meta Object Compiler,MOC)对C++文件中的类声明进行分析并生成用于初始化元对象的C++代码,元对象包含全部信号和槽的名字及指向槽函数的指针。 当MOC读C++源文件时,如果发现有Q_OBJECT宏声明的类,就会生成另外一个C++源文件,新生成的文件中包含该类的元对象代码。假设有一个头文件mysignal.h,在这个文件中包含信号或槽的声明,那么在编译之前MOC工具就会根据该文件自动生成一个名为mysignal.moc.h的C++源文件并将其提交给编译器; 对应的mysignal.cpp文件MOC工具将自动生成一个名为mysignal.moc.cpp的文件提交给编译器。 元对象代码是Signal/Slot机制所必需的。用MOC生成的C++源文件必须与类实现一起进行编译和连接,或者用#include语句将其包含到类的源文件中。MOC并不扩展#include或者#define宏定义,只是简单地跳过所遇到的任何预处理指令。 信号和槽函数的声明一般位于头文件中,同时在类声明的开始位置必须加上Q_OBJECT语句,Q_OBJECT语句将告诉编译器在编译之前必须先应用MOC工具进行扩展。关键字signals是对信号的声明,signals默认为protected等属性。关键字slots是对槽函数的声明,slots有public、private、protected等属性。signals、slots关键字是Qt 自己定义的,不是C++中的关键字。信号的声明类似于函数的声明而非变量的声明,左边要有类型,右边要有括号,如果要向槽中传递参数,则可在括号中指定每个形式参数的类型,而形式参数的个数可以多于一个。关键字slots指出随后开始槽的声明,这里slots用的也是复数形式。槽的声明与普通函数的声明一样,可以携带零或多个形式参数。既然信号的声明类似于普通C++函数的声明,那么信号也可采用C++中虚函数的形式进行声明,即同名但参数不同。例如,第1次定义的void mySignal()没有带参数,而第2次定义的却带有参数,从这里可以看出Qt的信号机制是非常灵活的。信号与槽之间的联系必须事先用connect()函数进行指定。如果要断开二者之间的联系,则可以使用disconnect()函数。 3.4.6案例: 标准信号槽 新建一个Qt Widgets Application项目(笔者的项目名称为MySignalSlotsDemo),基类选择QWidget,如图328所示,然后在构造函数中动态地创建一个按钮,实现单击按钮关闭窗口的功能。编译并运行该程序,效果如图329所示。本项目包含的代码如下: 注意: 该案例的完整工程代码可参考本书源码中的chapter3/MySignalSlotsDemo,建议读者先下载源码将工程运行起来,然后结合本书进行学习。 //chapter3/MySignalSlotsDemo/widget.h //widget.h头文件//// #ifndef WIDGET_H #define WIDGET_H #include <QWidget> namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = nullptr); ~Widget(); private: Ui::Widget *ui; }; #endif //WIDGET_H /////widget.cpp文件////// #include "widget.h" #include "ui_widget.h" #include <QPushButton> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); //创建一个按钮 QPushButton *btn = new QPushButton; //btn->show(); //以顶层方式弹出窗口控件 //让btn依赖在myWidget窗口中 btn->setParent(this);//this指当前窗口 btn->setText("关闭"); btn->move(100,100); //关联信号和槽:单击按钮关闭窗口 //参数1:信号发送者;参数2:发送的信号(函数地址) //参数3:信号接收者;参数4:处理槽函数地址 connect(btn, &QPushButton::clicked, this, &QWidget::close); } Widget::~Widget() { delete ui; } 图328Qt Widgets项目的基类选择 图329Qt信号槽的运行效果 3.4.7案例: 自定义信号槽 当Qt提供的标准信号和槽函数无法满足需求时,就需要用到自定义信号槽,可以使用emit关键字来发射信号。例如定义老师和学生两个类(都继承自QObject),当老师发出“下课”信号时,学生响应“去吃饭”的槽功能。由于“下课”不是Qt标准的信号,所以需要用到自定义信号槽机制。这里不再创建新的Qt项目,直接使用3.4.6节的MySignalSlotsDemo项目,先添加两个自定义类Teacher和Student,它们都继承自QObject。右击项目名称MySignalSlotsDemo,在弹出的菜单中选择 Add New... 菜单选项,如图330所示,然后在弹出的“新建文件”对话框中,单击左侧的C++模板,在右侧选择 C++ Class,如图331所示,接着在弹出的C++ Class对话框中,输入Class name(Teacher),在Base class下拉列表中选择QObject,如图332所示,再以同样的步骤创建Student类,成功后,项目中多了两个类(Teacher和Student),如图333所示。 图330Qt项目中通过Add New添加新项 图331Qt项目中选择C++ Class 图332Qt项目中添加新类并选择QObject基类 图333Qt项目中添加了两个类 在Teacher类中添加一个“下课”信号(finishClass),代码如下: //chapter3/MySignalSlotsDemo/student.h signals: //自定义信号,写到signals下 //返回值为void,只用申明,不需要实现 //可以有参数,也可以重载 void finishClass(); 在Student类中添加一个“去吃饭”槽(gotoEat),代码如下: //chapter3/MySignalSlotsDemo/student.h public slots: //早期Qt版本需要写到public slots下,高级版本可以写到public或全局下 //返回值为void,需要声明,也需要实现 //可以有参数,也可以重载 void gotoEat(); //sutdent.cpp void Student::gotoEat() { qDebug() << "准备去吃饭......"; } 在Widget类中声明老师类(Student)和学生类(Student)的成员变量,并在构造函数中通过new创建实例,然后通过connect()函数来关联老师类的“下课”信号和学生类的“去吃饭”槽,代码如下: //chapter3/MySignalSlotsDemo/widget.h private: Teacher *m_teacher; Student *m_student; //widget.cpp Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) { ... //创建老师对象 this->m_teacher = new Teacher(this); //创建学生对象 this->m_student = new Student(this); //连接老师的"下课"信号和学生的"去吃饭"槽函数 connect(m_teacher,&Teacher::finishClass, m_student,&Student::gotoEat); } 在界面上拖曳一个按钮,将文本内容修改为“下课”,用来模拟老师的下课信号,然后双击这个按钮,在Qt自动生成的Widget::on_pushButton_clicked()函数中添加的代码如下: //chapter3/MySignalSlotsDemo/widget.cpp void Widget::on_pushButton_clicked() { //通过emit发射信号 emit this->m_teacher->finishClass(); } 编译并运行该程序,单击“下课”按钮后会在控制台输出“准备去吃饭……”,证明学生类的槽函数被成功地触发了,如图334所示。在本案例中老师类和学生类的相关代码如下(其余代码读者可参考源码工程): //chapter3/MySignalSlotsDemo/teacher.h ////teacher.h//// #ifndef TEACHER_H #define TEACHER_H #include <QObject> class Teacher : public QObject { Q_OBJECT public: explicit Teacher(QObject *parent = nullptr); signals: //自定义信号,写到signals下 //返回值为void,只用声明,不需要实现 //可以有参数,也可以重载 void finishClass(); public slots: }; #endif //TEACHER_H ////teacher.cpp//// #include "teacher.h" Teacher::Teacher(QObject *parent) : QObject(parent) { } ////student.h//// #ifndef STUDENT_H #define STUDENT_H #include <QObject> class Student : public QObject { Q_OBJECT public: explicit Student(QObject *parent = nullptr); signals: public slots: //早期Qt版本需要写到public slots下,高级版本可以写到public或全局下 //返回值为void,需要声明,也需要实现 //可以有参数,也可以重载 void gotoEat(); }; #endif //STUDENT_H ////student.cpp//// #include "student.h" #include <QDebug> Student::Student(QObject *parent) : QObject(parent) { } void Student::gotoEat() { qDebug() << "准备去吃饭......"; } 图334Qt项目中自定义信号槽的应用 3.4.8Qt显示图像 Qt可显示基本的图像类型,利用QImage、QPxmap类可以实现图像的显示,并且利用类中的方法可以实现图像的基本操作(缩放、旋转等)。Qt可以直接读取并显示的格式有BMP、GIF、JPG、JPEG、PNG、TIFF、PBM、PGM、PPM、XBM和XPM等。可以使用QLabel显示图像,QLabel类有setPixmap()函数,可以用来显示图像。也可以直接用QPainter画出图像。如果图像过大,则可直接用QLabel显示,此时将会出现有部分图像显示不出来,可以用Scroll Area部件解决此问题。 首先使用QFileDialog类的静态函数getOpenFileName()打开一张图像,将图像文件加载进QImage对象中,再用QPixmap对象获得图像,最后用QLabel选择一个QPixmap图像对象进行显示。该过程的关键代码如下(完整代码可参考chapter3/QtImageDemo工程): //chapter3/QtImageDemo/widget.cpp //Qt显示图片 void Widget::on_btnShowImage_clicked() { QString filename; filename = QFileDialog::getOpenFileName(this, tr("选择图像"),"",tr("Images (*.png *.bmp *.jpg *.tif *.gif )")); if(filename.isEmpty()){ return; } else{ m_img = new QImage; if(! ( m_img->load(filename) ) ) //加载图像 { QMessageBox::information(this, tr("打开图像失败"),tr("打开图像失败!")); delete m_img; return; } ui->lblImage->setPixmap(QPixmap::fromImage(*m_img)); } } QImage对图像的像素级访问进行了优化,QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage则使用了独立于硬件的绘制系统。编译并运行该工程,单击“显示图像”按钮,选择一张本地的图片,如图335所示。 图335Qt使用QImage和QPixmap显示图像 3.4.9Qt缩放图像 Qt缩放图像可以用scaled函数实现,代码如下: //chapter3/qt-help-apis.txt QImage QImage::scaled (const QSize & size,Qt::AspectRatioMode aspectRatioMode = Qt::IgnoreAspectRatio, Qt::TransformationModetransformMode = Qt::FastTransformation) const; 利用上面已经加载成功的图像(m_img),在scaled函数中width和height表示缩放后图像的宽和高,即将原图像缩放到(width×height)大小。例如在本案例中显示的图像原始宽和高为(200×200),缩放后修改为(100×100),编译并运行,如图336所示,代码如下: //chapter3/QtImageDemo/widget.cpp void Widget::on_btnScale_clicked(){ QImage* imgScaled = new QImage; *imgScaled = m_img->scaled(100,100, Qt::KeepAspectRatio); ui->lblScale->setPixmap(QPixmap::fromImage(*imgScaled)); } 图336Qt缩放图像 3.4.10Qt旋转图像 Qt旋转图像可以用QMatrix类的rotate函数实现,代码如下: //chapter3/QtImageDemo/widget.cpp void Widget::on_btnRotate_clicked(){ QImage* imgRotate = new QImage; QMatrix matrix; matrix.rotate(270); *imgRotate = m_img->transformed(matrix); ui->lblRotate->setPixmap(QPixmap::fromImage(*imgRotate)); } 编译并运行该项目,依次单击“显示图像”“缩放”和“旋转”按钮,效果如图337所示。 图337Qt显示、缩放和旋转图像