实验性功能新增「差异放大」,并修正「构建为新信号」按钮的间距

1. 新增「实验性功能 → 差异放大」(与其它实验功能一样是带勾选的开关)
   - 用途:纹波类信号某个周期(甚至半个周期)的 M 形下凹明显偏弱时,
     把这段下凹按比例拉回周边振幅,恢复正常周期的对比度,便于看出波形异常。
   - 算法(全 O(n),不额外占用与信号长度成正比以外的内存):
     以半径约半个纹波周期的滑动平均作为纹波中心线,用邻域内「交流分量的最大值」
     作为局部纹波振幅,取全信号振幅的 75 分位作为目标振幅,
     逐点增益 = 目标振幅 / 局部振幅(夹在 1 到放大倍数之间),
     输出 = 中心线 + 增益 × 交流分量,再夹在周边包络内。
   - 关键性质(均已用合成正弦逐点验证):
     * 增益下界恒为 1,因此**只有被判定为"偏弱"的采样点会改变**,
       其余采样点输出与原值逐位相同 —— 实测逐周期表格中只有一个周期发生变化;
     * 放大结果夹在周边包络内,因此**任何一点都不超出原有的振幅范围**,
       整体值域与其它周期的峰峰值都不变;
     * 端点自动收敛到不放大,避免记录首尾被整片抬升。
   - 参数「窗口点数 X」(窗口长度,建议略大于纹波周期的一半)与
     「放大倍数 N」(增益上限)都可在浮动面板内按信号编辑。

2. 浮动面板
   - 与「均值滤波」完全同一套排版:自上而下逐信号条目,
     第一排信号名与复选框、第二排两个参数输入框;取消勾选即不显示该信号的参数与预览曲线;
     启用时默认只勾选网格内第一个可见信号。
   - 新增「构建为新信号」按钮,按各信号自己的参数把结果插入到源信号下方,
     与「均值滤波」的构建机制完全一致(统一走同一套请求与插入流程)。

3. 光标交互
   - 纵向光标额外标注差异放大曲线在该时刻的取值,配色与原始信号一致,
     数值尾部括号内容为「差异化」,双光标时同样带增量。

4. 修复「构建为新信号」按钮与面板下边界的间距
   - 原按钮行直接贴着面板下边框,与面板左右内边距及内容间距都不协调;
     现在按钮上方与下方各保留一个与左右一致的内边距(均值滤波与差异放大同时生效)。

测试:离屏验证套件新增两组检查(合成正弦上算法只改动一个周期、放大不越界、
参数上限生效、面板排版与按钮间距;再写一份合成纹波文件真正加载渲染并核对曲线数值),
合计 289 项检查全部通过;验证过程不显示窗口、不移动鼠标键盘、不抓屏。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 17:54:27 +08:00
1 parent 8801c8f019
commit d4206a8d67
6 files changed
+1188 -185

No files matched your search

