geopro/docs/superpowers/specs/2026-06-29-radar-bscan-revi...

25 KiB
Raw Blame History

雷达 B-Scan 详情页开发方案(修订版)— 2026-06-29

基于用户确认的 4 项关键决策修订:① 底部页签容器 ② dd_radar_2d/dd_radar_3d ③ 本地文件加载 ④ 单通道处理管线 + 用户提供算法代码。 基线分支:radar。上一版见同目录 2026-06-29-radar-bscan-development-plan.md


1. 一句话架构

雷达 B-Scan = 现有底部页签引擎(DatasetDetailController + ChartStrategyRegistry)内的一个全新 ViewKind::BScanProfile 视图BScanProfileView 内部自含紧凑工具条 + 左右分割B-Scan 自绘 canvas + A-Scan Qwt 曲线)+ 底部状态栏。数据从前端本地 .iprb/.iprh 文件读取,处理管线在 core/gpr_proc 纯 C++ 层跑,用户提供各算法实现填入管线节点。


2. 相比上一版的关键调整

上一版 修订版
容器 独立 ADS Dock 底部 DatasetDetailPanel 页签(走 DatasetDetailController
ddCode 待定 dd_radar_2d(单通道)/dd_radar_3d(多通道阵列)
数据入口 后端 API 或本地文件 纯本地文件ApiDatasetRepository 对 radar loaderKey 本地处理)
采集参数 后端 API 或头文件 全从 .iprh 头文件解析(复用/扩展现有 IprHeader
处理算法 我实现 用户提供代码,我只定义接口 + 管线框架
多通道 单通道独立跑 全载内存,顶部工具条通道切换,处理管线按当前选中单通道跑

3. 数据流(底部页签路线,端到端)

[DatasetListPanel 双击雷达数据集 item]
  main.cpp:1501  读 kDsIdRole + kDsDdCodeRole="dd_radar_2d"(或 dd_radar_3d
  main.cpp:1509  detailCtrl.openDataset(dsId, "dd_radar_2d", dsName, tmObjectId)
        │
        ▼
[DatasetDetailController::openDataset]
  registry_.find("dd_radar_2d") → Radar2dStrategy
  strategy->tabs() → [
      { "B-Scan剖面",  ViewKind::BScanProfile, "radar.profile",   lazy=false },
      { "采集参数",    ViewKind::Table,        "radar.info",      lazy=false },
      { "异常列表",    ViewKind::Table,        "radar.anomalies", lazy=false }
  ]
  emit datasetOpened(...) → DatasetDetailPanel 建页
  loadTab(dsId, "dd_radar_2d", 0) → loadTabImpl("radar.profile")
        │
        ▼
[ApiDatasetRepository::loadAsync("radar.profile", dsId)]
  // 不走网络!本地文件直接读取:
  1. 根据 dsId 推断本地路径(约定见 §4
  2. io::gpr::parseIprHeader(iprhPath) → 采集参数
  3. io::gpr::readIprb(iprbPath, header) → 单通道 B-scan2d
     或 assembleGprSurvey(channelPaths, ordPath) → 多通道 GprSurvey3d
  4. 组装 GprProfilePayload{原始数据, 头信息, 通道列表}
  5. return new LocalDetailLoad(payload) // 同步完成,立即 emit done
        │
        ▼
[DatasetDetailController::tabReady]
  emit tabReady(dsId, 0, payload)
        │
        ▼
[DatasetDetailPage::setTabPayload]
  views_[0]->setPayload(payload)   // BScanProfileView 自解包
        │
        ▼
[BScanProfileView]
  1. 解包 GprProfilePayload → 填充通道选择下拉
  2. 默认显示通道 0 的原始数据
  3. 若用户切换到 proc_data_1 标签:
     → 用当前 ProcessingParams 跑 GprProcessingPipeline
     → 管线输出 proc_data → ColorMapper → 重绘 canvas
  4. 用户调整参数 → 只重算脏节点 → 即时刷新

懒加载/分页:三个页签均 lazy=false(开页即载),paginated=falseB-Scan 全量,采集参数/异常列表量小一次性返回)。异常列表量若大,后续可改 paginated=true + DataTableView


4. 本地文件加载方案

4.1 约定式本地路径

雷达数据集在前端本地项目目录下的固定结构:

{projectDir}/datasets/{dsId}/
  ├── data.iprh          # 头文件(采集参数)
  ├── data.iprb          # B-scan 二进制dd_radar_2d单通道
  ├── data.ord           # 通道偏移dd_radar_3d 时需要)
  └── data.gps           # GPS 轨迹(可选,地形校正/里程归一化用)

dd_radar_3d 多通道扩展(若一个数据集含多线或多文件组):

{projectDir}/datasets/{dsId}/
  ├── data.iprh
  ├── ch01.iprb
  ├── ch02.iprb
  ...
  ├── ch16.iprb
  └── channel.ord

4.2 ApiDatasetRepository 雷达分支(零架构改动)

在现有 ApiDatasetRepository::loadAsync 中,对 "radar.*" loaderKey 直接本地处理,不发网络请求

// src/data/api/ApiDatasetRepository.cpp
DetailLoad* ApiDatasetRepository::loadAsync(const std::string& key,
                                            const std::string& dsId, ...) {
    if (key == "radar.profile")   return makeRadarProfile(dsId);
    if (key == "radar.info")      return makeRadarInfo(dsId);
    if (key == "radar.anomalies") return makeRadarAnomalies(dsId);
    // ... 现有网络请求逻辑不变
}

makeRadarProfile 内部:

  1. 根据 dsId 组装本地目录路径(LocalProjectPathResolver::datasetDir(dsId)
  2. .iprhIprHeader
  3. dd_radar_2d:读单个 .iprbBScan → 转 float
  4. dd_radar_3d:读多通道 .iprb + .ordassembleGprSurveyGprSurvey
  5. 返回 LocalDetailLoad(同步完成,QTimer::singleShot(0, ...) emit done

优势:零改动 DatasetDetailControllerIAsyncDatasetRepository 接口、页签引擎。雷达数据只是 ApiDatasetRepository 内部的一个本地分支。

4.3 IprHeader 扩展(采集参数)

现有 IprHeader 只有 samples/lastTrace/channels/timeWindowNs/soilVelocity/distanceInterval。需要从 .iprh 中解析更多字段:

// src/io/gpr/IprHeader.hpp 扩展
struct IprHeader {
    // 已有字段
    int samples = 0;
    long lastTrace = 0;
    int channels = 0;
    double timeWindowNs = 0;
    double soilVelocity = 0;      // m/s
    double distanceInterval = 0;  // m
    // 新增字段(从 .iprh 文本解析)
    QString date;                 // 采集日期,如 "2022-03-10"
    QString time;                 // 采集时刻,如 "10:46"
    QString antennaModel;         // 雷达硬件型号,如 "MALA MIRA"
    double antennaFreqMHz = 0;    // 天线中心频率 MHz
    // ... 其他字段按需追加
};

注意.iprh 是文本头,字段名可能不固定。解析器用关键词模糊匹配(如 "DATE""ANTENNA""FREQUENCY"),缺失字段留空/0 不抛错。


5. 算法接口契约(用户代码接入点)

用户提供处理算法代码,我只负责管线框架(节点编排、参数传递、脏链追踪、缓存、线程调度)。

5.1 算法函数签名(用户需实现)

// 所有算法统一签名:输入输出为行主序 float 矩阵 [trace][sample]
// meta 提供几何参数dx, dz, velocity 等)
// params 为各算法专属参数结构(见 §5.3
// 算法就地修改 outout 已由调用方分配,大小 = in.size()

using GprAlgoFunc = void(*)(const float* in, float* out,
                            const GprTraceMeta& meta,
                            const void* params);

内存布局约定(必须与用户提供代码一致):

  • in/out 大小 = meta.ntraces * meta.samples
  • 索引方式:in[t * meta.samples + s],其中 t=0..ntraces-1(道号),s=0..samples-1(采样点)
  • t=0 为测线起点,s=0 为地表(时间零点)

5.2 管线框架(我实现)

// src/core/gpr_proc/GprProcessingPipeline.hpp
namespace geopro::core {

struct GprTraceMeta {
    int ntraces = 0;
    int samples = 0;
    double dx = 0;        // 道距 (m)
    double dz = 0;        // 深度采样间隔 (m 或 ns看 zIsTime)
    double x0 = 0;
    double z0 = 0;
    bool zIsTime = true;  // true=ns, false=m
    double velocityMPerNs = 0.12;
};

// 节点描述(纯数据,无虚函数,零开销)
struct ProcNodeDesc {
    QString name;           // 唯一标识,如 "zero_time", "gain"
    GprAlgoFunc algo;       // 用户提供的算法函数指针
    const void* params;     // 指向参数结构体的指针(由调用方保证生命周期)
    bool enabled = true;    // 是否启用
};

class GprProcessingPipeline {
public:
    // 注册节点按顺序。params 指针必须指向稳定内存(如 BScanProfileView 成员)。
    void registerNode(const ProcNodeDesc& desc);
    // 执行管线:从 raw 输入开始,按注册顺序逐节点处理,返回最终输出
    // 内部缓存各节点输出,参数未变时直接返回缓存(脏链追踪)
    std::vector<float> run(const std::vector<float>& raw,
                            const GprTraceMeta& meta);
    // 标记某节点参数已变更(下次 run 时从该节点开始重算)
    void markDirty(const QString& nodeName);
    // 标记全部重算
    void markAllDirty();
private:
    std::vector<ProcNodeDesc> nodes_;
    std::unordered_map<QString, std::vector<float>> cache_;
    std::unordered_set<QString> dirty_;
};

} // namespace geopro::core

5.3 参数结构体(用户算法使用)

每个算法一个参数结构体,定义在 core/model/gpr_proc/ 下。用户按这些结构体传参:

// src/core/model/gpr_proc/GprAlgoParams.hpp
#pragma once

namespace geopro::core {

struct ZeroTimeParams {
    bool autoDetect = true;
    int cutSamples = 30;
    int frontSearchWindow = 180;
    double noiseSigmaMultiple = 3.0;
};

enum class ZeroDriftMode { DC, Sliding };
struct ZeroDriftParams {
    ZeroDriftMode mode = ZeroDriftMode::Sliding;
    int slidingWindowSamples = 100;
};

enum class BgMode { MeanAverage, SingularityFilter };
struct BackgroundParams {
    BgMode mode = BgMode::MeanAverage;
    int averageTraceCount = 301;
    double singularityThreshold = 1.8;
};

struct GainParams {
    bool enableSpherical = true;
    bool enableAbsorption = true;
    double velocityMPerNs = 0.12;
    double referenceDepthM = 0.01;
    double exponent = 1.5;
    double absorptionBeta = 1.0;
    double maxGain = 30.0;
};

struct BandpassParams {
    bool autoFreq = true;
    double lowFreqHz = 1000;
    double highFreqHz = 100;
    double antennaFreqMHz = 200.0;
};

struct SmoothParams {
    int smoothWindow = 2;
    bool verticalSmooth = true;
    bool horizontalSmooth = true;
};

enum class TraceBalanceMode { Global, Local };
struct TraceBalanceParams {
    TraceBalanceMode mode = TraceBalanceMode::Local;
    int horizontalWindowTraces = 31;
    double targetRms = 0.0;
    double maxGain = 4.0;
    double epsilon = 1.0;
};

struct SampleBalanceParams {
    int windowSamples = 120;
    double targetRms = 0.0;
    double maxGain = 6.0;
    double epsilon = 1.0;
};

struct HilbertParams {
    bool computeEnvelope = true;
};

struct MigrationParams {
    int sumWidth = 64;
    double velocityMPerNs = 0.12;
};

struct TopoParams {
    bool useAverageElevation = true;
    double baseElevation = 0.0;
};

struct ResampleParams {
    bool resampleTraces = false;
    bool resampleSamples = false;
    int newTraces = 0;
    int newSamples = 0;
};

struct PredictiveDeconParams {
    int predLag = 1;
    int filterLen = 10;
};

// 总参数BScanProfileView 持有一个实例,各处理对话框修改子字段
struct GprProcessingParams {
    ZeroTimeParams zeroTime;
    ZeroDriftParams zeroDrift;
    BackgroundParams background;
    GainParams gain;
    BandpassParams bandpass;
    SmoothParams smooth;
    TraceBalanceParams traceBalance;
    SampleBalanceParams sampleBalance;
    HilbertParams hilbert;
    MigrationParams migration;
    TopoParams topo;
    ResampleParams resample;
    PredictiveDeconParams predictiveDecon;
};

} // namespace geopro::core

5.4 用户提供代码的接入方式

方式 A推荐函数指针

用户把算法实现为 C 风格函数,放在 src/core/gpr_proc/algo/ 下:

// src/core/gpr_proc/algo/gpr_zero_time.cpp用户文件
#include "gpr_proc/GprAlgoParams.hpp"

void gprZeroTime(const float* in, float* out,
                 const geopro::core::GprTraceMeta& meta,
                 const void* params) {
    const auto& p = *static_cast<const geopro::core::ZeroTimeParams*>(params);
    // ... 用户算法实现 ...
}

管线注册:

pipeline.registerNode({"zero_time", gprZeroTime, &params_.zeroTime});

方式 B类接口若用户偏好 OOP

class IGprAlgorithm {
public:
    virtual ~IGprAlgorithm() = default;
    virtual void process(const float* in, float* out,
                         const GprTraceMeta& meta) = 0;
    virtual QString name() const = 0;
};

请用户确认偏好 A 还是 B。A 更轻量C 风格函数指针B 更利于封装状态。


6. UI 组件架构(适配底部页签空间)

底部页签高度有限(默认 ~250px用户可拖拽拉大。BScanProfileView 内部必须紧凑。

BScanProfileViewQWidget实现 IDetailView
├── 顶部工具条单行QHBoxLayout
│   ├── [通道选择] QComboBox          # dd_radar_3d 显示2d 隐藏
│   ├── [rawdata|proc_data_1] QButtonGroup  # 切换原始/处理后
│   ├── [色阶] QComboBox              # Gray / Rainbow / Seismic / ColdWarm
│   ├── [对比度] QSlider(0-200%) + QLabel
│   ├── [比例] QLabel("1:2") + QToolButton(调)
│   ├── [显示异常] QCheckBox
│   ├── [Wiggle] QToolButton(toggle)
│   ├── [道编辑] QToolButton
│   ├── [反向] QToolButton
│   └── [处理参数⚙] QToolButton      # 点击弹出 GprProcessingDialog
├── 主体QSplitter水平
│   ├── BScanCanvasQWidget自绘占 80%
│   │   ├── 原始/处理后 float 数据缓存
│   │   ├── ColorMapperfloat→ColorScale→QImage
│   │   ├── ViewTransform世界↔屏幕
│   │   ├── 交互:滚轮缩放/左键平移/左键框选异常/悬停读值/右键菜单
│   │   └── 异常标注列表(屏幕坐标实时投影)
│   └── AScanWaveformViewQWidget占 20%,最小 120px
│       └── QwtPlot + QwtPlotCurve单道波形
└── 底部状态栏QHBoxLayout
    ├── 道号: 1234 | 里程: 123.45m | 深度: 2.34m | 振幅: -1234.5
    └── 当前通道: CH01 | 当前数据: rawdata

6.1 空间压缩策略

原设计 底部页签适配
顶部多行控制栏23+参数) 单行快捷栏 + "⚙处理参数"按钮弹出对话框
大面积剖面图 BScanCanvas 占满可用空间,默认纵向压缩;用户拉大底部 panel 后自动扩展
右侧 A-Scan QSplitter 水平分割,可拖拽调宽;默认窄条(~120px
鼠标悬停信息面板 收进底部状态栏(单行文本),不盖剖面
异常编辑弹窗 模态对话框(BScanAnomalyDialog),不常驻
参数配置区 GprProcessingDialog(模态/非模态对话框),分组折叠面板

6.2 BScanCanvas 自绘核心

class BScanCanvas : public QWidget {
    // 数据
    std::vector<float> rawData_;      // 当前通道原始数据
    std::vector<float> procData_;     // 处理后数据(由管线输出)
    GprTraceMeta meta_;
    // 渲染
    QImage image_;                    // 当前显示的 RGB 图像(缓存)
    ColorMapper mapper_;              // float→QRgb复用 core::ColorScale
    // 视图变换
    ViewTransform view_;              // 世界坐标(m,m) ↔ 屏幕坐标(px)
    // 交互状态
    enum class Mode { Idle, Panning, Zooming, Marquee, Hover } mode_ = Mode::Idle;
    QPoint lastMousePos_;
    QRectF marqueeRect_;              // 框选异常(世界坐标)
    // 异常
    std::vector<GprAnomaly> anomalies_;
    // A-Scan 联动
    int hoverTrace_ = -1;             // 当前悬停道号
signals:
    void traceSelected(int traceIdx, const float* traceData, int samples);
    void anomalySelected(const GprAnomaly& anomaly);
    void hoverInfoChanged(const QString& info);
};

paintEvent 管线

  1. 若数据/参数/视口变化 → 重新生成 QImageColorMapper float→QRgb
  2. QPainter::drawImage 贴图
  3. 画网格线(里程/深度刻度,根据缩放动态抽稀)
  4. 画异常框(红框 + 标签)
  5. 画十字准星(鼠标悬停位置)
  6. 画打标线(如果有 Marker 数据)

缩放策略

  • 滚轮:以鼠标位置为中心,缩放 view_.scaleXview_.scaleY(可独立锁比例)
  • XY 比例按钮:固定 scaleY / scaleX = ratio(如 1:2 表示纵向放大 2 倍)
  • 全景:一键重置 view 到 fit

Wiggle 模式

  • 关闭:drawImage 正常 raster 图
  • 开启:每道画波形线(QPainter::drawPolyline),正半周填充色(如黑/红),负半周填充另一色(白/蓝),背景白

6.3 A-Scan 联动

// BScanCanvas 悬停/点击某道时 emit traceSelected
connect(canvas_, &BScanCanvas::traceSelected, this, [this](int t, const float* data, int n) {
    aScanCurve_->setSamples(data, n);  // QwtPlotCurve 更新
    aScanPlot_->replot();
});

7. Payload、ViewKind 与策略

7.1 新增 ViewKind

// src/controller/DatasetDetailTab.hpp
enum class ViewKind {
    Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table, WebMap,
    BScanProfile   // ← 新增
};

7.2 新增 Payload

// src/core/model/detail/DetailPayloads.hpp

// 单通道剖面数据B-Scan 核心载荷)
struct GprChannelData {
    QString channelName;              // 如 "CH01"
    std::vector<float> data;         // traces × samples行主序
};

struct GprProfilePayload {
    GprTraceMeta meta;                // 几何参数
    std::vector<GprChannelData> channels;  // 多通道数据2d 时 size=1
    int currentChannel = 0;           // 默认显示通道
    // 采集参数(从头文件解析,供采集参数页签展示)
    QString date, time, antennaModel;
    double antennaFreqMHz = 0;
    // 色阶(默认 Gray
    ColorScale defaultScale;
};

// 雷达异常标注前后端共用M1 先本地)
struct GprAnomaly {
    QString id;
    QString typeCode;    // cavity / loose / void / pipe
    QString typeName;
    int traceStart = 0, traceEnd = 0;
    int sampleStart = 0, sampleEnd = 0;
    double distStartM = 0, distEndM = 0;
    double depthStartM = 0, depthEndM = 0;
    double widthM = 0, heightM = 0, lengthM = 0;
    double burialDepthM = 0, clearanceM = 0;
    double confidence = 0;
    QString remark;
};

Q_DECLARE_METATYPE(geopro::core::GprProfilePayload)

7.3 策略

// src/app/panels/chart/Radar2dStrategy.hpp
struct Radar2dStrategy : controller::IDatasetChartStrategy {
    std::string ddCode() const override { return "dd_radar_2d"; }
    std::vector<controller::TabSpec> tabs() const override {
        return {
            {QStringLiteral("B-Scan剖面"), controller::ViewKind::BScanProfile,
             QStringLiteral("radar.profile"), false, false},
            {QStringLiteral("采集参数"), controller::ViewKind::Table,
             QStringLiteral("radar.info"), false, false},
            {QStringLiteral("异常列表"), controller::ViewKind::Table,
             QStringLiteral("radar.anomalies"), false, false},
        };
    }
};

// Radar3dStrategy.hpp同结构ddCode="dd_radar_3d"

7.4 工厂分支

// src/app/panels/chart/DetailViewFactory.cpp
case controller::ViewKind::BScanProfile: {
    auto* bscan = new BScanProfileView(parent);
    // 注入所需仓储/回调(如需)
    return std::unique_ptr<IDetailView>(bscan);
}

8. 文件清单与开发顺序

Phase 1数据模型 + 管线框架(无算法,无 UI

# 文件 说明
1.1 src/core/model/gpr_proc/GprTraceMeta.hpp 几何元数据
1.2 src/core/model/gpr_proc/GprAlgoParams.hpp 全部算法参数结构体
1.3 src/core/model/gpr_proc/GprAnomaly.hpp 异常标注模型
1.4 src/core/model/detail/DetailPayloads.hpp 追加 GprProfilePayload + Q_DECLARE_METATYPE
1.5 src/core/gpr_proc/GprProcessingPipeline.hpp/cpp 管线框架(注册/执行/缓存/脏链)
1.6 src/io/gpr/IprHeader.hpp/cpp 扩展头文件字段date/time/antenna/freq
1.7 tests/core/gpr_proc/test_pipeline.cpp 管线框架测试(用 stub 算法)

Phase 2页签引擎接入策略 + 本地加载 + 工厂)

# 文件 说明
2.1 src/controller/DatasetDetailTab.hpp 追加 BScanProfile ViewKind
2.2 src/app/panels/chart/Radar2dStrategy.hpp 2d 策略
2.3 src/app/panels/chart/Radar3dStrategy.hpp 3d 策略
2.4 src/app/panels/chart/BScanProfileView.hpp/cpp 壳子(先空白 QWidget
2.5 src/app/panels/chart/DetailViewFactory.cpp 追加 BScanProfile 分支
2.6 src/data/api/ApiDatasetRepository.hpp/cpp 追加 makeRadarProfile/Info/Anomalies
2.7 src/app/main.cpp 注册 Radar2d/3d 策略
2.8 src/app/CMakeLists.txt 追加新 .cpp
2.9 src/core/CMakeLists.txt 追加 gpr_proc 子目录

Phase 3B-Scan 自绘核心

# 文件 说明
3.1 src/app/panels/radar/ViewTransform.hpp/cpp 世界↔屏幕坐标变换(纯几何,可单测)
3.2 src/app/panels/radar/ColorMapper.hpp/cpp float→ColorScale→QRgb复用 core::ColorScale
3.3 src/app/panels/radar/BScanCanvas.hpp/cpp 自绘核心paintEvent + 数据缓存)
3.4 src/app/panels/radar/AScanWaveformView.hpp/cpp QwtPlot + QwtPlotCurve
3.5 src/app/panels/radar/BScanToolbar.hpp/cpp 顶部单行工具条
3.6 tests/render/test_bscan_canvas.cpp 自绘单元测试(用 fixture 数据画参考图)

Phase 4交互 + 处理对话框

# 文件 说明
4.1 src/app/panels/radar/BScanCanvas.cpp 补交互:滚轮/平移/框选/悬停/右键菜单
4.2 src/app/panels/radar/GprProcessingDialog.hpp/cpp 处理参数对话框(分组折叠面板)
4.3 src/app/panels/radar/BScanAnomalyDialog.hpp/cpp 异常编辑对话框
4.4 src/app/panels/radar/BScanProfileView.cpp 总装:工具条 + Canvas + A-Scan + 状态栏 + 管线集成

Phase 5算法接入用户提供代码后

# 文件 说明
5.1 src/core/gpr_proc/algo/ 用户算法文件目录(函数实现)
5.2 src/app/panels/radar/BScanProfileView.cpp 管线注册各节点,绑定参数
5.3 tests/core/gpr_proc/test_algo_*.cpp 各算法单元测试

9. 仍需确认的事项

🔴 接入方式确认1 项,阻塞 Phase 5

算法接入方式:用户提供处理代码时,偏好哪种接口?

  • A推荐C 风格函数指针 void algo(const float* in, float* out, const GprTraceMeta& meta, const void* params),放在 src/core/gpr_proc/algo/*.cpp
  • BC++ 抽象类 IGprAlgorithm,用户继承实现 process() 方法

🟡 中优先级(影响细节,不阻塞框架开发)

  1. .iprh 头文件完整字段列表:现有头文件只有 samples/lastTrace/channels/timeWindow/soilVelocity/distanceInterval。用户的"采集参数"还需要 Date/Time/Antenna/Frequency。.iprh 文本中这些字段的精确关键词是什么?(如 "DATE""TIME""ANTENNA""FREQUENCY"?)缺失字段是否允许留空?

  2. 色阶预置方案:用户提到"灰度图、彩虹色阶、冷暖色阶、振幅色阶"。这些是否已有标准定义?我可以先预置几套 core::ColorScale stops

    • Gray: 黑(0) → 白(max)
    • Rainbow: 紫→蓝→青→绿→黄→红
    • Seismic/ColdWarm: 蓝(负) → 白(0) → 红(正) 请确认或提供具体 RGB 断点。
  3. Wiggle 模式配色:正半周填什么色?负半周填什么色?线色?背景色?(标准地震勘探:正=黑/红,负=白/蓝,背景白)

  4. 异常类型完整枚举:用户提到"空洞/疏松/脱空/管线",是否还有其他类型?需要完整列表用于下拉选择。

  5. 本地文件路径约定{projectDir}/datasets/{dsId}/data.iprb 这个约定是否 OK还是需要从项目配置中读某个字段来确定根目录


10. 下一步行动建议

  1. 你确认算法接入方式A 函数指针 / B 抽象类) → 我锁定管线接口
  2. 我启动 Phase 1 + Phase 2(数据模型 + 管线框架 + 页签引擎接入)
  3. Phase 1/2 完成后,我给你一个可运行的空白 B-Scan 页签(能双击雷达数据集打开页签、加载本地文件、显示空白画布)
  4. 你同步准备算法代码(按我定义的接口签名实现)
  5. 我接 Phase 3/4(自绘核心 + 交互 + 对话框)
  6. 你提供算法代码后Phase 5 接入

是否按此顺序推进?还是先确认完所有事项再开始编码?