弱波增强:去掉前置滤波并新增「阶数 N」滑块;修复选区放大越界绘制与浮动面板 ✕ 交互

- 弱波增强不再做任何前置滤波(删除内部的 5 点滑动平均 BoostShapeAt):
  极值点直接在输入采样上识别。需要平滑时用户可以先用「均值滤波」构建一条
  滤波后的信号,再对它做弱波增强。
- 新增「阶数 N」参数(1~10,滑块输入),语义与均值滤波的阶数 N 一致 ——
  把同一处理重复应用 N 次,每阶以上一阶为输入:
  一阶时改定量严格等于「等级」对应的目标值;多阶会让相邻弱波互相抬高彼此的
  基准,从而把较宽的弱波区逐步恢复(实测三连弱波的中间峰 550→575→598.4)。
  某一阶若不再产生改动会提前结束迭代。
  `ComputeWeakWaveBoost(values, level, order, out)`;
  `Lane::weakBoostOrder`(默认 1)随 LaneCarry / SplitLane / 引用重映射同步,
  并进入 SceneContentHash;曲线缓存键变为「信号 + 等级 + 阶数」。
- 浮动面板同步新增一行「阶数 N」滑块(新拖拽模式 BOOST_ORDER、
  EdgePanelLayout::sliderOrder);「构建为新信号」命名带上阶数
  (形如 `<原信号> (弱波增强 60%×2阶)`)。
- 修复:选区放大时鼠标被捕获到绘图区之外(工具栏 / 信号面板 / 时间轴)会把
  选区块画到绘图区之外,而拖拽期间只失效绘图区 → 留下无法擦除的墨迹。
  现在拖拽时把选区夹在绘图区内,PaintRubberBand 再对 m_plotRect 求交一次。
- 修复:浮动面板的 ✕ 位于标题栏矩形内部,按下后会继续落到「标题栏 → 拖动面板」
  分支,表现为「按住 ✕ 却把面板拖走了」;现在按下 ✕ 直接消费该次点击(抬起时
  仍按原语义确认关闭),并把 ✕ 的光标改为手型,与其它关闭按钮一致。
- 修复:✕ / 滑块 / 底部按钮的悬停高亮依赖鼠标坐标,但从同一网格的波形移到面板上
  时悬停状态毫无变化 → OnMouseMove 返回 false → 宿主不重绘 → 高亮不出现。
  现在「进入 / 离开 / 在面板内移动」都会请求重绘。
- 离屏验证(窗口隐藏,不占用鼠标/键盘/屏幕):345 项检查全部通过,含
  阶数扫描(N=1 只改外侧弱峰、N=2 传导到中间弱峰、高 N 逼近健康幅值且不越界、
  健康峰在任何阶数下逐位不变)、两个滑块的按压/拖拽/钳制、选区越界零墨迹
  (绘图区外像素差 0)、面板 ✕ 悬停高亮与按下不拖动、光标拖拽始终在视界内,
  以及真实 20K 电机纹波录波(信号范围保持不变、20%+ 采样被调整)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 19:20:09 +08:00
1 parent 67f659ad23
commit 8c8508e5f8
5 files changed
+457 -240

No files matched your search

