FFmpeg 将多张图片编码成视频

news/2025/2/25 4:09:43

前言

本篇文章的需求是将相机获取到的图片进行编码,编码成一个视频,耗费了大约一个星期的时间在解决各种问题。这里阐述一下这篇文章所要解决的几个问题:

1、如何将多张图片编码成视频。

2、如何进行定时录制视频。

3、同时开启多线程进行视频录制。

4、对录制文件目录进行管理:每次都检测录制目录大小是否超过指定大小,如果超过,则删除指定大小的时间最早的一些文件。

正文

一、准备工作

1、下载FFmpeg的开发版

1、下载链接: https://ffmpeg.org/download.html

2、

image-20221227133638327

3、

image-20221227133702769

4、由于我是在Win10下,所以选择:

image-20221227133802854

2、使用环境

Win10 + Qt8.0.2(MSVC2019) + FFmpeg 4.4

二、整体流程解析

image-20230120161749141

上面的流程图,基本上就是这次这个项目的整体流程了,上面的 2 3 4点都是在单例层完成的。只有第一点是在FFmpegRecord那一层完成的。

三、单例模式——多线程视频录制

首先,确定目标,我是要“同时录制多个视频”,所以,必须得开多个线程,并且,在这一层,就必须要完成:

1、给上层应用的调用提供一个接口。

2、进行定时关闭录制的操作。

3、对底层FFmpegRecord对象进行管理。

4、对录制文件进行管理——在开启录制的时候,检测当前目录文件的大小。

先给出开录制与关录制中整体的代码,然后再慢慢进行解释吧.

//开启录制的接口
QString CRecordMgr::StartRecord(eRecordType eType, QString sFileName, int iRecordSTime)
{
    QMutexLocker oLocker(&m_mutex);
    QString sPath = "/myPath/recordfile";
    quint64 iCountByte = _DetectDiskInfo(sPath);//检测某文件路径下的所有文件字节数

    //当这个字节数大于某个指定的值得时候
    if (iCountByte > DetectMinMB*1024*1024)
    {
        _RemoveRecordFile(sPath);//移除时间最早的500MB的文件
        QThread::msleep(1000);
    }

    if (sFileName.isEmpty())
    {
        qint64 timestamp = QDateTime::currentDateTime().toSecsSinceEpoch();
        sFileName = QString("%1.mp4").arg(timestamp);
    }

    
    _PreActionStart();//开启要传图过来的相机
    QString sMissionId = CCommonFunc::CreateUUID();
    CFFmpegRecord *pFFmpegRecord = new CFFmpegRecord();//创建对象

    if (pFFmpegRecord)
    {
        pFFmpegRecord->setObjectName(sMissionId);//设置对象名词

        m_mapFFmpegRecord.insert(sMissionId, pFFmpegRecord);
        m_mapRecordFileName.insert(sMissionId, sFileName);
        m_mapRecordType.insert(sMissionId, eType);

        pFFmpegRecord->SetMissionId(sMissionId);
        pFFmpegRecord->SetRecordType((CFFmpegRecord::eRecordType)eType);
        pFFmpegRecord->SetRecordFileName(sFileName);
        pFFmpegRecord->SetRecordTime(iRecordSTime);
        pFFmpegRecord->Init();

        //为这个对象创建一个定时器   
        m_pRecordTimer = new QTimer(this);
        m_pRecordTimer->setSingleShot(true);
        m_pRecordTimer->setObjectName(sMissionId);
        connect(m_pRecordTimer, &QTimer::timeout, this, &CRecordMgr::SLOT_StopRecord);//超时了就调用对应的槽函数
        m_mapTimer.insert(sMissionId, m_pRecordTimer);

        QtConcurrent::run([this,iRecordSTime](){
            if (m_pRecordTimer)
            {
                QMetaObject::invokeMethod(m_pRecordTimer, "start", Qt::QueuedConnection,
                                          Q_ARG(int, iRecordSTime*1000));
            }
        });

        //将外部传入的信号传入编码的代码中
        m_oConnect = connect(gVideoMgr::instance(), &CVideoMgr::SIGNAL_CommonCameraImage,pFFmpegRecord, &CFFmpegRecord::SLOT_FFmpegImage, Qt::QueuedConnection);
        m_mapConnect.insert(sMissionId, m_oConnect);
    }

    return sMissionId;
}

