FFmpeg 编码和解码

文章目录

  • 音频格式
    • AAC
      • ADIF音频数据交换格式
      • ADTS音频数据传输流
    • 音频解码
    • 音频编码
  • 视频格式
    • H264
      • GOP图像组
      • I帧,P帧,B帧
      • H264压缩技术
      • H264压缩级别
      • H264视频级别
      • H264码流结构
      • SPS
      • PPS
    • 解码视频
    • 编码视频

音频格式

AAC

AAC全称 Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。出现于1997年,基于MPEG-2的音频编码技术。由Fraunhofer IIS、杜比实验室、AT&T、索尼等公司共同开发,目的是取代MP3格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的性价比。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。

优点:相较于mp3,AAC格式的音质更佳,文件更小。
不足:AAC属于有损压缩的格式,与时下流行的APE、FLAC等无损格式相比,音质存在本质上的差距。加之传输速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC头上小巧的光环不复存在。

AAC共有9种规格,以适应不同场合的需要

  • MPEG-2 AAC LC 低复杂度规格(low complexity),比较简单,没有增益控制,但提高了编码效率,在中等码率的编码效率及音质方面,都能找到平衡点
  • MPEG-2 AAC Main 主规格
  • MPEG-2 AAC SSR 可变采样率规格(scaleable sample rate)
  • MPEG-4 AAC LC 低复杂度规格,现在的手机比较常见的MP4文件中的音频部分就包括了该规格音频文件
  • MPEG-4 AAC Main 主规格。包含了除增益控制之外的全部功能,其音质最好。
  • MPEG-4 AAC SSR 可变采样率规格
  • MPEG-4 AAC LTP 长时期预测规格(Long Term Predicition)
  • MPEG-4 AAC LD 低延迟规格 (Low Delay)
  • MPEG-4 AAC HE 高效率规格(High Efficiency)这种规格适合用于低码率编码,有Nero AAC编码器支持

在这里插入图片描述
HE:High Efficiency(高效性)。 HE-AAC v1(又称AACPlusV1,SBR),用容器的方法实现了AAC(LC)+SBR技术。SBR其实代表的是Spectral Band Replication(频段复制)。简要叙述一下,音乐的主要频谱集中在低频段,高频段幅度很小,但很重要,决定了音质。如果对整个频段编码,若是为了保护高频就会造成低频段编码过细以致文件巨大,若是保存了低频的主要成分而失去高频成分就会丧失音质。SBR把频谱切割开来,低频单独编码保存主要成分,高频单独放大编码保存音质,“统筹兼顾”了,在减少文件大小的情况下还保存了音质,完美地化解了这一矛盾。
HEv2:用容器的方法包含了HE-AAC v1和PS技术。PS指parametric stereo(参数立体声)。原来的立体声文件大小是一个声道的两倍。但是两个声道的声音存在某种相似性,根据香农信息熵编码定理,相关性应该被去掉才能减小文件大小。所以PS技术存储了一个声道的全部信息,然后,花很少的字节用参数描述另一个声道和它不同的地方

目前使用最多的是LC和HE(适合低码率)。流行的Nero AAC编码程序只支持LC,HE、HEv2这三种规格,编码后的AAC音频,规格显示都是LC,HE其实就是AAC(LC)+SBR技术,HEv2就是AAC(LC)+SBR+PS技术。

AAC音频格式分为ADIF和ADTS

  • ADIF:audio data interchange format,音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
  • ADTS: audio data transport stream,音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始,它的特征类似于mp3数据流格式。
    简单的说,ADTS可以在任意帧解码,也就是说,它每一帧都有头信息。ADIF只有一个统一的头,所以必须得到所有的数据后解码。且这两种的header格式也是不同的,目前一般编码后的和抽取出的都是ADTS格式的音频流,两者具体的组织结构如下所示:
    在这里插入图片描述
    空白处表示前后帧

有时候当你编码AAC裸流的时候,会遇到写出来的AAC并不能在PC和手机上播放,很大可能就是AAC文件的每一帧里缺少了ADTS头信息文件的包装拼接。秩序奥加入头文件ADTS即可。

ADIF音频数据交换格式

其中adif_header如下表:
在这里插入图片描述
在这里插入图片描述
raw_data_stream如图所示
在这里插入图片描述
byte_alignment为了保持字节对齐用,
可以看到ADIF只有一个header,里面没有关于每帧的信息,因为每帧不是固定大小,故只能按照顺序进行解码,也无法跳播或快进快退。除非自己解码遍历整个文件,建立每帧位置表。

ADTS音频数据传输流

