LTTB 降采样算法原理与 QWT7 的 MinMax 桶实现¶
摘要 —— 当一条曲线的采样数远超画布的像素列数时,继续把每一个点交给光栅化器既昂贵又没有意义:绝大多数点共享同一个像素列,属于信息论意义上的冗余采样。屏幕的带宽瓶颈决定了能保留多少信息,保真度按像素列定义,用列包络度量。经典 LTTB(Largest Triangle Three Buckets)算法的三角形面积准则得到完整推导;QWT7 采用的 MinMax 桶变体把 LTTB 的"最大三角形"启发式替换为"每桶保留极值"的确定性规则,从而获得可证明的包络性质与 SIMD 友好的计算结构(AVX2/SSE4.2 运行时派发)。一组可复现的实验(8× 超采样光栅化度量)量化四种降采样策略在五种典型信号上的像素级保真度,后文再给出对应的代码实践。
摘要 — 核心结论
- 对百万级曲线,
FilterPointsMinMaxLTTB(MinMax 桶,v7.4.2 及之前的FilterPointsLTTB默认)在 1,000,000 点上把重绘耗时从 262 ms/帧(3.8 FPS)降到 133 ms/帧(7.5 FPS),且在等输出预算的策略中保真度最高(clean列占比 29%–97%,取决于信号)- MinMax 桶保留了经典 LTTB 的"视觉形状保持"优点,但提供了后者没有的确定性极值保留保证,并可使用 SIMD 指令加速逐桶 argmin/argmax
- 桶数的选择 \(N_b = 2W\)(每个像素列 2 个桶、4 个输出点)对应可视化的奈奎斯特极限:低于每列 0.5 周期的内容可被无失真复现
更新(v7.5.0) ——
FilterPointsLTTB现在映射到真 LTTB 实现(QwtPointMapper::LTTBReduce,qwtLttbReduce(),见第 3 节):最大三角形选择在映射后的屏幕坐标上进行,永远保留首尾样本,并能保住 MinMax 桶规则容易塌缩成竖线的窄峰形状。第 4 节分析的 MinMax 桶变体移到新增的FilterPointsMinMaxLTTB属性(MinMaxReduce);下文的基准数据均由它测得。此外,当绘制不按整数像素对齐(例如开启抗锯齿)时,两个算法现在都保留亚像素精度,不再把降采样后的折线吸附到整数像素。
1. 引言:可视化带宽的瓶颈¶
1.1 问题陈述¶
设时间序列(或任意按 \(x\) 单调排列的曲线)为
曲线最终要画在宽度为 \(W\)(像素列)的画布上。坐标映射是仿射变换:
其中 \(x_0', x_1'\) 是画布左右边缘的像素坐标。降采样问题是:给定 \(P\) 与输出预算 \(M \ll N\),求子序列 \(P' = \{(x_{j_k}, y_{j_k})\}_{k=0}^{M-1}\)(或一般折线),使得绘制 \(P'\) 与绘制 \(P\) 在视觉上不可区分,即"视觉等价":
其中 \(\mathcal{R}(\cdot)\) 是光栅化算子(把折线变换到像素集合)。由于像素是离散的,视觉等价实质上要求两条折线在每个像素列内的竖直覆盖范围一致(下文第 2 节给出精确定义)。
1.2 输入有多冗余:可视化的奈奎斯特极限¶
一个像素列只能表现约 \(\pm 0.5\) 列宽的空间频率。把数据视为以像素为单位的序列,其可分辨的最高频率(奈奎斯特频率)为
当 \(N \gg W\) 时,平均每个像素列落入 \(N/W\) 个采样点。这些点携带的空间频率远高于 \(f_{\text{Nyquist}}\),对最终的像素输出没有贡献,只会让光栅化器做无用功:坐标变换、浮点运算、线段求交、抗锯齿混合,全部按 \(N\) 次计算。实测(examples/bench/renderbench,680×490 画布、100 万点、100 帧):
| 方法 | 总耗时 (ms) | 单帧 (ms) | FPS |
|---|---|---|---|
| 无降采样 | 26,212 | 262.1 | 3.8 |
FilterPoints(去重复) |
44,472 | 444.7 | 2.2 |
FilterPointsAggressive(Quad 归并) |
17,539 | 175.4 | 5.7 |
FilterPointsPixel(像素列归并) |
23,216 | 232.2 | 4.3 |
FilterPointsMinMaxLTTB(MinMax 桶) |
13,349 | 133.5 | 7.5 |
(数据来自 QWT7.2.1 + Qt 5.15.16 的 renderbench 示例;不同硬件上绝对数值有差异,相对关系一致。)
上表的数字由仓库自带的基准示例程序 examples/bench/renderbench 实测而来,它是 examples/bench/ 下专门用于对比各种降采样算法的演示程序:

界面从左到右分别是:
- 控制面板:
Points(1,000 – 10,000,000,默认 1,000,000)、Frames(10 – 1000,默认 100)、Wave(平滑波 / 白噪声)、Method(None/FilterPoints/FilterPointsAggressive/FilterPointsPixel/FilterPointsLTTB/FilterPointsMinMaxLTTB六种绘制方式),以及Run(单跑当前方法)与Run All (Compare)(批量对比;当前实现只遍历前 5 种方法,FilterPointsMinMaxLTTB需单独运行); - 绘图区:数据由
WindowedSeriesData在滚动的窗口内按需生成,X 轴跟随窗口,因此每次重绘的工作量与真实的滚动场景一致; - 报告对话框:跑完自动弹出 Markdown 报告,含环境(QWT/Qt 版本、画布尺寸)、参数与所跑各方法的耗时 / 帧率汇总,可一键复制。
除了测速,这个例子的更大价值是肉眼对比:把 Method 在 FilterPointsLTTB 与 FilterPointsPixel 之间反复切换,同一批数据下窄峰的保存方式差异(见第 4.4 节与第 6 节表 1)会直接显现在眼前。
FilterPoints 反而比不降采样更慢:它只剔除完全重合的像素点,百万点里几乎没有完全重合的点,过滤检查本身成了额外开销。降采样必须真正做出合并决策,而不是筛选。
1.3 降采样算法的一般框架¶
绘制管线中的降采样分为两个阶段:
- 可见区间裁剪:通过二分把数据索引范围 \([\,\text{from}, \text{to}\,]\) 缩窄到落入画布的 \([\,\text{visFrom}, \text{visTo}\,]\)(对单调 \(x\) 数据,\(O(\log N)\));
- 桶化缩减:把可见区间划分成 \(N_b\) 个桶,每个桶输出少量代表点。
两种主流思路:
- 像素域分桶(
FilterPointsPixel):桶 = 像素列(空间均匀),输出每列首/最小/最大/末 4 点; - 索引域分桶(
FilterPointsLTTB/FilterPointsMinMaxLTTB):桶 = 等样本数段(索引均匀);LTTB 每桶输出最大三角形面积点,MinMax 每桶输出极值点。
第 3、4 节分别展开这两种思路;二者在"给每个像素列多少输出点"上共享同一个预算 \(4W\)(每列 4 点),差别在于桶的形状与代表点的选取方式。
下图把四种策略在同一窄峰附近放大对比(左上为全量光栅化的参考结果):