+113 -33
View File
@@ -1461,8 +1461,8 @@ namespace jdi
return true;
}
bool Application::BuildMeanFilterSignal(int fileIndex, int signalIndex, int points, int order,
bool makeVisible)
bool Application::BuildDerivedSignal(int fileIndex, int signalIndex, int points, int order,
bool diffAmp, bool makeVisible)
{
const DataFile* file = m_dataSet.FileAt(fileIndex);
if (file == nullptr || signalIndex < 0 || signalIndex >= static_cast<int>(file->signals.size()))
@@ -1472,21 +1472,37 @@ namespace jdi
const SignalData& source = file->signals[static_cast<std::size_t>(signalIndex)];
const std::wstring baseName = source.Title();
// 1. 计算多阶均值滤波采样值(与预览曲线共用同一实现)。
std::vector<float> filtered;
ComputeMovingAverageIterated(source.values, points, order, filtered);
// 2. 组装新信号:名称形如「<原信号名称> (N点均值x阶)」。
SignalData derived;
derived.name = baseName + L" (" + std::to_wstring(points) + L"点均值";
if (order > 1)
// 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。
std::vector<float> derived_values;
if (diffAmp)
{
derived.name += L"×" + std::to_wstring(order) + L"阶";
ComputeDiffAmplify(source.values, points, order, derived_values);
}
else
{
ComputeMovingAverageIterated(source.values, points, order, derived_values);
}
// 2. 组装新信号:均值滤波形如「<原信号名称> (N点均值×M阶)」,
// 差异放大形如「<原信号名称> (X点差异放大×N倍)」。
SignalData derived;
if (diffAmp)
{
derived.name = baseName + L" (" + std::to_wstring(points) + L"点差异放大×" +
std::to_wstring(order) + L"倍)";
}
else
{
derived.name = baseName + L" (" + std::to_wstring(points) + L"点均值";
if (order > 1)
{
derived.name += L"×" + std::to_wstring(order) + L"阶";
}
derived.name += L")";
}
derived.name += L")";
derived.rawHeader = derived.name;
derived.unit = source.unit;
derived.values.assign(filtered.begin(), filtered.end());
derived.values.assign(derived_values.begin(), derived_values.end());
// 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。
int newIndex = -1;
@@ -1518,24 +1534,67 @@ namespace jdi
return true;
}
bool Application::ApplyPendingMeanFilterBuild()
bool Application::ApplyPendingDerivedBuild()
{
int laneIndex = -1;
std::vector<PlotView::MeanFilterBuild> items;
if (!m_plot.TakePendingMeanFilterBuild(laneIndex, items))
// 两类「构建为新信号」请求(均值滤波 / 差异放大)机制完全一致,
// 统一整理成同一份构建清单后按下标降序处理。
struct BuildRequest
{
int fileIndex = -1;
int signalIndex = -1;
int first = 0;
int second = 0;
bool diffAmp = false;
};
std::vector<BuildRequest> requests;
{
int laneIndex = -1;
std::vector<PlotView::MeanFilterBuild> items;
if (m_plot.TakePendingMeanFilterBuild(laneIndex, items))
{
for (const PlotView::MeanFilterBuild& item : items)
{
BuildRequest request;
request.fileIndex = item.fileIndex;
request.signalIndex = item.signalIndex;
request.first = item.points;
request.second = item.order;
request.diffAmp = false;
requests.push_back(request);
}
}
}
{
int laneIndex = -1;
std::vector<PlotView::DiffAmpBuild> items;
if (m_plot.TakePendingDiffAmpBuild(laneIndex, items))
{
for (const PlotView::DiffAmpBuild& item : items)
{
BuildRequest request;
request.fileIndex = item.fileIndex;
request.signalIndex = item.signalIndex;
request.first = item.points;
request.second = item.gain;
request.diffAmp = true;
requests.push_back(request);
}
}
}
if (requests.empty())
{
return false;
}
if (m_tabs.empty() || laneIndex < 0 ||
laneIndex >= static_cast<int>(ActiveTab().Lanes().size()))
if (m_tabs.empty())
{
return false;
}
// **同一文件内必须按下标降序处理**:每次插入只让插入点之后的信号下标 +1,
// 降序处理可保证尚未处理的信号下标始终有效(升序会把新插入的信号当成原信号)。
std::sort(items.begin(), items.end(),
[](const PlotView::MeanFilterBuild& a, const PlotView::MeanFilterBuild& b)
std::sort(requests.begin(), requests.end(),
[](const BuildRequest& a, const BuildRequest& b)
{
if (a.fileIndex != b.fileIndex)
{
@@ -1545,27 +1604,38 @@ namespace jdi
});
int built = 0;
int lastPoints = 0;
int lastOrder = 1;
for (const PlotView::MeanFilterBuild& item : items)
int lastFirst = 0;
int lastSecond = 1;
bool lastDiffAmp = false;
for (const BuildRequest& request : requests)
{
if (BuildMeanFilterSignal(item.fileIndex, item.signalIndex, item.points, item.order, true))
if (BuildDerivedSignal(request.fileIndex, request.signalIndex, request.first,
request.second, request.diffAmp, true))
{
++built;
lastPoints = item.points;
lastOrder = item.order;
lastFirst = request.first;
lastSecond = request.second;
lastDiffAmp = request.diffAmp;
}
}
if (built > 0)
{
std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 " +
std::to_wstring(lastPoints) + L"点均值";
if (lastOrder > 1)
std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 ";
if (lastDiffAmp)
{
text += L"×" + std::to_wstring(lastOrder) + L"阶";
text += std::to_wstring(lastFirst) + L"点差异放大×" +
std::to_wstring(lastSecond) + L"倍信号。";
}
else
{
text += std::to_wstring(lastFirst) + L"点均值";
if (lastSecond > 1)
{
text += L"×" + std::to_wstring(lastSecond) + L"阶";
}
text += L"滤波信号。";
}
text += L"滤波信号。";
SetStatusMessage(text, false);
}
return built > 0;
@@ -1698,6 +1768,12 @@ namespace jdi
meanFilter.id = PLOT_CMD_LANE_MEAN_FILTER;
meanFilter.checked = m_plot.LaneMeanFilter(laneIndex);
experimental.submenu.push_back(meanFilter);
MenuItem diffAmp;
diffAmp.label = L"差异放大";
diffAmp.id = PLOT_CMD_LANE_DIFF_AMP;
diffAmp.checked = m_plot.LaneDiffAmp(laneIndex);
experimental.submenu.push_back(diffAmp);
}
items.push_back(experimental);
@@ -1812,6 +1888,10 @@ namespace jdi
// 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。
m_plot.ToggleLaneMeanFilter(laneIndex);
break;
case PLOT_CMD_LANE_DIFF_AMP:
// 「实验性功能 → 差异放大」:切换差异放大曲线与参数面板。
m_plot.ToggleLaneDiffAmp(laneIndex);
break;
case PLOT_CMD_LANE_SPLIT:
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
m_plot.SplitLane(laneIndex);
@@ -2311,7 +2391,7 @@ namespace jdi
// 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。
ApplyPendingRename();
// 「均值滤波 → 构建为新信号」按钮:立即构建新信号并同步各视图。
ApplyPendingMeanFilterBuild();
ApplyPendingDerivedBuild();
InvalidateRect(hwnd, nullptr, FALSE);
}
// 拖动期间 Windows 不再派发 WM_SETCURSOR,需在按下后立即刷新一次,