一个AAC原始数据块长度是可变的,对原始帧加上ADTS头进行ADTS的封装,就形成了ADTS帧,AAC音频文件的每一帧由ADTS Header和AAC Audio Data组成。结构体如下:
在这里插入图片描述
ADTS头部信息占据了整个文件中的前7或9个字节,分为2部分:

  • adts_fixed_header():固定头信息,头信息中的每一帧都相同,其中包括了一个固定的同步标记(syncword),该标记用于确定音频帧的边界位置。
  • adts_variable_header():可变头信息,头信息则在帧与帧之间可变
    一个完整的头信息就是固定头+可变头。
    以下是对头信息中各字段的详细介绍:
  1. 固定头
    在这里插入图片描述
  • syncword:同步字,12bit,同步字是ADTS文件的标志符,它用于确定音频帧的开始位置和结束位置,通常为0xFFF
  • ID:9bit,ID指示使用的MPEG版本。0表示MPEG-4,1表示MPEG-2
  • layer:2bit,Layer定义了音频流所属的层级,对于AAC来说,其值为0。
  • protection_absent:1bit,指示是否启动CRC错误校验。0表明数据经过CRC校验,否则未经过
  • profile:2bit,指示编码所使用的AAC规范类型,
    在MPEG-2 中定义了3种
    在这里插入图片描述
    MPEG-4中,profile的值=MPEG-4 Audio Object Type - 1
    在这里插入图片描述
  • sampling_frequency_index:4bit,表示采样率的索引,它告诉解码器当前音频数据的采样率。值的范围是0-15,每一个值表示一个特定的采样率。
    在这里插入图片描述
  • private_bit:1bit,为私有比特,通常被设置为0,没有实际作用。
  • channel_configuration:3bit,指示音频的通道数,范围也是0-15
    在这里插入图片描述
  • original_copy:1bit,指示编码数据是否被原始产生,通常为0
  • home:1bit,通常被设置为0,没有实际作用
  1. 可变头
    在这里插入图片描述
  • copyrighted_id_bit:编码时设置为0,解码时忽略
  • copyrighted_id_start:编码时设置为0,解码时忽略
  • aac_frame_length:ADTS帧长度,它包括ADTS长度和AAC声音数据长度。即aac_frame_length = ( protection_absent == 0 ? 9 : 7 ) + audio_data_length
  • adts_buffer_fullness: 固定位0x7FF。表示是码率可变的码流
  • number_of_raw_data_blocks_in_frame:ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。(一个AAC原始帧包含一段时间内1024个采样及相关数据)

AAC ES是AAC音频编码的一种基本数据格式,也是AAC音频数据在流式传输和文件存储中的常见格式之一。
不同于其他容器格式,它不包含额外的元数据或结构信息,仅包含未经任何封装或压缩处理的原始音频数据。这些原始数据可以作为音频文件或流传输的基础,同时也可以用于对AAC音频进行转码、编辑或者重组。
通常由一系列连续的AAC音频帧组成,每个帧以一个特定的标志符开始,该标志符表示这是一个AAC音频帧。在AAC ES中,每个音频帧拥有相同的长度,但并一定包含相同数量的采样点,因为采样率和声道数量可能会发生变化。
另一个关键特征是其比特流顺序,即数字音频数据的组织方式,AAC ES采用大端字节顺序,其中高位字节排在前面,地位字节排在后面。此外,在AAC ES中,音频数据按照从左到右、自上而下的顺序排列,与典型的文本文件不同。
总之,AAC ES是AAC音频编码的一种基本数据格式,它通常由一系列AAC音频帧组成,并且不包含任何附加的元数据或结构信息。AAC ES可以作为音频文件或流传输的基础,同时也可以用于对AAC音频进行转码、编辑或重组。由于其简单性和灵活性,AAC ES受到了广泛的应用,并且成为了数字音频编码领域的标准之一。

音频解码

void Widget::decode()
{char errbuf[1024];                             // 错误信息缓冲区const char *inputFile = "../source/audio.aac"; // 输入文件const char *outputFile = "../output/out.pcm";  // 输出文件AVFormatContext *pInFormatCtx = NULL;          // 打开文件上下文// 打开输入文件if (avformat_open_input(&pInFormatCtx, inputFile, NULL, NULL) != 0){av_strerror(1, errbuf, sizeof(errbuf));printf("无法打开输入文件:%s\n", errbuf);return;}// 获取输入文件信息if (avformat_find_stream_info(pInFormatCtx, NULL) < 0){av_strerror(1, errbuf, sizeof(errbuf));printf("无法获取输入文件信息:%s\n", errbuf);return;}// 查找音频流int audioStreamIndex = av_find_best_stream(pInFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);if (audioStreamIndex < 0){printf("无法找到音频流\n");return;}// 查找解码器const AVCodec *pCodec = avcodec_find_decoder(pInFormatCtx->streams[audioStreamIndex]->codecpar->codec_id);if (!pCodec){printf("无法找到解码器\n");return;}// 创建解码器上下文AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);if (!pCodecCtx){printf("无法创建解码器上下文\n");return;}// 复制解码器参数if (avcodec_parameters_to_context(pCodecCtx, pInFormatCtx->streams[audioStreamIndex]->codecpar) < 0){printf("无法复制解码器参数\n");return;}// 打开解码器if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0){printf("无法打开解码器\n");return;}// 创建输出文件FILE *pOutFile = fopen(outputFile, "wb");if (!pOutFile){printf("无法创建输出文件\n");return;}// 解码数据AVPacket packet;                    // 数据包AVFrame *pFrame = av_frame_alloc(); // 解码后的数据帧while (av_read_frame(pInFormatCtx, &packet) >= 0){// 解码数据包int ret = avcodec_send_packet(pCodecCtx, &packet);if (ret < 0){return; // 解码失败}while (ret >= 0){ret = avcodec_receive_frame(pCodecCtx, pFrame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){break;}else if (ret < 0){av_strerror(ret, errbuf, sizeof(errbuf));printf("解码数据帧失败:%s\n", errbuf);break;}int fmtByteSize = av_get_bytes_per_sample((AVSampleFormat)pFrame->format); // 每个样本的字节数for (int i = 0; i < pFrame->nb_samples; i++)                               // 每个样本{for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道{fwrite(pFrame->data[ch] + i * fmtByteSize, 1, fmtByteSize, pOutFile); // 写入文件}}av_frame_unref(pFrame); // 释放数据帧}av_packet_unref(&packet); // 释放数据包}// 编码器刷新,将剩余的数据帧编码为数据包int ret = avcodec_send_packet(pCodecCtx, NULL);while (ret >= 0){ret = avcodec_receive_frame(pCodecCtx, pFrame);if (ret < 0){break;}int fmtByteSize = av_get_bytes_per_sample((AVSampleFormat)pFrame->format); // 每个样本的字节数for (int i = 0; i < pFrame->nb_samples; i++)                               // 每个样本{for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道{fwrite(pFrame->data[ch] + i * fmtByteSize, 1, fmtByteSize, pOutFile); // 写入文件}}av_frame_unref(pFrame); // 释放数据帧}qDebug() << "解码完成";// 释放资源fclose(pOutFile);av_frame_free(&pFrame);avcodec_free_context(&pCodecCtx);avformat_close_input(&pInFormatCtx);
}

