热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

视频提取图片/图片合成视频ffmpeg(二十)

前言需求场景1(视频中提取照片):各大网站在线播放视频时,鼠标滑到某一时刻能够提前显示那一时刻的画面。短的视频编辑APP中,为了更好的对

前言


  • 需求场景1(视频中提取照片):
    各大网站在线播放视频时,鼠标滑到某一时刻能够提前显示那一时刻的画面。短的视频编辑APP中,为了更好的对视频进行编辑,会提取出视频各个时刻的画面进行预览,那么这些是如何实现的呢?本文将给出基于ffmpeg的实现代码以及实现思路。
  • 需求场景2(照片合成视频):
    摄影师经常不间断的拍摄一组连续的画面用于合成延时视频,剪印APP中也有时光相册这样通过照片生成视频的功能(不过剪印APP照片合成的视频采用了插值算法生成了额外的过度动画照片以及特效,功能更加复杂,但是不管怎样,最终还是会由照片合成视频)。本文基于ffmpeg实现简单的照片合成视频思路以及详细代码

实现思路分析

这里照片以JPG为例,视频以MP4为例,其它格式类似


  • 视频中提取照片:

1、fmpeg对将像素数据写入到JPG图片中也封装到了avformat_xxx系列接口中,它的使用流程和封装视频数据到mp4文件一模一样,只不过一个JPG文件中只包含了一帧视频数据而已;
2、ffmpeg对JPG文件的封装支持模式匹配,即如果想要将多张图片写入到多张jpg中只需要文件名包含百分号即可,例如 name%3d.jpg,那么在每一次调用av_write_frame()函数写入视频数据到jpg图片时都会生成一张jpg图片。这样做的好处是不需要每一张要写入的jpg文件都创建一个AVFormatContext与之对应。其它流程和写入一张jpg一样。


流程为:
1、先从MP4中提取指定时刻AVPacket解码成AVFrame
2、然后将步骤1得到的AVFrame进行从素格式YUV420P到JPG需要的YUVJ420P像素格式的转换
3、再重新编码,然后再封装到jpg中


  • 照片合成视频:
    因为JPG的编码方式为AV_CODEC_ID_MJPEG,MP4如果采用h264编码,那么两者的编码方式是不一致的,所以就需要先解码再编码,具体流程为:
    1、先将JPG解码成AVFrame
    2、将JPG解码后的源像素格式YUVJ420P转换成x264编码需要的YUV420P像素格式
    3、再重新编码,然后再封装到mp4中