2. 保真度度量:列包络(envelope)¶
像素化的直接后果:人类无法分辨同一像素列内的水平细节,因此按像素列定义保真度是自然的。
定义(列包络) 设折线 \(L\) 在画布上的覆盖像素为 \(\mathcal{C}(L) \subseteq \mathbb{Z}^2\),则像素列 \(c\) 的包络为
即折线在该列实际涂到的行范围(把曲线涂成 1 像素宽的线)。两条折线视觉等价的必要条件是其列包络一致。
定义(覆盖率) 给定参考折线 \(L^*\)(即原始 \(N\) 点折线的光栅化结果),降采样折线 \(L'\) 在该列的覆盖率定义为包络的重叠比例:
定义(干净列) 若 \(\mathrm{Env}_c(L^*)\) 与 \(\mathrm{Env}_c(L')\) 的上下界之差都不超过 1 个像素行,称该列为干净列。clean 指标 = 干净列占所有有内容列的比例。这是一个"像素级精确"的强指标。
下文的数值一律按 8×8 超采样光栅化计算(见第 6 节),以消除像素边界归因的伪差。
3. 经典 LTTB 算法¶
3.1 来源与直觉¶
LTTB(Largest Triangle Three Buckets,三桶最大三角形)由 Steinarsson 于 2013 年在冰岛大学的硕士论文 Downsampling Time Series for Visual Representation 中提出,是目前时序可视化领域最常用的降采样算法之一。
核心直觉:一个点对视觉形状的贡献,可以用它与前后两个"骨架点"围成的三角形面积来衡量。面积越大,说明该点偏离了由前一点和下一段趋势所确定的直线,越值得保留。LTTB 是一个贪心算法:每次只为一个桶选点,且只依赖"上一个已选点"与"下一个桶的质心"。
3.2 三桶几何¶

设已选出的上一点为 \(a\)。当前桶 \(B_i\) 内所有点都是候选;下一个桶 \(B_{i+1}\) 不参与选点,但它的质心 \(c\)(重心)充当"未来趋势的方向":
用质心而不是单点,是为了让"趋势"对桶内噪声稳健:单个代表点可能恰在噪声尖峰上,质心则平滑掉了它。
对每个候选 \(p \in B_i\),构造三角形 \(\triangle(a, p, c)\),其面积由鞋带公式(shoelace formula)给出:
实践中更常用的等价形式(以 \(a\) 为参照点展开):
几何意义:面积 = \(\frac{1}{2}\left| \overrightarrow{ac} \right| \times\) 点 \(p\) 到直线 \(ac\) 的距离。因此最大化面积 = 选择偏离"上一点 → 未来质心"连线最远的点,也就是对形状贡献最大的点。数学上,三点围成的有向面积等于叉积:
3.3 算法¶
除首尾点固定保留外,把中间 \(N-2\) 个点划分为 \(M-2\) 个尽量等长的桶:
算法 1:经典 LTTB
1 2 3 4 5 6 7 8 9 | |
复杂度:单遍扫描,\(O(N)\) 时间、\(O(M)\) 输出空间;每步需要下一桶的质心,因此是"桶对桶"的两阶段滑窗,常数因子很小。
3.4 性质¶
- 贪心最优性:在每个桶内,LTTB 选择局部"形状贡献"最大的点。它不保证全局最优(全局最优需要对所有 \(M\) 元组比较,是指数级问题),但经验上在所有简单策略中最接近原始形状。
- 极值不保:LTTB 不保证保留任何局部极值,一个孤立尖峰只有当它的三角形面积足够大时才会被选中。这是它最危险的失效模式。
- 索引均匀分桶:桶按索引等分,因此数据密集区域与稀疏区域获得相同的桶数,输出点在 \(x\) 方向上自适应分布,这是它与按像素分桶的本质区别。
3.5 失效模式:尖峰对¶
考虑间隔 2 个样本的上冲/下冲尖峰对(±3.5,宽度 2 个采样):

当一对尖峰落入同一个桶时,向上尖峰与向下尖峰各自构成面积相近的三角形,但 LTTB 每个桶只选一个点,两个方向中必有一个被丢弃:绘制结果要么只剩上冲、要么只剩下冲,尖峰对的包络被拦腰截断。实验测量(第 6 节):在 100,000 点、含 24 个尖峰对的信号上,经典 LTTB 的列包络误差 p99 达 228 像素行(整根尖峰丢失),只有 59% 的列是干净列。
这直接促成了 QWT7 的 MinMax 桶变体:把"每桶一个最大面积点"换成"每桶两个极值点",从机制上消除这类失效。
4. QWT7 的 MinMax 桶变体¶
4.1 定义¶
QwtPlotCurve::FilterPointsMinMaxLTTB 在内部映射为 QwtPointMapper::MinMaxReduce,调用 qwtMinMaxBucketReduce()。(v7.4.2 及之前的 FilterPointsLTTB 默认也走该标志;自 v7.5.0 起它改映射到 LTTBReduce / qwtLttbReduce()。)它与经典 LTTB 共享"索引域等样本数分桶"的骨架,但代表点从"最大三角形面积点"改为桶内 \(y\) 的最小值与最大值:
设可见样本数为 \(n = \text{visTo} - \text{visFrom} + 1\),画布宽度为 \(W\),定义
第 \(b\) 个桶为样本索引区间
(相邻桶无缝衔接、覆盖全部可见样本:\(\bigcup_b B_b = [\text{visFrom},\,\text{visTo}]\)。)
每个桶输出两个点,按索引顺序排列:
若两点重合(\(y_{\min} = y_{\max}\) 且 \(x_{\min} = x_{\max}\),即桶内所有点相同),只输出一个点。
算法 2:QWT7 MinMax 桶降采样(qwtMinMaxBucketReduce)
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
4.2 与经典 LTTB 的关系¶
| 经典 LTTB | QWT7 MinMax 桶 | |
|---|---|---|
| 选点准则 | 最大三角形面积(依赖下一桶质心) | 桶内 \(y\) 极值(无跨桶依赖) |
| 遍历 | 两阶段滑窗(先算质心再选点) | 单遍,每桶独立 |
| 极值保留 | 否:不保证 | 是:每桶极值必然保留 |
| 尖峰对 | 否:系统性丢弃(同桶二选一) | 是:上冲下冲同时保留 |
| SIMD 化 | 面积比较难以向量化 | argmin/argmax 天然可向量化 |
| NaN 语义 | 需要显式处理 | 预扫描 + NaN-aware 标量分支兜底;内核级 IEEE-754“天然忽略”不可靠(见 5.4 节与SIMD 基准篇) |
MinMax 桶是对 LTTB 的确定性近似:两者都以"索引域等数分桶 + 保留形状代表点"为骨架,区别在于代表点的选取准则。把面积准则换成极值准则之后,算法获得三个可证明的性质。
4.3 三条性质命题¶
命题 1(全局极值保留) MinMax 桶输出折线的 \(y\) 值域与输入一致:
证明:全局最小值落在某个桶 \(B_{b^*}\) 内,因而必是该桶的 \(\operatorname*{arg\,min}\),被原样输出(即使与最大值重合,\(y\) 值也保持不变)。\(\square\)
这是经典 LTTB 不具备的强保证:任何孤立尖峰只要是其所在桶的极值,就必然出现在输出中,尖峰对的双向笔画都会保留(上图右)。
命题 2(无自交) 对 \(x\) 单调的输入,输出点按索引顺序排列,因此输出折线的 \(x\) 坐标非降,折线不自交、不回溯。
命题 3(分辨率与奈奎斯特匹配) 设可见 \(x\) 跨度为 \(X\),则每桶在 \(x\) 方向覆盖 \(\Delta x_b \approx X/N_b = X/(2W)\),即半个像素列。每个桶输出上下两个极值点,相当于以每列 2 个极值对的采样率捕获包络;对周期大于 1 列(低于显示奈奎斯特频率)的震荡,每个波峰与波谷都能被完整捕获,包络不被截断。近奈奎斯特频率的内容(周期 1–2 列)仍保持极值,但桶间连线会有有限的弦偏差(见 6.3 节讨论)。
4.4 与 PixelColumnReduce(像素列桶)的对比¶
| MinMax 桶(LTTB) | 像素列桶(Pixel) | |
|---|---|---|
| 分桶维度 | 数据索引(等样本数) | 像素列(空间) |
| 代表点 | 桶内 \(y\) 极值(真实 \(x\) 保留) | 列内 first/min/max/last,极值放固定亚像素位(+0.25/+0.75) |
| 输出预算 | 2 点/桶 = 4W | ≤ 4 点/列 = 4W(通常 2) |
| \(x\) 精度 | 经映射 + 取整,无系统性偏移 | 固定 ±0.25 列偏移(对非对齐渲染有偏) |
| \(y\) 比较域 | 数据域(双精度) | 像素域(整数) |
| 空间复杂度 | \(O(N_b)\) | \(O(W)\) |
| 适用性 | 通用(含浮点输出、导出、抗锯齿) | 整数对齐的 widget 渲染 |
关键差异:MinMax 桶在数据域比较 \(y\),输出点的 \(x\) 是真实数据位置的映射;像素列桶在像素域比较整数 \(y\),并把极值钉在列内固定亚像素位置。前者的输出在浮点路径(QPolygonF,如 SVG/PDF 导出、非取整绘制)下是几何精确的;后者是专为整数光栅化优化的。
5. 工程实现¶
5.1 渲染管线中的位置¶
下图给出采样点在渲染管线中的完整路径:先由 qwtFindVisibleRange 二分裁剪可见区间,再按 QwtPointMapper 的标志优先级选择归约分支,最终生成的 QPolygonF 折线交给渲染平滑、裁剪与 QPainter。
flowchart TB
A["QwtSeriesData<QPointF><br/>N 个数据点"] --> B["qwtFindVisibleRange<br/>二分裁剪可见区间<br/>(单调 x,O(log N))"]
B --> C{"QwtPointMapper 标志优先级"}
C -->|"PixelColumnReduce"| D["qwtPixelColumnReduce<br/>≤ 4W 点"]
C -->|"MinMaxReduce"| E["qwtMinMaxBucketReduce<br/>≈ 4W 点"]
C -->|"LTTBReduce"| E2["qwtLttbReduce<br/>≈ 4W 点"]
C -->|"WeedOutIntermediatePoints"| F["qwtMapPointsQuad<br/>两遍 Quad 归并"]
C -->|"WeedOutPoints"| G["qwtToPolylineFiltered<br/>去重复"]
C -->|"默认(无标志)"| H["qwtToPoints<br/>全量变换"]
D --> I["QPolygonF 折线"]
E --> I
E2 --> I
F --> I
G --> I
H --> I
I --> J["渲染平滑<br/>(see 曲线平滑篇)"]
J --> K["裁剪 / 拟合 / QPainter"]
style E fill:#fdecea,stroke:#d62728
style D fill:#e8f5e9,stroke:#2ca02c
QwtPlotCurve::drawLines()(src/plot/qwt_plot_curve.cpp)把用户级绘制属性映射为映射器标志:
用户级 PaintAttribute |
内部 TransformationFlag |
生效条件 |
|---|---|---|
FilterPoints |
WeedOutPoints |
恒生效 |
FilterPointsAggressive |
WeedOutIntermediatePoints + WeedOutPoints |
仅当整数对齐取整(doAlign) |
FilterPointsPixel |
PixelColumnReduce |
恒生效 |
FilterPointsLTTB |
LTTBReduce |
恒生效 |
FilterPointsMinMaxLTTB |
MinMaxReduce |
恒生效 |
toPolygonF() 中的选择优先级(src/plot/qwt_point_mapper.cpp:1502):
PixelColumnReduce>LTTBReduce>MinMaxReduce>WeedOutIntermediatePoints>WeedOutPoints
即同时开启多个属性时,像素列桶覆盖 LTTB,LTTB 覆盖 MinMax 桶,MinMax 桶覆盖 Quad 归并,依此类推。默认开启 ClipPolygons | FilterPointsLTTB。
5.2 可见区间裁剪:二分搜索¶
对单调 \(x\) 且线性映射的数据,可见区间由两次二分确定:
各向外扩 1 个点作为交叠。单调性用至多 50 个均匀探针点快速验证(非单调或非线性映射则跳过,退化为全区间扫描)。复杂度 \(O(\log N) + O(50)\):当 \(N = 10^7\) 而可见窗口只有 \(10^4\) 点时,这一层把降采样的输入缩小了三个数量级。
5.3 三重路径¶
qwtMinMaxBucketReduce 按数据源的能力选择三条路径:
flowchart TD
A["桶循环开始"] --> B{"线性映射且能提取<br/>原始 Y 指针?"}
B -->|"否"| C["通用路径<br/>虚函数 sample(i) 逐点访问"]
B -->|"是"| D["非有限 y 预扫描<br/>(可见区间一遍)"]
D --> E{"区间内存在非有限 y?"}
E -->|"否"| F["SIMD 快速路径<br/>qwtSimdArgMinMax(raw.y+start, count)<br/>+ 每桶 x 后置校验"]
E -->|"是"| G["标量快速路径<br/>原始指针 + isfinite 双坐标检查"]
style F fill:#e8f5e9,stroke:#2ca02c
style G fill:#fff3e0,stroke:#ff9800
style C fill:#eceff1,stroke:#607d8b
- SIMD 快速路径:数据源是
QwtPointArrayData<double>、QwtCPointerData<double>、QwtValuePointData<double>、QwtCPointerValueData<double>之一时,通过dynamic_cast提取连续的double*数组,每个桶一次qwtSimdArgMinMax()拿到 argmin/argmax。\(y\) 值比较在数据域进行,屏幕坐标单独变换。每桶输出前对胜出样本的 \(x\) 做isfinite后置校验(仅当存在原始 \(x\) 指针):x 为 NaN/Inf 的胜出样本会经qRound(NaN)=0产出伪顶点,命中时该桶改用标量循环重算。 - 标量快速路径:存在非有限 \(y\) 时,SIMD 语义与"忽略 NaN"不符(见 5.4),退化为逐元素比较 +
!std::isfinite双坐标检查(x 或 y 非有限的样本都跳过,与通用路径语义一致)。 - 通用路径:无法提取原始指针时,逐点调用虚函数
sample(i),每桶线性扫描维护 min/max。
只有 \(y\) 值的数据(setRawSamples(yData, size))没有 \(x\) 指针:屏幕 \(x\) 直接用样本索引计算 \(sx = \text{round}(i \cdot xCnv + xOff)\)。
qwtLttbReduce()(v7.5.0,FilterPointsLTTB 的实现)在流水线中的位置相同,但结构更简单:两趟扫描(先桶均值、后三角形选择),数据源支持原始指针快路径,否则走通用 sample() 路径,没有 SIMD 变体,因为面积判据不像 argmin/argmax 那样易于向量化。它的桶预算为 \(N_b = 4W\)、每桶输出一点,与 MinMax 的 \(2 \times 2W\) 输出预算相当。
5.4 SIMD 加速:向量化 argmin/argmax¶
qwt_simd_argminmax.h/.cpp(位于 src/core/;v7.5.1 起 AVX2 内核独立在 qwt_simd_argminmax_avx2.cpp 并以 /arch:AVX2 编译,见SIMD 基准篇 4.5b)提供通用的 SIMD argmin/argmax,运行时 CPUID 检测一次,函数指针静态派发:
| CPU 特性 | 向量宽 | 每轮元素 | 核心指令 |
|---|---|---|---|
| AVX2 | 256 bit | 4 × double | _mm256_loadu_pd / _mm256_cmp_pd / _mm256_blendv_pd / _mm256_min_pd |
| SSE4.2 | 128 bit | 2 × double | _mm_loadu_pd / _mm_cmp_pd / _mm_blendv_pd / _mm_min_pd |
| 标量 | — | 1 × double | 普通比较 |
向量化内循环的数学结构:并行维护 4(或 2)个 lane 的跑者值向量与索引向量:
比较用有序谓词 _CMP_LT_OQ(ordered, quiet):\(\mathrm{NaN} < x\) 与 \(x < \mathrm{NaN}\) 均为假,NaN 永远不会赢得比较。但值向量走的 minpd/maxpd 硬件指令在任一操作数为 NaN 时返回第二操作数,NaN 可覆盖累加值,待后续真值将其恢复后便与索引向量脱节。因此 NaN 不会污染值结果(输出值仍是某个真实元素,尽管极值可能丢失),但索引语义在多 lane 归约下不可靠,存在索引指向 NaN 元素的组合输出(定量矩阵见SIMD 基准篇 2.3 节),这正是渲染管线必须先做 NaN 预扫描的原因。向量循环结束后做水平归约(跨 lane 求最小/最大),尾部不足一个向量宽的元素由标量循环收尾。最后若公开入口看到的结果仍含 NaN(例如全 NaN 输入),返回哨兵值 \(\{0, 0, \text{DBL\_MAX}, -\text{DBL\_MAX}\}\)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
为什么需要 NaN 预扫描? SIMD 路径与标量路径在"存在 NaN"时的输出语义不同:标量路径可以跳过 NaN 并把非 NaN 极值作为结果;SIMD 的 min/max 指令对 NaN 的行为是返回另一操作数(\(\min(\mathrm{NaN}, x) = x\)),但多 lane 之间无法保证"全局第一个非 NaN"的索引语义。预扫描一遍(\(O(n)\),与桶循环同阶)换来语义一致与分支消除,用一遍顺序扫描替代了每桶的分支判断。
加速比:AVX2 单桶 argmin/argmax 相对标量理论 3–4×;但对桶内样本很少的输入(AVX2 内核独立编译后的实测判据为 < 8 元素/桶,见SIMD 基准篇 4.5b),向量加载与水平归约的固定开销可能抵消并行收益,这也是 4.1 节"\(n \le 2N_b\) 时回退普通映射"阈值存在的原因之一。
6. 实验:像素级保真度¶
6.1 设计¶
- 度量:第 2 节的列包络、覆盖率与干净列比例,包络按 8×8 超采样光栅化计算(参考模型 = 抗锯齿线光栅化器)。
- 信号(\(N = 100{,}000\),画布 900×500 px,与 QWT7 示例同构):
- band-limited:多分量正弦(
sin(x/700)、sin(x/330)、sin(x/250)三个分量,周期约 39.6 / 18.7 / 14.1 列),最高约 0.07 周期/列(低于奈奎斯特); - peaks + noise:慢变包络 + 窄峰 + 白噪声(逼近真实频谱);
- spike pairs:24 个 ±3.5 的 2-样本尖峰对(LTTB 的病理信号);
- uneven x:窄峰 + 采样间距 ±40% 波动的非均匀 x;
- white noise:纯白噪声(最坏情况)。
- 输出预算:统一 ≈ 3600 点(= 4W);像素列桶按算法定义输出 ≤ 4W。
- 复现:全部实验脚本与数值见
docs/assets/algorithms/(downsampling_experiment.py、figures.py)。
6.2 结果¶
表 1:列包络指标(N = 100,000,900×500 画布)¶
| 信号 | 算法 | 输出点 | 覆盖率 | 最差列 | 平均误差(px) | p99(px) | 最大(px) | 干净列 |
|---|---|---|---|---|---|---|---|---|
| band-limited | uniform | 3705 | 0.495 | 0.000 | 14.4 | 32.5 | 36.2 | 0.1% |
| LTTB 经典 | 3600 | 0.625 | 0.000 | 13.0 | 33.9 | 40.0 | 3.0% | |
| MinMax | 3600 | 0.875 | 0.392 | 5.7 | 23.0 | 30.0 | 29.0% | |
| pixel col | 1800 | 0.995 | 0.714 | 0.5 | 5.5 | 8.0 | 85.8% | |
| peaks + noise | uniform | 3705 | 0.453 | 0.008 | 9.1 | 46.5 | 127.0 | 0.2% |
| LTTB 经典 | 3600 | 0.856 | 0.008 | 4.3 | 45.8 | 137.5 | 18.6% | |
| MinMax | 3600 | 0.989 | 0.526 | 1.3 | 17.1 | 59.0 | 79.9% | |
| pixel col | 1800 | 0.998 | 0.864 | 0.3 | 3.0 | 89.5 | 97.4% | |
| spike pairs | uniform | 3705 | 0.446 | 0.002 | 8.8 | 229.6 | 232.1 | 4.1% |
| LTTB 经典 | 3600 | 0.853 | 0.008 | 10.1 | 228.3 | 230.0 | 59.0% | |
| MinMax | 3600 | 0.994 | 0.667 | 1.0 | 2.0 | 227.0 | 97.3% | |
| pixel col | 1800 | 0.998 | 0.500 | 5.9 | 111.0 | 117.0 | 94.6% | |
| uneven x | uniform | 3705 | 0.444 | 0.000 | 12.9 | 93.8 | 482.0 | 0.4% |
| LTTB 经典 | 3600 | 0.863 | 0.000 | 6.3 | 104.6 | 482.0 | 14.4% | |
| MinMax | 3600 | 0.990 | 0.000 | 2.1 | 25.1 | 261.0 | 75.0% | |
| pixel col | 1800 | 0.999 | 0.773 | 0.6 | 4.0 | 205.0 | 98.0% | |
| white noise | uniform | 3705 | 0.447 | 0.001 | 107.4 | 201.0 | 233.1 | 0.0% |
| LTTB 经典 | 3600 | 0.871 | 0.001 | 39.0 | 118.1 | 204.1 | 4.4% | |
| MinMax | 3600 | 0.997 | 0.814 | 9.0 | 74.0 | 128.0 | 61.0% | |
| pixel col | 1800 | 0.999 | 0.761 | 0.3 | 11.0 | 80.0 | 98.3% |

表 2:桶预算与保真度(peaks + noise,MinMax 桶)¶
| 桶预算 | 桶数 | 输出点 | 覆盖率 | 干净列 | 平均误差(px) |
|---|---|---|---|---|---|
| 0.25 × 2W | 450 | 900 | 0.677 | 31.2% | 9.0 |
| 0.5 × 2W | 900 | 1800 | 0.926 | 63.8% | 3.9 |
| 1 × 2W(默认) | 1800 | 3600 | 0.989 | 79.9% | 1.3 |
| 2 × 2W | 3600 | 7200 | 0.995 | 88.8% | 0.8 |
| 4 × 2W | 7200 | 14400 | 0.998 | 93.4% | 0.4 |
| 8 × 2W | 14400 | 28800 | 1.000 | 96.9% | 0.2 |

6.3 讨论¶
1. 像素级保真度排序。 由高到低为 pixel column、MinMax、LTTB 经典、uniform。像素列桶在"像素级精确"指标上几乎总是最优,这是必然的:它直接在像素域优化每列的竖直包络。MinMax 桶以 2× 的点数(3600 vs 1800)取得 29%–97% 的干净列(取决于信号),这是"数据域保真"的代价。
2. 像素级不等于视觉级。 指标本身也包含度量伪差。表 1 中的非零误差主要来自三类效应:
- 边界归因伪差:8× 超采样把"恰好擦过次像素网格角点"的像素计为已涂色;降采样折线的长斜线段在极值附近会跨越列边界。此类差异对应 < 2% 覆盖率的次像素着色,人眼不可感知。
- 弦偏差:桶内两点之间的连线是直线;对曲率显著的近奈奎斯特内容(周期 1–2 列),弦偏离真实曲线的量级约为
其中 \(A\) 为振幅、\(f\) 为频率(周期/列)、\(\Delta x_b \approx 0.5\) 列,\(f \approx 0.45\) 周期/列时约 \(0.06A\),仅像素级可见。
- 极值列偏移:极值点的 \(x\) 经取整后最多移动半列;当极值恰在列边界附近时,其竖直笔画可能出现在相邻列(除尖峰对信号外,MinMax 的最大误差大多集中在这几列)。
3. 为什么 QWT7 默认 FilterPointsLTTB 而不是 pixel? 自 v7.5.0 起默认算法是真 LTTB:它在映射后的屏幕坐标上选择,对窄峰形状的保真是所有策略中最好的,永远保留曲线首尾点,并且与 MinMax 一样在浮点渲染路径(QPolygonF、PDF/SVG 导出、非取整绘制)下输出精确的映射坐标,而像素列桶的固定 +0.25/+0.75 亚像素摆放会引入系统性偏移。MinMax 桶变体(FilterPointsMinMaxLTTB,v7.4.2 及之前的默认)在需要确定性极值保证或 SIMD argmin/argmax 速度时仍有价值。此外 LTTB 是社区熟知的名字,用户容易理解它的含义。
4. 桶预算 2W 位于性能-质量拐点上(表 2):预算减半(0.5×)干净列从 80% 掉到 64%,翻倍(2×)仅提升 9 个百分点却多画 2 倍的点。这与"每列 2 桶 = 4 点/列"的奈奎斯特论证一致:低于显示极限的内容已被完全采样,继续加桶只是为亚像素细节付费。
5. 失效模式:① 尖峰对中相邻两列都含极值时,MinMax 的连线可能在两列间形成 1 列的"斜拉"伪差(该信号的最大误差列全部属于此类);② 白噪声信号上 MinMax 的干净列降到 61%,噪声的包络被完整保留,但桶间连线引入了额外的竖直线段;③ 近奈奎斯特内容(如宽度约 1 列的窄峰)出现可见的包络软化。
flowchart LR
subgraph 决策["如何选择降采样属性"]
A{"数据点数 > 10 × 画布宽度?"} -->|"否"| B["不需要降采样<br/>(或仅 FilterPoints)"]
A -->|"是"| C{"需要浮点/导出几何精确?"}
C -->|"是"| D["FilterPointsLTTB(默认)"]
C -->|"否"| E{"信号接近纯白噪声?"}
E -->|"是"| F["FilterPointsPixel<br/>(包络最稳)"]
E -->|"否"| G["FilterPointsLTTB<br/>(形状保真)"]
end
7. 代码实践¶
7.1 基本用法¶
FilterPointsLTTB 是默认开启的,显式设置亦无妨:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
警告 — 属性互斥
FilterPointsPixel、FilterPointsLTTB、FilterPointsMinMaxLTTB、FilterPointsAggressive同时开启时由QwtPointMapper内部优先级裁决:Pixel > LTTB > MinMax > Aggressive。不要同时开,生效语义即优先级最高者。
7.2 直接使用 SIMD 工具¶
qwtSimdArgMinMax 是 core 模块的公共 API,可在自己的算法里复用:
1 2 3 4 5 6 7 8 9 10 | |
7.3 调参建议¶
| 场景 | 推荐 |
|---|---|
| 一般波形、频谱(默认) | FilterPointsLTTB,无需额外设置 |
| 需要导出 PDF/SVG(浮点路径) | FilterPointsLTTB(亚像素精确映射) |
| 像素级包络最稳、widget 整数渲染 | FilterPointsPixel |
| 数据点数 < 4 × 画布宽度 | 无需降采样(降采样算法会自行回退普通映射) |
| 性能基准 | examples/bench/renderbench:可调数据量/波形/帧数,批量对比 |
8. 小结¶
MinMax 桶在 \(N_b = 2W\) 时的实测结果:在 100 万点曲线上,重绘从 262 ms/帧降到 133 ms/帧,保住约 99% 的列包络,代价是输出点数是像素列桶的两倍。经典 LTTB 没有极值保证,同一桶内的尖峰对会丢掉一个方向的笔画。自 v7.5.0 起,默认的 FilterPointsLTTB 是经典 \(N_b = 4W\) 预算下的真 LTTB(每桶一点),以峰值形状保真优先于极值保证;FilterPointsMinMaxLTTB 保留确定性极值保留与 SIMD 友好的 argmin/argmax。
9. 参考文献¶
- Steinarsson, S. Downsampling Time Series for Visual Representation. MSc thesis, University of Iceland, 2013.
- QWT7 源码:
src/plot/qwt_point_mapper.cpp(qwtLttbReduce、qwtMinMaxBucketReduce、qwtPixelColumnReduce、qwtFindVisibleRange)、src/core/qwt_simd_argminmax.cpp。 - QWT7 基准示例:
examples/bench/renderbench(五种降采样方法的实时耗时与批量对比)。 - QWT7 NaN 渲染示例:
examples/2D/nanperf(NaN 比例可调,对五种绘制属性组合做扫描对比:ClipPolygons基线、FilterPoints、FilterPointsAggressive、FilterPointsPixel、FilterPointsLTTB)。 - CHANGES.md,v7.3.0:"Added
FilterPointsPixelandFilterPointsLTTBpaint attributes…"
配套阅读: 曲线渲染平滑:高斯卷积、Savitzky-Golay 回归与特征保护 说明降采样之后的像素噪声如何被移除。