音频编码

写入adts头的函数

void Widget::writeADTSHeader(std::ofstream &file, const AVCodecContext *codecContext, const int packetSize)
{uint8_t adtsHeader[7]{0};// 写入ADTS头,注意位运算优先级比较低,所以括号不能省略adtsHeader[0] = 0xFF;adtsHeader[1] = 0xF1; // 0xF1表示MPEG-4 AAAC 校验位为1adtsHeader[2] = (codecContext->profile << 6) + (4 << 2) + (codecContext->ch_layout.nb_channels >> 2);adtsHeader[3] = ((codecContext->ch_layout.nb_channels & 3) << 6) + ((packetSize + 7) >> 11); //  固定头的后4位+可变头的前4位adtsHeader[4] = ((packetSize + 7) >> 3) & 0xFF;adtsHeader[5] = (((packetSize + 7) & 0x07) << 5) + 0x1F;adtsHeader[6] = 0xFC;file.write((char *)adtsHeader, 7);
}

编码函数

void Widget::encode()
{char errbuf[1024];                                // 错误信息缓冲区const char *inputFile = "../../source/audio.pcm"; // 输入文件const char *outputFile = "../../output/out1.aac"; // 输出文件// 查找AAC编码器const AVCodec *pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);if (!pCodec){printf("无法找到编码器\n");return;}// 创建编码器上下文AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);if (!pCodecCtx){printf("无法创建编码器上下文\n");return;}pCodecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP;           // 设置采样格式pCodecCtx->sample_rate = 44100;                       // 设置采样率AVChannelLayout ch_layout = AV_CHANNEL_LAYOUT_STEREO; // 设置声道布局av_channel_layout_copy(&pCodecCtx->ch_layout, &ch_layout);pCodecCtx->bit_rate = 128'000;                  // 设置比特率pCodecCtx->flags = AV_CODEC_FLAG_GLOBAL_HEADER; // 设置全局头标志pCodecCtx->profile = AV_PROFILE_AAC_HE_V2;      // 设置AAC编码器配置文件// 打开编码器if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0){printf("无法打开编码器\n");return;}std::ifstream pInFile(inputFile, std::ifstream::binary);   // 打开输入文件std::ofstream pOutFile(outputFile, std::ofstream::binary); // 打开输出文件if (!pInFile.is_open() || !pOutFile.is_open()){return;}// 创建数据包AVPacket *packet = av_packet_alloc();if (!packet){printf("无法创建数据包\n");return;}// 创建数据帧AVFrame *pFrame = av_frame_alloc();if (!pFrame){printf("无法创建数据帧\n");return;}pFrame->sample_rate = pCodecCtx->sample_rate;                      // 设置采样率pFrame->format = pCodecCtx->sample_fmt;                            // 设置采样格式av_channel_layout_copy(&pFrame->ch_layout, &pCodecCtx->ch_layout); // 设置声道布局pFrame->nb_samples = pCodecCtx->frame_size;                        // 设置每帧的样本数// 分配内存if (av_frame_get_buffer(pFrame, 0) < 0){printf("无法分配内存\n");return;}// 分配缓冲区int fmtByteSize = av_get_bytes_per_sample(pCodecCtx->sample_fmt);                                                // 获取每个样本的字节数uint8_t *pBuffer = (uint8_t *)av_malloc(fmtByteSize * pCodecCtx->frame_size * pCodecCtx->ch_layout.nb_channels); // 分配缓冲区if (!pBuffer){printf("无法分配缓冲区\n");return;}// 读取数据while (!pInFile.eof()){// 读取数据for (int i = 0; i < pFrame->nb_samples * pFrame->ch_layout.nb_channels / 2; i++){pInFile.read((char *)pBuffer, fmtByteSize * 2);if (pInFile.eof()){break;}for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道{memcpy(pFrame->data[ch] + i * fmtByteSize, pBuffer + ch * fmtByteSize, fmtByteSize); // 复制数据到数据帧}}// 编码int ret = avcodec_send_frame(pCodecCtx, pFrame);if (ret < 0)break;while (ret >= 0){ret = avcodec_receive_packet(pCodecCtx, packet);if (ret < 0)break;writeADTSHeader(pOutFile, pCodecCtx, packet->size); // 写入ADTS头pOutFile.write((char *)packet->data, packet->size); // 写入数据av_packet_unref(packet);                            // 释放数据包}// 清空缓冲区memset(pBuffer, 0, fmtByteSize * pFrame->nb_samples * pFrame->ch_layout.nb_channels);}// 清空编码器,防止缓冲区有数据int ret = avcodec_send_frame(pCodecCtx, nullptr);while (ret >= 0){ret = avcodec_receive_packet(pCodecCtx, packet);if (ret < 0)break;writeADTSHeader(pOutFile, pCodecCtx, packet->size); // 写入ADTS头pOutFile.write((char *)packet->data, packet->size); // 写入数据av_packet_unref(packet);                            // 释放数据包}// 释放资源av_frame_free(&pFrame);av_packet_free(&packet);avcodec_free_context(&pCodecCtx);pInFile.close();pOutFile.close();av_free(pBuffer);printf("转换完成\n");
}

视频格式

H264

