news 2026/10/13 5:44:02

率失真曲线平滑绘制:Matlab与Python的PCHIP插值实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
率失真曲线平滑绘制:Matlab与Python的PCHIP插值实战指南

科研画图这件事,平时看着不起眼,真到自己动手做的时候才发现坑比想象中多。尤其是率失真曲线这类图,横轴是码率、纵轴是失真或峰值信噪比,数据点来自多次实验的统计结果,往往只有十几个点,直接连线就是折线图,显得很生硬;强行平滑又可能丢掉真实趋势。这篇就围绕“散点连接并平滑”这个需求,把Matlab和Python两条路都梳理清楚,讲一讲背后的原理、具体的实现代码、以及我实际踩过的坑。

1. 动手之前先分清:你是要“光滑”还是“保真”

很多人拿到散点第一反应就是上样条插值,结果画出来的曲线又弯又绕,看起来确实顺滑了,但跟实验数据本身的关系反而不直观。这里的关键在于你要搞清楚这张图的用途:是给自己的论文做趋势展示,还是要放到审稿人面前接受检验。

1.1 科学数据图的底线:插值不能改变数据语义

率失真曲线的横纵坐标都有明确物理意义。横轴是单位像素或单位帧的比特数,纵轴是重建失真或PSNR,理论上应该是一条单调递减或递减趋势明显的曲线。你可以用插值让曲线变平滑,但插值方法不能引入原本不存在的波动,尤其是不能在中段出现异常的“下凹”或“上凸”,否则审稿人一眼就能看出问题。

我之前见过有人用普通三次样条对一组只有8个点的率失真数据做插值,结果在码率中等区域出现了一个明显的“鼓包”,看起来像是算法在中码率性能更好,实际上完全是样条的过冲效应。这种图放到论文里,等于主动给审稿人递刀。所以选择平滑方法之前,先问自己一句:这条曲线的总体趋势我是否已经心里有数?如果答案是肯定的,再决定用什么插值。

1.2 保真优先的推荐路线:PCHIP优于普通Spline

保单调、保形状的插值方法里,PCHIP(分段三次Hermite插值)是科研绘图里最稳妥的选择。它不会像普通三次样条那样产生过冲,趋势转折点处也更克制。Matlab里用interp1指定pchip,Python里用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', markersize=7, label='实验数据') ax.plot(xq, yq_pchip, '-', linewidth=1.8, label='PCHIP插值') ax.plot(xq, yq_spline, '--', linewidth=1.2, label='三次样条插值') ax.set_xlabel('Bitrate (Mbps)', fontsize=12) ax.set_ylabel('PSNR (dB)', fontsize=12) ax.legend(loc='lower right') ax.grid(True, linestyle='--', alpha=0.6) plt.tight_layout() plt.savefig('rd_curve_python.pdf', dpi=300, bbox_inches='tight') plt.show()

Python版里PCHIP类用起来比Matlab还直观一点,可以直接调用pchip(xq)得到插值结果,不需要每次指定方法名。

4.2 置信带绘制:fill_between的正确姿势

和Matlab的fill对应的是ax.fill_between:

psnr_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, color='steelblue', alpha=0.15) ax.errorbar(bitrate, psnr_mean, yerr=psnr_std, fmt='o', color='steelblue', markersize=5, capsize=3, linewidth=1, label='实验均值$\\pm$标准差') ax.plot(xq, yq_pchip, '-', color='steelblue', linewidth=1.8, label='PCHIP插值曲线') ax.set_xlabel('Bitrate (Mbps)', fontsize=12) ax.set_ylabel('PSNR (dB)', fontsize=12) ax.legend(loc='lower right') ax.grid(True, linestyle='--', alpha=0.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, linestyle=styles[i]['linestyle'], color=styles[i]['color'], linewidth=1.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.family='Times New Roman',没有要求就用系统默认

这些参数看似琐碎,但全部统一之后,整篇论文的图表风格会非常一致。审稿人看多了风格混乱的图,遇到一套干净标准的图,第一印象就会好很多。

最后的实际体会

刚刚说的那个用虚拟点抑制端点斜率的办法,算是我自己踩过坑之后沉淀下来的经验。某次实验,我在低码率端只测了0.2 Mbps一个点,PCHIP曲线画出来后,低码率段掉头往下,看起来像算法在小码率性能骤降。后来加了虚拟起点,曲线回归正常趋势,审稿人看图和看数据对得上,就没有再刁难。类似的细节还有不少,比如误差带颜色透明度、图表导出格式、坐标轴留白比例,这些琐碎的东西单独看都不起眼,但合在一起就是一张图专不专业的分水岭。画率失真曲线,说白了不是算法难,而是你有没有把这些细节真正当回事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:42:38

MinGW免安装版:嵌入式与CI场景下的可复现工具链方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:42:10

C++深拷贝原理与双重释放陷阱解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:42:01

OpenSSL 1.1.1 离线安装包制作与内网部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:41:03

从模糊代号到最小闭环:我用四层问题拆解一个叫“rea”的项目

有人丢给你一个项目,名字只有三个字母:“rea”。没有文档,没有需求清单,没有背景介绍,就一个词。你会怎么办?直接开干?那你大概率会踩进深坑。我这些年看到太多从“看不懂的代号”到“重写系统”…

作者头像 李华
网站建设 2026/10/13 5:40:35

大模型Agent技能库工程实战:从Prompt硬扛到可复用执行层

如果最近你也在折腾大模型应用,多半和我一样绕不开“agent-skills”这个词。我最早看到“agent-skills”时还以为是某个炫酷的模型能力包,后来动手做了一整套基于技能库(skills)的AI助手才发现,它真正解决的是大模型“…

作者头像 李华
网站建设 2026/10/13 5:39:38

Oracle 11g 64位下 exp.exe 与 imp.exe 实战:老DBA的导出导入避坑指南

简介:这份资源是Oracle 11g 64位版本的bin目录完整打包,面向需要做数据库导入导出、备份迁移的DBA与开发人员,尤其适合习惯在PL/SQL Developer中通过图形界面调用命令行工具的用户。包内共691个文件,以372个dll动态库、118个exe可…

作者头像 李华