流程图


  • 视频中提取照片流程图

     

    #include "cppcommon/CLog.h"
    #include
    #include
    #include
    #include
    #include
    #include
    }
    using namespace std;class VideoJPG
    {
    public:VideoJPG();~VideoJPG();/** 功能:实现提取任意时刻视频的某一帧并将其转化为JPG图片输出到当前目录*/void doJpgGet();/** 功能:将多张JPG照片合并成一段视频*/void doJpgToVideo();
    private:AVFrame *de_frame;AVFrame *en_frame;// 用于视频像素转换SwsContext *sws_ctx;// 用于读取视频AVFormatContext *in_fmt;// 用于解码AVCodecContext *de_ctx;// 用于编码AVCodecContext *en_ctx;// 用于封装jpgAVFormatContext *ou_fmt;int video_ou_index;void releaseSources();void doDecode(AVPacket *in_pkt);void doEncode(AVFrame *en_frame);};

    备注:
    视频提取图片的实现位于doJpgGet();函数,图片合成视频位于doJpgToVideo();函数

    实现文件

    #include "VideoJpg.hpp"VideoJPG::VideoJPG()
    {sws_ctx = NULL;de_frame = NULL;en_frame = NULL;in_fmt = NULL;ou_fmt = NULL;de_ctx = NULL;en_ctx = NULL;
    }VideoJPG::~VideoJPG()
    {}void VideoJPG::releaseSources()
    {if (in_fmt) {avformat_close_input(&in_fmt);in_fmt = NULL;}if (ou_fmt) {avformat_free_context(ou_fmt);ou_fmt = NULL;}if (en_frame) {av_frame_unref(en_frame);en_frame = NULL;}if (de_frame) {av_frame_unref(de_frame);de_frame = NULL;}if (en_ctx) {avcodec_free_context(&en_ctx);en_ctx = NULL;}if (de_ctx) {avcodec_free_context(&de_ctx);de_ctx = NULL;}
    }/** 写入jpg说明:* 1、ffmpeg对将像素数据写入到JPG图片中也封装到了avformat_xxx系列接口中,它的使用流程和封装视频数据到mp4文件一模一样* 只不过一个JPG文件中只包含了一帧视频数据而已;* 2、ffmpeg对JPG文件的封装支持模式匹配,即如果想要将多张图片写入到多张jpg中只需要文件名包含百分号即可,例如 name%3d.jpg,那么在每一次调用av_write_frame()* 函数写入视频数据到jpg图片时都会生成一张jpg图片。这样做的好处是不需要每一张要写入的jpg文件都创建一个AVFormatContext与之对应。其它流程和写入一张jpg一样,具体* 参考如下示例:* 3、jpg对应的封装器为ff_image2_muxer,对应的编码器为ff_mjpeg_encoder*/
    #define Get_More 1 // 1代表使用模式匹配,一次可以写入多张jpg图片。0代表一次写入1张图片
    void VideoJPG::doJpgGet()
    {string curFile(__FILE__);unsigned long pos = curFile.find("2-video_audio_advanced");if (pos == string::npos) {LOGD("not find file");return;}string srcDic = curFile.substr(0,pos) + "filesources/";string srcPath = srcDic + "test_1280x720_3.mp4";
    #if Get_Morestring dstPath = srcDic + "1-doJpg_get%3d.jpg";int num = 5;
    #elsestring dstPath = srcDic + "1-doJpg_get.jpg";
    #endifint video_index &#61; -1;// 要截取的时刻string start &#61; "00:00:05";int64_t start_pts &#61; stoi(start.substr(0,2));start_pts &#43;&#61; stoi(start.substr(3,2));start_pts &#43;&#61; stoi(start.substr(6,2));if (avformat_open_input(&in_fmt,srcPath.c_str(),NULL,NULL) <0) {LOGD("avformat_open_input fail");return;}if (avformat_find_stream_info(in_fmt, NULL) <0) {LOGD("avformat_find_stream_info fail");return;}// 遍历出视频索引for (int i &#61; 0; inb_streams; i&#43;&#43;) {AVStream *stream &#61; in_fmt->streams[i];if (stream->codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) { // 说明是视频video_index &#61; i;// 初始化解码器用于解码AVCodec *codec &#61; avcodec_find_decoder(stream->codecpar->codec_id);de_ctx &#61; avcodec_alloc_context3(codec);if (!de_ctx) {LOGD("video avcodec_alloc_context3 fail");releaseSources();return;}// 设置解码参数&#xff0c;这里直接从源视频流中拷贝if (avcodec_parameters_to_context(de_ctx, stream->codecpar) <0) {LOGD("video avcodec_parameters_to_context");releaseSources();return;}// 初始化解码器上下文if (avcodec_open2(de_ctx, codec, NULL) <0) {LOGD("video avcodec_open2() fail");releaseSources();return;}break;}}// 初始化编码器;因为最终是要写入到JPEG&#xff0c;所以使用的编码器ID为AV_CODEC_ID_MJPEGAVCodec *codec &#61; avcodec_find_encoder(AV_CODEC_ID_MJPEG);en_ctx &#61; avcodec_alloc_context3(codec);if (!en_ctx) {LOGD("avcodec_alloc_context3 fail");releaseSources();return;}// 设置编码参数AVStream *in_stream &#61; in_fmt->streams[video_index];en_ctx->width &#61; in_stream->codecpar->width;en_ctx->height &#61; in_stream->codecpar->height;// 如果是编码后写入到图片中&#xff0c;那么比特率可以不用设置&#xff0c;不影响最终的结果(也不会影响图像清晰度)en_ctx->bit_rate &#61; in_stream->codecpar->bit_rate;// 如果是编码后写入到图片中&#xff0c;那么帧率可以不用设置&#xff0c;不影响最终的结果en_ctx->framerate &#61; in_stream->r_frame_rate;en_ctx->time_base &#61; in_stream->time_base;// 对于MJPEG编码器来说&#xff0c;它支持的是YUVJ420P/YUVJ422P/YUVJ444P格式的像素en_ctx->pix_fmt &#61; AV_PIX_FMT_YUVJ420P;// 初始化编码器上下文if (avcodec_open2(en_ctx, codec, NULL) <0) {LOGD("avcodec_open2() fail");releaseSources();return;}// 创建用于输出JPG的封装器if (avformat_alloc_output_context2(&ou_fmt, NULL, NULL, dstPath.c_str()) <0) {LOGD("avformat_alloc_output_context2");releaseSources();return;}/** 添加流* 对于图片封装器来说&#xff0c;也可以把它想象成只有一帧视频的视频封装器。所以它实际上也需要一路视频流&#xff0c;而事实上图片的流是视频流类型*/AVStream *stream &#61; avformat_new_stream(ou_fmt, NULL);// 设置流参数&#xff1b;直接从编码器拷贝参数即可if (avcodec_parameters_from_context(stream->codecpar, en_ctx) <0) {LOGD("avcodec_parameters_from_context");releaseSources();return;}/** 初始化上下文* 对于写入JPG来说&#xff0c;它是不需要建立输出上下文IO缓冲区的的&#xff0c;所以avio_open2()没有调用到&#xff0c;但是最终一样可以调用av_write_frame()写入数据*/if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) <0) {LOGD("avio_open2 fail");releaseSources();return;}}/** 为输出文件写入头信息* 不管是封装音视频文件还是图片文件&#xff0c;都需要调用此方法进行相关的初始化&#xff0c;否则av_write_frame()函数会崩溃*/if (avformat_write_header(ou_fmt, NULL) <0) {LOGD("avformat_write_header() fail");releaseSources();return;}/** 创建视频像素转换上下文* 因为源视频的像素格式是yuv420p的&#xff0c;而jpg编码需要的像素格式是yuvj420p的&#xff0c;所以需要先进行像素格式转换*/sws_ctx &#61; sws_getContext(in_stream->codecpar->width, in_stream->codecpar->height, (enum AVPixelFormat)in_stream->codecpar->format,en_ctx->width, en_ctx->height, en_ctx->pix_fmt,0, NULL, NULL, NULL);if (!sws_ctx) {LOGD("sws_getContext fail");releaseSources();return;}// 创建编码解码用的AVFramede_frame &#61; av_frame_alloc();en_frame &#61; av_frame_alloc();en_frame->width &#61; en_ctx->width;en_frame->height &#61; en_ctx->height;en_frame->format &#61; en_ctx->pix_fmt;if (av_frame_get_buffer(en_frame, 0) <0) {LOGD("av_frame_get_buffer fail");releaseSources();return;}if (av_frame_make_writable(en_frame) <0) {LOGD("av_frame_make_writeable fail");releaseSources();return;}AVPacket *in_pkt &#61; av_packet_alloc();AVPacket *ou_pkt &#61; av_packet_alloc();AVRational time_base &#61; in_fmt->streams[video_index]->time_base;AVRational frame_rate &#61; in_fmt->streams[video_index]->r_frame_rate;// 一帧的时间戳int64_t delt &#61; time_base.den/frame_rate.num;start_pts *&#61; time_base.den;/** 因为想要截取的时间处的AVPacket并不一定是I帧&#xff0c;所以想要正确的解码&#xff0c;得先找到离想要截取的时间处往前的最近的I帧* 开始解码&#xff0c;直到拿到了想要获取的时间处的AVFrame* AVSEEK_FLAG_BACKWARD 代表如果start_pts指定的时间戳处的AVPacket非I帧&#xff0c;那么就往前移动指针&#xff0c;直到找到I帧&#xff0c;那么* 当首次调用av_frame_read()函数时返回的AVPacket将为此I帧的AVPacket*/if (av_seek_frame(in_fmt, video_index, start_pts, AVSEEK_FLAG_BACKWARD) <0) {LOGD("av_seek_frame fail");releaseSources();return;}bool found &#61; false;while (av_read_frame(in_fmt, in_pkt) &#61;&#61; 0) {if (in_pkt->stream_index !&#61; video_index) {continue;}// 先解码avcodec_send_packet(de_ctx, in_pkt);LOGD("video pts %d(%s)",in_pkt->pts,av_ts2timestr(in_pkt->pts,&in_stream->time_base));while (true) {int ret &#61; avcodec_receive_frame(de_ctx, de_frame);if (ret <0) {break;}/** 解码得到的AVFrame中的pts和解码前的AVPacket中的pts是一一对应的&#xff0c;所以可以利用AVFrame中的pts来判断此帧是否在想要截取的时间范围内*/LOGD("sucess pts %d",de_frame->pts);// 成功解码出来了
    #if Get_More// 取多帧视频并写入到文件static int i&#61;0;delt &#61; delt*num;if (abs(de_frame->pts - start_pts) #else// 去一帧帧视频并写入到文件if (abs(de_frame->pts - start_pts) #endifLOGD("找到了这一帧");// 因为源视频帧的格式和目标视频帧的格式可能不一致&#xff0c;所以这里需要转码ret &#61; sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize);if (ret <0) {LOGD("sws_scale fail");releaseSources();return;}#if Get_More// 重新编码en_frame->pts &#61; i;avcodec_send_frame(en_ctx, en_frame);// 拿到指定数目的AVPacket后再清空缓冲区if (i > num) {avcodec_send_frame(en_ctx, NULL);}
    #else// 重新编码;因为只有一帧&#xff0c;所以这里直接写1 即可en_frame->pts &#61; 1;avcodec_send_frame(en_ctx, en_frame);// 因为只编码一帧&#xff0c;所以发送一帧视频后立马清空缓冲区avcodec_send_frame(en_ctx, NULL);
    #endifret &#61; avcodec_receive_packet(en_ctx, ou_pkt);if (ret <0) {LOGD("fail ");releaseSources();return;}// 写入文件if(av_write_frame(ou_fmt, ou_pkt) <0) {LOGD("av_write_frame fail");releaseSources();return;}
    #if Get_Moreif (i>num) {found &#61; true;}#elsefound &#61; true;
    #endifbreak;}}av_packet_unref(in_pkt);if (found) {LOGD("has get jpg");break;}}/** 写入文件尾* 对于写入视频文件来说&#xff0c;此函数必须调用&#xff0c;但是对于写入JPG文件来说&#xff0c;不调用此函数也没关系&#xff1b;*/
    // av_write_trailer(ou_fmt);// 释放资源releaseSources();
    }/** 多张图片合成为一段视频说明&#xff1a;* 1、ffmpeg对jpg的解封装和对视频的解封装一样&#xff0c;都封装到了avformat_xxxx系列接口里面。流程和解封装音视频的流程一模一样,对一张jpg图片的解封装可以理解为对只包含一帧* 视频的视频文件的解封装* 2、ffmpeg对jpeg的解封装支持模式匹配&#xff0c;例如对于name%3d.jpg进行解封装时(假如目录中包含的jpg列表为* name001.jpg,name002.jpg,name004.jpg,........)&#xff0c;每次调用av_frame_read()函数&#xff0c;它将按照name001.jpg,name002.jpg,name004.jpg,........的顺序依次进行读取* 3、jpg对应的解封装器为ff_image2_demuxer&#xff0c;对应的编码器为ff_mjpeg_decoder*//** 将前面视频生成的jpg合成mp4文件* 因为JPG的编码方式为AV_CODEC_ID_MJPEG&#xff0c;MP4如果采用h264编码&#xff0c;那么两者的编码方式是不一致的&#xff0c;所以就需要先解码再编码&#xff0c;具体流程为&#xff1a;* 1、先将JPG解码成AVFrame* 2、将JPG解码后的源像素格式YUVJ420P转换成x264编码需要的YUV420P像素格式* 3、再重新编码&#xff0c;然后再封装到mp4中*/
    void VideoJPG::doJpgToVideo()
    {string curFile(__FILE__);unsigned long pos &#61; curFile.find("2-video_audio_advanced");if (pos &#61;&#61; string::npos) {LOGD("not find file");return;}string srcDic &#61; curFile.substr(0,pos) &#43; "filesources/";string srcPath &#61; srcDic &#43; "1-doJpg_get%3d.jpg";string dstPath &#61; srcDic &#43; "1-dojpgToVideo.mp4";int video_index &#61; -1;// 创建jpg的解封装上下文if (avformat_open_input(&in_fmt, srcPath.c_str(), NULL, NULL) <0) {LOGD("avformat_open_input fail");return;}if (avformat_find_stream_info(in_fmt, NULL) <0) {LOGD("avformat_find_stream_info()");releaseSources();return;}// 创建解码器及初始化解码器上下文用于对jpg进行解码for (int i&#61;0; inb_streams; i&#43;&#43;) {AVStream *stream &#61; in_fmt->streams[i];/** 对于jpg图片来说&#xff0c;它里面就是一路视频流&#xff0c;所以媒体类型就是AVMEDIA_TYPE_VIDEO*/if (stream->codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) {AVCodec *codec &#61; avcodec_find_decoder(stream->codecpar->codec_id);if (!codec) {LOGD("not find jpg codec");releaseSources();return;}de_ctx &#61; avcodec_alloc_context3(codec);if (!de_ctx) {LOGD("jpg codec_ctx fail");releaseSources();return;}// 设置解码参数;文件解封装的AVStream中就包括了解码参数&#xff0c;这里直接流中拷贝即可if (avcodec_parameters_to_context(de_ctx, stream->codecpar) <0) {LOGD("set jpg de_ctx parameters fail");releaseSources();return;}// 初始化解码器及解码器上下文if (avcodec_open2(de_ctx, codec, NULL) <0) {LOGD("avcodec_open2() fail");releaseSources();return;}video_index &#61; i;break;}}// 创建mp4文件封装器if (avformat_alloc_output_context2(&ou_fmt,NULL,NULL,dstPath.c_str()) <0) {LOGD("MP2 muxer fail");releaseSources();return;}// 添加视频流AVStream *stream &#61; avformat_new_stream(ou_fmt, NULL);video_ou_index &#61; stream->index;// 创建h264的编码器及编码器上下文AVCodec *en_codec &#61; avcodec_find_encoder(AV_CODEC_ID_H264);if (!en_codec) {LOGD("encodec fail");releaseSources();return;}en_ctx &#61; avcodec_alloc_context3(en_codec);if (!en_ctx) {LOGD("en_codec ctx fail");releaseSources();return;}// 设置编码参数AVStream *in_stream &#61; in_fmt->streams[video_index];en_ctx->width &#61; in_stream->codecpar->width;en_ctx->height &#61; in_stream->codecpar->height;en_ctx->pix_fmt &#61; (enum AVPixelFormat)in_stream->codecpar->format;en_ctx->bit_rate &#61; 0.96*1000000;en_ctx->framerate &#61; (AVRational){5,1};en_ctx->time_base &#61; (AVRational){1,5};// 某些封装格式必须要设置&#xff0c;否则会造成封装后文件中信息的缺失if (ou_fmt->oformat->flags & AVFMT_GLOBALHEADER) {en_ctx->flags |&#61; AV_CODEC_FLAG_GLOBAL_HEADER;}// x264编码特有if (en_codec->id &#61;&#61; AV_CODEC_ID_H264) {// 代表了编码的速度级别av_opt_set(en_ctx->priv_data,"preset","slow",0);en_ctx->flags2 &#61; AV_CODEC_FLAG2_LOCAL_HEADER;}// 初始化编码器及编码器上下文if (avcodec_open2(en_ctx,en_codec,NULL) <0) {LOGD("encodec ctx fail");releaseSources();return;}// 设置视频流参数;对于封装来说&#xff0c;直接从编码器上下文拷贝即可if (avcodec_parameters_from_context(stream->codecpar, en_ctx) <0) {LOGD("copy en_code parameters fail");releaseSources();return;}// 初始化封装器输出缓冲区if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) <0) {LOGD("avio_open2 fail");releaseSources();return;}}// 创建像素格式转换器sws_ctx &#61; sws_getContext(de_ctx->width, de_ctx->height, de_ctx->pix_fmt,en_ctx->width, en_ctx->height, en_ctx->pix_fmt,0, NULL, NULL, NULL);if (!sws_ctx) {LOGD("sws_getContext fail");releaseSources();return;}// 写入封装器头文件信息&#xff1b;此函数内部会对封装器参数做进一步初始化if (avformat_write_header(ou_fmt, NULL) <0) {LOGD("avformat_write_header fail");releaseSources();return;}// 创建编解码用的AVFramede_frame &#61; av_frame_alloc();en_frame &#61; av_frame_alloc();en_frame->width &#61; en_ctx->width;en_frame->height &#61; en_ctx->height;en_frame->format &#61; en_ctx->pix_fmt;av_frame_get_buffer(en_frame, 0);av_frame_make_writable(en_frame);AVPacket *in_pkt &#61; av_packet_alloc();while (av_read_frame(in_fmt, in_pkt) &#61;&#61; 0) {if (in_pkt->stream_index !&#61; video_index) {continue;}// 先解码doDecode(in_pkt);av_packet_unref(in_pkt);}// 刷新解码缓冲区doDecode(NULL);av_write_trailer(ou_fmt);LOGD("结束。。。");// 释放资源releaseSources();
    }void VideoJPG::doDecode(AVPacket *in_pkt)
    {static int num_pts &#61; 0;// 先解码avcodec_send_packet(de_ctx, in_pkt);while (true) {int ret &#61; avcodec_receive_frame(de_ctx, de_frame);if (ret &#61;&#61; AVERROR_EOF) {doEncode(NULL);break;} else if(ret <0) {break;}// 成功解码了&#xff1b;先进行格式转换然后再编码if(sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize) <0) {LOGD("sws_scale fail");releaseSources();return;}// 编码前要设置好pts的值&#xff0c;如果en_ctx->time_base为{1,fps}&#xff0c;那么这里pts的值即为帧的个数值en_frame->pts &#61; num_pts&#43;&#43;;doEncode(en_frame);}}void VideoJPG::doEncode(AVFrame *en_frame1)
    {avcodec_send_frame(en_ctx, en_frame1);while (true) {AVPacket *ou_pkt &#61; av_packet_alloc();if (avcodec_receive_packet(en_ctx, ou_pkt) <0) {av_packet_unref(ou_pkt);break;}// 成功编码了;写入之前要进行时间基的转换AVStream *stream &#61; ou_fmt->streams[video_ou_index];av_packet_rescale_ts(ou_pkt, en_ctx->time_base, stream->time_base);LOGD("video pts %d(%s)",ou_pkt->pts,av_ts2timestr(ou_pkt->pts, &stream->time_base));av_write_frame(ou_fmt, ou_pkt);}
    }

     


    项目地址

    https://github.com/nldzsz/ffmpeg-demo

    位于cppsrc目录下
    VideoJpg.hpp/VideoJpg.cpp文件

    项目下示例可运行于iOS/android/mac平台&#xff0c;工程分别位于demo-ios/demo-android/demo-mac三个目录下&#xff0c;可根据需要选择不同平台


推荐阅读
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文讨论了如何使用GStreamer来删除H264格式视频文件中的中间部分,而不需要进行重编码。作者提出了使用gst_element_seek(...)函数来实现这个目标的思路,并提到遇到了一个解决不了的BUG。文章还列举了8个解决方案,希望能够得到更好的思路。 ... [详细]
  • 用pandas库修改excel文件里的内容,并把excel文件格式存为csv格式,再将csv格式改为html格式
    假设有Excel文件data.xlsx,其中内容为:     ID age height    sex weight张三  1  39    181 female     85李四  2  40    180   male     80王五  3  38    178 female     78赵六  4  59    1 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • qt学习(六)数据库注册用户的实现方法
    本文介绍了在qt学习中实现数据库注册用户的方法,包括登录按钮按下后出现注册页面、账号可用性判断、密码格式判断、邮箱格式判断等步骤。具体实现过程包括UI设计、数据库的创建和各个模块调用数据内容。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • Gitlab接入公司内部单点登录的安装和配置教程
    本文介绍了如何将公司内部的Gitlab系统接入单点登录服务,并提供了安装和配置的详细教程。通过使用oauth2协议,将原有的各子系统的独立登录统一迁移至单点登录。文章包括Gitlab的安装环境、版本号、编辑配置文件的步骤,并解决了在迁移过程中可能遇到的问题。 ... [详细]
  • 颜色迁移(reinhard VS welsh)
    不要谈什么天分,运气,你需要的是一个截稿日,以及一个不交稿就能打爆你狗头的人,然后你就会被自己的才华吓到。------ ... [详细]
  • OAuth2.0指南
    引言OAuth2.0是一种应用之间彼此访问数据的开源授权协议。比如,一个游戏应用可以访问Facebook的用户数据,或者一个基于地理的应用可以访问Foursquare的用户数据等。 ... [详细]
  • 刘连响:为什么看好小程序音视频在教育行业的应用?
    作者简介:刘连响,一起玩耍科技创始人。2013年起开始研究WebRTC,对音视频处理、直播、实时音视频相关技术非常感兴趣,具 ... [详细]
  • 一.avcodec_find_decoder获取解码器。在使用之前必须保证所用到的解码器已经注册,最简单的就是调用avcodec_register_all()函数,就像之前注册 ... [详细]
author-avatar
elgin2010
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有