Qt-FFmpeg开发-视频播放【软解码】

更多精彩内容
个人内容分类汇总
音视频开发

1、概述

介四里沒有挽过的船新版本,挤需感受三番钟,里造会干我一样,爱象节个版本

  • 最近研究了一下FFmpeg开发,功能实在是太强大了,网上ffmpeg3、4的文章还是很多的,但是学习嘛,最新的还是不能放过,就选了一个最新的ffmpeg n5.1.2版本,和3、4版本api变化还是挺大的;
  • 在这个Demo里主要使用Qt + FFmpeg开发一个简单的视频播放器,这里使用的是软解码,硬解码后续再讲;
  • 同时为了尽可能的简单,这里没有进行音频解码和播放,只是单独的进行视频解码播放;
  • 视频显示没有上来就OpenGL、SDL、D3D,这对于初学者不太友好,所以这里使用了QPainter进行绘制,所以CPU占用还是挺高的,后面换成OpenGL就好了;

开发环境说明

  • 系统:Windows10、Ubuntu20.04
  • Qt版本:V5.12.5
  • 编译器:MSVC2017-64、GCC/G++64
  • FFmpeg版本:n5.1.2

2、实现效果

  1. 使用ffmpeg音视频库【软解码】实现的视频播放器;
  2. 支持打开本地视频文件(如mp4、mov、avi等)、网络视频流(rtsp、rtmp、http等);
  3. 支持视频【匀速播放】;
  4. 采用QPainter进行显示,支持【自适应】窗口缩放;
  5. 视频播放支持实时【开始/关闭、暂停/继续】播放;
  6. 视频解码、线程控制、显示各部分功能分离,【低耦合度】。
  7. 采用最新的【5.1.2版本】ffmpeg库进行开发,【超详细注释信息】,将所有踩过的坑、解决办法、注意事项都得很写清楚。

3、FFmpeg软解码流程