H264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。H264是MPG-4标准所定义的最新编码格式,同时也是技术含量最高、代表最新技术水平的视频编码格式之一,在ITU的标准称为H.264,在MPEG的标准里是MPEG-4的一个组成部分(MPEG-4 Part10),又叫Advanced Video Codec,因此常常被称为MPG-4 AVC或者直接叫AVC。H264视频格式是经过有损压缩的,但在技术上尽可能做的降低存储体积下获得较好图像质量和低带宽图像快速传输。压缩比大约是1%

GOP图像组

GOP(Group of Pictures)顾名思义,就是一组图片,在实际操作中,就是一组完整的视频帧,怎么叫做完整的视频帧?就是说一个GOP拿出来,必须能够完整地播放、显示。也就是两个I帧之间的间隔。
在这里插入图片描述

I帧,P帧,B帧

  • I帧(intraframe frame),也叫关键帧,采用帧内压缩技术,将其解压出来就是一张完整的图片。GOP中第一个I帧也被称为IDR帧。
  • P帧(forward Predicted frame),向前参考帧,也叫预测帧。压缩时,只参考前面已经处理的帧,采用帧间压缩技术,它只占用I帧一半大小。
  • B帧(Bidirectionally predicted frame),双向参考帧,也叫双向预测帧。压缩时既参考前面已经处理的帧,也参考后面的帧,使用帧间压缩技术。它占I帧1/4大小

IDR帧和I帧的区别
一个序列的第一个图像叫做IDR帧(立即刷新图像),IDR帧都是I帧。I帧和IDR帧都是用帧内预测。不用参考任何帧但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样。

其核心作用是,当解码器解码到IDR帧时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,使错误不致传播,从IDR帧开始算新的序列,开始编码。

H264压缩技术

H264首先会把一张图片分成一个个的片,片里面又会分成一个个宏块。
宏块是视频压缩操作的基本单元。无论是帧内压缩还是帧间压缩,它们都以宏块为单位。
以下面的图片为例
在这里插入图片描述
划分片
在这里插入图片描述
划分宏块
H264默认是用16x16 大小的区域作为一个宏块,也可以划分成8x8大小。划分好宏块后,计算宏块的像素值
在这里插入图片描述
划分子块
H264对比较平坦的图像使用16x16大小的宏块。但是为了更高的压缩率,还可以再这个宏块上划分出更小的子块,子块的大小可以是8x16,16x8,8x8,4x8,8x4,4x4非常的灵活
在这里插入图片描述
上图红框内的16x16宏块大部分是蓝色背景,而三只鹰的部分图像被划在了该宏块内,为了更好地处理三只鹰部分的图像, 就在这个宏块内又划分出了多个子块。
这样再经过帧内压缩,可以得到更高效的数据。

常见的宏块尺寸如下

在这里插入图片描述

帧内压缩也称为空间压缩,当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立地解码、显示。帧内压缩一般达不到很高的压缩,跟编码JPEG差不多。

帧间压缩,相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。帧间压缩也称为时间压缩,它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的,帧差值算法是一种典型的时间压缩法,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。

帧内预测
如图所示,左侧显示的是H264对于帧内预测提供的9种模式
在这里插入图片描述
9种帧内预测模式
在这里插入图片描述

  1. 第一种是纵列的即垂直模式,左边以及上边都是已经推测出来的宏块,这个时候就可以将下面4x4的宏块预测出来,对于垂直模式来说,上边A对应下面的一整列都是A,第二个B,对应一列下面也都是B,以此类推。
  2. 第二种是横向的水平模式,还是左边以及上边都是都是预测出来的数据,预测结果第一行是I,第二行是j,以此类推。
  3. 第三种是求平均值,在目标的4x4宏块内都是ABCD加上IJKL的求平均值,例如宏块内的小a,它的值是ABCD加IJKL求平均值得出,也就是下面每一个4x4内的像素值都是一样的。需要注意的是:像素a对应的值是(ABCD+IJKL)/2,而不是(A+I)/2。

H264压缩级别

H264 Profile 是对视频压缩特性的描述,Profile越高,说明采用了越高级的压缩特性。
在这里插入图片描述

在这里插入图片描述

H264视频级别

H264 Level是对视频的描述,Level越高,视频的码率、分辨率、FPS越高。
在这里插入图片描述

H264码流结构

H264的两种码流格式,它们分别为:字节流格式和RTP包格式。

  • 字节流格式(Annexb):这是在H264官方协议文档中规定的格式,处于文档附录B(Annex-B Byte stream format)中,所以它也成为了大多数编码器,默认的输出格式。它的基本数据单位为NAL单元,也即NALU。为了从字节流中提取出NALU,协议规定,在每个NALU的前面加上起始码:0x000001或0x00000001
  • RTP包格式:这种格式并没有在H264中规定,那为什么还要介绍它呢?是因为在H264的官方参考软件JM里,有这种封装格式的实现。在这种格式中,NALU并不需要起始码Start_Code来进行识别,而是在NALU开始的若干字节(1,2,4字节),代表NALU的长度。

码流分层图
在这里插入图片描述
接下来我们主要讲字节流格式,H264码流分为2层,NAL层和VCL层

  • NAL(Network Abstraction Layer)层,视频数据网络抽象层,是H264码流的基本单元。
  • VCL(Video Coding Layer)层,视频数据编码层,VCL结构关系如下图所示,在视频帧序列由图像组成,图像中包含分片,分片里面包含宏块,宏块里面又包含子块
    在这里插入图片描述
    接下来我们来看NALU,NALU由NALU头部和NALU载荷 (Raw Byte Sequence Payload ,RBSP) 组成,其中NALU头部包括禁止位 (forbidden_zero_bit)、参考标识(nal_ref_idc)和NALU类型(nal_uint_type)等信息。NALU载荷则是包含实际视频数据的原始字节序列。