//使用任务Id对录制任务进行关闭

bool CRecordMgr::StopRecord(QString sId)
{
CFFmpegRecord *pFFmpegRecord = m_mapFFmpegRecord.value(sId);

if (pFFmpegRecord)
{
    pFFmpegRecord->StopRecord();//对底下的录制任务进行关闭
    QThread::msleep(500);
    QMetaObject::Connection oConnect = m_mapConnect.value(sId);
    disconnect(oConnect);

    CRecordMgr::eRecordType eRecordType = m_mapRecordType.value(sId);
    QString sFileName = m_mapRecordFileName.value(sId);
    QTimer *pTimer = m_mapTimer.value(sId);

    if (pTimer && pTimer->isActive())
        pTimer->stop();

    if (m_mapFFmpegRecord.size() == 0)
    {
        _PreActionEnd();//当没有任务录制任务的时候,关闭相机
    }

    pFFmpegRecord->deleteLater();//最好使用deletelater() 直接删除,可能此时某些缓存的图片还没传输结束
    pFFmpegRecord = nullptr;
    emit SIGNAL_RecordTask_Finished(eRecordType, sId, sFileName);//给外部提供录制结束的接口

    m_mapFFmpegRecord.remove(sId);
    m_mapRecordType.remove(sId);
    m_mapRecordFileName.remove(sId);
    m_mapTimer.remove(sId);
}

return true;

}

1、给上层应用一个开启录制与关闭录制的接口

基本上,就是直接调用就可以了,由于我这边是单例,所以我直接调用接口就可以了,如果你们没有用单例,就看你们怎么设计了。

gRecordMgr::instance->StartRecord(eAlarmRecord, "");
gRecordMgr::instance->StopRecord();

2、定时录制的功能

定时录制,最开始是在最底层去操作的,也就是在FFmpegRecord层去操作的,结果至少花费了我一天多的时间在找出问题的所在,明明定时器的设置,定时器的触发都很简单,但就是没办法触发。后面才发觉,原来是最底层的FFmpegRecord还需要去接收外部的图片,基本资源都一直被占用着,所以,根本轮不到定时器的触发,这就很糟糕,所以,最后觉得在RecordMgr层去做定时器的操作,才得以完成。 使用了两种方式,可以使用QTimer, 也可以使用timeEvent,startTimer;其实可能使用startTimer来触发,是更合适的,因为是要有多个定时器的。

方法一

 m_pRecordTimer = new QTimer(this);
        m_pRecordTimer->setSingleShot(true);
        m_pRecordTimer->setObjectName(sMissionId);
        connect(m_pRecordTimer, &QTimer::timeout, this, &CRecordMgr::SLOT_StopRecord);//超时了就调用对应的槽函数
        m_mapTimer.insert(sMissionId, m_pRecordTimer);

        QtConcurrent::run([this,iRecordSTime](){
            if (m_pRecordTimer)
            {
                QMetaObject::invokeMethod(m_pRecordTimer, "start", Qt::QueuedConnection,
                                          Q_ARG(int, iRecordSTime*1000));
            }
        });

方法二

int iTimerId = startTimer(m_pRecordTimer);

void timeEvent(QTimeEvent *event);

3、对录制对象进行管理

CFFmpegRecord *pFFmpegRecord = new CFFmpegRecord();//创建对象

    if (pFFmpegRecord)
    {
        pFFmpegRecord->setObjectName(sMissionId);//设置对象名称
        m_mapFFmpegRecord.insert(sMissionId, pFFmpegRecord);//将这个对象,使用QMap进行存储
    }

4、对录制文件进行管理

调用方式:

QString sPath = "/myPath/recordfile";
    quint64 iCountByte = _DetectDiskInfo(sPath);//检测某文件路径下的所有文件字节数

    //当这个字节数大于某个指定的值得时候
    if (iCountByte > DetectMinMB*1024*1024)
    {
        _RemoveRecordFile(sPath);//移除时间最早的500MB的文件
        QThread::msleep(1000);
    }

具体的函数:

quint64 CRecordMgr::_DetectDiskInfo(QString _sPath)
{
    QDir dir(_sPath);
    quint64 size = 0;

    foreach(QFileInfo fileInfo, dir.entryInfoList(QDir::Files))
    {
        //计算文件大小
        size += fileInfo.size();
    }

    foreach(QString subDir, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
    {
        //若存在子目录,则递归调用dirFileSize()函数
        size += _DetectDiskInfo(_sPath + QDir::separator() + subDir);
    }

    return size;
}

void CRecordMgr::_RemoveRecordFile(QString _sPath)
{
    QDir dir(_sPath);

    if (!dir.exists())
    {
        return;
    }

    QStringList lstFileName =_GetFilePath(_sPath);

    QList<QString> listPath;

    for (auto index : lstFileName)
    {
        listPath.push_back(index);
    }

    qSort(listPath.begin(), listPath.end(),[](const QString &str1, const QString &str2){
        QStringList lst1 = str1.split("/");
        QStringList lst2 = str2.split("/");
        QString sComStr1 = lst1.last();
        QString sComStr2 = lst2.last();

        if (sComStr1.compare(sComStr2)==0)
            return sComStr1 < sComStr2;

        return sComStr1 < sComStr2;
    });

    lstFileName.clear();

    for (auto index : listPath)
    {
        lstFileName.append(index);
    }

    int iFileCount = lstFileName.size();

    qint64 iDeleteSize = 0;
    const qint64 ciSize = 500 * 1024 * 1024; // 每次删除超过500M即停止删除

    for (int i = 0; i < iFileCount; i++)
    {
        QString sFileName = lstFileName.at(i);
        QFileInfo oFileInfo(sFileName);

        iDeleteSize += oFileInfo.size();
        dir.remove(sFileName);
        lstFileName.pop_back();

        if (iDeleteSize > ciSize)
        {
            break;
        }
    }
}

QStringList CRecordMgr::_GetFilePath(QString _sPath)
{
    QStringList listFileInfo;
    QDir dir(_sPath);

    if (!dir.exists())
    {
        return listFileInfo;
    }

    //获取filePath下所有文件夹和文件
    dir.setFilter(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);//文件夹|文件|不包含./和../

    //排序文件夹优先
    dir.setSorting(QDir::DirsFirst);

    //获取文件夹下所有文件(文件夹+文件)
    QFileInfoList list = dir.entryInfoList();

    for (int i = 0; i < list.size(); i++)
    {
        QFileInfo fileInfo = list.at(i);
        QStringList listSingleFileInfo;
        QString sFilePath = fileInfo.filePath();

        if (fileInfo.isDir())//判断是否为文件夹
        {
            listSingleFileInfo = _GetFilePath(fileInfo.filePath());//递归开始
            listFileInfo.append(listSingleFileInfo);
        }
        else
        {
            listFileInfo.append(sFilePath);
        }
    }

    return listFileInfo;
}

这部分的内容,基本上移植是比较方便的,注意使用环境是在Arm上的,未尝试过在windows环境进行操作。

四、FFmpegRecord编码层

老规矩,先把整体的代码贴出来,后面再详细解释一下。
在这里插入图片描述

CFFmpegRecord::CFFmpegRecord(QObject *parent) : QObject(parent)
{

}

CFFmpegRecord::~CFFmpegRecord()
{
    LOG_INFO << "--> ~CFFmpegRecord Start";
    av_free(m_pBuffer);
    av_free(m_pYuvBuffer);
    sws_freeContext(m_SwsImgCtx);

    if (m_pOutPutFormatCtx)
    {
        avio_close(m_pOutPutFormatCtx->pb);
        avformat_free_context(m_pOutPutFormatCtx);
    }

    if (m_pEncodecCtx)
        avcodec_free_context(&m_pEncodecCtx);

    if (m_pRgbFrame)
        av_frame_free(&m_pRgbFrame);

    if (m_pYuvFrame)
        av_frame_free(&m_pYuvFrame);

    m_pYuvBuffer = nullptr;
    m_pBuffer = nullptr;
    m_SwsImgCtx = nullptr;
    m_pRgbFrame = nullptr;
    m_pYuvFrame = nullptr;
    m_pOutPutFormatCtx = nullptr;
    m_pEncodecCtx = nullptr;

    if (m_pThread)
    {
        m_pThread->quit();
        m_pThread->exit();
    }

    LOG_INFO << "--> ~CFFmpegRecord End";
}

void CFFmpegRecord::Init()
{
    m_pThread = new QThread();
    m_pThread->setObjectName("CFFmpegRecord");
    this->moveToThread(m_pThread);
    m_pThread->start();
    m_bExitThread = false;
}

void CFFmpegRecord::InitFFmpeg()
{
    int ret = 0;
    m_iPerFrameCnt = 10;
    m_iIndex = 0;
    m_pEnPacket = av_packet_alloc();
    string sRecordName = m_sRecordFileName.toStdString();
    m_cRecordFileName = sRecordName.c_str();

    ret = avformat_alloc_output_context2(&m_pOutPutFormatCtx, NULL, NULL, m_cRecordFileName);

    if (ret < 0)
    {
        LOG_INFO << "Cannot alloc output file context";
        return;
    }

    ret = avio_open(&m_pOutPutFormatCtx->pb, m_cRecordFileName, AVIO_FLAG_READ_WRITE);

    if (ret < 0)
    {
        LOG_INFO << "output file open failed";
        return;
    }

    pEncodec = avcodec_find_encoder(AV_CODEC_ID_H264);

    if (pEncodec == NULL)
    {
        LOG_INFO << "Cannot find any endcoder";
        return;
    }

    m_pEncodecCtx = avcodec_alloc_context3(pEncodec);

    if (m_pEncodecCtx == NULL)
    {
        LOG_INFO << "Cannot alloc AVCodecContext";
        return;
    }

    m_pVideoStream = avformat_new_stream(m_pOutPutFormatCtx, pEncodec);

    if (m_pVideoStream == NULL) {
        LOG_INFO << "failed create new video stream";
        return;
    }

    m_pVideoStream->time_base = AVRational{ 1,10 };

    m_pCodecParam = m_pVideoStream->codecpar;
    m_pCodecParam->width = m_iWidth;
    m_pCodecParam->height = m_iHeight;
    m_pCodecParam->codec_type = AVMEDIA_TYPE_VIDEO;

    ret = avcodec_parameters_to_context(m_pEncodecCtx, m_pCodecParam);

    if (ret < 0)
    {
        LOG_INFO << "Cannot copy codec para";
        return;
    }

    m_pEncodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    m_pEncodecCtx->time_base = AVRational{ 1,10 };
    m_pEncodecCtx->bit_rate = 500000;
    m_pEncodecCtx->gop_size = 100;

    // 某些封装格式必须要设置该标志,否则会造成封装后文件中信息的缺失,如:mp4
    if (m_pOutPutFormatCtx->oformat->flags & AVFMT_GLOBALHEADER)
    {
        m_pEncodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    AVDictionary *param = 0;

    if (pEncodec->id == AV_CODEC_ID_H264)
    {
        m_pEncodecCtx->qmin = 10;
        m_pEncodecCtx->qmax = 51;
        m_pEncodecCtx->qcompress = (float)0.6;
        m_pEncodecCtx->max_b_frames = 0;

        av_dict_set(&param, "preset", "fast", 0);
        av_dict_set(&param, "tune", "zerolatency", 0);
    }

    ret = avcodec_open2(m_pEncodecCtx, pEncodec, &param);

    if (ret < 0)
    {
        LOG_INFO << "Open encoder failed";
        return;
    }

    //再将codecCtx设置的参数传给param,用于写入头文件信息
    avcodec_parameters_from_context(m_pCodecParam, m_pEncodecCtx);

    m_pRgbFrame = av_frame_alloc();
    m_pYuvFrame = av_frame_alloc();

    m_pYuvFrame->width = m_iWidth;
    m_pYuvFrame->height = m_iHeight;
    m_pYuvFrame->format = m_pEncodecCtx->pix_fmt;

    m_pRgbFrame->width = m_iWidth;
    m_pRgbFrame->height = m_iHeight;
    m_pRgbFrame->format = AV_PIX_FMT_BGR24;

    m_iBufferSize = av_image_get_buffer_size((AVPixelFormat)m_pRgbFrame->format, m_iWidth, m_iHeight, 1);
    m_pBuffer = (uint8_t*)av_malloc(m_iBufferSize);
    ret = av_image_fill_arrays(m_pRgbFrame->data, m_pRgbFrame->linesize, m_pBuffer, (AVPixelFormat)m_pRgbFrame->format, m_iWidth, m_iHeight, 1);

    if (ret < 0)
    {
        LOG_INFO << "Cannot filled rgbFrame";
        return;
    }

    int yuvSize = av_image_get_buffer_size((AVPixelFormat)m_pYuvFrame->format, m_iWidth, m_iHeight, 1);
    m_pYuvBuffer = (uint8_t*)av_malloc(yuvSize);

    ret = av_image_fill_arrays(m_pYuvFrame->data, m_pYuvFrame->linesize, m_pYuvBuffer, (AVPixelFormat)m_pYuvFrame->format, m_iWidth, m_iHeight, 1);

    if (ret < 0)
    {
        LOG_INFO << "Cannot filled yuvFrame";
        return;
    }

    m_SwsImgCtx = sws_getContext(m_iWidth, m_iHeight, AV_PIX_FMT_BGR24, m_iWidth, m_iHeight, m_pEncodecCtx->pix_fmt, 0, NULL, NULL, NULL);

    ret = avformat_write_header(m_pOutPutFormatCtx, NULL);

    if (ret != AVSTREAM_INIT_IN_WRITE_HEADER)
    {
        LOG_INFO << "Write file header fail";
        return;
    }

    av_new_packet(m_pEnPacket, m_iBufferSize);
}

void CFFmpegRecord::SetMissionId(QString _sMissionId)
{
    m_sMissionId = _sMissionId;
}

void CFFmpegRecord::SetRecordType(eRecordType eType)
{
    m_iRecordType = eType;
}

void CFFmpegRecord::SetRecordFileName(QString _sFileName)
{
    QString sRecordDir;
    QDir dir;
    switch (m_iRecordType) {
    case eAlarmRecord:
    {
        sRecordDir = QString("/ics/recordfile/alarm/");

        if (!dir.exists(sRecordDir))
        {
            dir.mkpath(sRecordDir);
        }
        m_sRecordFileName = sRecordDir.append(_sFileName);
    }
        break;
    case eOperateRecord:
    {
        sRecordDir = QString("/ics/recordfile/operate/");

        if (!dir.exists(sRecordDir))
        {
            dir.mkpath(sRecordDir);
        }

        m_sRecordFileName = sRecordDir.append(_sFileName);
    }
        break;
    case eTakeRecord:
    {
        sRecordDir = QString("/ics/recordfile/take/");

        if (!dir.exists(sRecordDir))
        {
            dir.mkpath(sRecordDir);
        }

        m_sRecordFileName = sRecordDir.append(_sFileName);
    }
        break;
    case eReturnRecord:
    {
        sRecordDir = QString("/ics/recordfile/return/");

        if (!dir.exists(sRecordDir))
        {
            dir.mkpath(sRecordDir);
        }

        m_sRecordFileName = sRecordDir.append(_sFileName);
    }
        break;
    default:
        break;
    }

    LOG_INFO << "--> CFFmpegRecord::SetRecordFileName FileName:"<<m_sRecordFileName.toStdString();
}

void CFFmpegRecord::SetRecordTime(int _iRecordTime)
{
    m_iRecordTime = _iRecordTime;
}

void CFFmpegRecord::StopRecord()
{
    m_bExitThread = true;
}

void CFFmpegRecord::writeImageToMp4(QImage _img)
{
    DLOG_TRACE <<"--> CFFmpegRecord::writeImageToMp4 start";
    QTime time;
    time.start();

    if (_img.isNull())
    {
        LOG_INFO << "--> writeImageToMp4 img is NULL";
        return;
    }

    Mat img = QImage2Mat(_img);
    memcpy(m_pBuffer, img.data, m_iBufferSize);
    sws_scale(m_SwsImgCtx,
              m_pRgbFrame->data,
              m_pRgbFrame->linesize,
              0,
              m_pEncodecCtx->height,
              m_pYuvFrame->data,
              m_pYuvFrame->linesize);

    m_iIndex ++;
    m_pYuvFrame->pts = m_iIndex;
    rgb2mp4Encode(m_pEncodecCtx, m_pYuvFrame, m_pEnPacket, m_pVideoStream, m_pOutPutFormatCtx);

    DLOG_TRACE <<"--> CFFmpegRecord::writeImageToMp4 end"<<time.elapsed();
}

Mat CFFmpegRecord::QImage2Mat(QImage _img)
{
    cv::Mat mat;

    switch (_img.format())
    {
    case QImage::Format_ARGB32:
    case QImage::Format_RGB32:
    case QImage::Format_ARGB32_Premultiplied:
        mat = cv::Mat(_img.height(), _img.width(), CV_8UC4, (void*)_img.constBits(), _img.bytesPerLine());
        break;
    case QImage::Format_RGB888:
        mat = cv::Mat(_img.height(), _img.width(), CV_8UC3, (void*)_img.constBits(), _img.bytesPerLine());
        cv::cvtColor(mat, mat, CV_BGR2RGB);
        break;
    case QImage::Format_Indexed8:
        mat = cv::Mat(_img.height(), _img.width(), CV_8UC1, (void*)_img.constBits(), _img.bytesPerLine());
        break;
    }
    return mat;
}

int CFFmpegRecord::rgb2mp4Encode(AVCodecContext* codecCtx, AVFrame* yuvFrame, AVPacket* pkt, AVStream* vStream, AVFormatContext* fmtCtx)
{
    int ret = 0;

    if (avcodec_send_frame(codecCtx, yuvFrame) >= 0)
    {
        while (avcodec_receive_packet(codecCtx, pkt) >= 0)
        {
            pkt->stream_index = vStream->index;
            pkt->pos = -1;

            av_packet_rescale_ts(pkt, codecCtx->time_base, vStream->time_base);
            DLOG_TRACE << "encoder success:" << pkt->size << endl;

            ret = av_interleaved_write_frame(fmtCtx, pkt);

            if (ret < 0)
            {
                char errStr[256];
                av_strerror(ret, errStr, 256);
                DLOG_TRACE << "error is:" << errStr << endl;
            }
        }
    }
    return ret;
}

void CFFmpegRecord::SLOT_FFmpegImage(const QByteArray &_oYuv420, int _iWidth, int _iHeight)
{
    QMutexLocker oLocker(&m_mutex);
    DLOG_TRACE << "--> CFFmpegRecord::SLOT_FFmpegImage Start";

    QTime time;
    time.start();

    m_iWidth = _iWidth;
    m_iHeight = _iHeight;

    if (nullptr == m_pRGBData)
    {
        m_pRGBData = new uchar[_iWidth * _iHeight * 3];
        m_oImage = std::move(QImage(m_pRGBData, _iWidth, _iHeight, QImage::Format_RGB888));
    }

    CPixelFormatConverter::YUV420ToRGB24((uchar*)_oYuv420.data(), _iWidth, _iHeight, &m_pRGBData);

    if (false == m_bInit)
    {
        InitFFmpeg();
        m_bInit = true;
    }

    if (!m_bExitThread)
    {
        writeImageToMp4(m_oImage);
    }

    if (m_bExitThread && m_bExit)
    {
        rgb2mp4Encode(m_pEncodecCtx, NULL, m_pEnPacket, m_pVideoStream, m_pOutPutFormatCtx);
        av_write_trailer(m_pOutPutFormatCtx);
        m_bExit = false;
    }

    LOG_INFO << "--> CFFmpegRecord::SLOT_FFmpegImage End Time is :"<<time.elapsed();
}

参考


http://www.niftyadmin.cn/n/28882.html

相关文章

Leetcode.126 单词接龙 II

题目链接 Leetcode.126 单词接龙 II 题目描述 按字典 wordList完成从单词 beginWord到单词 endWord转化&#xff0c;一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk这样的单词序列&#xff0c;并满足&#xff1a; 每对相邻的单词之间…

客快物流大数据项目(一百零五):启动ElasticSearch

文章目录 启动ElasticSearch 一、启动ES服务端 二、​​​​​​​启动Kibana 启动ElasticSearch

QPSK和16QAM基带信号解调误比特率理论限和仿真对比

重要声明:为防止爬虫和盗版贩卖,文章中的核心代码和数据集可凭【CSDN订阅截图或公z号付费截图】私信免费领取,一律不认其他渠道付费截图! 背景 对于各种调制类型在高斯信道下的比特误码率理论限,matlab给出了函数berawgn,简单介绍如下: 语法ber = berawgn(EbNo,modtyp…

代码随想录算法训练营第22天 二叉树 java :235. 二叉树的最近公共祖先 701.二叉搜索树中的插入操作 450.删除二叉搜索树中的节点

文章目录LeetCode 236. 二叉树的最近公共祖先题目讲解思路LeetCode 701.二叉搜索树中的插入操作题目讲解思路LeetCode 450.删除二叉搜索树中的节点题目讲解思路示图总结既然还是要生活&#xff0c;那么就学会主宰生活LeetCode 236. 二叉树的最近公共祖先 题目讲解 思路 求最小…

计算机网络学习笔记(六)数据链路层

文章目录链路层概述1.链路层的基本概念2.链路层提供的服务&#xff08;1&#xff09;服务概览&#xff1a;&#xff08;2&#xff09;链路层在何处实现差错校验和纠正技术1.奇偶校验2.检验和方法3.循环冗余检测&#xff08;1&#xff09;CRC的编码操作多路访问链路和协议1.信道…

Tkinter的Label与Button

Tkinter是Python的一个内置包&#xff0c;主要用于简单的界面设计&#xff0c;使用起来非常方便。 目录 一、创建界面 1. 具体步骤 1.1 导入tkinter包 1.2 tk.Tk()函数&#xff1a;创建一个主界面&#xff0c;并命名为root 1.3 root.title()函数&#xff1a;给root界面设置…

【Ajax】了解Ajax与jQuery中的Ajax

一、了解Ajax什么是AjaxAjax 的全称是 Asynchronous Javascript And XML&#xff08;异步 JavaScript 和 XML&#xff09;。通俗的理解&#xff1a;在网页中利用 XMLHttpRequest 对象和服务器进行数据交互的方式&#xff0c;就是Ajax。2. 为什么要学Ajax之前所学的技术&#xf…

FPGA 20个例程篇:19.OV7725摄像头实时采集送HDMI显示(三)

第七章 实战项目提升&#xff0c;完善简历 19.OV7725摄像头实时采集送HDMI显示&#xff08;三&#xff09; 在详细介绍过OV7725 CMOS Sensor的相关背景知识和如何初始化其内部寄存器达到输出预期视频流的目的后&#xff0c;就到了该例程的核心内容即把OV7725输出的视频流预先缓…