4、主要代码

  • 啥也不说了,直接上代码,一切有注释

  • videodecode.h文件

    1. /******************************************************************************
    2. * @文件名 videodecode.h
    3. * @功能 视频解码类,在这个类中调用ffmpeg打开视频进行解码
    4. *
    5. * @开发者 mhf
    6. * @邮箱 1603291350@qq.com
    7. * @时间 2022/09/15
    8. * @备注
    9. *****************************************************************************/
    10. #ifndef VIDEODECODE_H
    11. #define VIDEODECODE_H
    12. #include <QString>
    13. #include <QSize>
    14. struct AVFormatContext;
    15. struct AVCodecContext;
    16. struct AVRational;
    17. struct AVPacket;
    18. struct AVFrame;
    19. struct SwsContext;
    20. struct AVBufferRef;
    21. class QImage;
    22. class VideoDecode
    23. {
    24. public:
    25. VideoDecode();
    26. ~VideoDecode();
    27. bool open(const QString& url = QString()); // 打开媒体文件,或者流媒体rtmp、strp、http
    28. QImage read(); // 读取视频图像
    29. void close(); // 关闭
    30. bool isEnd(); // 是否读取完成
    31. const qint64& pts(); // 获取当前帧显示时间
    32. private:
    33. void initFFmpeg(); // 初始化ffmpeg库(整个程序中只需加载一次)
    34. void showError(int err); // 显示ffmpeg执行错误时的错误信息
    35. qreal rationalToDouble(AVRational* rational); // 将AVRational转换为double
    36. void clear(); // 清空读取缓冲
    37. void free(); // 释放
    38. private:
    39. AVFormatContext* m_formatContext = nullptr; // 解封装上下文
    40. AVCodecContext* m_codecContext = nullptr; // 解码器上下文
    41. SwsContext* m_swsContext = nullptr; // 图像转换上下文
    42. AVPacket* m_packet = nullptr; // 数据包
    43. AVFrame* m_frame = nullptr; // 解码后的视频帧
    44. int m_videoIndex = 0; // 视频流索引
    45. qint64 m_totalTime = 0; // 视频总时长
    46. qint64 m_totalFrames = 0; // 视频总帧数
    47. qint64 m_obtainFrames = 0; // 视频当前获取到的帧数
    48. qint64 m_pts = 0; // 图像帧的显示时间
    49. qreal m_frameRate = 0; // 视频帧率
    50. QSize m_size; // 视频分辨率大小
    51. char* m_error = nullptr; // 保存异常信息
    52. bool m_end = false; // 视频读取完成
    53. uchar* m_buffer = nullptr; // YUV图像需要转换位RGBA图像,这里保存转换后的图形数据
    54. };
    55. #endif // VIDEODECODE_H
  • videodecode.cpp文件

    1. #include "videodecode.h"
    2. #include <QDebug>
    3. #include <QImage>
    4. #include <QMutex>
    5. #include <qdatetime.h>
    6. extern "C" { // 用C规则编译指定的代码
    7. #include "libavcodec/avcodec.h"
    8. #include "libavformat/avformat.h"
    9. #include "libavutil/avutil.h"
    10. #include "libswscale/swscale.h"
    11. #include "libavutil/imgutils.h"
    12. }
    13. #define ERROR_LEN 1024 // 异常信息数组长度
    14. #define PRINT_LOG 1
    15. VideoDecode::VideoDecode()
    16. {
    17. // initFFmpeg(); // 5.1.2版本不需要调用了
    18. m_error = new char[ERROR_LEN];
    19. }
    20. VideoDecode::~VideoDecode()
    21. {
    22. close();
    23. }
    24. /**
    25. * @brief 初始化ffmpeg库(整个程序中只需加载一次)
    26. * 旧版本的ffmpeg需要注册各种文件格式、解复用器、对网络库进行全局初始化。
    27. * 在新版本的ffmpeg中纷纷弃用了,不需要注册了
    28. */
    29. void VideoDecode::initFFmpeg()
    30. {
    31. static bool isFirst = true;
    32. static QMutex mutex;
    33. QMutexLocker locker(&mutex);
    34. if(isFirst)
    35. {
    36. // av_register_all(); // 已经从源码中删除
    37. /**
    38. * 初始化网络库,用于打开网络流媒体,此函数仅用于解决旧GnuTLS或OpenSSL库的线程安全问题。
    39. * 一旦删除对旧GnuTLS和OpenSSL库的支持,此函数将被弃用,并且此函数不再有任何用途。
    40. */
    41. avformat_network_init();
    42. isFirst = false;
    43. }
    44. }
    45. /**
    46. * @brief 打开媒体文件,或者流媒体,例如rtmp、strp、http
    47. * @param url 视频地址
    48. * @return true:成功 false:失败
    49. */
    50. bool VideoDecode::open(const QString &url)
    51. {
    52. if(url.isNull()) return false;
    53. AVDictionary* dict = nullptr;
    54. av_dict_set(&dict, "rtsp_transport", "tcp", 0); // 设置rtsp流使用tcp打开,如果打开失败错误信息为【Error number -135 occurred】可以切换(UDP、tcp、udp_multicast、http),比如vlc推流就需要使用udp打开
    55. av_dict_set(&dict, "max_delay", "3", 0); // 设置最大复用或解复用延迟(以微秒为单位)。当通过【UDP】 接收数据时,解复用器尝试重新排序接收到的数据包(因为它们可能无序到达,或者数据包可能完全丢失)。这可以通过将最大解复用延迟设置为零(通过max_delayAVFormatContext 字段)来禁用。
    56. av_dict_set(&dict, "timeout", "1000000", 0); // 以微秒为单位设置套接字 TCP I/O 超时,如果等待时间过短,也可能会还没连接就返回了。
    57. // 打开输入流并返回解封装上下文
    58. int ret = avformat_open_input(&m_formatContext, // 返回解封装上下文
    59. url.toStdString().data(), // 打开视频地址
    60. nullptr, // 如果非null,此参数强制使用特定的输入格式。自动选择解封装器(文件格式)
    61. &dict); // 参数设置
    62. // 释放参数字典
    63. if(dict)
    64. {
    65. av_dict_free(&dict);
    66. }
    67. // 打开视频失败
    68. if(ret < 0)
    69. {
    70. showError(ret);
    71. free();
    72. return false;
    73. }
    74. // 读取媒体文件的数据包以获取流信息。
    75. ret = avformat_find_stream_info(m_formatContext, nullptr);
    76. if(ret < 0)
    77. {
    78. showError(ret);
    79. free();
    80. return false;
    81. }
    82. m_totalTime = m_formatContext->duration / (AV_TIME_BASE / 1000); // 计算视频总时长(毫秒)
    83. #if PRINT_LOG
    84. qDebug() << QString("视频总时长:%1 ms,[%2]").arg(m_totalTime).arg(QTime::fromMSecsSinceStartOfDay(int(m_totalTime)).toString("HH:mm:ss zzz"));
    85. #endif
    86. // 通过AVMediaType枚举查询视频流ID(也可以通过遍历查找),最后一个参数无用
    87. m_videoIndex = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    88. if(m_videoIndex < 0)
    89. {
    90. showError(m_videoIndex);
    91. free();
    92. return false;
    93. }
    94. AVStream* videoStream = m_formatContext->streams[m_videoIndex]; // 通过查询到的索引获取视频流
    95. // 获取视频图像分辨率(AVStream中的AVCodecContext在新版本中弃用,改为使用AVCodecParameters)
    96. m_size.setWidth(videoStream->codecpar->width);
    97. m_size.setHeight(videoStream->codecpar->height);
    98. m_frameRate = rationalToDouble(&videoStream->avg_frame_rate); // 视频帧率
    99. // 通过解码器ID获取视频解码器(新版本返回值必须使用const)
    100. const AVCodec* codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
    101. m_totalFrames = videoStream->nb_frames;
    102. #if PRINT_LOG
    103. qDebug() << QString("分辨率:[w:%1,h:%2] 帧率:%3 总帧数:%4 解码器:%5")
    104. .arg(m_size.width()).arg(m_size.height()).arg(m_frameRate).arg(m_totalFrames).arg(codec->name);
    105. #endif
    106. // 分配AVCodecContext并将其字段设置为默认值。
    107. m_codecContext = avcodec_alloc_context3(codec);
    108. if(!m_codecContext)
    109. {
    110. #if PRINT_LOG
    111. qWarning() << "创建视频解码器上下文失败!";
    112. #endif
    113. free();
    114. return false;
    115. }
    116. // 使用视频流的codecpar为解码器上下文赋值
    117. ret = avcodec_parameters_to_context(m_codecContext, videoStream->codecpar);
    118. if(ret < 0)
    119. {
    120. showError(ret);
    121. free();
    122. return false;
    123. }
    124. m_codecContext->flags2 |= AV_CODEC_FLAG2_FAST; // 允许不符合规范的加速技巧。
    125. m_codecContext->thread_count = 8; // 使用8线程解码
    126. // 初始化解码器上下文,如果之前avcodec_alloc_context3传入了解码器,这里设置NULL就可以
    127. ret = avcodec_open2(m_codecContext, nullptr, nullptr);
    128. if(ret < 0)
    129. {
    130. showError(ret);
    131. free();
    132. return false;
    133. }
    134. // 分配AVPacket并将其字段设置为默认值。
    135. m_packet = av_packet_alloc();
    136. if(!m_packet)
    137. {
    138. #if PRINT_LOG
    139. qWarning() << "av_packet_alloc() Error!";
    140. #endif
    141. free();
    142. return false;
    143. }
    144. // 分配AVFrame并将其字段设置为默认值。
    145. m_frame = av_frame_alloc();
    146. if(!m_frame)
    147. {
    148. #if PRINT_LOG
    149. qWarning() << "av_frame_alloc() Error!";
    150. #endif
    151. free();
    152. return false;
    153. }
    154. // 分配图像空间
    155. int size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_size.width(), m_size.height(), 4);
    156. /**
    157. * 【注意:】这里可以多分配一些,否则如果只是安装size分配,大部分视频图像数据拷贝没有问题,
    158. * 但是少部分视频图像在使用sws_scale()拷贝时会超出数组长度,在使用使用msvc debug模式时delete[] m_buffer会报错(HEAP CORRUPTION DETECTED: after Normal block(#32215) at 0x000001AC442830370.CRT delected that the application wrote to memory after end of heap buffer)
    159. * 特别是这个视频流http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4
    160. */
    161. m_buffer = new uchar[size + 1000]; // 这里多分配1000个字节就基本不会出现拷贝超出的情况了,反正不缺这点内存
    162. // m_image = new QImage(m_buffer, m_size.width(), m_size.height(), QImage::Format_RGBA8888); // 这种方式分配内存大部分情况下也可以,但是因为存在拷贝超出数组的情况,delete时也会报错
    163. m_end = false;
    164. return true;
    165. }
    166. /**
    167. * @brief
    168. * @return
    169. */
    170. QImage VideoDecode::read()
    171. {
    172. // 如果没有打开则返回
    173. if(!m_formatContext)
    174. {
    175. return QImage();
    176. }
    177. // 读取下一帧数据
    178. int readRet = av_read_frame(m_formatContext, m_packet);
    179. if(readRet < 0)
    180. {
    181. avcodec_send_packet(m_codecContext, m_packet); // 读取完成后向解码器中传如空AVPacket,否则无法读取出最后几帧
    182. }
    183. else
    184. {
    185. if(m_packet->stream_index == m_videoIndex) // 如果是图像数据则进行解码
    186. {
    187. // 计算当前帧时间(毫秒)
    188. #if 1 // 方法一:适用于所有场景,但是存在一定误差
    189. m_packet->pts = qRound64(m_packet->pts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
    190. m_packet->dts = qRound64(m_packet->dts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
    191. #else // 方法二:适用于播放本地视频文件,计算每一帧时间较准,但是由于网络视频流无法获取总帧数,所以无法适用
    192. m_obtainFrames++;
    193. m_packet->pts = qRound64(m_obtainFrames * (qreal(m_totalTime) / m_totalFrames));
    194. #endif
    195. // 将读取到的原始数据包传入解码器
    196. int ret = avcodec_send_packet(m_codecContext, m_packet);
    197. if(ret < 0)
    198. {
    199. showError(ret);
    200. }
    201. }
    202. }
    203. av_packet_unref(m_packet); // 释放数据包,引用计数-1,为0时释放空间
    204. int ret = avcodec_receive_frame(m_codecContext, m_frame);
    205. if(ret < 0)
    206. {
    207. av_frame_unref(m_frame);
    208. if(readRet < 0)
    209. {
    210. m_end = true; // 当无法读取到AVPacket并且解码器中也没有数据时表示读取完成
    211. }
    212. return QImage();
    213. }
    214. m_pts = m_frame->pts;
    215. // 为什么图像转换上下文要放在这里初始化呢,是因为m_frame->format,如果使用硬件解码,解码出来的图像格式和m_codecContext->pix_fmt的图像格式不一样,就会导致无法转换为QImage
    216. if(!m_swsContext)
    217. {
    218. // 获取缓存的图像转换上下文。首先校验参数是否一致,如果校验不通过就释放资源;然后判断上下文是否存在,如果存在直接复用,如不存在进行分配、初始化操作
    219. m_swsContext = sws_getCachedContext(m_swsContext,
    220. m_frame->width, // 输入图像的宽度
    221. m_frame->height, // 输入图像的高度
    222. (AVPixelFormat)m_frame->format, // 输入图像的像素格式
    223. m_size.width(), // 输出图像的宽度
    224. m_size.height(), // 输出图像的高度
    225. AV_PIX_FMT_RGBA, // 输出图像的像素格式
    226. SWS_BILINEAR, // 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR
    227. nullptr, // 输入图像的滤波器信息, 若不需要传NULL
    228. nullptr, // 输出图像的滤波器信息, 若不需要传NULL
    229. nullptr); // 特定缩放算法需要的参数(?),默认为NULL
    230. if(!m_swsContext)
    231. {
    232. #if PRINT_LOG
    233. qWarning() << "sws_getCachedContext() Error!";
    234. #endif
    235. free();
    236. return QImage();
    237. }
    238. }
    239. // AVFrame转QImage
    240. uchar* data[] = {m_buffer};
    241. int lines[4];
    242. av_image_fill_linesizes(lines, AV_PIX_FMT_RGBA, m_frame->width); // 使用像素格式pix_fmt和宽度填充图像的平面线条大小。
    243. ret = sws_scale(m_swsContext, // 缩放上下文
    244. m_frame->data, // 原图像数组
    245. m_frame->linesize, // 包含源图像每个平面步幅的数组
    246. 0, // 开始位置
    247. m_frame->height, // 行数
    248. data, // 目标图像数组
    249. lines); // 包含目标图像每个平面的步幅的数组
    250. QImage image(m_buffer, m_frame->width, m_frame->height, QImage::Format_RGBA8888);
    251. av_frame_unref(m_frame);
    252. return image;
    253. }
    254. /**
    255. * @brief 关闭视频播放并释放内存
    256. */
    257. void VideoDecode::close()
    258. {
    259. clear();
    260. free();
    261. m_totalTime = 0;
    262. m_videoIndex = 0;
    263. m_totalFrames = 0;
    264. m_obtainFrames = 0;
    265. m_pts = 0;
    266. m_frameRate = 0;
    267. m_size = QSize(0, 0);
    268. }
    269. /**
    270. * @brief 视频是否读取完成
    271. * @return
    272. */
    273. bool VideoDecode::isEnd()
    274. {
    275. return m_end;
    276. }
    277. /**
    278. * @brief 返回当前帧图像播放时间
    279. * @return
    280. */
    281. const qint64 &VideoDecode::pts()
    282. {
    283. return m_pts;
    284. }
    285. /**
    286. * @brief 显示ffmpeg函数调用异常信息
    287. * @param err
    288. */
    289. void VideoDecode::showError(int err)
    290. {
    291. #if PRINT_LOG
    292. memset(m_error, 0, ERROR_LEN); // 将数组置零
    293. av_strerror(err, m_error, ERROR_LEN);
    294. qWarning() << "DecodeVideo Error:" << m_error;
    295. #else
    296. Q_UNUSED(err)
    297. #endif
    298. }
    299. /**
    300. * @brief 将AVRational转换为double,用于计算帧率
    301. * @param rational
    302. * @return
    303. */
    304. qreal VideoDecode::rationalToDouble(AVRational* rational)
    305. {
    306. qreal frameRate = (rational->den == 0) ? 0 : (qreal(rational->num) / rational->den);
    307. return frameRate;
    308. }
    309. /**
    310. * @brief 清空读取缓冲
    311. */
    312. void VideoDecode::clear()
    313. {
    314. // 因为avformat_flush不会刷新AVIOContext (s->pb)。如果有必要,在调用此函数之前调用avio_flush(s->pb)。
    315. if(m_formatContext && m_formatContext->pb)
    316. {
    317. avio_flush(m_formatContext->pb);
    318. }
    319. if(m_formatContext)
    320. {
    321. avformat_flush(m_formatContext); // 清理读取缓冲
    322. }
    323. }
    324. void VideoDecode::free()
    325. {
    326. // 释放上下文swsContext。
    327. if(m_swsContext)
    328. {
    329. sws_freeContext(m_swsContext);
    330. m_swsContext = nullptr; // sws_freeContext不会把上下文置NULL
    331. }
    332. // 释放编解码器上下文和与之相关的所有内容,并将NULL写入提供的指针
    333. if(m_codecContext)
    334. {
    335. avcodec_free_context(&m_codecContext);
    336. }
    337. // 关闭并失败m_formatContext,并将指针置为null
    338. if(m_formatContext)
    339. {
    340. avformat_close_input(&m_formatContext);
    341. }
    342. if(m_packet)
    343. {
    344. av_packet_free(&m_packet);
    345. }
    346. if(m_frame)
    347. {
    348. av_frame_free(&m_frame);
    349. }
    350. if(m_buffer)
    351. {
    352. delete [] m_buffer;
    353. m_buffer = nullptr;
    354. }
    355. }

6、完整源代码

Qt-FFmpeg开发-视频播放【软解码】(1)的更多相关文章

  1. QT+FFMPEG实现视频播放

    开发环境:MinGW+QT5.9+FFMPEG20190212 一.开发环境搭建 FFMPEG的开发环境部署比如容易,在官网下载库文件,然后在QT里面指定路径,把相关dll文件放到exe目录下就可以了 ...

  2. Qt+FFmpeg 简单实现视频播放

    这里使用 Qt + FFmpeg 实现了一个简单播放视频的例子.先看下按下按钮播放视频时的效果图: 完整工程下载链接:Github-FFmpeg_demo 注意:一定要将 bin 目录下的 dll 文 ...

  3. 海思3519 qt ffmpeg 软解码播放avi

    在海思3519上基于qt采用ffmpeg对avi进行解码显示,其中ffmpeg的配置,qt的配置在前文中已经说明,在此不再赘述. 解码 解码在单独的线程中进行,具体的代码如下: void VideoP ...

  4. FFmpeg开发笔记(四):ffmpeg解码的基本流程详解

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  5. FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  6. FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放

    前言   ffmpeg播放rtsp网络流和摄像头流.   Demo   使用ffmpeg播放局域网rtsp1080p海康摄像头:延迟0.2s,存在马赛克     使用ffmpeg播放网络rtsp文件流 ...

  7. FFmpeg开发笔记(十):ffmpeg在ubuntu上的交叉编译移植到海思HI35xx平台

    FFmpeg和SDL开发专栏(点击传送门) 上一篇:<FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放>下一篇:敬请期待   前言   将ffmpeg移植到海思H ...

  8. [转]FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

  9. [总结]FFMPEG视音频编解码零基础学习方法--转

    ffmpeg编解码学习   目录(?)[-] ffmpeg程序的使用ffmpegexeffplayexeffprobeexe 1 ffmpegexe 2 ffplayexe 3 ffprobeexe ...

  10. FFMPEG视音频编解码零基础学习方法

    在CSDN上的这一段日子,接触到了很多同行业的人,尤其是使用FFMPEG进行视音频编解码的人,有的已经是有多年经验的“大神”,有的是刚开始学习的初学者.在和大家探讨的过程中,我忽然发现了一个问题:在“ ...

随机推荐

  1. HMS Core 6.8.0版本发布公告

    分析服务 ◆ 游戏行业新增"区服分析"埋点模板及分析报告,支持开发者分服务器查看用户付费.留存等指标,可进一步评估不同服务器的玩家质量: ◆ 新增营销活动报告,可查看广告任务带来的 ...

  2. day36-ThreadLocal

    ThreadLocal 线程数据共享和安全 1.什么是ThreadLocal? ThreadLocal的作用,可以实现在同一个线程数据共享,从而解决多线程数据安全问题 当http请求发送到Tomcat ...

  3. Spring之后置处理器

      Spring的后置处理器是Spring对外开发的重要扩展点,允许我们接入Bean的实例化流程中,以达到动态注册BeanDefinition.动态修改BeanDefinition.动态修改Bean的 ...

  4. docker 第一课

    centos安装docker yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo ...

  5. 彻底弄懂Javascript模块导入导出

    笔者开始学习Javascript的时候,对模块不太懂,不知道怎么导入模块,导出模块,就胡乱一通试 比如 import xx from 'test.js' 不起作用,就加个括号 import {xx} ...

  6. [OpenCV实战]29 使用OpenCV实现红眼自动去除

    目录 1 红眼消除 1.1 眼部检测 1.2 红眼遮掩 1.3 清除瞳孔掩模空洞 1.4 红眼修复 2 结果与完整代码 2.1 结果 2.2 代码 3 参考 在本教程中,我们将学习如何完全自动地从照片 ...

  7. [python] 基于matplotlib_venn实现维恩图的绘制

    文章目录 VENN DIAGRAM(维恩图) 1. 具有2个分组的基本的维恩图 Venn diagram with 2 groups 2. 具有3个组的基本维恩图 Venn diagram with ...

  8. Android applink 踩坑指南

    Android applink 踩坑指南 原理 接入步骤 将链接与activity关联起来 加入meta data 生成身份验证JSON 真机测试 结论 官方文档 原理 与url scheme不同的地 ...

  9. ResourceQuota与LimitRange区别

    ResourceQuota与LimitRange区别 ResourceQuota ResourceQuota 用来限制 namespace 中所有的 Pod 占用的总的资源 request 和 lim ...

  10. 01-逻辑仿真工具VCS使用

    1 逻辑仿真工具VCS的使用 在书写完成RTL代码之后,确保自己书写的代码和自己想要的逻辑是一致的. VCS是synopsys公司下的的仿真工具. 1 VCS仿真过程 编译-->仿真--> ...