feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
5 changed files with 304 additions and 1 deletions
Showing only changes of commit 9af363080a - Show all commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@ -680,6 +680,79 @@ geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) {
return cs; return cs;
} }
// 地震高对比色阶seismic 红-白-蓝):两端饱和亮色(强正=亮红、强负=亮蓝),
// 零附近白。比 structural 更亮、对比更狠,正负反射一眼分开。
geopro::core::ColorScale makeSeismicColorScale(double vmin, double vmax) {
geopro::core::ColorScale cs;
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
auto at = [&](double t) { return vmin + span * t; };
cs.addStop(at(0.00), geopro::core::Rgba{30, 60, 255, 255}); // 亮蓝(强负)
cs.addStop(at(0.30), geopro::core::Rgba{120, 180, 255, 255}); // 浅蓝
cs.addStop(at(0.50), geopro::core::Rgba{255, 255, 255, 255}); // 白(零)
cs.addStop(at(0.70), geopro::core::Rgba{255, 170, 120, 255}); // 浅橙
cs.addStop(at(1.00), geopro::core::Rgba{255, 40, 30, 255}); // 亮红(强正)
return cs;
}
// jet 类高饱和色阶(蓝-青-绿-黄-红):全程高亮高饱和,最大化色彩动态范围,
// 弱信号也能映到鲜明色相,适合「一眼铺满层次」的取向。
geopro::core::ColorScale makeJetColorScale(double vmin, double vmax) {
geopro::core::ColorScale cs;
const double span = (vmax > vmin) ? (vmax - vmin) : 1.0;
auto at = [&](double t) { return vmin + span * t; };
cs.addStop(at(0.00), geopro::core::Rgba{0, 0, 200, 255}); // 蓝
cs.addStop(at(0.25), geopro::core::Rgba{0, 200, 255, 255}); // 青
cs.addStop(at(0.50), geopro::core::Rgba{0, 230, 60, 255}); // 绿
cs.addStop(at(0.75), geopro::core::Rgba{255, 230, 0, 255}); // 黄
cs.addStop(at(1.00), geopro::core::Rgba{255, 30, 0, 255}); // 红
return cs;
}
// 「实体感」不透明度包络Task 12d gallery与 structural 双端斜坡不同,这里让
// 中高值段普遍可见——背景(近零)仍压低但不归零,中高段从 floorOpacity 平滑升到
// maxOpacity使体读起来像半透明实心块、内部层次而非只剩两端薄壳可见。
// floorOpacity近零背景的最低不透明度0.05~0.12,压住但不消失)
// maxOpacity 强反射端的不透明度峰值0.85 时近实心)
// midOpacity 中值段半幅处的不透明度0.3~0.5,决定「半透明实心」观感)
vtkSmartPointer<vtkVolumeProperty> makeSolidVolumeProperty(
const geopro::core::Quant& q, const geopro::core::ColorScale& cs,
double vminPhys, double vmaxPhys, double floorOpacity, double midOpacity,
double maxOpacity) {
constexpr int kTransferSamples = 64;
if (vminPhys >= vmaxPhys) vmaxPhys = vminPhys + 1.0;
const double qminD = static_cast<double>(q.toQ(vminPhys));
const double qmaxD = static_cast<double>(q.toQ(vmaxPhys));
vtkNew<vtkColorTransferFunction> color;
for (int t = 0; t < kTransferSamples; ++t) {
const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1);
const auto qvLevel = static_cast<std::int16_t>(std::lround(qd));
const double phys = q.toPhys(qvLevel);
const auto c = cs.colorAt(phys);
color->AddRGBPoint(qd, c.r / 255.0, c.g / 255.0, c.b / 255.0);
}
// 不透明度V 形(中段=零附近背景=floor正负两端=max但全程 ≥floor 且中值
// 段≈mid → 整体半透明实心、内部层次可见,而非两端薄壳。
vtkNew<vtkPiecewiseFunction> opacity;
opacity->AddPoint(
static_cast<double>(geopro::core::ScalarVolumeI16::kBlank), 0.0);
const double qmid = 0.5 * (qminD + qmaxD);
const double half = 0.5 * (qmaxD - qminD);
opacity->AddPoint(qminD, maxOpacity); // 强负反射:近实心
opacity->AddPoint(qmid - 0.55 * half, midOpacity); // 中负段:半透明实心
opacity->AddPoint(qmid, floorOpacity); // 近零背景:压低但可见
opacity->AddPoint(qmid + 0.55 * half, midOpacity); // 中正段:半透明实心
opacity->AddPoint(qmaxD, maxOpacity); // 强正反射:近实心
auto prop = vtkSmartPointer<vtkVolumeProperty>::New();
prop->SetColor(color);
prop->SetScalarOpacity(opacity);
prop->SetInterpolationTypeToLinear();
prop->ShadeOff();
return prop;
}
// 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。 // 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。
// 不透明度调高时光线提前终止fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。 // 不透明度调高时光线提前终止fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。
vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty( vtkSmartPointer<vtkVolumeProperty> makeTunedVolumeProperty(
@ -1560,6 +1633,23 @@ void savePng(vtkRenderWindow* rw, const std::string& path) {
writer->Write(); writer->Write();
} }
// 画面平均亮度0~255取前缓冲 RGB 求 luma 均值。Task 12d gallery 报告用,
// 量化「整体偏暗 vs 变亮」——背景占多数,故这是含背景的全屏均亮(横向对比有效)。
double meanBrightness(vtkRenderWindow* rw, int w, int h) {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, w - 1, h - 1, /*front=*/1, px);
const vtkIdType np = px->GetNumberOfTuples();
if (np == 0) return 0.0;
double sum = 0.0;
for (vtkIdType i = 0; i < np; ++i) {
const double r = px->GetComponent(i, 0);
const double g = px->GetComponent(i, 1);
const double b = px->GetComponent(i, 2);
sum += 0.299 * r + 0.587 * g + 0.114 * b;
}
return sum / static_cast<double>(np);
}
int cmdRenderLOD(int argc, char** argv) { int cmdRenderLOD(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2); const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) { if (a.positional.empty()) {
@ -2369,11 +2459,205 @@ std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) {
return one.size(); return one.size();
} }
// ============================================================================
// 视觉调参画廊Task 12d galleryview --preview --variant N
// ============================================================================
//
// 同一局部段(沿线中段 kViewDefaultLocalBricks 列全分辨率) + 同一相机框法
// (ResetCamera→Elevation/Azimuth→Zoom),只换「不透明度包络 / 配色 / 取景角度 /
// 背景」四组视觉参数,各存一张 PNG 供控制方挑选。fps 对视觉调参近乎中性,每组实测验证。
enum class OpacityProfile {
kSolid, // V 形实体感:中高值段普遍可见,半透明实心块
kStructural, // 现有双端斜坡:仅正负两端不透明(对照基线)
};
enum class ColorChoice { kStructural, kSeismic, kJet };
struct GalleryVariant {
const char* name; // 文件名后缀view-<name>.png
OpacityProfile profile;
ColorChoice color;
double floorOpacity; // 近零背景不透明度kSolid 用)
double midOpacity; // 中值段不透明度kSolid 用)
double maxOpacity; // 两端峰值不透明度
double exagg; // 垂向夸张
double elevation; // ResetCamera 后 Elevation
double azimuth; // 再 Azimuth
double zoom; // 再 Zoom 填满画面
double bg[3]; // 背景 RGB
const char* desc; // 报告用中文说明
};
// 4 组视觉参数。值经离屏实跑挑出(详见报告)。
const GalleryVariant kGalleryVariants[] = {
// var1高不透明度实体感——seismic 亮配色 + V 形包络(中段 0.40/两端 0.85)
// floor 压到 0.04:近零层间隙近透明,亮层面浮出 → 内部层状结构可读。
{"var1", OpacityProfile::kSolid, ColorChoice::kSeismic,
0.04, 0.40, 0.85, 8.0, 22.0, 28.0, 1.9, {0.05, 0.05, 0.09},
"高不透明度实体感V形包络(floor0.04/mid0.40/max0.85)+seismic 亮配色,"
"半透明实心、内部层次可见"},
// var2高对比配色——jet 全程高饱和 + 中等不透明度 V 形包络。
{"var2", OpacityProfile::kSolid, ColorChoice::kJet,
0.04, 0.32, 0.70, 8.0, 22.0, 28.0, 1.9, {0.06, 0.06, 0.10},
"高对比配色jet 蓝青绿黄红全程高饱和 + 中等 V 形包络(mid0.32/max0.70)"},
// var3居中正对纵截面——低 Elevation/Azimuth 摆平、正对 X-Z 长侧面(层状反射沿
// X 延展最清晰)、Zoom2.0 填满 ~70%floor 压更低让层间隙透明、层面立体。
{"var3", OpacityProfile::kSolid, ColorChoice::kSeismic,
0.03, 0.38, 0.82, 9.0, 10.0, 12.0, 2.0, {0.05, 0.05, 0.09},
"居中正对纵截面:低 El10/Az12 摆平正对 X-Z 长侧面、Zoom2.0 填满视野,"
"floor0.03 凸显层面exagg9 放大薄轴"},
// var4最像真实 GPR 三维图——seismic + 略提背景亮 + 微立体角 + 实体包络。
{"var4", OpacityProfile::kSolid, ColorChoice::kSeismic,
0.035, 0.38, 0.84, 8.0, 18.0, 22.0, 2.0, {0.07, 0.08, 0.11},
"综合最佳seismic + 实体包络(floor0.035/mid0.38/max0.84) + 微立体取景"
"(El18/Az22/Zoom2.0) + 略亮冷灰背景"},
};
geopro::core::ColorScale pickColor(ColorChoice c, double vmin, double vmax) {
switch (c) {
case ColorChoice::kSeismic: return makeSeismicColorScale(vmin, vmax);
case ColorChoice::kJet: return makeJetColorScale(vmin, vmax);
case ColorChoice::kStructural:
default: return makeStructuralColorScale(vmin, vmax);
}
}
// 渲一组画廊变体并存 PNG报告 结构像素 / 平均亮度 / fps。返回 0=OK。
int runGalleryVariant(const std::string& dir, const GalleryVariant& v,
int frames) {
const int winW = 1280, winH = 800;
geopro::data::ChunkedVolumeStore store(dir);
const geopro::data::StoreMeta& m = store.meta();
const double vmin = m.vminPhys, vmax = m.vmaxPhys;
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
vtkSmartPointer<vtkVolumeProperty> prop;
if (v.profile == OpacityProfile::kSolid) {
prop = makeSolidVolumeProperty(m.quant, cs, vmin, vmax, v.floorOpacity,
v.midOpacity, v.maxOpacity);
} else {
prop = makeTunedVolumeProperty(m.quant, cs, vmin, vmax, v.maxOpacity);
}
auto rw = makeOffscreenWindow(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]);
rw->AddRenderer(ren);
vtkNew<vtkMultiBlockVolumeMapper> mapper;
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
volume->SetScale(1.0, v.exagg, v.exagg);
ren->AddVolume(volume);
// 局部段(沿线中段,同 viewSetupDefaultFrame 的取段法)。
const int totBx = store.bricksX(0);
const int localBx = std::min(kViewDefaultLocalBricks, totBx);
const int bx0 = std::max(0, totBx / 2 - localBx / 2);
vtkSmartPointer<vtkImageData> locImg =
buildLocalLevel0Image(store, m, bx0, localBx);
std::vector<vtkSmartPointer<vtkImageData>> one{locImg};
auto mb = makeMultiBlock(one);
mapper->SetInputDataObject(mb);
mapper->Update();
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(v.elevation);
cam->Azimuth(v.azimuth);
cam->Zoom(v.zoom);
ren->ResetCameraClippingRange();
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
rw->Render();
const fs::path shotDir =
fs::path("docs") / "superpowers" / "plans" / "poc-lod-shots";
fs::create_directories(shotDir);
const std::string pngPath =
(shotDir / (std::string("view-") + v.name + ".png")).string();
savePng(rw.Get(), pngPath);
// 结构像素:任一通道 >50排除暗背景度量「确有体结构」。
auto countStructPixels = [&]() -> vtkIdType {
auto px = vtkSmartPointer<vtkUnsignedCharArray>::New();
rw->GetRGBACharPixelData(0, 0, winW - 1, winH - 1, /*front=*/1, px);
vtkIdType n = 0;
const vtkIdType np = px->GetNumberOfTuples();
for (vtkIdType i = 0; i < np; ++i) {
if (px->GetComponent(i, 0) > 50 || px->GetComponent(i, 1) > 50 ||
px->GetComponent(i, 2) > 50) {
++n;
}
}
return n;
};
const vtkIdType structPx = countStructPixels();
const double bright = meanBrightness(rw.Get(), winW, winH);
rw->Render(); // 预热再测 fps
Stopwatch sw;
for (int f = 0; f < frames; ++f) {
cam->Azimuth(360.0 / frames);
rw->Render();
}
const double ms = sw.elapsedMs();
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
const bool texErr = capWin->textureError();
vtkOutputWindow::SetInstance(nullptr);
const bool ok = !texErr && structPx > 0;
std::cout << "\n--- gallery " << v.name << " ---\n";
std::cout << "参数 : " << v.desc << "\n";
std::cout << "存图 : " << pngPath << "\n";
std::cout << "结构像素(>50) : " << structPx << " / " << (winW * winH)
<< " (" << (100.0 * structPx / (winW * winH)) << "%)\n";
std::cout << "平均亮度(0-255) : " << bright << "\n";
std::cout << "真实 fps : " << (ok ? std::to_string(fps) : "INVALID")
<< " (" << frames << " 帧旋相机)\n";
std::cout << "结果 : " << (ok ? "OK" : "FAIL") << "\n";
writeMetricLine(
"view-gallery," + std::string(v.name) + ",dir=" + dir +
",profile=" + (v.profile == OpacityProfile::kSolid ? "solid" : "struct") +
",floor=" + std::to_string(v.floorOpacity) +
",mid=" + std::to_string(v.midOpacity) +
",max=" + std::to_string(v.maxOpacity) +
",exagg=" + std::to_string(v.exagg) +
",el=" + std::to_string(v.elevation) +
",az=" + std::to_string(v.azimuth) + ",zoom=" + std::to_string(v.zoom) +
",structPx=" + std::to_string(structPx) +
",bright=" + std::to_string(bright) +
",fps=" + (ok ? std::to_string(fps) : "INVALID") +
",png=" + pngPath);
return ok ? 0 : 1;
}
// view --gallery依次渲全部 4 组变体。
int cmdViewGallery(const std::string& dir, int frames) {
std::cout << "[view --gallery] storeDir=" << dir << " frames=" << frames
<< "\n[view --gallery] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[view --gallery] 闸门失败,中止。\n";
return 1;
}
int rc = 0;
for (const auto& v : kGalleryVariants) {
if (runGalleryVariant(dir, v, frames) != 0) rc = 1;
}
std::cout << "\n[view --gallery] 完成4 张图存于 "
"docs/superpowers/plans/poc-lod-shots/view-var{1..4}.png\n";
return rc;
}
int cmdView(int argc, char** argv) { int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2); const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) { if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] " std::cerr << "用法: gpr_poc view <storeDir> [--exagg 8] [--opacity 0.5] "
"[--budget 64] [--smoke] [--preview] [--frames 90]\n"; "[--budget 64] [--smoke] [--preview] [--variant N] "
"[--gallery] [--frames 90]\n";
return 2; return 2;
} }
const std::string dir = a.positional[0]; const std::string dir = a.positional[0];
@ -2389,6 +2673,25 @@ int cmdView(int argc, char** argv) {
}; };
const bool smoke = hasFlag("smoke"); const bool smoke = hasFlag("smoke");
const bool preview = hasFlag("preview"); const bool preview = hasFlag("preview");
// 画廊模式Task 12d渲 4 组视觉调参图供挑选。优先于其余路径。
if (hasFlag("gallery")) {
return cmdViewGallery(dir, frames);
}
// 单变体view --preview --variant NN=1..4),只渲第 N 组。
if (preview && a.kv.count("variant")) {
const int vi = std::stoi(a.get("variant", "1"));
const int n = static_cast<int>(sizeof(kGalleryVariants) /
sizeof(kGalleryVariants[0]));
if (vi < 1 || vi > n) {
std::cerr << "[view] --variant 需在 1.." << n << " 之间\n";
return 2;
}
std::cout << "[view] storeDir=" << dir << " 单变体 variant=" << vi << "\n";
if (cmdOffscreenSmoke() != 0) return 1;
return runGalleryVariant(dir, kGalleryVariants[vi - 1], frames);
}
std::cout << "[view] storeDir=" << dir << " exagg=" << exagg std::cout << "[view] storeDir=" << dir << " exagg=" << exagg
<< " opacity=" << opacity << " budget=" << budget << " opacity=" << opacity << " budget=" << budget
<< (preview ? " [PREVIEW 离屏存图+测fps]" << (preview ? " [PREVIEW 离屏存图+测fps]"