
上个月接到一个视频算法测试任务输入是一堆裸YUV序列要求用libx265把它们压缩成H.265码流再交给后续的识别模型做输入验证。其实这类工作在视频编解码和算法团队里太常见了YUV是摄像头、视频采集卡、标准测试序列最常见的原始格式而H.265HEVC是当前兼容性和压缩效率平衡得最好的编码标准之一用libx265对YUV做编码几乎是视频领域的“基本功”。但真正动手做一遍才发现难点根本不在编码器本身而在你手上的YUV到底是什么格式分辨率、位深、色度采样、存储布局任何一个搞错出来的画面就是一片花绿而你还在怀疑是编码器坏了。这篇文章我会把整个流程完整过一遍从YUV格式判断、libx265环境搭建到参数选择和实际编码最后附上我踩过的坑和排查思路适合刚开始接触编码的测试工程师、算法同学也适合需要对视频素材做归档压缩的开发者直接照抄。1. 项目背景与核心思路拆解1.1 为什么原始YUV必须压缩一秒钟90MB的账要算清开篇先算一笔账。假设你手里是一段1920×1080、30帧每秒、8bit的YUV420序列。一帧的大小是1920×1080×1.5字节算下来2,073,600×1.53,110,400字节约2.97MB。每秒30帧就是约89MB十分钟就是53GB多。我见过不少团队直接把YUV文件扔在NAS上共享没几天磁盘就爆了。而用libx265编码成H.265后同样内容按CRF26中等质量压下来10分钟1080p视频通常只有200MB上下压缩比能达到一两百比一。这个差距对于存储、传输、以及后续算法读取I/O的影响是决定性的。H.265本身相比H.264在同等主观质量下一般能省30%~50%码率。既然YUV是未压缩的原始素材编码目标就非常明确用合理的码率保留视觉关键信息同时去掉人眼不敏感的空间细节、时间冗余和色彩冗余。这个“去冗余”的过程就是H.265编码器的核心工作也是后面参数调优要围绕的主线。1.2 项目整体思路与工具选型我这次的项目需求分三层输入是多个标准YUV测试序列格式不统一有YUV420PI420也有NV12还有一路是10bit的YUV444P。需要把所有内容编码为H.265码流输出用于算法模型的离线验证因此对画质有一定要求又不能太大。部分素材最后要封装成MP4交给其他同学播放检查所以除了裸流还要能快速封装。基于这三点工具链其实没什么悬念编码核心选用libx265原因有三它是目前开源生态里最成熟的H.265编码实现编码质量、参数灵活性、更新频率都靠谱而且x265的命令行工具本身可以直接吃裸YUV输入不需要额外封装同时它又能作为FFmpeg的第三方编码器批量处理和封装一条命令搞定。所以我最终使用了x265 CLI做裸流编码再配合FFmpeg完成封装。整套流程不需要写一行代码全部靠命令行完成。1.3 我最终确定的处理链路整个链路大概是这样的第一步拿到YUV文件先用工具计算尺寸、确认格式必要时截取几帧转成PNG肉眼检查第二步按目标格式选择对应的x265参数输出HEVC裸流第三步核对输出文件的码率、分辨率、帧率是否与预期一致第四步需要封装时用FFmpeg把裸流包装成MP4或者MKV并带上正确的色彩元数据。到这一步项目里原本“素材太大无法共享”“读取耗时太长”的问题就基本解决了。后面的大部分工作其实都是在跟YUV格式和x265参数做搏斗。2. YUV格式基础一帧数据到底怎么算这部分我觉得必须先说清楚因为后面所有参数设定都建立在这个基础上。YUV不是某一种固定格式而是一个大家族。你在命令行里写错一个像素格式编码器不会报错但画面就是花的。2.1 常见采样格式与存储布局先明确三个概念采样格式、位深、存储布局。采样格式决定了一个像素用几个字节表示。以最常用的YUV420为例它每4个亮度像素Y共用一组色度像素Cb、Cr所以每个像素平均占1.5字节。YUV422则是每两个亮度像素共用一组色度每像素平均2字节。YUV444是每个像素都有独立的色度每像素平均3字节。位深方面8bit每分量一个字节10bit每分量两个字节文件体积直接翻倍。存储布局是另一个容易踩坑的地方。同样是YUV420可以分成好几类I420也叫YUV420P先存全部Y再存全部U最后存全部V三个平面依次排列YV12也是三个平面但顺序是Y、V、UNV12Y平面完整存储然后是一个交织的UV平面按U、V、U、V顺序排列这叫半平面格式也叫semi-planarNV21同上但交织顺序是V、U。摄像头采集和常见播放器里NV12很常见标准测试序列和FFmpeg默认的rawvideo输出经常是I420。如果你搞混了尤其把NV12当成I420喂给x265画面会直接变成类似“调错色的马赛克”。2.2 帧大小与时长的快速计算任何YUV文件在编码前务必先确认格式和总帧数。计算公式其实很简单yuv420一帧字节数 宽 × 高 × 3 ÷ 2 yuv422一帧字节数 宽 × 高 × 2 yuv444一帧字节数 宽 × 高 × 3如果是10bit记得再乘以2。文件总字节数除以单帧字节数就能得到总帧数再除以帧率就是视频时长。我一般会用Python先算一遍import os width, height, fps 1920, 1080, 30 frame_size width * height * 3 // 2 # 8bit yuv420 total_frames os.path.getsize(input.yuv) // frame_size duration_seconds total_frames / fps print(ftotal_frames{total_frames}, duration{duration_seconds:.2f}s)这一步看似简单但实际项目中经常发现文件大小对不上帧数最后排查出来是文件末尾多了几个字节的padding或者没清干净的缓存。所以编码前先算帧数宁可多花一分钟也不要让编码器在中途突然读到不完整的数据。2.3 色域转换与Matlab里的坑热词里有人搜matlab中rgb、yuv、lab色域我顺手多说一句。很多人拿Matlab做图像研究时以为rgb2ycbcr出来的就是YUV然后直接把结果存成.yuv文件喂给编码器结果颜色不对。这里有两个常见坑第一Matlab的rgb2ycbcr默认用的是BT.601标准的limited range也就是Y的范围是16~235Cb/Cr的范围是16~240。而很多编码器默认假定输入是全范围full range0~255或者反过来。范围不匹配的直接后果是画面发灰、黑色不黑、白色不白。第二Matlab里还有一个rgb2lab这个Lab和视频编码用的YUV完全不是一回事。Lab是感知均匀颜色空间主要用于颜色度量不能直接当成YUV喂给编码器。做视频编码时统一用YCbCr这一套并且要清楚自己素材的色彩原语color primaries、传递函数transfer characteristics和矩阵系数matrix coefficients是什么。x265里对应的就是--colorprim、--transfer、--colormatrix三个参数后面我会再讲到。3. libx265环境准备与验证说回正题实操第一步是保证手里有一个能用的libx265编码器。3.1 编译安装libx265libx265的官方下载地址是VideoLAN的仓库GitHub上也有镜像。如果你在Ubuntu上开发最省事的方式是直接用包管理器sudo apt install x265这条命令会同时装上x265命令行程序。验证一下版本x265 --version但有个问题发行版仓库里的x265版本往往偏旧而且默认只编译了8bit支持。如果你要处理10bit甚至12bit的YUV建议还是自己编译。编译流程不复杂git clone https://bitbucket.org/multicoreware/x265_git.git cd x265_git/build/linux ./make-Makefiles.bash make -j$(nproc) sudo make install编译时需要注意CLI程序默认会和静态库一起生成路径一般在build/linux/x265。如果你只想要命令行工具编译完直接在build目录里取x265这个可执行文件就能用不需要安装。如果需要集成到FFmpeg里则在配置FFmpeg时通过--enable-libx265打开并把x265的头文件和库路径指对。3.2 集成进FFmpeg并确认编码器可用我通常的策略是x265 CLI用于裸流精细调参FFmpeg用于批处理和封装。所以两个都要。集成方式git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg cd ffmpeg ./configure --enable-gpl --enable-libx265 --enable-libx264 make -j$(nproc)如果你用的是发行版FFmpeg可以先检查有没有带libx265ffmpeg -encoders | grep x265能输出libx265这一行就说明可以直接用。如果只有hevc而没有libx265说明你的FFmpeg编译时没开第三方库至少要在调参灵活性上打折扣。还有一个小细节无论是x265 CLI还是FFmpeg的libx265对多核CPU的利用都很好编码前最好在BIOS/系统中检查一下CPU频率策略别让睿频被限制否则编码速度会莫名慢很多。3.3 测试数据应该怎么准备环境就绪后先别急着拿全部素材试。我建议准备一个小尺寸的测试片段比如截取YUV文件的前30帧存成一个小文件用来调参。截帧可以用FFmpegffmpeg -f rawvideo -pix_fmt yuv420p -s 1920x1080 -r 30 -i input.yuv -frames:v 30 -c:v rawvideo test_30f.yuv或者直接用dd按字节数截取先算出30帧的字节数再dd ifinput.yuv oftest_30f.yuv bs3110400 count30测试片段能让你在几秒内验证参数而不是每次等十几分钟。我在实际项目里总结的经验是小片段验证、大文件全量是避免浪费时间的最佳节奏。4. 编码实操命令行参数逐项拆解现在进入正题x265命令行到底怎么写参数怎么选。这里我全程用x265 CLI演示因为它的YUV输入参数最直接。4.1 最基础的编码命令假设你有一个1920×1080、30fps、8bit、I420格式的输入input.yuv最基础的编码命令是x265 --input input.yuv --input-res 1920x1080 --fps 30 --input-csp i420 --input-depth 8 --crf 26 --preset medium --output output.hevc拆开看几个关键项--input输入YUV路径--input-res宽高必须填编码器无法从裸流里猜出分辨率--fps帧率必须填编码器需要依赖帧率计算码率和时间戳--input-csp输入色度格式i420对应I420nv12对应NV12i422对应YUV422Pi444对应YUV444P--input-depth输入位深8bit默认可以不写10bit必须写10--crf恒定质量因子--preset速度/压缩率平衡档位--output输出HEVC裸流文件后缀一般是.hevc或者.h265。这条命令跑完后你会得到一个不含封装信息的HEVC裸流。裸流可以用播放器直接播放吗很多播放器支持但更稳妥的做法是封装成MP4后面我会演示。4.2 码率控制方式怎么选x265的码率控制大体分三类按使用频率排第一是CRFConstant Rate Factor恒定质量模式。你指定一个质量值编码器根据画面复杂度自动分配码率复杂场景多给码率简单场景少给。这是我最常用的模式适合原始素材归档、算法测试这类“只看质量不看码率”的场景。CRF值范围0~51越小越清晰默认是28。我实测下来普通视频用26到28都能接受24以下肉眼就很难看出差别了但文件会大不少。如果对质量要求高且存储不敏感可以压到20左右。第二是ABRAverage Bitrate平均码率模式。你给出期望码率比如--bitrate 5000表示平均5Mbps。编码器会尽量把码率控制在目标附近。适合有明确带宽限制的场景比如流媒体预设。第三是CBRConstant Bitrate恒定码率通常在--bitrate基础上配合--vbv-maxrate和--vbv-bufsize一起用。它保证码率波动很小适合网络传输但相对CRF来说画质利用率低。给个参考选型如果你只想快速跑通用CRF26如果是为了对比算法在固定码率下的表现ABR5000或者8000更合适。4.3 preset、profile、level这些约束怎么配合--preset控制的是编码器的搜索复杂度。预设从ultrafast到veryslow常见的有ultrafast、fast、medium、slow、veryslow。档位越高编码越慢压缩率越好。我的经验是ultrafast只用来验证流程是否跑通fast日常预览和格式转码够用medium默认档省心绝大多数场景都能接受slow最终归档或者对码率敏感的项目用速度慢30%~50%但同码率下质量会好一点veryslow除非批量离线且时间充足否则不建议日常用收益不明显。--profile限制的是输出流的兼容级别。8bit YUV420对应main10bit YUV420对应main108bit YUV444对应main444。如果你写--profile main10x265会自动把输出位深设为10bit但前提是已经编译了10bit支持。--level对应HEVC的级别比如4.1是1080p广播常见级别限制了解码端的帧尺寸和码率上限。一般本地使用可以不写level编码器会根据分辨率自动选一个。4.4 GOP、B帧、参考帧对产物影响编码参数里还有一组影响画质和随机访问能力的选项虽然不写也有默认值但我建议理解一下。--keyint是GOP大小也就是两个关键帧之间的最大距离默认250帧。关键帧间隔越大相同画质下码率越低但拖动进度条时越费劲。如果视频要剪辑、打点或做逐帧分析建议把keyint设成帧率的两倍左右比如--keyint 60。--bframes是连续B帧数量默认4。B帧能大幅提高压缩效率但会增加解码延迟。低延迟场景建议设0容忍延迟且追求压缩率的场景保留默认即可。--ref是参考帧数量影响运动估计的精度值越大压缩率越高但更慢。默认值是4medium预设下已经比较均衡不需要特别调。还有一个容易被忽略的--tune参数。对画面里有大量胶片颗粒或者传感器噪声的视频加--tune grain会保留更多纹理细节而不是把细节抹成块。对打算算PSNR/SSIM的实验可以加--tune psnr或--tune ssim让编码器针对该指标优化但肉眼观感不一定最好。5. 完整实操过程与效果记录下面用一个实际例子完整走一遍。5.1 输入YUV先做一次体检我这次拿到的文件是basketball_playground_1920x1080.yuv大小加总后发现300帧左右格式从文件名推断是YUV420P。我先用Python算了一下单帧大小单帧1920×1080×3/23110400字节300帧就是933120000字节约0.87GB。然后用FFmpeg截了一帧转成PNG看一眼ffmpeg -f rawvideo -pix_fmt yuv420p -s 1920x1080 -r 30 -i basketball_playground.yuv -frames:v 1 first_frame.png打开PNG确认内容正常没有花屏和横纹说明I420格式的假设成立。这一步千万别省尤其是素材来源不统一的时候。5.2 实战编码命令和输出分析体检通过后我用以下命令编码x265 --input basketball_playground.yuv \ --input-res 1920x1080 \ --fps 30 \ --input-csp i420 \ --frames 300 \ --crf 26 \ --preset medium \ --output basketball_playground.hevc编码完成后x265会在标准输出里打印很多统计信息包括“encoded 300 frames”和最终码率比如某次实测的产物是4.8Mbps左右。换算一下输出文件大小4.8Mbps×10秒/86MB。原来的0.87GB压到6MB压缩比约150:1这个量级基本符合预期。这里要注意CRF模式下的码率会随内容复杂度浮动。我另一个全是白墙人走动的测试序列CRF28压下来只有1Mbps而一个镜头快速运动的序列CRF28飙到了12Mbps。所以只看“目标CRF”不看内容没法预先知道确切码率这很正常。5.3 用ffmpeg封装成MP4并验证裸流编码完成后我为了给同事播放还封装成了MP4ffmpeg -f hevc -i basketball_playground.hevc \ -c:v copy \ -color_primaries bt709 \ -color_trc bt709 \ -colorspace bt709 \ basketball_playground.mp4这里的-c:v copy是直接拷贝码流不重新编码速度极快。加color相关参数是为了把色彩元数据写进容器保证播放器按BT.709来显示。如果你不确定源视频的色彩空间宁可不要乱加让播放器自己猜也不要加错导致颜色异常。封装完再验证一下ffprobe basketball_playground.mp4看输出里的分辨率、帧率、编码器名称、像素格式是否都正确。确认无误后这个项目的数据处理部分就算闭环了。6. 常见问题与排查技巧实录这部分是纯经验我按遇到过的频率排序。6.1 花屏、绿屏、马赛克花屏九成是输入参数和文件实际格式不一致。最常见的是把NV12当成I420喂或者把YUV422当成YUV420喂。x265不会报错它只会把字节流硬按你指定的格式解释结果就是满屏色块。排查步骤确认文件命名或者采集设备说明里的格式用FFmpeg按你假定的格式截一帧转成PNG看是否正常如果不正常换一种pix_fmt再试多试几次就能对上。我还遇到过一种特殊花屏前面正常到某帧之后突然花了。这种情况多半是文件被截断或者编码参数里的--frames超过了实际帧数。用前面算好的帧数仔细核对。6.2 颜色不对、发灰、发绿颜色问题要分两类看。一类是色度格式搞错比如颜色看起来是“糊的”“彩虹纹”那是Cb/Cr平面排列不对。另一类是整体偏灰、偏暗那基本是range问题。x265 CLI默认假定输入是limited range还是full range不同版本行为不一样最稳妥的做法是在编码时显式指定--range limited # 或 --range full如果你源是全范围素材但编码器按limited处理画面就会变灰。FFmpeg侧也是这样-color_range和输入输出的像素格式转换会影响最终效果。我建议建立一套固定认知采集设备、标准测试序列多数是limited range自己用Matlab或Python生成的YUV往往是full range。编码前把range这事确认清楚能省很多扯皮。6.3 编码速度慢、CPU占用低x265本身就慢这是H.265复杂度决定的。但如果你发现CPU占用率不到50%检查一下是不是用了--pools限制线程或者输入分辨率不是常见的偶数尺寸导致分块不均衡。还有一种情况是编译时没开-DCMAKE_BUILD_TYPEReleaseDebug版性能差得很离谱重新编译即可。在确定要跑大文件之前先用小片段试一次记录耗时可以粗略估算整体时间。比如30帧medium预设耗时20秒那3000帧就是2000秒左右提前评估掉时间和资源避免编码跑到一半超时。6.4 解码播放不正常裸流文件后缀如果是.hevc部分播放器可能识别不了。优先封装成MP4或MKV。如果封装后还是不能播放先看是不是用了10bit输出而播放器不支持或者在FFmpeg里强行转成8bit再播放。只要确认ffprobe能看到正常的编码信息问题基本就在播放器侧换VLC或者MPV基本能解决。7. YUV到H.265编码原理与调优心得内容走到这里我把背后的原理稍微串一下。7.1 H.265编码原理速览从宏块到CUH.265和H.264类似都采用混合编码框架但H.265把宏块概念扩展成了更灵活的编码树单元Coding Tree UnitCTU。编码器会把图像切成一个个64×64甚至更大的CTU然后用四叉树递归划分成不同大小的编码单元CU大块用于平坦区域小块用于细节丰富的区域这样能更精准地分配码率。每一帧的图像一部分使用帧内预测参考当前帧已经编码好的相邻像素从33种角度预测模式加上Planar和DC共35种模式里选最合适的得到残差。另一部分使用帧间预测在参考帧里做运动估计找到最匹配的块记录运动矢量残差用变换、量化、熵编码处理最后再经过环路滤波去块效应和SAO补偿输出码流。这整个流程其实就是在做“去冗余”帧内预测去除空间冗余帧间预测去除时间冗余色度下采样和变换量化去除人眼不敏感的视觉冗余。回到项目里我们提供的YUV恰好是编码器最舒服的输入形态亮度和色度已经分离色度通常也做了下采样编码器可以直接进入预测、变换流程不需要像RGB输入那样还要先做色彩空间转换。这也是为什么很多编码测试序列都直接提供YUV而不是RGB。7.2 日常调优的个人经验最后说几条我自己的调优经验可能不全面但很实用。第一CRF和preset要一起考虑。高速预览用--preset ultrafast --crf 30正式归档用--preset medium --crf 24或--preset slow --crf 26稳定且效果好。不要单独只调CRF不同preset下的相同CRF画质不一定等价。第二如果你打算把编出来的素材再二次编辑建议CRF不超过23最好用--tune grain保留细节因为二次编码会放大第一次的损失。若只是分发播放CRF26到28就够没有必要追求视觉无损。第三批量转码时我习惯先跑一遍FFprobe把每个YUV的分辨率、帧率、格式都记下来生成一个参数表再根据参数表批量生成x265命令。这样可以避免手动改参数时出现低级错误。第四别忘了日志。x265默认会在编码结束后打印平均码率、PSNR、SSIM等指标。把这些输出重定向到文件x265 ... encode_log.txt 21日志里能看到每帧ENCODE TIME、帧类型、码率分配遇到质量异常时回溯定位非常有用。最后一个小技巧在编码前用FFmpeg截一段30帧的小YUV单独试参数既快又安全。这个习惯帮我省下过无数个小时希望你也能用上。