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

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

+4 -3
View File
@@ -247,7 +247,7 @@ namespace jdi
*
* @return 实际构建了新信号返回 true。
*/
bool ApplyPendingMeanFilterBuild();
bool ApplyPendingDerivedBuild();
/**
* @brief 把一条多阶均值滤波派生信号插入数据层并同步各视图。
@@ -258,8 +258,9 @@ namespace jdi
* @param [IN] makeVisible 是否在当前标签页中勾选新信号。
* @return 构建并插入成功返回 true。
*/
bool BuildMeanFilterSignal(int fileIndex, int signalIndex, int points, int order,
bool makeVisible);
bool BuildDerivedSignal(int fileIndex, int signalIndex, int points, int order,
bool diffAmp,
bool makeVisible);
/**
* @brief 弹出文件选择对话框并加载所选文件。
+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,需在按下后立即刷新一次,
+110 -3
View File
@@ -57,7 +57,9 @@ namespace jdi
EDGE_CENTER_POINTS, /**< 编辑「中心轴边沿计数」的均值点数 X。 */
EDGE_CENTER_ORDER, /**< 编辑「中心轴边沿计数」的阶数 N。 */
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
MEAN_FILTER_ORDER /**< 编辑「均值滤波」的阶数 N。 */
MEAN_FILTER_ORDER, /**< 编辑「均值滤波」的阶数 N。 */
DIFF_AMP_POINTS, /**< 编辑「差异放大」的窗口点数 X。 */
DIFF_AMP_ORDER /**< 编辑「差异放大」的放大倍数 N。 */
};
/**
@@ -74,7 +76,8 @@ namespace jdi
PLOT_CMD_LANE_EDGE_COUNT = 7, /**< 切换本网格的「实验性功能 → 边沿计数」开关。 */
PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */
PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
PLOT_CMD_LANE_MEAN_FILTER = 10 /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
PLOT_CMD_LANE_DIFF_AMP = 11 /**< 切换本网格的「实验性功能 → 差异放大」开关。 */
};
/**
@@ -84,7 +87,8 @@ namespace jdi
{
COUNT = 0, /**< 边沿计数。 */
CENTER = 1, /**< 边沿计数(中心轴)。 */
MEAN = 2 /**< 均值滤波。 */
MEAN = 2, /**< 均值滤波。 */
DIFF_AMP = 3 /**< 差异放大。 */
};
/**
@@ -518,6 +522,86 @@ namespace jdi
*/
bool TakePendingMeanFilterBuild(int& laneIndex, std::vector<MeanFilterBuild>& items);
/**
* @brief 「实验性功能 → 差异放大」的「构建为新信号」单个请求
* (一个被勾选的信号 + 自己的参数)。
*/
struct DiffAmpBuild
{
int fileIndex = -1; /**< 信号所属文件下标。 */
int signalIndex = -1; /**< 文件内信号下标。 */
int points = 80; /**< 窗口点数 X(略大于纹波周期的一半)。 */
int gain = 2; /**< 放大倍数 N。 */
};
/**
* @brief 取出「差异放大 → 构建为新信号」请求(由面板底部按钮触发)。
* @param [OUT] laneIndex 触发该请求的网格下标。
* @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。
* @return 存在待处理请求返回 true(取出后清空)。
*/
bool TakePendingDiffAmpBuild(int& laneIndex, std::vector<DiffAmpBuild>& items);
/**
* @brief 切换某网格的「差异放大」开关。
* @param [IN] laneIndex 网格下标。
* @param [IN] enabled 是否开启。
* @return 状态发生变化返回 true。
*/
bool SetLaneDiffAmp(int laneIndex, bool enabled);
/**
* @brief 查询某网格的「差异放大」开关。
* @param [IN] laneIndex 网格下标。
* @return 已开启返回 true。
*/
bool LaneDiffAmp(int laneIndex) const;
/**
* @brief 切换某网格的「差异放大」开关。
* @param [IN] laneIndex 网格下标。
* @return 切换后的状态。
*/
bool ToggleLaneDiffAmp(int laneIndex);
/**
* @brief 获取差异放大曲线(按「信号 + 窗口点数 + 放大倍数」缓存)。
* @param [IN] signal 目标信号。
* @param [IN] points 窗口点数 X。
* @param [IN] gain 放大倍数 N。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
*/
const std::vector<float>& DiffAmpCurveFor(const SignalData& signal, int points, int gain) const;
/**
* @brief 取某个时刻的差异放大曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] points 窗口点数 X(略大于纹波周期的一半)。
* @param [IN] gain 放大倍数 N。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool DiffAmpValueAt(const DataFile& file, const SignalData& signal, int points, int gain,
double time, double& value) const;
/**
* @brief 切换某个信号在「差异放大」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 切换后的勾选状态。
*/
bool ToggleLaneDiffAmpSignal(int laneIndex, const SignalRef& ref);
/**
* @brief 查询某个信号在「差异放大」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 已勾选返回 true。
*/
bool LaneDiffAmpSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
* @param [IN] laneIndex 网格下标。
@@ -1091,6 +1175,17 @@ namespace jdi
void PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int points, int order) const;
/**
* @brief 绘制某条信号的差异放大曲线(与均值滤波曲线同一配色派生规则)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] points 窗口点数 X(局部差异强度统计半径)。
* @param [IN] gain 放大倍数 N(最大增益)。
*/
void PaintDiffAmpCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int points, int gain) const;
/**
* @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
*/
@@ -1305,7 +1400,9 @@ namespace jdi
bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */
int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */
int m_pendingDiffAmpLane = -1; /**< 待构建的差异放大信号所属网格下标(-1 表示无请求)。 */
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
std::vector<DiffAmpBuild> m_pendingDiffAmpItems; /**< 待构建的差异放大信号及其参数。 */
/**
* @brief 实验性功能浮动面板的 ✕ 处于"已按下、等待抬起确认"状态。
@@ -1361,6 +1458,16 @@ namespace jdi
};
mutable std::deque<MeanCurveEntry> m_meanCurveCache;
/** 多阶均值滤波曲线缓存项(按「信号指针 + 均值点数 + 阶数」标识)。 */
struct DiffAmpCurveEntry
{
const SignalData* signal = nullptr;
int points = 80; /**< 窗口点数 X。 */
int gain = 2; /**< 放大倍数 N。 */
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
};
mutable std::deque<DiffAmpCurveEntry> m_diffAmpCurveCache;
/** 中心轴边沿统计缓存项(增量统计机制与 `EdgeCountEntry` 完全一致)。 */
struct EdgeCenterCountEntry
{
+380 -75
View File
@@ -70,7 +70,7 @@ namespace jdi
const float kEdgePanelMeanEditHeight = 24.0f; /**< 逐条目面板中每条信号的参数行高度。 */
const float kEdgePanelMeanRowGap = 2.0f; /**< 名称行与参数行之间的间距。 */
const float kEdgePanelMeanCheckSize = 15.0f; /**< 逐条目面板中条目复选框尺寸。 */
const wchar_t* const kMeanFilterBuildLabel = L"构建为新信号"; /**< 均值滤波面板底部按钮文案。 */
const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 差异放大共用)。 */
const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */
/**
@@ -228,8 +228,11 @@ namespace jdi
*
* 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。
*/
const EdgePanelKind kEdgePanelKinds[] = {
EdgePanelKind::COUNT, EdgePanelKind::CENTER, EdgePanelKind::MEAN};
const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::DIFF_AMP};
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
/**
* @brief 时间间隔的显示单位。
@@ -1126,6 +1129,7 @@ namespace jdi
m_edgeCountCache.clear();
m_edgeCenterCountCache.clear();
m_meanCurveCache.clear();
m_diffAmpCurveCache.clear();
// 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。
m_hoverSignal = SignalRef();
m_hoverLegend = SignalRef();
@@ -1700,6 +1704,96 @@ namespace jdi
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].meanFilter;
}
bool PlotView::LaneDiffAmp(int laneIndex) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].diffAmp;
}
bool PlotView::SetLaneDiffAmp(int laneIndex, bool enabled)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
if (lane.diffAmp == enabled)
{
return false;
}
lane.diffAmp = enabled;
if (enabled)
{
// 与「均值滤波」完全一致:默认只勾选网格内**第一个可见信号**。
if (lane.diffAmpItems.empty())
{
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data != nullptr && resolved.visible)
{
lane.EnsureDiffAmpItem(ref);
break;
}
}
}
lane.diffAmpPanelOpen = true;
}
else
{
lane.diffAmpPanelOpen = false;
}
return true;
}
bool PlotView::ToggleLaneDiffAmp(int laneIndex)
{
const bool enabled = !LaneDiffAmp(laneIndex);
SetLaneDiffAmp(laneIndex, enabled);
return enabled;
}
bool PlotView::ToggleLaneDiffAmpSignal(int laneIndex, const SignalRef& ref)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
if (lane.DiffAmpEnabled(ref))
{
lane.RemoveDiffAmpItem(ref);
return false;
}
lane.EnsureDiffAmpItem(ref);
return true;
}
bool PlotView::LaneDiffAmpSignalEnabled(int laneIndex, const SignalRef& ref) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].DiffAmpEnabled(ref);
}
bool PlotView::TakePendingDiffAmpBuild(int& laneIndex, std::vector<DiffAmpBuild>& items)
{
if (m_pendingDiffAmpLane < 0)
{
return false;
}
laneIndex = m_pendingDiffAmpLane;
items = m_pendingDiffAmpItems;
m_pendingDiffAmpLane = -1;
m_pendingDiffAmpItems.clear();
return true;
}
bool PlotView::SetLaneMeanFilter(int laneIndex, bool enabled)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
@@ -1828,6 +1922,53 @@ namespace jdi
return m_meanCurveCache.back().values;
}
const std::vector<float>& PlotView::DiffAmpCurveFor(const SignalData& signal, int points,
int gain) const
{
const int window = std::max(1, points);
const int multiplier = std::max(1, gain);
for (const DiffAmpCurveEntry& entry : m_diffAmpCurveCache)
{
if (entry.signal == &signal && entry.points == window && entry.gain == multiplier)
{
return entry.values;
}
}
DiffAmpCurveEntry entry;
entry.signal = &signal;
entry.points = window;
entry.gain = multiplier;
// 与「构建为新信号」共用同一实现(Utilities 内的自由函数),
// 因此预览曲线与构建出的采样值逐位一致。
ComputeDiffAmplify(signal.values, window, multiplier, entry.values);
m_diffAmpCurveCache.push_back(std::move(entry));
return m_diffAmpCurveCache.back().values;
}
bool PlotView::DiffAmpValueAt(const DataFile& file, const SignalData& signal, int points, int gain,
double time, double& value) const
{
const int index = FindNearestSample(file, time);
if (index < 0)
{
return false;
}
const std::vector<float>& curve = DiffAmpCurveFor(signal, points, gain);
if (static_cast<std::size_t>(index) >= curve.size())
{
return false;
}
const float sampled = curve[static_cast<std::size_t>(index)];
if (!std::isfinite(sampled))
{
return false;
}
value = static_cast<double>(sampled);
return true;
}
void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points,
int order, bool hasRange, double rangeMin, double rangeMax,
std::size_t& rising, std::size_t& falling) const
@@ -2067,11 +2208,16 @@ namespace jdi
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool diffAmp = (kind == EdgePanelKind::DIFF_AMP);
// 「均值滤波」「中心轴边沿计数」「差异放大」都是逐条目面板(名称 + 输入框 [+ 统计])。
const bool itemized = (centerAxis || meanFilter || diffAmp);
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” + 光标区间尾缀)。
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “差异放大”
// + 光标区间尾缀)。
const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix());
: (diffAmp ? std::wstring(L"差异放大")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix()));
float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale +
8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) +
kEdgePanelTitleHeight * m_scale;
@@ -2086,7 +2232,7 @@ namespace jdi
continue;
}
const float nameWidth = UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->Title());
if (meanFilter || centerAxis)
if (itemized)
{
// 逐条目面板:第一排 = 名称 + 复选框;第三排(中心轴)= 统计结果。
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
@@ -2094,7 +2240,7 @@ namespace jdi
}
std::size_t rising = 0;
std::size_t falling = 0;
if (meanFilter)
if (meanFilter || diffAmp)
{
continue;
}
@@ -2120,12 +2266,14 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth));
}
if (centerAxis || meanFilter)
if (centerAxis || meanFilter || diffAmp)
{
// 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。
// (「均值点数 X」/「阶数 N」的中心轴面板与均值滤波面板排布完全一致。)
// (「均值点数 X」/「阶数 N」的布局对三个逐条目面板通用:标签与数值框各占半行,
// 标签取较宽者以保证“放大倍数 N”这类更长的标签不被压住。)
const float labelX = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X");
const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"阶数 N");
const float labelN = std::max(UiMeasureTextWidth(m_fonts->uiSmall, L"阶数 N"),
UiMeasureTextWidth(m_fonts->uiSmall, L"放大倍数 N"));
const float fieldWidth =
std::max(kEdgePanelEditFieldWidth * m_scale * 0.75f,
UiMeasureTextWidth(m_fonts->monoSmall, L"1000000") + 18.0f * m_scale);
@@ -2134,11 +2282,11 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
}
if (meanFilter)
if (meanFilter || diffAmp)
{
// 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。
const float buttonWidth =
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterBuildLabel) + 28.0f * m_scale;
UiMeasureTextWidth(m_fonts->uiSmall, kBuildSignalLabel) + 28.0f * m_scale;
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
}
@@ -2661,10 +2809,12 @@ namespace jdi
if (m_editTarget == PlotEditTarget::EDGE_CENTER_POINTS ||
m_editTarget == PlotEditTarget::EDGE_CENTER_ORDER ||
m_editTarget == PlotEditTarget::MEAN_FILTER_POINTS ||
m_editTarget == PlotEditTarget::MEAN_FILTER_ORDER)
m_editTarget == PlotEditTarget::MEAN_FILTER_ORDER ||
m_editTarget == PlotEditTarget::DIFF_AMP_POINTS ||
m_editTarget == PlotEditTarget::DIFF_AMP_ORDER)
{
// 「均值点数 X」/「阶数 N」:输入框就落在对应的字段矩形上
//(中心轴面板与均值滤波面板的字段矩形含义相同)。
//(中心轴、均值滤波与差异放大面板的字段矩形含义相同)。
if (m_editFieldRect.Width <= 0.0f)
{
return gd::RectF();
@@ -2831,30 +2981,37 @@ namespace jdi
}
// 「均值点数 X」/「阶数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。
// 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用对象是它自己的条目列表。
// 「中心轴边沿计数」「差异放大」用**完全相同的参数与语义**,
// 只是各自作用于自己的条目列表。
const bool centerTarget =
(target == PlotEditTarget::EDGE_CENTER_POINTS || target == PlotEditTarget::EDGE_CENTER_ORDER);
const bool diffTarget =
(target == PlotEditTarget::DIFF_AMP_POINTS || target == PlotEditTarget::DIFF_AMP_ORDER);
if (target == PlotEditTarget::MEAN_FILTER_POINTS || target == PlotEditTarget::MEAN_FILTER_ORDER ||
centerTarget)
centerTarget || diffTarget)
{
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool isPoints = (target == PlotEditTarget::MEAN_FILTER_POINTS ||
target == PlotEditTarget::EDGE_CENTER_POINTS);
target == PlotEditTarget::EDGE_CENTER_POINTS ||
target == PlotEditTarget::DIFF_AMP_POINTS);
const Lane::MeanFilterItem* existing =
centerTarget ? lane.FindEdgeCenterItem(m_editMeanRef) : lane.FindMeanFilterItem(m_editMeanRef);
centerTarget ? lane.FindEdgeCenterItem(m_editMeanRef)
: (diffTarget ? lane.FindDiffAmpItem(m_editMeanRef)
: lane.FindMeanFilterItem(m_editMeanRef));
if (existing == nullptr)
{
return true;
}
const int maximum = isPoints ? 1000000 : 1000;
int value = static_cast<int>(std::lround(parsed));
// 阶数至少为 1(0 阶等于不做滤波,没有意义);均值点数可以为 0(不平滑)。
// 阶数/倍数至少为 1(1 阶等于不做滤波、1 倍等于不放大);均值点数可以为 0。
value = isPoints ? std::max(0, std::min(value, maximum))
: std::max(1, std::min(value, maximum));
Lane::MeanFilterItem& item =
centerTarget ? lane.EnsureEdgeCenterItem(m_editMeanRef)
: lane.EnsureMeanFilterItem(m_editMeanRef);
: (diffTarget ? lane.EnsureDiffAmpItem(m_editMeanRef)
: lane.EnsureMeanFilterItem(m_editMeanRef));
if (isPoints)
{
item.points = value;
@@ -2866,6 +3023,7 @@ namespace jdi
// 参数变化会改变曲线:清掉派生缓存后重绘。
m_meanCurveCache.clear();
m_edgeCenterCountCache.clear();
m_diffAmpCurveCache.clear();
InvalidateSceneCache();
return true;
}
@@ -3108,7 +3266,8 @@ namespace jdi
{
return LoadCursorW(nullptr, IDC_SIZEALL);
}
if (kind == EdgePanelKind::MEAN && layout.button.Width > 0.0f &&
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::DIFF_AMP) &&
layout.button.Width > 0.0f &&
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
{
// 「构建为新信号」按钮:手型光标作为可点击提示。
@@ -3379,6 +3538,10 @@ namespace jdi
lane.meanFilterPanelX = left;
lane.meanFilterPanelY = top;
break;
case EdgePanelKind::DIFF_AMP:
lane.diffAmpPanelX = left;
lane.diffAmpPanelY = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanelX = left;
@@ -3584,7 +3747,7 @@ namespace jdi
// * 底部按钮 → 触发「构建为新信号」请求;
// * 标题栏 → 拖动面板;
// * 面板主体 → 消费点击,但**不拖动也不穿透**(用户明确要求只有标题栏可拖拽)。
for (int pass = 0; pass < 3; ++pass)
for (int pass = 0; pass < kEdgePanelKindCount; ++pass)
{
const EdgePanelKind kind = kEdgePanelKinds[pass];
bool handledPanel = false;
@@ -3607,17 +3770,23 @@ namespace jdi
m_pendingPanelCloseKind = kind;
}
// 「中心轴边沿计数」/「均值滤波」面板:逐信号条目(同一套排布与交互)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER) && m_tab != nullptr)
// 「中心轴边沿计数」/「均值滤波」/「差异放大」面板:逐信号条目
// (同一套排布与交互,只是输入框绑定的编辑目标与条目列表不同)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
kind == EdgePanelKind::DIFF_AMP) &&
m_tab != nullptr)
{
const bool meanMode = (kind == EdgePanelKind::MEAN);
const bool diffMode = (kind == EdgePanelKind::DIFF_AMP);
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
const auto findItem = [&]() -> const Lane::MeanFilterItem*
{
return meanMode ? tabLane.FindMeanFilterItem(ref) : tabLane.FindEdgeCenterItem(ref);
return meanMode ? tabLane.FindMeanFilterItem(ref)
: (diffMode ? tabLane.FindDiffAmpItem(ref)
: tabLane.FindEdgeCenterItem(ref));
};
// 第一排右侧的复选框 → 切换该信号的预览勾选状态。
if (layout.rowChecks[rowIndex].Width > 0.0f &&
@@ -3627,25 +3796,33 @@ namespace jdi
{
ToggleLaneMeanFilterSignal(index, ref);
}
else if (diffMode)
{
ToggleLaneDiffAmpSignal(index, ref);
}
else
{
ToggleLaneEdgeCenterSignal(index, ref);
}
m_meanCurveCache.clear();
m_edgeCenterCountCache.clear();
m_diffAmpCurveCache.clear();
InvalidateSceneCache();
m_dragMode = PlotDragMode::NONE;
return true;
}
// 第二排的两个输入框 → 就地编辑该信号的均值点数 X / 阶数 N。
// 第二排的两个输入框 → 就地编辑该信号的均值点数 X / 阶数 N
//(差异放大面板是窗口点数 X / 放大倍数 N,交互完全一致)。
if (layout.rowPoints[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowPoints[rowIndex], fx, fy))
{
const Lane::MeanFilterItem* item = findItem();
m_editMeanRef = ref;
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS,
index, std::to_wstring(item != nullptr ? item->points : 5),
: (diffMode ? PlotEditTarget::DIFF_AMP_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS),
index,
std::to_wstring(item != nullptr ? item->points : (diffMode ? 64 : 5)),
layout.rowPoints[rowIndex]);
return true;
}
@@ -3655,13 +3832,51 @@ namespace jdi
const Lane::MeanFilterItem* item = findItem();
m_editMeanRef = ref;
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ORDER
: PlotEditTarget::EDGE_CENTER_ORDER,
index, std::to_wstring(item != nullptr ? item->order : 1),
: (diffMode ? PlotEditTarget::DIFF_AMP_ORDER
: PlotEditTarget::EDGE_CENTER_ORDER),
index,
std::to_wstring(item != nullptr ? item->order : (diffMode ? 2 : 1)),
layout.rowOrder[rowIndex]);
return true;
}
}
if (diffMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{
// 「差异放大 → 构建为新信号」:与均值滤波同一套请求机制。
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(index)];
m_pendingDiffAmpItems.clear();
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || !resolved.visible)
{
continue;
}
const Lane::DiffAmpItem* item = lane.FindDiffAmpItem(ref);
if (item == nullptr)
{
continue;
}
DiffAmpBuild build;
build.fileIndex = ref.fileIndex;
build.signalIndex = ref.signalIndex;
build.points = item->points;
build.gain = item->order;
m_pendingDiffAmpItems.push_back(build);
}
if (!m_pendingDiffAmpItems.empty())
{
const DiffAmpBuild& first = m_pendingDiffAmpItems.front();
SetHint(L"正在构建差异放大信号(" + std::to_wstring(first.points) +
L" 点 " + std::to_wstring(first.gain) + L" 倍,共 " +
std::to_wstring(m_pendingDiffAmpItems.size()) + L" 条)…");
m_pendingDiffAmpLane = index;
}
m_dragMode = PlotDragMode::NONE;
return true;
}
if (meanMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{
// 「构建为新信号」:只登记请求(含每个信号自己的 X/N),
@@ -3844,6 +4059,9 @@ namespace jdi
case EdgePanelKind::MEAN:
SetLaneMeanFilter(lane, false);
break;
case EdgePanelKind::DIFF_AMP:
SetLaneDiffAmp(lane, false);
break;
case EdgePanelKind::COUNT:
default:
SetLaneEdgeCount(lane, false);
@@ -4810,6 +5028,16 @@ namespace jdi
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.order));
}
// 「差异放大」的开关与每个被勾选信号各自的窗口点数 X / 放大倍数 N 同理。
mix(Tab().Lanes()[laneIndex].diffAmp ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].diffAmpItems.size()));
for (const Lane::DiffAmpItem& item : Tab().Lanes()[laneIndex].diffAmpItems)
{
mix(static_cast<std::uint64_t>(item.ref.fileIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.order));
}
mix(signals.size());
for (const SignalRef& ref : signals)
{
@@ -5622,6 +5850,9 @@ namespace jdi
// 「均值滤波」开启时,为**被勾选**的信号各叠加一条多阶滑动平均曲线
//(均值点数 X、阶数 N 由该信号自己的条目给出)。
const bool meanFilterOn = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].meanFilter;
// 「差异放大」开启时同样只为**被勾选**的信号叠加曲线
//(窗口点数 X、放大倍数 N 由该信号自己的条目给出)。
const bool diffAmpOn = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].diffAmp;
for (const SignalRef& ref : laneSignals)
{
@@ -5758,6 +5989,16 @@ namespace jdi
PaintMeanFilterCurve(g, resolved, lane, item->points, item->order);
}
}
// 差异放大曲线:同样是仅用于预览的叠加曲线,只在被勾选时绘制。
if (diffAmpOn)
{
const Lane::DiffAmpItem* item =
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].FindDiffAmpItem(ref);
if (item != nullptr)
{
PaintDiffAmpCurve(g, resolved, lane, item->points, item->order);
}
}
}
// g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。
@@ -6472,14 +6713,19 @@ namespace jdi
DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline);
UiText(g, text, m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER);
// 「边沿计数(中心轴)」/「均值滤波」开启时,纵向光标还要额外标注**派生曲线**
// 在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。
// 两条曲线的参数与语义完全一致,故共用同一段绘制逻辑。
const auto paintDerivedCapsule = [&](int first, int second, const std::wstring& suffix)
// 「边沿计数(中心轴)」/「均值滤波」/「差异放大」开启时,纵向光标还要额外标注
// **派生曲线**在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。
// 三条曲线的参数与标注规则完全一致,故共用同一段绘制逻辑。
const auto paintDerivedCapsule = [&](int first, int second, const std::wstring& suffix,
bool diffAmp)
{
double derivedValue = 0.0;
if (!MeanFilterValueAt(*resolved.file, *resolved.data, first, second, time,
derivedValue))
const bool ok =
diffAmp ? DiffAmpValueAt(*resolved.file, *resolved.data, first, second, time,
derivedValue)
: MeanFilterValueAt(*resolved.file, *resolved.data, first, second, time,
derivedValue);
if (!ok)
{
return;
}
@@ -6498,8 +6744,12 @@ namespace jdi
if (showDelta)
{
double otherDerived = 0.0;
if (MeanFilterValueAt(*resolved.file, *resolved.data, first, second,
otherTime, otherDerived))
const bool otherOk =
diffAmp ? DiffAmpValueAt(*resolved.file, *resolved.data, first, second,
otherTime, otherDerived)
: MeanFilterValueAt(*resolved.file, *resolved.data, first, second,
otherTime, otherDerived);
if (otherOk)
{
const double delta = derivedValue - otherDerived;
centerText += (delta < 0.0) ? L"-" : L"+";
@@ -6549,7 +6799,7 @@ namespace jdi
m_tab->Lanes()[laneIndex].FindEdgeCenterItem(ref);
if (centerItem != nullptr)
{
paintDerivedCapsule(centerItem->points, centerItem->order, L"中心轴");
paintDerivedCapsule(centerItem->points, centerItem->order, L"中心轴", false);
}
}
if (m_tab->Lanes()[laneIndex].meanFilter)
@@ -6559,7 +6809,17 @@ namespace jdi
m_tab->Lanes()[laneIndex].FindMeanFilterItem(ref);
if (meanItem != nullptr)
{
paintDerivedCapsule(meanItem->points, meanItem->order, L"均值滤波");
paintDerivedCapsule(meanItem->points, meanItem->order, L"均值滤波", false);
}
}
if (m_tab->Lanes()[laneIndex].diffAmp)
{
// 差异放大曲线:同样是只标注被勾选的信号,尾缀为「差异化」。
const Lane::DiffAmpItem* diffItem =
m_tab->Lanes()[laneIndex].FindDiffAmpItem(ref);
if (diffItem != nullptr)
{
paintDerivedCapsule(diffItem->points, diffItem->order, L"差异化", true);
}
}
}
@@ -6642,9 +6902,15 @@ namespace jdi
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool enabled = meanFilter ? lane.meanFilter : (centerAxis ? lane.edgeCenter : lane.edgeCount);
const bool diffAmp = (kind == EdgePanelKind::DIFF_AMP);
// 「均值滤波」与「差异放大」共用同一套「逐条目 + 底部按钮」排布。
const bool itemized = (centerAxis || meanFilter || diffAmp);
const bool hasButton = (meanFilter || diffAmp);
const bool enabled =
meanFilter ? lane.meanFilter : (diffAmp ? lane.diffAmp : (centerAxis ? lane.edgeCenter : lane.edgeCount));
const bool open = meanFilter ? lane.meanFilterPanelOpen
: (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen);
: (diffAmp ? lane.diffAmpPanelOpen
: (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen));
if (!enabled || !open)
{
return layout;
@@ -6669,11 +6935,10 @@ namespace jdi
const float titleHeight = kEdgePanelTitleHeight * m_scale;
const float rowHeight = kEdgePanelRowHeight * m_scale;
const float pad = kEdgePanelPad * m_scale;
// 「中心轴边沿计数」与「均值滤波」共用同一套「逐条目」排布(三排/两排),
// 「中心轴边沿计数」「均值滤波」「差异放大」共用同一套「逐条目」排布(三排/两排),
// 因此不再有独立的底部编辑行;只有「边沿计数」保留两行文本式排版。
const float buttonHeight = meanFilter ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
const float rowsPerSignal = meanFilter ? 1.0f : 2.0f;
const bool itemized = meanFilter || centerAxis;
// 逐条目面板:每个条目 = 第一排(信号名 + 复选框)+ 第二排(均值点数 X / 阶数 N)
// + (中心轴)第三排(统计结果);后两排**只在该条目被勾选时才占位**,
@@ -6692,7 +6957,9 @@ namespace jdi
continue;
}
itemsHeight += meanRowHeight;
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref) : lane.EdgeCenterEnabled(ref);
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref)
: (diffAmp ? lane.DiffAmpEnabled(ref)
: lane.EdgeCenterEnabled(ref));
if (checked)
{
itemsHeight += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale;
@@ -6702,7 +6969,10 @@ namespace jdi
}
}
}
height = titleHeight + pad + itemsHeight + pad + buttonHeight;
// 底部按钮行:上方与条目之间、下方与面板下边界之间各留一个 `pad`,
// 与面板的左右内边距完全一致(原先按钮直接贴着下边框,看起来很不协调)。
height = titleHeight + pad + itemsHeight + pad + buttonHeight +
(hasButton ? pad : 0.0f);
}
else
{
@@ -6711,9 +6981,11 @@ namespace jdi
// 首次使用(位置为负)时自动放到绘图区右侧,并各网格依次错开,避免完全重叠。
const double storedX = meanFilter ? lane.meanFilterPanelX
: (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX);
: (diffAmp ? lane.diffAmpPanelX
: (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX));
const double storedY = meanFilter ? lane.meanFilterPanelY
: (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY);
: (diffAmp ? lane.diffAmpPanelY
: (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY));
float left = (storedX >= 0.0)
? static_cast<float>(storedX)
: (m_plotRect.X + m_plotRect.Width - width - 16.0f * m_scale);
@@ -6736,9 +7008,12 @@ namespace jdi
if (itemized)
{
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
if (meanFilter)
if (hasButton)
{
layout.button = gd::RectF(left + pad, top + height - buttonRowHeight,
// 底部按钮与面板下边界之间留出与左右、与内容相同的 `pad` 间距
//(原先按钮行直接贴着下边框,视觉上与其它元素不协调);
// 面板高度已含这份 pad,因此这里只上移按钮行即可。
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
}
@@ -6762,7 +7037,8 @@ namespace jdi
left + width - pad - checkSize, rowY + (meanRowHeight - checkSize) * 0.5f, checkSize,
checkSize));
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref)
: lane.EdgeCenterEnabled(ref);
: (diffAmp ? lane.DiffAmpEnabled(ref)
: lane.EdgeCenterEnabled(ref));
if (checked)
{
const float editTop = rowY + meanRowHeight;
@@ -7132,6 +7408,17 @@ namespace jdi
PaintDerivedCurve(g, resolved, lane, MeanFilterCurveFor(*resolved.data, points, order));
}
void PlotView::PaintDiffAmpCurve(gd::Graphics& g, const ResolvedSignal& resolved,
const LaneVisual& lane, int points, int gain) const
{
if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr)
{
return;
}
// 派生曲线一律使用同一段绘制逻辑与配色规则(差异放大曲线同样如此)。
PaintDerivedCurve(g, resolved, lane, DiffAmpCurveFor(*resolved.data, points, gain));
}
void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
{
if (m_tab == nullptr || m_dataSet == nullptr)
@@ -7236,13 +7523,17 @@ namespace jdi
}
// ---- 统计浮动面板(最后绘制,覆盖在波形之上)----
// 「边沿计数」「边沿计数(中心轴)」「均值滤波」共用同一套样式,
// 仅标题、内容行与底部编辑行/按钮不同。
for (int pass = 0; pass < 3; ++pass)
// 「边沿计数」「边沿计数(中心轴)」「均值滤波」「差异放大」共用同一套样式,
// 仅标题、内容行与底部按钮不同。
for (int pass = 0; pass < kEdgePanelKindCount; ++pass)
{
const EdgePanelKind kind = kEdgePanelKinds[pass];
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool diffAmp = (kind == EdgePanelKind::DIFF_AMP);
// 「均值滤波」与「差异放大」共用「逐条目 + 底部按钮」排布。
const bool itemized = (centerAxis || meanFilter || diffAmp);
const bool hasButton = (meanFilter || diffAmp);
for (std::size_t index = 0; index < count; ++index)
{
const Lane& tabLane = tabLanes[index];
@@ -7277,7 +7568,9 @@ namespace jdi
m_theme->text, TextAlign::CENTER);
const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix());
: (diffAmp ? std::wstring(L"差异放大")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
EdgeRangeSuffix()));
UiText(g, title, m_fonts->uiSmall,
gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y,
panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight,
@@ -7305,10 +7598,10 @@ namespace jdi
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
1.6f * m_scale);
// 「中心轴边沿计数」/「均值滤波」面板都采用「逐条目」排布:
// 第一排 = 名称 + 复选框,第二排 = 均值点数 X / 阶数 N(仅勾选的条目),
// 「中心轴边沿计数」/「均值滤波」/「差异放大」面板都采用「逐条目」排布:
// 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目),
// 中心轴再多第三排 = 对应的统计结果(同样仅勾选的条目)。
if (meanFilter || centerAxis)
if (itemized)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
@@ -7321,7 +7614,8 @@ namespace jdi
const std::uint32_t signalColor = StyleColor(*resolved.style);
const gd::RectF& checkRect = layout.rowChecks[rowIndex];
const bool checked = meanFilter ? tabLane.MeanFilterEnabled(ref)
: tabLane.EdgeCenterEnabled(ref);
: (diffAmp ? tabLane.DiffAmpEnabled(ref)
: tabLane.EdgeCenterEnabled(ref));
// 名称:第一排左侧,避开右侧复选框。
const float nameWidth =
@@ -7338,15 +7632,26 @@ namespace jdi
continue;
}
// 第二排:两个输入框,左侧标“均值点数 X”,右侧标“阶数 N”。
const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
const int shownPoints = (item != nullptr) ? item->points : 5;
const int shownOrder = (item != nullptr) ? item->order : 1;
const PlotEditTarget pointsTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS;
const PlotEditTarget orderTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_ORDER
: PlotEditTarget::EDGE_CENTER_ORDER;
// 第二排:两个输入框。均值滤波是“均值点数 X / 阶数 N”,
// 差异放大是“窗口点数 X / 放大倍数 N”(同一套排版)。
const Lane::MeanFilterItem* item =
meanFilter ? tabLane.FindMeanFilterItem(ref)
: (diffAmp ? tabLane.FindDiffAmpItem(ref)
: tabLane.FindEdgeCenterItem(ref));
const int shownPoints =
(item != nullptr) ? item->points : (diffAmp ? 64 : 5);
const int shownOrder =
(item != nullptr) ? item->order : (diffAmp ? 2 : 1);
const wchar_t* pointsLabel = diffAmp ? L"窗口点数 X" : L"均值点数 X";
const wchar_t* orderLabel = diffAmp ? L"放大倍数 N" : L"阶数 N";
const PlotEditTarget pointsTarget =
meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS
: (diffAmp ? PlotEditTarget::DIFF_AMP_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS);
const PlotEditTarget orderTarget =
meanFilter ? PlotEditTarget::MEAN_FILTER_ORDER
: (diffAmp ? PlotEditTarget::DIFF_AMP_ORDER
: PlotEditTarget::EDGE_CENTER_ORDER);
const auto paintField = [&](const gd::RectF& rect, const wchar_t* label,
int value, PlotEditTarget target)
{
@@ -7383,8 +7688,8 @@ namespace jdi
m_theme->textStrong, TextAlign::CENTER);
}
};
paintField(layout.rowPoints[rowIndex], L"均值点数 X", shownPoints, pointsTarget);
paintField(layout.rowOrder[rowIndex], L"阶数 N", shownOrder, orderTarget);
paintField(layout.rowPoints[rowIndex], pointsLabel, shownPoints, pointsTarget);
paintField(layout.rowOrder[rowIndex], orderLabel, shownOrder, orderTarget);
// 第三排(中心轴):该信号在当前参数下的上升沿/下降沿统计结果。
if (centerAxis && rowIndex < layout.rowStats.size() &&
@@ -7414,7 +7719,7 @@ namespace jdi
{
continue;
}
if (meanFilter || centerAxis)
if (itemized)
{
// 逐条目绘制已在上面完成。
continue;
@@ -7438,8 +7743,8 @@ namespace jdi
rowY += rowHeight * 2.0f;
}
// 均值滤波面板底部按钮行:「构建为新信号」(悬停高亮)。
if (meanFilter)
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮)。
if (hasButton)
{
const gd::RectF& buttonRect = layout.button;
if (buttonRect.Width > 0.0f)
@@ -7453,7 +7758,7 @@ namespace jdi
UiFillRoundRect(g, inner, 4.0f * m_scale,
hot ? WithAlpha(m_theme->accent, 0x3A) : m_theme->cardBg);
UiDrawRoundRect(g, inner, 4.0f * m_scale, m_theme->accent, 1.2f);
UiText(g, kMeanFilterBuildLabel, m_fonts->uiSmall, inner, m_theme->accent,
UiText(g, kBuildSignalLabel, m_fonts->uiSmall, inner, m_theme->accent,
TextAlign::CENTER);
}
}
+90
View File
@@ -373,6 +373,15 @@ namespace jdi
/** 中心轴边沿计数的平滑参数:与均值滤波共用同一结构与同一套语义。 */
using EdgeCenterItem = MeanFilterItem;
/**
* @brief 「差异放大」中某个信号自己的参数。
*
* 字段含义与均值滤波/中心轴不同(但同样是「一个信号 + 两个整数参数」,
* 因此复用同一结构以共享面板、缓存与引用重映射的实现):
* `points` = 窗口点数 X(趋势窗口半径),`order` = 放大倍数 N。
*/
using DiffAmpItem = MeanFilterItem;
std::wstring name; /**< 网格名称。 */
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
@@ -490,6 +499,48 @@ namespace jdi
/** 均值滤波面板左上角纵坐标(<0 表示自动定位)。 */
double meanFilterPanelY = -1.0;
/**
* @brief 「实验性功能 → 差异放大」开关。
*
* 开启后为本网格内**被勾选**的信号额外绘制一条「差异放大」曲线:
* 把信号与局部趋势线的差值按“局部差异强度”归一化后放大,
* 使不明显的下凹(如纹波里的弱 M 形)变明显,而差异已经很大的区域**逐点不变**。
* 这条曲线只是叠加显示,不会作为信号加入左侧列表。
*/
bool diffAmp = false;
/**
* @brief 参与差异放大预览的信号及其参数(只包含被勾选的信号)。
*
* `points` = 窗口点数 X(局部差异强度统计半径,约取纹波周期的一半),
* `order` = 放大倍数 N(最大放大倍数)。
*/
std::vector<DiffAmpItem> diffAmpItems;
/** 取某个信号的差异放大参数(未勾选时返回空指针)。 */
const DiffAmpItem* FindDiffAmpItem(const SignalRef& ref) const;
/** 取得或创建某信号的差异放大参数(用于勾选)。 */
DiffAmpItem& EnsureDiffAmpItem(const SignalRef& ref);
/** 取消某信号的差异放大勾选。 */
void RemoveDiffAmpItem(const SignalRef& ref);
/** 取某信号的差异放大勾选状态。 */
bool DiffAmpEnabled(const SignalRef& ref) const
{
return FindDiffAmpItem(ref) != nullptr;
}
/** 差异放大浮动面板是否展开。 */
bool diffAmpPanelOpen = true;
/** 差异放大面板左上角位置(客户区像素;<0 表示自动定位)。 */
double diffAmpPanelX = -1.0;
/** 差异放大面板左上角纵坐标(<0 表示自动定位)。 */
double diffAmpPanelY = -1.0;
};
/**
@@ -519,6 +570,45 @@ namespace jdi
*/
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out);
/**
* @brief 计算 N 点滑动平均(float 输入版本)。
*
* 与 `ComputeMovingAverage(const std::vector<double>&, ...)` 完全同一实现,
* 供「差异放大」在 float 缓冲上二次滑动平均使用:亿级采样下若用 double 中转
* 会多占一倍内存,而曲线本身只要求 float 精度。
*
* @param [IN] values 原始采样值。
* @param [IN] radius 平滑半径 N(0 表示原样返回)。
* @param [OUT] out 输出数组(长度与 values 相同)。
*/
void ComputeMovingAverage(const std::vector<float>& values, int radius, std::vector<float>& out);
/**
* @brief 「差异放大」:把不明显的局部差异放大,而差异已经很大的区域逐点不变。
*
* 算法(三部分,全部 O(n)):
* 1. **趋势线** `trend` = 以 8×window 为半径的滑动平均 —— 把“整体”(低频成分)分离出来;
* 该半径必须远大于纹波周期,趋势线才不带着纹波。
* 2. **局部差异强度** `strength` = |采样值 − 趋势线| 的滑动平均(半径 = window)
* —— 衡量该处“纹波有多明显”。窗口恰好覆盖一个纹波周期时该值与相位无关
* (正弦为 2A/π),因此 `window` 应取**纹波周期的一半左右**;
* 3. **逐点增益** `gain = clamp(目标强度 / 该处强度, 1, 放大倍数)`,
* 目标强度取全信号差异强度的 75 分位(用直方图统计,O(1) 额外内存;
* 用均值会被个别尖峰抬高,用最大值则完全被最尖处主导)。
*
* 输出 `out = trend + gain × (采样值 − trend)`。
* **增益下界为 1**,所以差异强度不低于目标强度的采样点(即绝大多数“正常”数据点)
* 输出与原值逐位相同 —— 这正是“不影响其它数据点的振幅范围”的实现方式;
* 只有强度低于目标强度的区域(如被削弱的那个周期)才被放大,最多放大到 `gain` 倍。
*
* @param [IN] values 原始采样值。
* @param [IN] window 窗口点数 X(局部差异强度统计半径,应取纹波周期的一半左右)。
* @param [IN] gain 放大倍数 N(最大增益,至少 1)。
* @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。
*/
void ComputeDiffAmplify(const std::vector<double>& values, int window, int gain,
std::vector<float>& out);
/**
* @brief 计算多阶(迭代)滑动平均:把 N 点滑动平均重复应用 `order` 次。
*
+491 -71
View File
@@ -2,7 +2,10 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cwctype>
#include <deque>
#include <limits>
#include "Theme.hpp"
@@ -131,6 +134,11 @@ namespace jdi
bool meanFilterPanelOpen = true;
double meanFilterPanelX = -1.0;
double meanFilterPanelY = -1.0;
bool diffAmp = false;
std::vector<Lane::DiffAmpItem> diffAmpItems;
bool diffAmpPanelOpen = true;
double diffAmpPanelX = -1.0;
double diffAmpPanelY = -1.0;
};
// 先记下每个信号原来所在网格的网格自身状态:
@@ -156,6 +164,11 @@ namespace jdi
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
carry.meanFilterPanelX = lane.meanFilterPanelX;
carry.meanFilterPanelY = lane.meanFilterPanelY;
carry.diffAmp = lane.diffAmp;
carry.diffAmpItems = lane.diffAmpItems;
carry.diffAmpPanelOpen = lane.diffAmpPanelOpen;
carry.diffAmpPanelX = lane.diffAmpPanelX;
carry.diffAmpPanelY = lane.diffAmpPanelY;
for (const SignalRef& ref : lane.signals)
{
previousCarry.emplace_back(ref, carry);
@@ -192,6 +205,11 @@ namespace jdi
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
lane.meanFilterPanelX = carry.meanFilterPanelX;
lane.meanFilterPanelY = carry.meanFilterPanelY;
lane.diffAmp = carry.diffAmp;
lane.diffAmpItems = carry.diffAmpItems;
lane.diffAmpPanelOpen = carry.diffAmpPanelOpen;
lane.diffAmpPanelX = carry.diffAmpPanelX;
lane.diffAmpPanelY = carry.diffAmpPanelY;
};
// 收集全部可见信号引用。
@@ -893,86 +911,231 @@ namespace jdi
// ============================ 采样辅助 ============================
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
namespace
{
const int window = std::max(0, radius);
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0 || window == 0)
/**
* @brief 滑动平均的共用实现(输入只要可转成 double)。
*
* 窗口 = [max(0,i-N), min(count-1,i+N)],每前进一步只增减边界处的两个采样点,
* 整体 O(n),不额外占用前缀和内存。“差异放大”需要在 float 缓冲上再做一次
* 滑动平均,若先转成 double 会因为亿级采样多占一倍内存,因此做成模板。
*/
template <typename T>
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out);
/**
* @brief 滑动窗口最大值(只要最大值)。
*
* 与 `SlidingExtrema` 同一个单调队列思路,但**不额外分配最小值数组**:
* 亿级采样下一次全量计算会多占 100 MB 以上,而“差异放大”只需要最大值。
*
* @param [IN] values 采样值。
* @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。
* @param [OUT] outMax 邻域最大值(整窗都是 NaN 时为 NaN)。
*/
template <typename T>
void SlidingMax(const std::vector<T>& values, std::size_t radius, std::vector<float>& outMax)
{
// 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。
const std::size_t count = values.size();
outMax.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
std::deque<std::size_t> maxQueue; // 递减:队首最大。
std::size_t filled = 0;
for (std::size_t index = 0; index < count; ++index)
{
if (std::isfinite(values[index]))
const std::size_t wantHigh = std::min(count - 1, index + radius);
while (filled <= wantHigh)
{
out[index] = static_cast<float>(values[index]);
const double incoming = values[filled];
if (std::isfinite(incoming))
{
while (!maxQueue.empty() && values[maxQueue.back()] <= incoming)
{
maxQueue.pop_back();
}
maxQueue.push_back(filled);
}
++filled;
}
}
return;
}
// 滑动窗口求和:窗口 = [max(0,i-N), min(count-1,i+N)],
// 每前进一步只增减边界处的两个采样点,整体 O(n),不额外占用前缀和内存。
double sum = 0.0;
std::size_t finiteCount = 0;
std::size_t windowLow = 0;
std::size_t windowHigh = 0; // 闭区间右端。
const auto addValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum += value;
++finiteCount;
}
};
const auto removeValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum -= value;
--finiteCount;
}
};
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t low = (index > static_cast<std::size_t>(window))
? (index - static_cast<std::size_t>(window))
: 0;
const std::size_t high = std::min(count - 1, index + static_cast<std::size_t>(window));
if (index == 0)
{
windowLow = low;
windowHigh = high;
for (std::size_t i = windowLow; i <= windowHigh; ++i)
const std::size_t low = (index > radius) ? (index - radius) : 0;
while (!maxQueue.empty() && maxQueue.front() < low)
{
addValue(values[i]);
maxQueue.pop_front();
}
}
else
{
while (windowLow < low)
if (!maxQueue.empty())
{
removeValue(values[windowLow]);
++windowLow;
outMax[index] = static_cast<float>(values[maxQueue.front()]);
}
while (windowHigh < high)
{
++windowHigh;
addValue(values[windowHigh]);
}
}
// 中心点本身无效(NaN)时该处不产生有效结果。
if (finiteCount > 0 && std::isfinite(values[index]))
{
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
}
}
} // namespace
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
{
MovingAverageImpl(values, radius, out);
}
void ComputeMovingAverage(const std::vector<float>& values, int radius, std::vector<float>& out)
{
MovingAverageImpl(values, radius, out);
}
namespace
{
template <typename T>
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out)
{
const int window = std::max(0, radius);
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0 || window == 0)
{
// 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。
for (std::size_t index = 0; index < count; ++index)
{
const double value = static_cast<double>(values[index]);
if (std::isfinite(value))
{
out[index] = static_cast<float>(value);
}
}
return;
}
// 滑动窗口求和:每前进一步只增减边界处的两个采样点,整体 O(n)。
double sum = 0.0;
std::size_t finiteCount = 0;
std::size_t windowLow = 0;
std::size_t windowHigh = 0; // 闭区间右端。
const auto addValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum += value;
++finiteCount;
}
};
const auto removeValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum -= value;
--finiteCount;
}
};
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t low = (index > static_cast<std::size_t>(window))
? (index - static_cast<std::size_t>(window))
: 0;
const std::size_t high =
std::min(count - 1, index + static_cast<std::size_t>(window));
if (index == 0)
{
windowLow = low;
windowHigh = high;
for (std::size_t i = windowLow; i <= windowHigh; ++i)
{
addValue(static_cast<double>(values[i]));
}
}
else
{
while (windowLow < low)
{
removeValue(static_cast<double>(values[windowLow]));
++windowLow;
}
while (windowHigh < high)
{
++windowHigh;
addValue(static_cast<double>(values[windowHigh]));
}
}
// 中心点本身无效(NaN)时该处不产生有效结果。
const double center = static_cast<double>(values[index]);
if (finiteCount > 0 && std::isfinite(center))
{
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
}
}
}
/**
* @brief 滑动窗口极值:一次求出每个中心点邻域(±radius)内的最小值与最大值。
*
* 两个单调队列(最小值队列递增、最大值队列递减)同步滑动,O(n) 时间,
* 内存只有窗口长度。NaN 不参与(整窗都是 NaN 时输出 NaN)。
*
* @param [IN] values 采样值。
* @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。
* @param [OUT] outMin 邻域最小值。
* @param [OUT] outMax 邻域最大值。
*/
template <typename T>
void SlidingExtrema(const std::vector<T>& values, std::size_t radius,
std::vector<float>& outMin, std::vector<float>& outMax)
{
const std::size_t count = values.size();
outMin.assign(count, std::numeric_limits<float>::quiet_NaN());
outMax.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
std::deque<std::size_t> minQueue; // 递增:队首最小。
std::deque<std::size_t> maxQueue; // 递减:队首最大。
std::size_t filled = 0;
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t wantHigh = std::min(count - 1, index + radius);
while (filled <= wantHigh)
{
const double incoming = values[filled];
if (std::isfinite(incoming))
{
while (!minQueue.empty() && values[minQueue.back()] >= incoming)
{
minQueue.pop_back();
}
minQueue.push_back(filled);
while (!maxQueue.empty() && values[maxQueue.back()] <= incoming)
{
maxQueue.pop_back();
}
maxQueue.push_back(filled);
}
++filled;
}
const std::size_t low = (index > radius) ? (index - radius) : 0;
while (!minQueue.empty() && minQueue.front() < low)
{
minQueue.pop_front();
}
while (!maxQueue.empty() && maxQueue.front() < low)
{
maxQueue.pop_front();
}
if (!minQueue.empty())
{
outMin[index] = static_cast<float>(values[minQueue.front()]);
}
if (!maxQueue.empty())
{
outMax[index] = static_cast<float>(values[maxQueue.front()]);
}
}
}
} // namespace
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int order,
std::vector<float>& out)
{
@@ -999,6 +1162,204 @@ namespace jdi
}
}
void ComputeDiffAmplify(const std::vector<double>& values, int window, int gain,
std::vector<float>& out)
{
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
// 窗口点数 X:局部“差异强度”的统计**窗口长度**(点数),窗口 = [i−r, i+r],
// r = (X−1)/2。窗口长度**应略大于纹波周期的一半**:
// 只有当窗口跨越的相位达到 180° 时,窗口内才必定包含一个波峰或波谷,
// “局部 |交流分量| 的峰值”因而恒等于纹波振幅 A,与相位无关
//(正常区域恒为 A、弱区恒为 A/2,比值恰为 2);窗口再小就会跟随相位起伏
//(把正常区域的波峰也一起放大),远大于一个周期时又会把弱区与邻近正常区一起统计掉。
// 窗口越短、混叠区(弱区边界各 ±r)越窄,对“只有一两个周期(甚至半周期)变弱”的
// 情形越有效。
const int span = std::max(3, std::min(window, 1000000));
const int strengthRadius = std::max(1, (span - 1) / 2);
// 趋势窗口取局部窗口的 2 倍(≈ 一个完整纹波周期):只有按整周期取平均,
// 趋势线才恰好是纹波的中心线(交流分量均值为零),待放大的弱区也就**不会**
// 因为自己变“扁”而把趋势线一起拉偏。
const int trendRadius = std::max(1, strengthRadius * 2);
// 包络窗口固定取局部窗口的 8 倍:用来给出“周边本来的振幅范围”,
// 既作为放大基准(中心值)也作为输出的夹取上下界。
const std::size_t envelopeRadius =
static_cast<std::size_t>(std::max(1, std::min(strengthRadius * 8, 16384)));
const double maxGain = static_cast<double>(std::max(1, gain));
// 1) 周边包络:邻域内采样值的最小/最大值(单调队列,O(n))。
// 邻域**必须明显大于一个纹波周期**,否则包络会跟着纹波起伏,
// 放大后会把相邻正常区域的相位也一并扭曲。
std::vector<float> envMin;
std::vector<float> envMax;
SlidingExtrema(values, envelopeRadius, envMin, envMax);
// 2) 局部差异强度 = 邻域内 |采样值 − 趋势线| 的**最大值**(即局部纹波振幅)。
//
// 这里刻意**不用**另外两种看起来更自然的做法:
// * “|采样值 − 趋势线| 的平均”:趋势线是低频量,会被待放大的弱区本身拉偏,
// 于是弱区周边本来正常的采样点也会被判成“差异偏小”而被一起放大;
// * “原始采样值的峰峰值”:峰峰值要同时覆盖波峰与波谷才与相位无关,
// 这要求窗口跨越 360°,窗口会大到把弱区与邻近正常区平均掉
// (实测一个周期宽的弱区只能恢复三分之一)。
// |交流分量| 的最大值只需要窗口跨越 180°(窗口约半个周期)就与相位无关,
// 既能准确识别弱区,又因为窗口很短而具备足够好的定位精度。
//
// 又因为增益下界恒为 1(见第 4 步),正常区域的增益恰好是 1,
// 输出与输入**逐位相同**,因此对其它数据点真正没有任何影响。
//
// 极值窗口按**实际长度**(端点截断)统计,端点因此会低估振幅、
// 导致增益被高估;这一端点效应由第 4 步的收敛系数消除。
std::vector<float> trend;
ComputeMovingAverage(values, trendRadius, trend);
std::vector<float> strength;
{
// `ac` 只在求局部最大值时用到:单独放在一个作用域里,
// 亿级采样下这一份 100 MB 级的中转数组能尽早归还给系统。
std::vector<float> ac(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) && std::isfinite(trend[index]))
{
ac[index] = static_cast<float>(std::fabs(value - static_cast<double>(trend[index])));
}
}
SlidingMax(ac, static_cast<std::size_t>(strengthRadius), strength);
}
// 3) 目标强度:差异强度的 75 分位。用直方图统计 → O(1) 额外内存,
// 且不会被个别尖峰抬高(用均值会),也不会被最尖处完全主导(用最大值会)。
//
// ⚠️ 统计时**排除首尾的端点收敛区**(见第 4 步):那里的趋势窗口被截断,
// “|交流分量| 的最大值”整体偏大,会把分位数抬高、导致目标强度偏大。
// 待统计的采样点数不足时退回全段统计。
const std::size_t edgeGuard =
static_cast<std::size_t>(2) * static_cast<std::size_t>(trendRadius);
// 剩下足够多的采样点时才有必要排除端点区(短信号就直接全段统计)。
constexpr std::size_t kEdgeGuardMinimum = 256;
const bool useGuard = (count > edgeGuard * 2 + kEdgeGuardMinimum);
const auto inGuard = [count, edgeGuard, useGuard](std::size_t index)
{
if (!useGuard)
{
return true;
}
return (index >= edgeGuard) && ((count - 1 - index) >= edgeGuard);
};
float maxStrength = 0.0f;
for (std::size_t index = 0; index < count; ++index)
{
const float value = strength[index];
if (inGuard(index) && std::isfinite(value) && value > maxStrength)
{
maxStrength = value;
}
}
if (!(maxStrength > 0.0f))
{
// 完全平坦(或全为 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);
}
}
return;
}
constexpr int kBinCount = 2048;
std::vector<int> histogram(static_cast<std::size_t>(kBinCount), 0);
std::size_t sampleCount = 0;
for (std::size_t index = 0; index < count; ++index)
{
const float value = strength[index];
if (!inGuard(index) || !std::isfinite(value))
{
continue;
}
int bin = static_cast<int>(static_cast<double>(value) /
static_cast<double>(maxStrength) * kBinCount);
bin = std::max(0, std::min(bin, kBinCount - 1));
++histogram[static_cast<std::size_t>(bin)];
++sampleCount;
}
double target = static_cast<double>(maxStrength);
if (sampleCount > 0)
{
const std::size_t wanted = (sampleCount * 75) / 100;
std::size_t accumulated = 0;
for (int bin = 0; bin < kBinCount; ++bin)
{
accumulated += static_cast<std::size_t>(histogram[static_cast<std::size_t>(bin)]);
if (accumulated > wanted)
{
// 取桶中心,避免目标强度贴到桶的下边界而整体偏小。
target = (static_cast<double>(bin) + 0.5) / kBinCount *
static_cast<double>(maxStrength);
break;
}
}
}
// 4) 逐点增益并合成输出。
// * 基准 = 趋势线(纹波的中心线,与信号整体同步,纹波在其中上下摆动);
// * 增益下界为 1 → 局部振幅不比目标值小的采样点**输出与原值逐位相同**
// (基准被 1 倍增益完全消掉,因此对其它数据点没有任何影响);
// * 放大后的取值再夹到周边包络内 → **任何一点都不会超出原有的振幅范围**
// (尤其重要:被放大区域两侧自身的振幅本来就很大,不应被继续抬升)。
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (!std::isfinite(value))
{
continue;
}
const bool hasEnvelope = std::isfinite(envMin[index]) && std::isfinite(envMax[index]);
const double base =
std::isfinite(trend[index]) ? static_cast<double>(trend[index]) : value;
double pointGain = 1.0;
const float local = strength[index];
if (std::isfinite(local) && local > 0.0f)
{
pointGain = target / static_cast<double>(local);
}
pointGain = std::max(1.0, std::min(pointGain, maxGain));
// 端点收敛:首尾各 2r 个采样点的**趋势窗口与极值窗口都被截断**
//(趋势窗口半径 2r、极值窗口半径 r),局部振幅与中心线都被系统性估偏。
// 因此这些采样点一律保持原样(增益恰为 1),再在接下来的 2r 个点内
// 平滑过渡到完整增益,避免记录首尾被整片抬升。
//
// ⚠️ 不能靠“镜像延拓”解决:镜像后的窗口覆盖的真实下标集合与截断窗口相同
//(例如 i=16、r=40 时两者都只覆盖真实下标 0..56),因此不会带来任何新信息。
const double taperStart = static_cast<double>(trendRadius);
const double taperSpan = std::max(1.0, static_cast<double>(trendRadius));
const double distanceToEdge =
static_cast<double>(std::min(index, count - 1 - index));
const double taper =
std::max(0.0, std::min(1.0, (distanceToEdge - taperStart) / taperSpan));
pointGain = 1.0 + (pointGain - 1.0) * taper;
double amplified = base + pointGain * (value - base);
if (hasEnvelope)
{
amplified = std::max(static_cast<double>(envMin[index]),
std::min(amplified, static_cast<double>(envMax[index])));
}
out[index] = static_cast<float>(amplified);
}
}
const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const
{
for (const MeanFilterItem& item : meanFilterItems)
@@ -1069,6 +1430,46 @@ namespace jdi
edgeCenterItems.end());
}
const Lane::DiffAmpItem* Lane::FindDiffAmpItem(const SignalRef& ref) const
{
for (const DiffAmpItem& item : diffAmpItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::DiffAmpItem& Lane::EnsureDiffAmpItem(const SignalRef& ref)
{
for (DiffAmpItem& item : diffAmpItems)
{
if (item.ref == ref)
{
return item;
}
}
DiffAmpItem item;
item.ref = ref;
// 默认参数:窗口点数 X = 80(长度)、放大倍数 N = 2。
// 窗口长度需略大于纹波周期的一半(见 ComputeDiffAmplify 的说明),
// 80 点可覆盖周期约 160 采样以内的纹波。
item.points = 80;
item.order = 2;
diffAmpItems.push_back(item);
return diffAmpItems.back();
}
void Lane::RemoveDiffAmpItem(const SignalRef& ref)
{
diffAmpItems.erase(
std::remove_if(diffAmpItems.begin(), diffAmpItems.end(),
[&ref](const DiffAmpItem& item) { return item.ref == ref; }),
diffAmpItems.end());
}
int FindNearestSample(const DataFile& file, double time)
{
const std::size_t count = file.time.size();
@@ -1313,6 +1714,7 @@ namespace jdi
};
purgeRefs(lane.meanFilterItems);
purgeRefs(lane.edgeCenterItems);
purgeRefs(lane.diffAmpItems);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
@@ -1604,10 +2006,18 @@ namespace jdi
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.diffAmp = source.diffAmp;
lane.diffAmpPanelOpen = source.diffAmpPanelOpen;
lane.diffAmpPanelX = source.diffAmpPanelX;
lane.diffAmpPanelY = source.diffAmpPanelY;
lane.meanFilter = source.meanFilter;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴条目。
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 差异放大条目。
lane.meanFilterItems.clear();
lane.edgeCenterItems.clear();
lane.diffAmpItems.clear();
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
{
if (item.ref == ref)
@@ -1622,13 +2032,16 @@ namespace jdi
lane.edgeCenterItems.push_back(item);
}
}
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
for (const Lane::DiffAmpItem& item : source.diffAmpItems)
{
if (item.ref == ref)
{
lane.diffAmpItems.push_back(item);
}
}
lane.signals.push_back(ref);
pieces.push_back(std::move(lane));
}
// 用 pieces 替换原网格(插入到原位置,因此其后方的网格整体顺次后移)。
m_lanes.erase(m_lanes.begin() + laneIndex);
m_lanes.insert(m_lanes.begin() + laneIndex, pieces.begin(), pieces.end());
@@ -1680,6 +2093,13 @@ namespace jdi
++item.ref.signalIndex;
}
}
for (Lane::DiffAmpItem& item : lane.diffAmpItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
}
}