对于一个H264裸流,它就是由一个起始码(StartCode)和一个NALU单元组成
在这里插入图片描述
由上图可以知道,NALU单元 = NALU Header + RBSP ,其中RBSP原始字节序列载荷,即在SODB的后面添加了trailing bit,即一个bit 1和若干个bit0,以使字节对齐,SODB(String of Data Bits)原始数据比特流,原始数据比特流长度不一定是8的倍数,故需要补齐,他是由VCL层产生的
在这里插入图片描述

在每一个I帧之前,都会存在一个SPS和PPS。

  • SPS(Sequence Parameter Set) 序列参数集,作用于一连串连续的视频图像。如seq_parameter_set_id、帧数及POC(picture order count)的约束、参考帧数目、解码图像尺寸和熵编码模式选择标识等
  • PPS(Picture Parameter Set)图像参数集,作用于视频序列中的图像。如pic_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等
    在这里插入图片描述

SPS

SPS结构如下:
在这里插入图片描述

  • profile_idc:标识当前H264码流的profile。我们知道H264中定义了三种常用的档次profile:
    • 基准档次:baseline profile
    • 主要档次:main profile
    • 扩展档次:extended profile
      在H264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:
    • profile_idc = 66 → baseline profile
    • profile_idc = 77 → main profile
    • profile_idc = 88 → extended profile
      在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。另外,constraint_set0_flag ~ constraint_set5_flag是在编码的档次方面对码流增加的其他一些额外限制性条件。
  • level_idc:标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的Level由level_idc指定。
  • seq_parameter_set_id:表示当前的序列参数集的id,通过该id值,图像参数集pps可以引用其代表的sps中的参数。
  • log2_max_frame_num_minus4:用于计算MaxFrameNum的值。计算公式为 M a x F r a m e N u m = 2 l o g 2 m a x f r a m e n u m m i n u s 4 + 4 MaxFrameNum = 2^{log2_max_frame_num_minus4 + 4} MaxFrameNum=2log2maxframenumminus4+4。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。
  • pic_order_cnt_type:表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2
  • log2_max_pic_order_cnt_lsb_minus4:用于计算MaxPicOrderCntLsb的值,该值表示POC的上限。计算方法为 M a x P i c O r d e r C n t L s b = 2 l o g 2 m a x p i c o r d e r c n t l s b m i n u s 4 + 4 MaxPicOrderCntLsb = 2^{log2_max_pic_order_cnt_lsb_minus4 + 4} MaxPicOrderCntLsb=2log2maxpicordercntlsbminus4+4
    • max_num_ref_frames:用于表示参考帧的最大数目。
  • gaps_in_frame_num_value_allowed_flag:标识位,说明frame_num中是否允许不连续的值。

  • pic_width_in_mbs_minus1:用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:

    f r a m e w i d t h = 16 × ( p i c w i d t h i n m b s m i n u s 1 + 1 ) frame_width = 16 \times (pic_width_in_mbs_minus1 + 1) framewidth=16×(picwidthinmbsminus1+1)

  • pic_height_in_map_units_minus1:使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:

    P i c H e i g h t I n M a p U n i t s = p i c h e i g h t i n m a p u n i t s m i n u s 1 + 1 PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1 PicHeightInMapUnits=picheightinmapunitsminus1+1

  • frame_mbs_only_flag:标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。

    按照宏块计算的图像实际高度FrameHeightInMbs的计算方法为:

    F r a m e H e i g h t I n M b s = ( 2 − f r a m e m b s o n l y f l a g ) × P i c H e i g h t I n M a p U n i t s FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) \times PicHeightInMapUnits FrameHeightInMbs=(2framembsonlyflag)×PicHeightInMapUnits

  • mb_adaptive_frame_field_flag

  • 标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。

  • direct_8x8_inference_flag:标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。

  • frame_cropping_flag:标识位,说明是否需要对输出的图像帧进行裁剪。

  • vui_parameters_present_flag:标识位,说明SPS中是否存在VUI信息。

  • 在这里插入图片描述

PPS

PPS结构如下:
在这里插入图片描述
其中的每一个语法元素及其含义如下:

  • pic_parameter_set_id:表示当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值。该值的取值范围为[0,255]。

  • seq_parameter_set_id:表示当前PPS所引用的激活的SPS的id。通过这种方式,PPS中也可以取到对应SPS中的参数。该值的取值范围为[0,31]。

  • entropy_coding_mode_flag:熵编码模式标识,该标识位表示码流中熵编码/解码选择的算法。对于部分语法元素,在不同的编码配置下,选择的熵编码方式不同。例如在一个宏块语法元素中,宏块类型mb_type的语法元素描述符为“ue(v)| ae(v)”,在baseline profile等设置下采用指数哥伦布编码,在main profile等设置下采用CABAC编码。

    标识位entropy_coding_mode_flag的作用就是控制这种算法选择。当该值为0时,选择左边的算法,通常为指数哥伦布编码或者CAVLC;当该值为1时,选择右边的算法,通常为CABAC。

  • bottom_field_pic_order_in_frame_present_flag:标识位,用于表示另外条带头中的两个语法元素delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识。这两个语法元素表示了某一帧的底场的POC的计算方法。

  • num_slice_groups_minus1:表示某一帧中slice group的个数。当该值为0时,一帧中所有的slice都属于一个slice group。slice group是一帧中宏块的组合方式,定义在协议文档的3.141部分。

  • num_ref_idx_l0_default_active_minus1、num_ref_idx_l0_default_active_minus1表示当Slice Header中的num_ref_idx_active_override_flag标识位为0时,P/SP/Bslice的语法元素num_ref_idx_l0_active_minus1和num_ref_idx_l1_active_minus1的默认值。

  • weighted_pred_flag:标识位,表示在P/SP slice中是否开启加权预测。

  • weighted_bipred_idc:表示在B Slice中加权预测的方法,取值范围为[0,2]。0表示默认加权预测,1表示显式加权预测,2表示隐式加权预测。

  • pic_init_qp_minus26和pic_init_qs_minus26:表示初始的量化参数。实际的量化参数由该参数、slice header中的 s l i c e q p d e l t a ÷ s l i c e q s d e l t a slice_qp_delta \div slice_qs_delta sliceqpdelta÷sliceqsdelta计算得到。

  • chroma_qp_index_offset:用于计算色度分量的量化参数,取值范围为[-12,12]。

  • deblocking_filter_control_present_flag:标识位,用于表示Slice header中是否存在用于去块滤波器控制的信息。当该标志位为1时,slice header中包含去块滤波相应的信息;当该标识位为0时,slice header中没有相应的信息。

  • constrained_intra_pred_flag:若该标识为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位0,表示I宏块可以使用来自Inter类型宏块的信息。

  • redundant_pic_cnt_present_flag:标识位,用于表示Slice header中是否存在redundant_pic_cnt语法元素。当该标志位为1时,slice header中包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息。