+16 -4
View File
@@ -513,6 +513,15 @@ namespace jdi
/** 弱波增强等级(0–1),0% = 目标取左右极值中较小/较大的那一个,100% = 取另一个。 */
double weakBoostLevel = 0.5;
/**
* @brief 弱波增强的阶数 N(1–10)。
*
* 与「均值滤波」的阶数 N 一样表示“把同一处理重复应用 N 次”(每一阶以上一阶为输入):
* 一阶时改定量就已严格等于「等级」对应的目标值;多阶会把相邻弱波互相抬高
* 彼此的基准,从而把较宽的弱波区逐步恢复。
*/
int weakBoostOrder = 1;
/** 参与弱波增强预览的信号条目(只包含被勾选的信号)。 */
std::vector<WeakBoostItem> weakBoostItems;
@@ -585,8 +594,11 @@ namespace jdi
/**
* @brief 「弱波增强」:把比周边更弱的纹波波峰/波谷恢复(自适应弱波增强)。
*
* 思路(全部 O(n)):
* 1. **识别趋势与极值**:对轻度平滑后的信号扫描单调段,方向发生翻转处即为
* 本算法**不做任何前置滤波**:需要平滑时用户可以先用「均值滤波」构建一条
* 滤波后的信号,再对它做弱波增强。极值点直接在输入采样上识别。
*
* 单阶处理(全 O(n)):
* 1. **识别趋势与极值**:扫描单调段,方向发生翻转处的上一个采样点即为
* 波峰(先升后降)或波谷(先降后升);波峰与波谷交替出现。
* 2. **判定“弱”**:对第 k 个波峰,取同类的左右相邻波峰(k−2、k+2)。
* 若本波峰低于两者的均值,说明它是被削弱的那个;波谷反之(高于两者均值)。
@@ -598,14 +610,14 @@ namespace jdi
* 4. **平滑偏移**:极值点本身取其目标值,两侧的数据点按相邻两个极值点的偏移量
* 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱波附近被推回。
*
* 与“全局缩放/局部放大”不同,这里**只改动被判为弱波的极值及其两侧**,
* 正常周期的极值不满足条件、偏移量恒为 0,因此输出与输入逐位相同。
*
* @param [IN] values 原始采样值。
* @param [IN] level 弱波增强等级(0–1,超出范围会被夹取)。
* @param [IN] order 阶数 N(重复应用次数,至少为 1;与均值滤波的阶数同一语义)。
* @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。
*/
void ComputeWeakWaveBoost(const std::vector<double>& values, double level,
void ComputeWeakWaveBoost(const std::vector<double>& values, double level, int order,
std::vector<float>& out);
/**
+177 -157
View File
@@ -137,6 +137,7 @@ namespace jdi
bool weakBoost = false;
std::vector<Lane::WeakBoostItem> weakBoostItems;
double weakBoostLevel = 0.5;
int weakBoostOrder = 1;
bool weakBoostPanelOpen = true;
double weakBoostPanelX = -1.0;
double weakBoostPanelY = -1.0;
@@ -168,6 +169,7 @@ namespace jdi
carry.weakBoost = lane.weakBoost;
carry.weakBoostItems = lane.weakBoostItems;
carry.weakBoostLevel = lane.weakBoostLevel;
carry.weakBoostOrder = lane.weakBoostOrder;
carry.weakBoostPanelOpen = lane.weakBoostPanelOpen;
carry.weakBoostPanelX = lane.weakBoostPanelX;
carry.weakBoostPanelY = lane.weakBoostPanelY;
@@ -210,6 +212,7 @@ namespace jdi
lane.weakBoost = carry.weakBoost;
lane.weakBoostItems = carry.weakBoostItems;
lane.weakBoostLevel = carry.weakBoostLevel;
lane.weakBoostOrder = carry.weakBoostOrder;
lane.weakBoostPanelOpen = carry.weakBoostPanelOpen;
lane.weakBoostPanelX = carry.weakBoostPanelX;
lane.weakBoostPanelY = carry.weakBoostPanelY;
@@ -1056,41 +1059,179 @@ namespace jdi
};
/**
* @brief 取用于“识别波峰/波谷”的轻度平滑值。
* @brief 「弱波增强」的单阶处理:把偏弱的波峰上拉、偏弱的波谷下拉。
*
* 直接对原始采样做方向判定时,采样噪声会被当成大量假波峰/波谷,使增强曲线抖动;
* 因此先看一下 5 点滑动平均(半径 2)之后的“形状”。这里按需就地计算,
* 不额外分配与采样数同量级的缓冲。
* **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号):
* 极值点直接在输入采样上通过单调段的方向翻转识别。
*
* @param [IN] values 原始采样值。
* @param [IN] index 采样下标。
* @return 轻度平滑后的取值;邻域内没有有效值时返回 NaN。
* @param [IN] input 本阶的输入采样值。
* @param [IN] strength 弱波增强等级(0–1,已夹取)。
* @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。
* @return 确实有采样点被改动返回 true;完全无弱波时返回 false。
*/
double BoostShapeAt(const std::vector<double>& values, std::size_t index)
bool ApplyWeakBoostStage(const std::vector<float>& input, double strength,
std::vector<float>& output)
{
const std::size_t count = values.size();
const std::size_t low = (index > 2) ? (index - 2) : 0;
const std::size_t high = std::min(count - 1, index + 2);
double sum = 0.0;
int used = 0;
for (std::size_t i = low; i <= high; ++i)
const std::size_t count = input.size();
output.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
const double value = values[i];
return false;
}
// 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。
for (std::size_t index = 0; index < count; ++index)
{
const float value = input[index];
if (std::isfinite(value))
{
sum += value;
++used;
output[index] = value;
}
}
if (used == 0)
// 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。
// 波峰(先升后降)与波谷(先降后升)必然交替出现,
// 因此同类极值就是极值序列里下标 ±2 的两个。
std::vector<BoostExtremum> extremes;
{
return std::numeric_limits<double>::quiet_NaN();
int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。
bool hasPrevious = false;
std::size_t previousIndex = 0;
double previousValue = 0.0;
for (std::size_t index = 0; index < count; ++index)
{
const double value = static_cast<double>(input[index]);
if (!std::isfinite(value))
{
// 无效采样作为断点:不跨越它判定极值。
hasPrevious = false;
continue;
}
if (!hasPrevious)
{
hasPrevious = true;
previousIndex = index;
previousValue = value;
continue;
}
const int step = (value > previousValue) ? 1 : ((value < previousValue) ? -1 : 0);
if (step != 0)
{
if (direction == 0)
{
direction = step;
}
else if (step != direction)
{
// 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。
BoostExtremum extreme;
extreme.index = previousIndex;
extreme.value = previousValue;
extreme.type = direction;
extremes.push_back(extreme);
direction = step;
}
}
previousIndex = index;
previousValue = value;
}
}
return sum / static_cast<double>(used);
// 识别不到足够多的极值(常量 / 单调 / 采样点太少):本阶无需任何改动。
if (extremes.size() < 5)
{
return false;
}
// 2) 逐个极值点判定“是否偏弱”,并算出它自己的目标偏移量。
const std::size_t extremeCount = extremes.size();
std::vector<double> shift(extremeCount, 0.0);
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
{
const BoostExtremum& current = extremes[k];
const BoostExtremum& left = extremes[k - 2];
const BoostExtremum& right = extremes[k + 2];
if (left.type != current.type || right.type != current.type)
{
continue;
}
if (!std::isfinite(current.value) || !std::isfinite(left.value) ||
!std::isfinite(right.value))
{
continue;
}
const double mean = (left.value + right.value) * 0.5;
const double lower = std::min(left.value, right.value);
const double upper = std::max(left.value, right.value);
double destination = 0.0;
if (current.type > 0)
{
// 波峰:低于左右两个波峰的均值 → 判定为弱波,上拉到等级对应的目标值。
if (!(current.value < mean))
{
continue;
}
destination = lower + strength * (upper - lower);
}
else
{
// 波谷:高于左右两个波谷的均值 → 判定为弱波,下拉到等级对应的目标值。
if (!(current.value > mean))
{
continue;
}
destination = upper - strength * (upper - lower);
}
shift[k] = destination - current.value;
}
// 3) 把偏移量平滑地摊到极值点之间的数据点上:
// 相邻两个极值点之间用平滑步进过渡(两端导数为 0,波形不会出现折角),
// 数据首尾则从 0 渐变到最近的极值点偏移。
// 注意:极值序列的首尾各两个元素的偏移量恒为 0(缺少同类的左右邻居),
// 因此这里只需要填充 shift 非零的区间。
bool changed = false;
const auto applySpan = [&input, &output, count](std::size_t begin, std::size_t end,
double beginShift, double endShift)
{
if (end <= begin)
{
if (begin < count && std::isfinite(input[begin]))
{
output[begin] = static_cast<float>(input[begin] + beginShift);
}
return;
}
const double span = static_cast<double>(end - begin);
for (std::size_t index = begin; index <= end; ++index)
{
const float value = input[index];
if (!std::isfinite(value))
{
continue;
}
const double t = static_cast<double>(index - begin) / span;
const double weight = t * t * (3.0 - 2.0 * t);
output[index] = static_cast<float>(value + beginShift * (1.0 - weight) +
endShift * weight);
}
};
for (std::size_t k = 0; k + 1 < extremeCount; ++k)
{
if (shift[k] == 0.0 && shift[k + 1] == 0.0)
{
continue;
}
applySpan(extremes[k].index, extremes[k + 1].index, shift[k], shift[k + 1]);
changed = true;
}
return changed;
}
} // namespace
void ComputeWeakWaveBoost(const std::vector<double>& values, double level, std::vector<float>& out)
void ComputeWeakWaveBoost(const std::vector<double>& values, double level, int order,
std::vector<float>& out)
{
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
@@ -1099,155 +1240,33 @@ namespace jdi
return;
}
// 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。
// 第 1 阶:从原始采样值出发(非有限值保持 NaN)。
std::vector<float> current(count, std::numeric_limits<float>::quiet_NaN());
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (std::isfinite(value))
{
out[index] = static_cast<float>(value);
current[index] = static_cast<float>(value);
}
}
// 增强等级:波峰取左右两个波峰中较小者为 0%、较大者为 100%,
// 目标值 = 较小 + 等级 × (较大 − 较小)(波谷相反)。
// 「阶数 N」与「均值滤波」的阶数语义一致:把同一处理**重复应用 N 次**,
// 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「等级」对应的目标值;
// 多阶时相邻的弱波会互相抬高彼此的基准,从而把较宽的弱波区逐步恢复。
const int stages = std::max(1, std::min(order, 1000));
const double strength = std::max(0.0, std::min(1.0, level));
// 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。
// 波峰(先升后降)与波谷(先降后升)必然交替出现,因此同类极值就是下标 ±2 的两个。
std::vector<BoostExtremum> extremes;
std::vector<float> next;
for (int stage = 0; stage < stages; ++stage)
{
int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。
bool hasPrevious = false;
std::size_t previousIndex = 0;
double previousShape = 0.0;
for (std::size_t index = 0; index < count; ++index)
if (!ApplyWeakBoostStage(current, strength, next))
{
if (!std::isfinite(values[index]))
{
// 无效采样作为断点:不跨越它判定极值。
hasPrevious = false;
continue;
}
const double shape = BoostShapeAt(values, index);
if (!std::isfinite(shape))
{
hasPrevious = false;
continue;
}
if (!hasPrevious)
{
hasPrevious = true;
previousIndex = index;
previousShape = shape;
continue;
}
const int step = (shape > previousShape) ? 1 : ((shape < previousShape) ? -1 : 0);
if (step != 0)
{
if (direction == 0)
{
direction = step;
}
else if (step != direction)
{
// 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。
BoostExtremum extreme;
extreme.index = previousIndex;
extreme.value = values[previousIndex];
extreme.type = direction;
extremes.push_back(extreme);
direction = step;
}
}
previousIndex = index;
previousShape = shape;
// 这一阶没有任何采样点需要调整:后续阶数也不会再有变化,提前结束。
break;
}
current.swap(next);
}
if (extremes.size() < 3)
{
// 找不到足够的极值(例如常量或单调信号):无可增强之处。
return;
}
// 2) 逐个极值点判定“是否偏弱”,并算出它自己的目标偏移量。
const std::size_t extremeCount = extremes.size();
std::vector<double> shift(extremeCount, 0.0);
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
{
const BoostExtremum& current = extremes[k];
const BoostExtremum& left = extremes[k - 2];
const BoostExtremum& right = extremes[k + 2];
if (left.type != current.type || right.type != current.type)
{
continue;
}
if (!std::isfinite(current.value) || !std::isfinite(left.value) ||
!std::isfinite(right.value))
{
continue;
}
const double mean = (left.value + right.value) * 0.5;
const double lower = std::min(left.value, right.value);
const double upper = std::max(left.value, right.value);
double destination = 0.0;
if (current.type > 0)
{
// 波峰:低于左右两个波峰的均值 → 判定为弱波,上拉到等级对应的目标值。
if (!(current.value < mean))
{
continue;
}
destination = lower + strength * (upper - lower);
}
else
{
// 波谷:高于左右两个波谷的均值 → 判定为弱波,下拉到等级对应的目标值。
if (!(current.value > mean))
{
continue;
}
destination = upper - strength * (upper - lower);
}
shift[k] = destination - current.value;
}
// 3) 把偏移量平滑地摊到极值点之间的数据点上:
// 相邻两个极值点之间用平滑步进过渡(两端导数为 0,波形不会出现折角),
// 数据首尾则从 0 渐变到最近的极值点偏移,保证端点原样不动。
const auto applySpan = [&values, &out, count](std::size_t begin, std::size_t end,
double beginShift, double endShift)
{
if (end <= begin)
{
if (begin < count && std::isfinite(values[begin]))
{
out[begin] = static_cast<float>(values[begin] + beginShift);
}
return;
}
const double span = static_cast<double>(end - begin);
for (std::size_t index = begin; index <= end; ++index)
{
const double value = values[index];
if (!std::isfinite(value))
{
continue;
}
const double t = static_cast<double>(index - begin) / span;
const double weight = t * t * (3.0 - 2.0 * t);
out[index] = static_cast<float>(value + beginShift * (1.0 - weight) +
endShift * weight);
}
};
applySpan(0, extremes.front().index, 0.0, shift.front());
for (std::size_t k = 0; k + 1 < extremeCount; ++k)
{
applySpan(extremes[k].index, extremes[k + 1].index, shift[k], shift[k + 1]);
}
applySpan(extremes.back().index, count - 1, shift.back(), 0.0);
out.swap(current);
}
const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const
@@ -1893,6 +1912,7 @@ namespace jdi
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.weakBoost = source.weakBoost;
lane.weakBoostLevel = source.weakBoostLevel;
lane.weakBoostOrder = source.weakBoostOrder;
lane.weakBoostPanelOpen = source.weakBoostPanelOpen;
lane.weakBoostPanelX = source.weakBoostPanelX;
lane.weakBoostPanelY = source.weakBoostPanelY;