科研画图这件事平时看着不起眼真到自己动手做的时候才发现坑比想象中多。尤其是率失真曲线这类图横轴是码率、纵轴是失真或峰值信噪比数据点来自多次实验的统计结果往往只有十几个点直接连线就是折线图显得很生硬强行平滑又可能丢掉真实趋势。这篇就围绕“散点连接并平滑”这个需求把Matlab和Python两条路都梳理清楚讲一讲背后的原理、具体的实现代码、以及我实际踩过的坑。1. 动手之前先分清你是要“光滑”还是“保真”很多人拿到散点第一反应就是上样条插值结果画出来的曲线又弯又绕看起来确实顺滑了但跟实验数据本身的关系反而不直观。这里的关键在于你要搞清楚这张图的用途是给自己的论文做趋势展示还是要放到审稿人面前接受检验。1.1 科学数据图的底线插值不能改变数据语义率失真曲线的横纵坐标都有明确物理意义。横轴是单位像素或单位帧的比特数纵轴是重建失真或PSNR理论上应该是一条单调递减或递减趋势明显的曲线。你可以用插值让曲线变平滑但插值方法不能引入原本不存在的波动尤其是不能在中段出现异常的“下凹”或“上凸”否则审稿人一眼就能看出问题。我之前见过有人用普通三次样条对一组只有8个点的率失真数据做插值结果在码率中等区域出现了一个明显的“鼓包”看起来像是算法在中码率性能更好实际上完全是样条的过冲效应。这种图放到论文里等于主动给审稿人递刀。所以选择平滑方法之前先问自己一句这条曲线的总体趋势我是否已经心里有数如果答案是肯定的再决定用什么插值。1.2 保真优先的推荐路线PCHIP优于普通Spline保单调、保形状的插值方法里PCHIP分段三次Hermite插值是科研绘图里最稳妥的选择。它不会像普通三次样条那样产生过冲趋势转折点处也更克制。Matlab里用interp1指定pchipPython里用scipy.interpolate.PCHIP即可。如果你的数据量很少比如只有5到10个点我建议你先画一个不带平滑的散点图看看整体趋势再叠加PCHIP曲线如果数据点之间有明显的跳变不要用插值隐藏它那可能是实验条件不一致需要回头查数据而不是硬画图。2. 数据预处理散点连接前的关键一步率失真曲线的散点往往不是均匀分布的。码率低的时候点比较密码率高的时候点比较稀这是编码实验的常态。直接对原始散点做插值高码率段的曲线容易被拉得不够平滑直接连线低码率段又显得锯齿感明显。2.1 排序问题x轴必须严格递增这是最基础也最容易被忽略的一步。interp1、PCHIP这类插值函数都要求横坐标严格递增如果你的散点是从多个日志文件里合并来的顺序往往混乱必须先按横轴排序。排序之后最好顺手做一次重复点检查同一个码率点如果对应多个失真值直接取平均或取中位数即可。我之前处理某次实验数据时因为两个测试序列的日志合并时没有去重导致同一横坐标出现了两个不同的纵坐标结果插值函数直接报错。后来养成习惯任何散点进插值流程之前先做“排序去重平均”三件套。2.2 降采样不是偷懒是为了让趋势更清楚有时候你的点其实很多比如做了几十个码率点的实验这种数据画出来已经比较平滑了不需要额外插值但点太多也会让图显得拥挤尤其在论文单栏排版里。这时候可以降采样比如每3个点取1个或者按码率区间均匀抽点。我倾向的做法是点少于15个直接插值平滑点多于30个先降采样到15到20个点左右再做插值。这样既保证曲线视觉平滑又不至于让插值过程因为数据过密而出现过拟合式的弯曲。3. Matlab实操三种插值方法对比与代码实现Matlab画率失真曲线核心就是interp1函数加上plot和errorbar的组合。3.1 基础版本PCHIP插值画出平滑曲线% 原始数据示例 bitrate [0.5, 1, 2, 3, 4, 6, 8]; % 单位Mbps或bpp psnr [31.2, 33.5, 35.1, 35.9, 36.4, 36.8, 37.0]; % 单位dB % 生成细密的横轴采样点 xq linspace(min(bitrate), max(bitrate), 200); % PCHIP插值保单调、无过冲 yq_pchip interp1(bitrate, psnr, xq, pchip); % 普通三次样条插值对比用看趋势差异 yq_spline interp1(bitrate, psnr, xq, spline); figure plot(bitrate, psnr, o, MarkerSize, 6, LineWidth, 1.2) hold on plot(xq, yq_pchip, -, LineWidth, 1.5, Color, [0.2 0.4 0.8]) plot(xq, yq_spline, --, LineWidth, 1.2, Color, [0.8 0.3 0.3]) xlabel(Bitrate (Mbps)) ylabel(PSNR (dB)) legend({实验数据, PCHIP插值, 三次样条插值}, Location, southeast) grid on这段代码跑出来你就能直观看到两种插值的区别样条在中段可能产生轻微波动而PCHIP基本贴着散点的自然趋势走。实测下来论文里用PCHIP通常是最稳妥的。3.2 带误差棒展示多次实验的波动范围率失真曲线很少只跑一次实验一般会跑多个内容源或多个随机种子这时候需要在散点上叠加误差棒再对均值做插值。% 假设每个码率点做了3次实验 bitrate [0.5, 1, 2, 3, 4, 6, 8]; psnr_mean [31.0, 33.2, 34.9, 35.8, 36.2, 36.6, 36.9]; psnr_std [0.4, 0.3, 0.25, 0.3, 0.2, 0.15, 0.1]; xq linspace(min(bitrate), max(bitrate), 200); yq interp1(bitrate, psnr_mean, xq, pchip); % 用误差棒上限和下限分别插值绘制浅色置信带 yq_upper interp1(bitrate, psnr_mean psnr_std, xq, pchip); yq_lower interp1(bitrate, psnr_mean - psnr_std, xq, pchip); figure fill([xq fliplr(xq)], [yq_upper fliplr(yq_lower)], ... [0.2 0.4 0.8], FaceAlpha, 0.15, EdgeColor, none) hold on plot(bitrate, psnr_mean, o-, MarkerSize, 6, LineWidth, 1.2, ... Color, [0.2 0.4 0.8]) errorbar(bitrate, psnr_mean, psnr_std, o, MarkerSize, 4, ... Color, [0.2 0.4 0.8], LineWidth, 0.8, CapSize, 3) xlabel(Bitrate (Mbps)) ylabel(PSNR (dB)) grid on这里有个关键细节误差带不是直接连接散点上下界的包络而是对“均值标准差”和“均值-标准差”分别做插值。这样生成的带状区域才是自然的不会出现拐来拐去的锯齿形阴影。3.3 Matlab里的平滑降噪smoothdata的适用场景如果你的数据点很多且噪声大可以考虑先对纵坐标做平滑再插值psnr_smoothed smoothdata(psnr, gaussian, 5);但要注意smoothdata默认的窗口大小需要根据点数调整窗口太大容易把真实趋势也抹平。我一般只在点数超过20且趋势明显的前提条件下使用这招否则宁可不平滑。4. Python实操SciPy插值与Pyplot美化Python做率失真曲线路线也是一样numpy整理数据scipy.interpolate做插值matplotlib画图。4.1 PCHIP插值代码模板import numpy as np from scipy.interpolate import PCHIP import matplotlib.pyplot as plt # 原始数据 bitrate np.array([0.5, 1., 2., 3., 4., 6., 8.]) psnr np.array([31.2, 33.5, 35.1, 35.9, 36.4, 36.8, 37.0]) # 生成插值用的细密横坐标 xq np.linspace(bitrate.min(), bitrate.max(), 200) # PCHIP插值 pchip PCHIP(bitrate, psnr) yq_pchip pchip(xq) # 对比普通三次样条 from scipy.interpolate import CubicSpline cs CubicSpline(bitrate, psnr) yq_spline cs(xq) fig, ax plt.subplots(figsize(6, 4.5)) ax.plot(bitrate, psnr, o, markersize7, label实验数据) ax.plot(xq, yq_pchip, -, linewidth1.8, labelPCHIP插值) ax.plot(xq, yq_spline, --, linewidth1.2, label三次样条插值) ax.set_xlabel(Bitrate (Mbps), fontsize12) ax.set_ylabel(PSNR (dB), fontsize12) ax.legend(loclower right) ax.grid(True, linestyle--, alpha0.6) plt.tight_layout() plt.savefig(rd_curve_python.pdf, dpi300, bbox_inchestight) plt.show()Python版里PCHIP类用起来比Matlab还直观一点可以直接调用pchip(xq)得到插值结果不需要每次指定方法名。4.2 置信带绘制fill_between的正确姿势和Matlab的fill对应的是ax.fill_betweenpsnr_mean np.array([31.0, 33.2, 34.9, 35.8, 36.2, 36.6, 36.9]) psnr_std np.array([0.4, 0.3, 0.25, 0.3, 0.2, 0.15, 0.1]) yq_upper PCHIP(bitrate, psnr_mean psnr_std)(xq) yq_lower PCHIP(bitrate, psnr_mean - psnr_std)(xq) fig, ax plt.subplots(figsize(6, 4.5)) ax.fill_between(xq, yq_lower, yq_upper, colorsteelblue, alpha0.15) ax.errorbar(bitrate, psnr_mean, yerrpsnr_std, fmto, colorsteelblue, markersize5, capsize3, linewidth1, label实验均值$\\pm$标准差) ax.plot(xq, yq_pchip, -, colorsteelblue, linewidth1.8, labelPCHIP插值曲线) ax.set_xlabel(Bitrate (Mbps), fontsize12) ax.set_ylabel(PSNR (dB), fontsize12) ax.legend(loclower right) ax.grid(True, linestyle--, alpha0.6) plt.tight_layout()这里注意errorbar和plot的线宽、标记大小要协调否则图例里会显得粗细不一看起来不够专业。4.3 多个算法对比时的配色和线型管理率失真曲线最常见的使用场景是多个算法横向对比。我推荐一组稳定的配置基准算法黑色实线加圆点标记改进算法1蓝色实线加方块标记改进算法2红色虚线加三角标记上界参考灰色点线styles [ {color: black, marker: o, linestyle: -, label: Baseline}, {color: #1f77b4, marker: s, linestyle: -, label: Method A}, {color: #d62728, marker: ^, linestyle: --, label: Method B}, {color: #7f7f7f, marker: D, linestyle: :, label: Upper Bound}, ] for i, (bitrate_i, psnr_i) in enumerate(datasets): ax.plot(bitrate_i, psnr_i, **styles[i]) # 也可以叠加PCHIP插值曲线 xq_i np.linspace(bitrate_i.min(), bitrate_i.max(), 200) yq_i PCHIP(bitrate_i, psnr_i)(xq_i) ax.plot(xq_i, yq_i, linestylestyles[i][linestyle], colorstyles[i][color], linewidth1.5)这样画出来的图即便打印成黑白版本靠散点形状和线型也能区分不同的算法避免审稿人在黑白打印稿里分不清曲线。5. 进阶处理横轴取对数、纵轴语义与多子图排版很多率失真曲线的横轴跨度特别大从0.1 Mbps到几十 Mbps直接用线性坐标会导致低码率段的数据挤在一起看不清楚。5.1 对数横轴与插值顺序的关系如果你决定把横轴改成对数坐标要注意插值应该在原始数据空间做还是在取对数后的空间做。我的经验是先在原始数据空间做PCHIP插值再用plt.xscale(log)修改显示方式。fig, ax plt.subplots() ax.plot(bitrate, psnr, o) xq np.logspace(np.log10(bitrate.min()), np.log10(bitrate.max()), 200) yq PCHIP(bitrate, psnr)(xq) ax.plot(xq, yq, -) ax.set_xscale(log)这里使用logspace生成插值采样点是因为在对数坐标显示时横轴等距的采样点在视觉上才是均匀的。如果还是用linspace对数坐标下曲线的前半段会显得稀疏后半段密得挤在一起。5.2 用双纵轴或归一化横轴表达不同语义有时候你要对比的不是直接的PSNR而是相对增益比如“相对于基准算法的BD-Rate变化”。这种情况下把多个算法的曲线画在一张图上反而很难读更适合的做法是用子图或归一化曲线。归一化的方式很简单把横轴除以基准算法的最大码率纵轴除以基准算法的最低失真。这样可以突出相对趋势但要注意图例和坐标轴的说明里写清楚归一化基准避免读者误解。5.3 多子图排版的推荐比例论文里率失真曲线通常放在实验部分一张图2到4条曲线即可。子图组合形态一般为上下两个图上方是率失真曲线下方是对应的增益柱状图两者共享横轴。如果用matplotlib可以用gridspec控制高度比例建议曲线图占高度约65%柱状图占35%。from matplotlib import gridspec fig plt.figure(figsize(7, 6)) gs gridspec.GridSpec(2, 1, height_ratios[1.6, 1]) ax1 fig.add_subplot(gs[0]) ax2 fig.add_subplot(gs[1])这种布局比单图信息量大word里排版也很方便。6. 常见问题速查与避坑经验6.1 典型问题排查表现象可能原因解决方案插值报错x must be strictly increasing横坐标未排序或存在重复点先sort再对重复点取平均曲线在中段出现不自然的凸起普通三次样条的过冲效应改用PCHIP插值平滑后曲线偏离散点趋势平滑窗口太大或插值采样点太密减小窗口或改用保形插值对数坐标下曲线前半段稀疏插值采样用了linspace而非logspace改用logspace生成采样点误差带边界有锯齿直接连接原始散点的上下界对上下界分别插值再填充图例遮挡曲线关键趋势loc设置不当根据曲线走势选lower right或手工指定坐标导出PDF后字体模糊未嵌入字体或矢量格式设置不对用pdf保存并在matplotlib里设置rcParams[pdf.fonttype]42黑白打印无法区分算法只用颜色区分增加标记样式和线型差异6.2 独家经验别忽视曲线端点的“边界细节”插值曲线在端点区域最容易出问题。PCHIP在端点处虽然不会像样条那样发散但如果你的数据在低码率端采样不足插值曲线可能会在端点处出现斜率异常。我通常会在低码率端额外加一个虚拟采样点比如原点(0, 最低点PSNR)或第一个点往前一点的位置这样能抑制端点区域的斜率失真。添加的点要在图注或代码里注明是虚拟点不参与数值分析只用于绘图。6.3 标准化输出参数建议我最后把自己的固定参数列在这里方便你直接复制使用散点标记大小6号黑色边框渐变填充曲线线宽1.5到2.0之间坐标轴字号10号或12号够大不抢戏图例框架线框关闭网格线虚线、透明度0.5以下字体论文要求如果是LaTeX用font.familyTimes New Roman没有要求就用系统默认这些参数看似琐碎但全部统一之后整篇论文的图表风格会非常一致。审稿人看多了风格混乱的图遇到一套干净标准的图第一印象就会好很多。最后的实际体会刚刚说的那个用虚拟点抑制端点斜率的办法算是我自己踩过坑之后沉淀下来的经验。某次实验我在低码率端只测了0.2 Mbps一个点PCHIP曲线画出来后低码率段掉头往下看起来像算法在小码率性能骤降。后来加了虚拟起点曲线回归正常趋势审稿人看图和看数据对得上就没有再刁难。类似的细节还有不少比如误差带颜色透明度、图表导出格式、坐标轴留白比例这些琐碎的东西单独看都不起眼但合在一起就是一张图专不专业的分水岭。画率失真曲线说白了不是算法难而是你有没有把这些细节真正当回事。
阅读完成 · 觉得有帮助?