解码视频

void Widget::H264Decode()
{char errbuf[1024];const char *infile = "../../source/video.h264";const char *outfile = "../../output/out.yuv";AVFormatContext *fmt_ctx = nullptr; // 输入文件上下文// 打开输入文件if (avformat_open_input(&fmt_ctx, infile, nullptr, nullptr) < 0){av_log(NULL, AV_LOG_ERROR, "无法打开资源文件 %s\n", infile);return;}// 获取资源信息if (avformat_find_stream_info(fmt_ctx, nullptr) < 0){av_log(NULL, AV_LOG_ERROR, "无法获取资源信息\n");return;}// 查找视频流int video_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if (video_stream_index < 0){av_log(NULL, AV_LOG_ERROR, "无法找到视频流\n");return;}// 查找解码器const AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_stream_index]->codecpar->codec_id);if (!codec){av_log(NULL, AV_LOG_ERROR, "无法找到解码器\n");return;}// 创建解码器上下文AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx){av_log(NULL, AV_LOG_ERROR, "无法创建解码器上下文\n");return;}// 填充解码器上下文if (avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar) < 0){av_log(NULL, AV_LOG_ERROR, "无法填充解码器上下文\n");return;}// 打开解码器if (avcodec_open2(codec_ctx, codec, nullptr) < 0){av_log(NULL, AV_LOG_ERROR, "无法打开解码器\n");return;}// 打开输出文件std::ofstream outfileSream(outfile, std::ios::binary);if (!outfileSream.is_open()){av_log(NULL, AV_LOG_ERROR, "无法打开输出文件\n");return;}// 读取数据包AVPacket *pkt = av_packet_alloc(); // 分配数据包AVFrame *frame = av_frame_alloc(); // 分配帧int ret = 0;while (av_read_frame(fmt_ctx, pkt) >= 0){// 解码ret = avcodec_send_packet(codec_ctx, pkt); // 发送数据包到解码器if (pkt->stream_index != video_stream_index)continue; // 不是视频流就跳过if (ret < 0){av_log(NULL, AV_LOG_ERROR, "解码失败\n");break;}while (ret >= 0){ret = avcodec_receive_frame(codec_ctx, frame); // 从解码器接收解码后的帧if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){av_log(NULL, AV_LOG_ERROR, "解码失败\n");break;}// 写入文件for (int i = 0; i < frame->height; i++){outfileSream.write((char *)frame->data[0] + i * frame->linesize[0], frame->width); // 写入Y分量}for (int i = 0; i < frame->height / 2; i++){outfileSream.write((char *)frame->data[1] + i * frame->linesize[1], frame->width / 2); // 写入U分量}for (int i = 0; i < frame->height / 2; i++){outfileSream.write((char *)frame->data[2] + i * frame->linesize[2], frame->width / 2); // 写入V分量}av_frame_unref(frame); // 清空帧}av_packet_unref(pkt); // 清空数据包}// 清空编码器ret = avcodec_send_packet(codec_ctx, nullptr);while (ret >= 0){ret = avcodec_receive_frame(codec_ctx, frame);if (ret < 0)break;// 写入文件for (int i = 0; i < frame->height; i++){outfileSream.write((char *)frame->data[0] + i * frame->linesize[0], frame->width); // 写入Y分量}for (int i = 0; i < frame->height / 2; i++){outfileSream.write((char *)frame->data[1] + i * frame->linesize[1], frame->width / 2); // 写入U分量}for (int i = 0; i < frame->height / 2; i++){outfileSream.write((char *)frame->data[2] + i * frame->linesize[2], frame->width / 2); // 写入V分量}}qDebug() << "解码完成";// 释放资源av_frame_free(&frame);av_packet_free(&pkt);outfileSream.close();avcodec_free_context(&codec_ctx);avformat_close_input(&fmt_ctx);
}

编码视频

