修复非固定周期采样数据放大后波形出现断开的问题

问题:加载 Problem_Data.csv(约 27µs 且不固定的采样周期、含 100–277µs 空档),勾选
.UD\SLC_1ms\rtb_BodyIAC_K40+0x2,放大到主时间轴一大格 10ms 时波形大量断开。

根因:该缩放级别下波形走「像素列上下包络」渲染,而包络是按像素列聚合的,
每列只统计「落在本列内的采样点」。本文件的采样间隔(约 27µs)与一个像素列的宽度
(约 60µs)是同量级,因此一次跃变的两个端点常常分别落在相邻两列中:对每一列而言
列内采样点都在同一边,于是这段竖直折线在两侧的包络里都不出现,被整段丢掉,
看上去就是波形断开(与逐点描边渲染对照,1224 列中有 32 列完全没有波形像素)。

修复:在列包络中补上「跨越列边界的两段折线在边界处的取值」。新增 PolylineValueAt()
取折线在列左/右边界处的线性插值并计入该列包络。由于折线在列内是线性的,其极值必然
出现在采样点或列边界上,因此「列内采样点 + 两侧边界取值」就是该列包络的精确解;
相邻两列在公共边界取到同一个值,包络带因而严丝合缝。该处理同时覆盖了「整列没有采样点」
的情况(此时两侧边界取自同一条线段),因此也一并消除了采样稀疏处原有的断口。

验证:
- 用临时开关强制走逐点描边路径作为对照基准,按像素列比对覆盖率:
  修复前 32 列缺失,修复后 0 列缺失(1224/1224 全覆盖),像素总量与对照基准相当;
- 同一区域的 12 倍放大截图显示波形已恢复为连续的方波,与逐点描边结果一致;
- 回归 VERY_BIG_DATA.CSV(稠密包络)与 BIG_DATA.csv(稀疏描边 + 包络)渲染正常,
  make clean && make all 无警告。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 10:21:57 +08:00
1 parent 79c84edea8
commit 18b8f3e38e
1 file changed
+88 -11
+88 -11
View File
@@ -59,6 +59,51 @@ namespace jdi
const wchar_t* const kMeanFilterBuildLabel = L"构建为新信号"; /**< 均值滤波面板底部按钮文案。 */ const wchar_t* const kMeanFilterBuildLabel = L"构建为新信号"; /**< 均值滤波面板底部按钮文案。 */
const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */ const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */
/**
* @brief 取折线(相邻采样点之间线性连接)在给定时刻的取值。
*
* 包络是按像素列聚合的,因此某一段折线可能**跨越列边界**:跨越边界的那一小段
* 在两个列里都不属于"落在本列内的采样点",只看列内采样点就会把它整段丢掉。
* 该函数用于在列边界处取回这段折线的取值,使相邻两列的包络严丝合缝。
*
* @param [IN] time 时间轴。
* @param [IN] values 采样值。
* @param [IN] count 可用采样点数量。
* @param [IN] firstIndex 第一个时间不小于查询时刻的采样下标(相交线段为 firstIndex-1 → firstIndex)。
* @param [IN] t 查询时刻。
* @param [OUT] out 输出取值。
* @return 折线覆盖该时刻且取到有效值返回 true。
*/
bool PolylineValueAt(const std::vector<double>& time, const std::vector<double>& values,
std::size_t count, std::size_t firstIndex, double t, double& out)
{
if (firstIndex == 0 || firstIndex >= count)
{
// 该时刻之前(或之后)没有采样点,折线并未延伸到这里。
return false;
}
const double t0 = time[firstIndex - 1];
const double t1 = time[firstIndex];
const double v0 = values[firstIndex - 1];
const double v1 = values[firstIndex];
if (!std::isfinite(t0) || !std::isfinite(t1) || !std::isfinite(v0) || !std::isfinite(v1) ||
!(t1 > t0))
{
return false;
}
double ratio = (t - t0) / (t1 - t0);
if (ratio < 0.0)
{
ratio = 0.0;
}
else if (ratio > 1.0)
{
ratio = 1.0;
}
out = v0 + (v1 - v0) * ratio;
return true;
}
/** /**
* @brief 统计相邻采样对 (k-1, k) 中穿越判定基准的边沿数量。 * @brief 统计相邻采样对 (k-1, k) 中穿越判定基准的边沿数量。
* *
@@ -3929,13 +3974,6 @@ namespace jdi
std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(i0), std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(i0),
rangeEnd, t1) - rangeEnd, t1) -
time.begin()); time.begin());
if (i1 <= i0)
{
// 该列没有采样点:说明数据在此处存在时间间隙,断开当前折线。
inSegment = false;
flushBandSegment();
continue;
}
double low = std::numeric_limits<double>::infinity(); double low = std::numeric_limits<double>::infinity();
double high = -std::numeric_limits<double>::infinity(); double high = -std::numeric_limits<double>::infinity();
@@ -3943,8 +3981,8 @@ namespace jdi
std::size_t highOrder = 0; std::size_t highOrder = 0;
bool broken = false; bool broken = false;
// 一列至少覆盖两个分组时才用分组层(否则直接扫原始采样更准且更省)。 // (一)落在本列区间内的采样点。
if (hasCoarse && (i1 - i0) >= 2 * coarseStride) if (i1 > i0 && hasCoarse && (i1 - i0) >= 2 * coarseStride)
{ {
const std::size_t firstBucket = i0 / coarseStride; const std::size_t firstBucket = i0 / coarseStride;
const std::size_t lastBucket = const std::size_t lastBucket =
@@ -3971,7 +4009,7 @@ namespace jdi
} }
} }
} }
else else if (i1 > i0)
{ {
for (std::size_t index = i0; index < i1; ++index) for (std::size_t index = i0; index < i1; ++index)
{ {
@@ -3995,9 +4033,48 @@ namespace jdi
} }
} }
// (二)跨越列边界的两段折线在边界处的取值。
//
// 波形按相邻采样点之间的**直线**绘制,而包络是按像素列聚合的。采样间隔
// 可能大于一个像素列(本仓库的 Problem_Data.csv 相邻采样间隔约 27µs 且不固定,
// 放大到 10ms/大格时一个像素列约 60µs),此时一次跃变的两个端点会分别落在
// 相邻两列里:对每一列来说"落在本列内的采样点"都在同一边,于是这段竖向折线
// 在两侧的包络里都不出现 → 波形在这些跃变处被整段丢掉,看上去就是"断开"。
//
// 把列边界处的折线取值计入本列包络即可消除该现象:折线在列内是线性的,
// 其极值必然出现在采样点或列边界上,因此「列内采样点 + 两侧边界取值」就是
// 该列包络的精确解,且相邻两列在边界处取到同一个值,包络带严丝合缝。
double boundary = 0.0;
if (PolylineValueAt(time, values, count, i0, t0, boundary))
{
if (boundary < low)
{
low = boundary;
lowOrder = (i0 > 0) ? (i0 - 1) : 0;
}
if (boundary > high)
{
high = boundary;
highOrder = (i0 > 0) ? (i0 - 1) : 0;
}
}
if (PolylineValueAt(time, values, count, i1, t1, boundary))
{
if (boundary < low)
{
low = boundary;
lowOrder = i1;
}
if (boundary > high)
{
high = boundary;
highOrder = i1;
}
}
if (!(low <= high)) if (!(low <= high))
{ {
// 本列全是无效值。 // 本列没有任何有效取值:折线尚未开始、已经结束,或该处数据缺失。
inSegment = false; inSegment = false;
flushBandSegment(); flushBandSegment();
continue; continue;