资讯动态

率失真曲线绘制实战:PCHIP插值平滑与Matlab/Python对比技巧

发布时间:2026/10/13 12:08:20 来源:尧图企业网站定制
做压缩算法评估的同学对“率失真曲线图”肯定不陌生。横轴是码率纵轴是失真或质量散点是编码器在不同配置下的实测工作点干这行的人每天都得盯着它看。但真正画过的人都知道直接拿这些散点连线出来的图基本没法用——不是锯齿明显就是曲线回弹甚至出现码率高了画质反而变差的荒谬结果。如果再加上多条算法对比、不同码率范围参差不齐、还要导出印刷级矢量图那这事儿就得好好梳理一遍了。这篇文章把我自己在Matlab和Python两套环境下的完整绘图经验写出来重点解决三个问题散点怎么连才平滑、多条曲线怎么对比才公平、参数怎么选才能保证“既好看又不失真”。内容适合正在写论文、做汇报、准备技术评审的同行参考基础是中等偏上但我会把每一步的原理和代码都写透确保拿来就能用。1. 率失真曲线的本质需求与常见误区1.1 率失真曲线到底在表达什么率失真曲线的理论源头是信息论里的率失真函数描述的是在给定失真上限的条件下信源压缩所需的最小码率。落到工程实践里我们通常不是算那条理想边界而是拿一套编码器在不同量化参数下跑实验得到一组码率和质量配对的工作点。质量指标常用PSNR、SSIM码率就是输出比特流的大小除以时长。举个例子某实验室的A同学做视频编码优化他设定的量化参数从20到45每5取一个档位就能得到6个码率PSNR点。把这6个点画出来用一条平滑曲线连起来就是这条编码配置的率失真曲线。横轴可以是kbps纵轴是dB曲线越靠左上说明用更小的码率换到了更高的质量算法性能越好。这个图的用途很广横向对比多个编码方案看谁在同等码率下质量更高验证优化方向是否有效比如改进后的曲线是否整体左移为后面的BD-rate计算提供数据基础。但恰恰是“把点连成曲线”这个动作最容易翻车。1.2 为什么直接连线会翻车很多同学拿到数据后第一件事就是用plot把点连起来然后发现结果惨不忍睹。原因主要有三个。第一工作点太少。编码实验通常只跑5到8个QP点直接线性连接得到的是折线看起来像心电图完全没有科研论文里那种丝滑的曲线感。如果你用三次样条硬插点太少还会出现剧烈的过冲振荡。第二数据不单调。率失真理论保证最优编码器下质量应该随码率单调递增但实测数据因为编码器实现、内容特性、统计误差等原因经常出现局部抖动。直接连接会导致曲线来回扭曲审稿人一眼就能看出问题。第三横坐标的分布不均匀。码率在低QP段可能跳得很快比如从200跳到800而在高QP段则挤在一起。这种非均匀分布会让线性插值在曲率变化大的地方严重失真。1.3 插值、拟合和平滑三件事别混为一谈画率失真曲线时有一个基本功必须清楚插值、拟合和平滑是完全不同的三件事混用是新手最常见的错误。插值让曲线精确穿过每一个已知点不改变原始数据的位置。常用PCHIP、Cubic Spline。适合工作点本身可靠、误差小的情况。拟合用一个带参数的模型去逼近数据不要求过点比如多项式拟合、指数拟合。适合数据噪声大、需要提炼趋势的场景。平滑对已有序列做滤波比如移动平均、Savitzky-Golay、LOWESS。它的目标是去掉局部抖动保留整体形状。率失真曲线图的正确打开方式一般是用“插值”而不是拟合或平滑。因为你的工作点是编码器实测出来的本身就代表真实性能不能随意篡改数值但相邻点之间可以用单调插值补出平滑的形状。只有当你的数据是从大批随机样本统计得到、点位严重噪声时才考虑平滑处理。2. 数据准备与坐标体系的处理逻辑2.1 实验数据怎么整理最省心科研绘图最费时间的永远不是画图本身而是数据整理。率失真曲线的输入数据一般长这样配置量化参数码率(kbps)PSNR(dB)方案A4515231.26方案A4031834.41方案A3556136.84方案A3093238.67方案A25168040.23方案A20286041.35我自己惯用的方式是导出一份CSV列分别是method, qp, rate, metric。然后在Matlab或Python里按方法分组再按rate排序。不要直接在画图代码里硬编码数据因为一张图里通常有多条曲线硬编码改起来非常痛苦。排序问题尤其要留意。如果不排序直接传给插值函数结果会是一团乱麻。因为插值函数默认横坐标是升序的而实验日志里的顺序往往是按配置顺序记录的不一定和码率大小一致。2.2 横轴要不要取对数率失真曲线的横轴经常跨度非常大。比如码率从150kbps到3000kbps差了20倍如果直接用线性坐标低码率端的点会被压缩到左侧一小块区域曲线形状根本看不清楚。这种时候通常有两种选择横轴取对数semilogx在Matlab里一行搞定Python里ax.set_xscale(log)纵轴取对数通常不需要因为PSNR的单位是dB本身就是对数域的量双对数只在极少见的长尾数据里使用常规RD曲线不推荐。我的建议是只要码率跨度超过一个数量级就优先考虑横轴取对数。而且取对数之后插值也应该在对数域里进行或者至少意识到垂直方向上的差距在不同横轴位置上的视觉权重会变化。实际操作中把码率取log后再插值曲线会更平滑因为码率轴变得均匀了。2.3 排序、去重与异常点剔除数据预处理有三个简单但关键的步骤顺序不能乱。第一步按横轴升序排序。Python里用np.argsort(rate)拿到索引Matlab里用sort(rate)。排序之后后面的插值函数才可能正常工作。第二步去重。如果两个工作点码率相同但PSNR不同插值函数会报错或者产生NaN。这种情况虽然少见但在多路并行实验汇总时会出现。简单做法是保留平均值或者保留更优的那个点。第三步剔除肉眼可见的异常点。比如码率突然从800跳到2200而PSNR只涨了0.1dB这类点大多是实验配置出错所致留着会让曲线在局部出现明显凹陷。你可以用差分比值定位异常delta_psnr / delta_rate如果这个比值突然掉到周围数值的十分之一以下就要检查原始日志了。3. Matlab实现从散点到平滑曲线的完整方案3.1 基础版本PCHIP插值画单条曲线Matlab里画率失真曲线最顺手的方式是用pchip做插值。PCHIP全称是分段三次Hermite插值多项式它的核心优势是“保单调”只要你的原始数据是单调的插值结果就不会出现多余的振荡。这一点对率失真曲线特别重要因为理论上码率增加质量不应下降。先上代码% 原始工作点数据 rate [152, 318, 561, 932, 1680, 2860]; psnr [31.26, 34.41, 36.84, 38.67, 40.23, 41.35]; % 按横轴排序 [rate_sorted, idx] sort(rate); psnr_sorted psnr(idx); % 生成密集的横坐标点 rate_fine linspace(min(rate_sorted), max(rate_sorted), 500); % PCHIP插值 psnr_fine pchip(rate_sorted, psnr_sorted, rate_fine); % 绘图 figure(Color, w); plot(rate_fine, psnr_fine, -, LineWidth, 2, Color, [0 0.4470 0.7410]); hold on; plot(rate_sorted, psnr_sorted, o, MarkerSize, 8, ... MarkerFaceColor, [0 0.4470 0.7410], MarkerEdgeColor, w); xlabel(Bitrate (kbps), FontSize, 12); ylabel(PSNR (dB), FontSize, 12); grid on; legend(方案A, 实测点, Location, southeast);这里有一点值得展开为什么用linspace生成500个点而不是直接用原始点连折线因为插值的目的是让曲线在视觉上平滑连续500个点在绝大多数屏幕上已经足够密再多也没意义反而让文件体积变大。如果后续要做BD-rate计算这500个点可以作为积分基础精度已经够用。PCHIP的“保单调”特性不是空话。你可以拿同样的数据去跑spline会发现spline曲线在码率900到1600之间容易出现一个明显的波浪看起来似乎编码器在某段码率上性能“回退”了而这实际是插值方法带来的假象。3.2 进阶多条曲线对比与工作点标注真实论文里很少只画一条曲线至少是三五种方案叠在一起。Matlab的多曲线绘图要特别注意两个方面颜色和图例。我推荐用Matlab默认的lines色图但不直接调用而是手动指定一组高对比且适合打印的颜色。比如method_colors [0 0.4470 0.7410; 0.8500 0.3250 0.0980; ... 0.4940 0.1840 0.5560; 0.4660 0.6740 0.1880]; marker_styles {o, s, d, ^};这样四条曲线排出来不至于出现两条深蓝和两条浅蓝分不清的情况。每条曲线的工作点用不同marker标注曲线本身用实线不同颜色配合图例放在右下角或右上角。图例位置用Location指定后还要检查是否遮挡了曲线遮挡时可以改用Location, best或手动调整。对于工作点标注我建议只标marker不标数值。数值标上去会让图非常拥挤审稿时看不清。如果确实需要展示具体工作点数值可以用表格放在正文或者附录里图面保持简洁。多曲线对比时还有一个细节各条曲线的横坐标范围可能不一致。比如方案A的码率范围是150到2800方案B只跑到2200如果直接全部绘制方案B的曲线会在右侧缺一大截看上去对比不充分。处理方式是在同一个图里所有曲线只绘制公共码率区间内的部分。比如找到所有方法的最小码率最大值和最大码率最小值把它们作为统一的横轴边界。Mtlab里可以用xlim配合intersect逻辑实现但最简单的做法是先对每条曲线各自插值到密集点再统一用xlim([common_min, common_max])截断显示范围。3.3 Matlab里平滑参数怎么选如果实验数据噪声比较大PCHIP插值出来的曲线仍会有点“毛”。这时候可以用moothdata系列函数。Matlab提供了smoothdata支持的平滑方法包括movmean、lowess、sgolay等。实际经验是只缺几个毛刺时用sgolay窗口长度取5或7注意窗口必须是奇数曲线整体抖动幅度较大时用lowessspan取0.1到0.25之间比例太大容易把真实起伏也削平只在视觉上“圆润”一下用movmean窗口取9到15。但这里必须提醒平滑会把原始工作点的位置改变如果一场图里同时展示实测散点和平滑曲线两者会错开不懂的读者会以为数据对不上。所以我的习惯是只对插值后的密集点做适当平滑而原始工作点永远保持原样绘制。这样既保留实测数据的权威性又让曲线看起来更舒服。4. Python实现Matplotlib SciPy的等效方案4.1 核心代码PCHIP插值加散点叠加Python生态里的等效方案是基于Matplotlib绘图加上SciPy里的PchipInterpolator。代码写出来和Matlab很像但有一些细节值得注意。import numpy as np import matplotlib.pyplot as plt from scipy.interpolate import PchipInterpolator # 原始工作点数据 rate np.array([152, 318, 561, 932, 1680, 2860]) psnr np.array([31.26, 34.41, 36.84, 38.67, 40.23, 41.35]) # 排序 order np.argsort(rate) rate_sorted rate[order] psnr_sorted psnr[order] # PCHIP插值 pchip PchipInterpolator(rate_sorted, psnr_sorted) rate_fine np.linspace(rate_sorted.min(), rate_sorted.max(), 500) psnr_fine pchip(rate_fine) # 绘图 fig, ax plt.subplots(figsize(7, 5)) ax.plot(rate_fine, psnr_fine, linewidth2, colorC0, label方案A) ax.plot(rate_sorted, psnr_sorted, o, markersize7, colorC0, markeredgecolorwhite, markeredgewidth1.2, label实测点) ax.set_xlabel(Bitrate (kbps)) ax.set_ylabel(PSNR (dB)) ax.grid(True, linestyle--, alpha0.6) ax.legend(loclower right) plt.tight_layout() plt.show()这里有一个和Matlab不同的地方Matlab的pchip直接接受原始点数组返回插值结果而Python需要先创建PchipInterpolator对象再调用。本质上都是分段三次Hermite插值单调性保证是一致的。如果实际数据和Matlab一致那两条曲线的结果应该完全重合只是渲染风格略有区别。这说明方案之间可以无缝迁移。4.2 用LOWESS处理噪声偏大的实测数据当你的工作点来自多次实验的平均值、或者参数扫描不是严格对齐时PCHIP也不一定稳。更合适的方案是LOWESS局部加权回归。它比多项式拟合灵活不需要预设模型还能保留局部趋势。statsmodels里直接有实现from statsmodels.nonparametric.smoothers_lowess import lowess # 对排序后的数据做LOWESS平滑 smoothed lowess(psnr_sorted, rate_sorted, frac0.4) rate_smooth smoothed[:, 0] psnr_smooth smoothed[:, 1] plt.plot(rate_smooth, psnr_smooth, linewidth2, labelLOWESS) plt.plot(rate_sorted, psnr_sorted, o, markersize7, label实测点)frac参数是核心它控制每个局部窗口覆盖多少比例的数据点。frac越大曲线越光滑但容易丢失细节frac越小曲线越贴合原始数据但噪声也越大。我的建议是先从0.4起步如果你有7到10个工作点这个比例大概让每个窗口覆盖3到4个点属于比较均衡的选择。LOWESS和PCHIP的取舍在于PCHIP是插值必须经过每个点LOWESS是平滑不要求过点。如果你的实验点每个都是确定性的编码结果用PCHIP如果每个数据本身带有明显统计波动用LOWESS。4.3 Savitzky-Golay滤波的使用时机还有一种常用的平滑手段是Savitzky-Golay滤波用scipy.signal.savgol_filter实现。它的优点是能够在平滑的同时保持峰谷形状不会像移动平均那样把拐角都抹平。但用SG滤波有一个隐藏坑它要求输入是等间距的序列。而你的实测码率点几乎不可能等间距所以不能直接对原始散点做SG滤波。正确做法是先PCHIP插值到等间距密集点再用savgol_filter做平滑。from scipy.signal import savgol_filter # 先插值到等间距点 pchip PchipInterpolator(rate_sorted, psnr_sorted) rate_dense np.linspace(rate_sorted.min(), rate_sorted.max(), 300) psnr_dense pchip(rate_dense) # 再对插值序列做SG平滑 psnr_sg savgol_filter(psnr_dense, window_length15, polyorder3) plt.plot(rate_dense, psnr_dense, alpha0.4, labelPCHIP) plt.plot(rate_dense, psnr_sg, linewidth2, labelPCHIPSG)window_length和polyorder的选择逻辑窗口长度越大越平滑但太长会削掉真实弯曲多项式阶数通常取2到3阶数太高反而会跟着噪声一起抖动。对于率失真曲线这种总体单调、局部轻微弯曲的形状我实测window_length15、polyorder3的组合效果很好。如果你只有5到6个原始点窗口长度可以缩到9因为插值密集点本身已经比较平滑。最后提一个细节无论用哪种平滑都要注意“插值→平滑→绘图”的链条里排序必须在最开始完成。否则横坐标乱序会让后续所有步骤失真。5. 实操中跑不掉的坑与排查清单5.1 插值后曲线出现非单调回弹这个现象我用图表形式总结一下方便随手排查现象原因解决方案曲线局部出现凹坑码率增加PSNR反而下降用spline插值过冲振荡换成PCHIP插值曲线整体太平滑现场数据对不上平滑过度工作点位置被篡改只做插值对散点平滑参数调小曲线起始端剧烈弯曲端点数据点较少插值自由度大增加起始端工作点或用LOWESS截断处理多条曲线右端长短不一各方案码率范围不一致取公共区间再对比横轴取log后曲线变得奇怪插值在线性域进行横轴却被log显示先对log(rate)做插值再转回非单调回弹是论文返修的高频理由。你有没有想过为什么审稿人一眼就能看出曲线不对劲因为在率失真框架下任何“码率增加、质量下降”都是违背理论最优性的表现曲线一旦回弹说明你的对比方案存在本质上无法解释的退化。假如你确实遇到非单调的原始数据怎么办两个选择如果你确信是某个异常点剔掉它并保证你有原始日志作为依据如果这个现象反复出现说明编码器在某些配置下出现了异常降级这本身就是值得写成问题的发现不要用平滑去掩盖。5.2 对比曲线如何保证公平多方案对比最常见的不公平是横坐标范围不一致。比如方案A标称最小码率100kbps方案B标称300kbps那方案A曲线在左侧多出来一段视觉上似乎“性能更好”。实际上这可能只是方案B没有跑更极端的配置而已。我的做法是先计算公共区间common_min max(np.min(rate_a), np.min(rate_b), np.min(rate_c)) common_max min(np.max(rate_a), np.max(rate_b), np.max(rate_c)) ax.set_xlim(common_min, common_max)然后用numpy.interp或PCHIP把各条曲线重采样到公共区间内的同一组横坐标上这样后续做面积对比、BD-rate计算时才是公平的。还有一个容易踩的坑如果方案A的实测点明显比方案B多A的曲线辨识度更高读者会下意识觉得A更可靠。解决方式是统一marker大小和线条宽度甚至可以用不同线型来降低点密度差异造成的影响。5.3 导出矢量图与字号设置论文级别的科研图优先导出矢量格式。Matlab里用exportgraphics(gcf, rd_curve.pdf, ContentType, vector);Python里用fig.savefig(rd_curve.pdf, formatpdf, dpi600, bbox_inchestight)只要涉及PDF或SVG图里的文字就会保留为矢量信息放大到多大都不虚。位图的话PNG至少要300dpi600dpi更稳妥。字号方面我的默认习惯是图里正文文本不小于9pt坐标轴标签不小于11pt。很多期刊要求图内文字在缩放后不低于6pt所以如果你事先不确定缩放比例宁可设得大一点。字太小是审稿意见里非常常见的一条而且改起来特别烦。配色上黑白打印的论文不要用太浅的颜色。像黄色的曲线在灰度打印后会变成浅灰色几乎看不到。我一般用深蓝、深红、深绿、紫色这四类既能满足彩色阅读灰度打印也分得清。6. 个人实操体会与扩展建议画率失真曲线这件事看起来是“随手一画”的小工作实际做起来恰恰是最锤炼基本功的地方。我自己踩过几次坑之后现在的流程基本固定为原始数据CSV化、排序去重、PCHIP插值、多方案公共区间对齐、导出PDF矢量图。这套流程在好几个模拟项目和跨平台系统里都验证过效果稳妥。有一个小技巧顺便分享当你需要把30张以上的RD曲线批量生成时不要每张图都手动调参数。把颜色、线宽、marker、图例字体封装成一个绘图配置函数改一处全程生效。我在某次对比实验里就是用这个思路把之前两天的出图工作量压缩到了半小时。后续如果你想进一步量化算法性能插值后的密集点已经为BD-rate计算铺好了路。计算两条曲线之间的面积差或者用分段积分求平均码率节省比例都可以直接在这些密集点上做数值积分误差会比用原始稀疏点小很多。我自己常用的做法是在公共区间内对两条曲线的差值做梯形积分代码不超过十行。等哪天需要细讲BD-rate的数值计算我再开一篇单独展开。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