void Widget::H264Encode()
{char errorBuffer[1024];const char *inputFile = "../../source/video.yuv";const char *outputFile = "../../output/out.h264";// 查找H264编码器const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!codec){qDebug() << "找不到编码器";return;}// 创建编码器上下文AVCodecContext *codecContext = avcodec_alloc_context3(codec);if (!codecContext){qDebug() << "创建编码器上下文失败";return;}// 设置编码器参数codecContext->profile = AV_PROFILE_H264_HIGH_444; // 设置编码器压缩级别codecContext->level = 50;                         // 设置编码器视频质量级别,50表示5.0codecContext->width = 1280;                       // 设置编码器视频宽度codecContext->height = 720;                       // 设置编码器视频高度codecContext->pix_fmt = AV_PIX_FMT_YUV420P;       // 设置编码器视频像素格式codecContext->time_base = AVRational{1, 30};      // 设置编码器时间基准codecContext->framerate = AVRational{30, 1};      // 设置编码器帧率codecContext->gop_size = 30;                      // 设置编码器I帧间隔codecContext->max_b_frames = 3;                   // 设置编码器B帧最大数量codecContext->keyint_min = 3;                     // 设置编码器最小I帧间隔codecContext->has_b_frames = 1;                   // 设置编码器是否支持B帧codecContext->refs = 3;                           // 设置编码器参考帧数量codecContext->bit_rate = 2000000;                 // 设置编码器比特率// 打开编码器CoUninitialize(); // 初始化COM库,头文件<objbase.h>,这里防止报错COM must not be in STA modeint ret = avcodec_open2(codecContext, codec, nullptr);if (ret < 0){av_strerror(ret, errorBuffer, sizeof(errorBuffer));qDebug() << "打开编码器失败:" << errorBuffer;return;}// 打开输入文件FILE *inputFilePtr = fopen(inputFile, "rb");if (!inputFilePtr){qDebug() << "Could not open input file";return;}// 打开输出文件FILE *outputFilePtr = fopen(outputFile, "wb");if (!outputFilePtr){qDebug() << "Could not open output file";return;}// 创建AVFrame对象AVFrame *frame = av_frame_alloc();if (!frame){qDebug() << "Could not allocate video frame";return;}frame->format = codecContext->pix_fmt;frame->width = codecContext->width;frame->height = codecContext->height;// 分配AVFrame对象缓冲区ret = av_frame_get_buffer(frame, 0);if (ret < 0){av_strerror(ret, errorBuffer, sizeof(errorBuffer));qDebug() << "不能为帧分配缓冲区:" << errorBuffer;return;}// 创建AVPacket对象AVPacket *packet = av_packet_alloc();if (!packet){qDebug() << "不能分配数据包";return;}// 读取输入文件数据int ySize = codecContext->width * codecContext->height;int frameCount = 0;while (!feof(inputFilePtr)){fread(frame->data[0], ySize, 1, inputFilePtr);     // Y分量fread(frame->data[1], ySize / 4, 1, inputFilePtr); // U分量fread(frame->data[2], ySize / 4, 1, inputFilePtr); // V分量frame->pts = frameCount;                           // 设置帧时间戳// 编码一帧数据ret = avcodec_send_frame(codecContext, frame);if (ret < 0){av_strerror(ret, errorBuffer, sizeof(errorBuffer));qDebug() << "编码失败:" << errorBuffer;return;}while (ret >= 0){ret = avcodec_receive_packet(codecContext, packet);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){av_strerror(ret, errorBuffer, sizeof(errorBuffer));qDebug() << "编码失败:" << errorBuffer;return;}// 写入输出文件fwrite(packet->data, packet->size, 1, outputFilePtr);av_packet_unref(packet);}frameCount++; // 帧计数器加1}// 清空编码器ret = avcodec_send_frame(codecContext, nullptr);while (ret >= 0){ret = avcodec_receive_packet(codecContext, packet);if (ret < 0)break;fwrite(packet->data, packet->size, 1, outputFilePtr); // 写入输出文件}// 释放资源av_packet_free(&packet);av_frame_free(&frame);avcodec_free_context(&codecContext);fclose(inputFilePtr);fclose(outputFilePtr);qDebug() << "编码完成!";
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/498105.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机的错误计算(一百九十六)

摘要 用两个大模型计算 arccos(0.444). 结果保留 4位有效数字。两个大模型的计算结果相同&#xff0c;并均有误差。 例1. 计算 arccos(0.444). 结果保留 4位有效数字。 下面是与一个大模型的对话。 以上为与一大模型的对话。 下面是与另一大模型的对话。 点评&#xff1a; &…

【pytorch】循环神经网络

如果说卷积神经网络可以有效地处理空间信息&#xff0c;那么循环神经网络则可以更好地处理序列信息。循环神经网络通过引入状态变量存储过去的信息和当前的输入&#xff0c;从而可以确定当前的输出。 1 循环神经网络 隐藏层和隐状态指的是两个截然不同的概念。隐藏层是在从输…

MySQL root用户密码忘记怎么办(Reset root account password)

在使用MySQL数据库的的过程中&#xff0c;不可避免的会出现忘记密码的现象。普通用户的密码如果忘记&#xff0c;可以用更高权限的用户&#xff08;例如root&#xff09;进行重置。但是如果root用户的密码忘记了&#xff0c;由于root用户本身就是最高权限&#xff0c;那这个方法…

GPU 进阶笔记(二):华为昇腾 910B GPU

大家读完觉得有意义记得关注和点赞&#xff01;&#xff01;&#xff01; 1 术语 1.1 与 NVIDIA 术语对应关系1.2 缩写2 产品与机器 2.1 GPU 产品2.2 训练机器 底座 CPU功耗操作系统2.3 性能3 实探&#xff1a;鲲鹏底座 8*910B GPU 主机 3.1 CPU3.2 网卡和网络3.3 GPU 信息 3.3…

word中插入zotero引用

1、参考文献末尾没有文献&#xff1f; 在文献条目要显示的地方点击“refresh” 2、参考文献条目没有悬挂缩进&#xff1f; 把“书目”添加到样式库中&#xff0c;修改样式为悬挂缩进1.5字符 3、交叉引用&#xff1f; 宏 新建一个宏 粘贴下面代码 Public Sub ZoteroLinkCita…

【服务器项目部署】⭐️将本地项目部署到服务器!

目录 &#x1f378;前言 &#x1f37b;一、服务器选择 &#x1f379; 二、服务器环境部署 2.1 java 环境部署 2.2 mysql 环境部署 &#x1f378;三、项目部署 3.1 静态页面调整 3.2 服务器端口开放 3.3 项目部署 ​ &#x1f379;四、测试 &#x1f378;前言 小伙伴们大家好…

【mysql】MVCC及实现原理

【mysql】MVCC及实现原理 【一】介绍【1】什么是MVCC【2】什么是当前读和快照读【3】当前读&#xff0c;快照读和MVCC的关系【4】MVCC 能解决什么问题&#xff0c;好处&#xff08;1&#xff09;数据库并发场景有三种&#xff0c;分别为&#xff1a;&#xff08;2&#xff09;M…

AI对话机器人简单实现--智谱BigModel+SpringBoot+Vue2+ElementUI

成品展示 一、首先去注册个账号然后申请个API keys 二、引入依赖 <dependency><groupId>cn.bigmodel.openapi</groupId><artifactId>oapi-java-sdk</artifactId><version>release-V4-2.3.0</version></dependency><depend…

FPGA多路红外相机视频拼接输出,提供2套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的所有工程源码总目录----方便你快速找到自己喜欢的项目我这里已有的红外相机图像处理解决方案本博已有的已有的FPGA视频拼接叠加融合方案 3、工程详细设计方案工程设计原理框图红外相机FDMA多路视频拼接算法FDMA图像缓存视…

Navicat 连接 SQL Server 详尽指南

Navicat 是一款功能强大的数据库管理工具&#xff0c;它提供了直观的图形界面&#xff0c;使用户能够轻松地管理和操作各种类型的数据库&#xff0c;包括 SQL Server。本文将详尽介绍如何使用 Navicat 连接到 SQL Server 数据库&#xff0c;包括安装设置、连接配置、常见问题排…

【JAVA】Java常用注解汇总

一、注解的定义 Java注解是Java编程语言中的一种特殊形式的元数据&#xff0c;它们可以用于为程序的各个元素&#xff08;例如类、方法、字段等&#xff09;添加额外的信息和属性。注解是在Java 5中引入的&#xff0c;通过在代码中使用注解&#xff0c;开发人员可以提供关于程…

debian安装Nginx

编译安装Nginx sudo apt-get update 环境准备 编译Nginx需要gcc的环境支持&#xff0c;build-essential内包含gcc套件&#xff0c;所以我们安装build-essential即可&#xff1a; sudo apt-get install build-essential 因为nginx.conf中使用了正则表达式&#xff0c;所以编…

基于PLC的电梯控制系统(论文+源码)

1.系统设计 电梯采用了PLC控制方式&#xff0c;通过对PLC进行逻辑程序设计&#xff0c;电梯不仅在控制水平上得到了质的提升&#xff0c;同时在安全性上也得到了大大提高。控制系统在构造上实现了简洁化&#xff0c;不仅优化了硬件接线方便了线路施工&#xff0c;同时对控制要…

MySQL从入门到入土---MySQL表的约束 (内含实践)---详细版

目录 引入&#xff1a; null 与not null default&#xff1a; comment列描述 &#xff1a; not null 和 default&#xff1a; zerofill &#xff1a; 主键&#xff1a;primary key 复合主键&#xff1a; 自增长:auto_increment 唯一键&#xff1a;unique key 外键&a…

linux安装nginxs报错:openssl not found

系统&#xff1a; linux 版本&#xff1a;centOS7 nginx版本&#xff1a;nginx-1.20.2 linux安装nginx时 执行下面命令时报错&#xff1a; ./configure --with-http_stub_status_module --with-http_ssl_module --prefix/usr/local/nginxchecking for OpenSSL library ... not …

Flutter:打包apk,详细图文介绍

困扰了一天&#xff0c;终于能正常打包apk安装了&#xff0c;记录下打包的流程。建议参考我这篇文章时&#xff0c;同时看下官网的构建说明。 官网构建并发布 Android 应用详情 1、AS创建Flutter项目 2、cmd执行命令 生成一个sunluyi.jks的文件&#xff0c;可以自行把sunluyi替…

shell命令以及运行原理

目录 一、命令解释器 1、什么是命令行解释器 shell和bash联系 2、为什么用命令行解释器 作用 存在意义 二、Linux权限 1、用户分类 2、Linux权限管理 1&#xff09;权限身份 2&#xff09;文件类型和访问权限 3&#xff09;文件访问权限的相关设置方法 a. chmod …

精准识别花生豆:基于EfficientNetB0的深度学习检测与分类项目

精准检测花生豆&#xff1a;基于EfficientNet的深度学习分类项目 在现代农业生产中&#xff0c;作物的质量检测和分类是确保产品质量的重要环节。针对花生豆的检测与分类需求&#xff0c;我们开发了一套基于深度学习的解决方案&#xff0c;利用EfficientNetB0模型实现高效、准…

CSS利用浮动实现文字环绕右下角,展开/收起效果

期望实现 文字最多展示 N 行&#xff0c;超出部分截断&#xff0c;并在右下角显示 “…” “更多”&#xff1b; 点击更多&#xff0c;文字展开全部内容&#xff0c;右下角显示“收起”。效果如下&#xff1a; 思路 尽量使用CSS控制样式&#xff0c;减少JS代码复杂度。 利…

FOC控制原理-HALL传感器测量电角度

0、相关文章 【电机控制算法】基于霍尔位置传感器(HALL)估算连续电角度&#xff08;基于STM32F407CubeMXHAL&#xff09;_峰岹hall-CSDN博客 电机控制【FOC】_SimpleFOC_通过 Hall 计算电机角度和速度原理 - 大大通(简体站) (wpgdadatong.com.cn) STM32 FOC SDK2.0中使用hall传…