diff --git a/docs/superpowers/plans/poc-lod-shots/view-var1.png b/docs/superpowers/plans/poc-lod-shots/view-var1.png new file mode 100644 index 0000000..53e5f45 Binary files /dev/null and b/docs/superpowers/plans/poc-lod-shots/view-var1.png differ diff --git a/docs/superpowers/plans/poc-lod-shots/view-var2.png b/docs/superpowers/plans/poc-lod-shots/view-var2.png new file mode 100644 index 0000000..92afd81 Binary files /dev/null and b/docs/superpowers/plans/poc-lod-shots/view-var2.png differ diff --git a/docs/superpowers/plans/poc-lod-shots/view-var3.png b/docs/superpowers/plans/poc-lod-shots/view-var3.png new file mode 100644 index 0000000..3a11c09 Binary files /dev/null and b/docs/superpowers/plans/poc-lod-shots/view-var3.png differ diff --git a/docs/superpowers/plans/poc-lod-shots/view-var4.png b/docs/superpowers/plans/poc-lod-shots/view-var4.png new file mode 100644 index 0000000..2e3b739 Binary files /dev/null and b/docs/superpowers/plans/poc-lod-shots/view-var4.png differ diff --git a/tools/gpr_poc/main.cpp b/tools/gpr_poc/main.cpp index 08ba397..7d63c0c 100644 --- a/tools/gpr_poc/main.cpp +++ b/tools/gpr_poc/main.cpp @@ -680,6 +680,79 @@ geopro::core::ColorScale makeStructuralColorScale(double vmin, double vmax) { 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 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(q.toQ(vminPhys)); + const double qmaxD = static_cast(q.toQ(vmaxPhys)); + + vtkNew color; + for (int t = 0; t < kTransferSamples; ++t) { + const double qd = qminD + (qmaxD - qminD) * t / (kTransferSamples - 1); + const auto qvLevel = static_cast(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 opacity; + opacity->AddPoint( + static_cast(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::New(); + prop->SetColor(color); + prop->SetScalarOpacity(opacity); + prop->SetInterpolationTypeToLinear(); + prop->ShadeOff(); + return prop; +} + // 参数化量化域传函:与 makeI16VolumeProperty 同逻辑,但 kMaxOpacity 可由 --opacity 控。 // 不透明度调高时光线提前终止,fps 近乎中性甚至更快(探针认知,报告打印前后对照证实)。 vtkSmartPointer makeTunedVolumeProperty( @@ -1560,6 +1633,23 @@ void savePng(vtkRenderWindow* rw, const std::string& path) { writer->Write(); } +// 画面平均亮度(0~255):取前缓冲 RGB 求 luma 均值。Task 12d gallery 报告用, +// 量化「整体偏暗 vs 变亮」——背景占多数,故这是含背景的全屏均亮(横向对比有效)。 +double meanBrightness(vtkRenderWindow* rw, int w, int h) { + auto px = vtkSmartPointer::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(np); +} + int cmdRenderLOD(int argc, char** argv) { const Args a = parseArgs(argc, argv, 2); if (a.positional.empty()) { @@ -2369,11 +2459,205 @@ std::size_t viewSetupDefaultFrame(ViewState* st, vtkRenderer* ren) { return one.size(); } +// ============================================================================ +// 视觉调参画廊(Task 12d gallery):view --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-.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 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 ren; + ren->SetBackground(v.bg[0], v.bg[1], v.bg[2]); + rw->AddRenderer(ren); + + vtkNew mapper; + mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode); + auto volume = vtkSmartPointer::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 locImg = + buildLocalLevel0Image(store, m, bx0, localBx); + std::vector> 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::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::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) { const Args a = parseArgs(argc, argv, 2); if (a.positional.empty()) { std::cerr << "用法: gpr_poc view [--exagg 8] [--opacity 0.5] " - "[--budget 64] [--smoke] [--preview] [--frames 90]\n"; + "[--budget 64] [--smoke] [--preview] [--variant N] " + "[--gallery] [--frames 90]\n"; return 2; } const std::string dir = a.positional[0]; @@ -2389,6 +2673,25 @@ int cmdView(int argc, char** argv) { }; const bool smoke = hasFlag("smoke"); const bool preview = hasFlag("preview"); + + // 画廊模式(Task 12d):渲 4 组视觉调参图供挑选。优先于其余路径。 + if (hasFlag("gallery")) { + return cmdViewGallery(dir, frames); + } + // 单变体:view --preview --variant N(N=1..4),只渲第 N 组。 + if (preview && a.kv.count("variant")) { + const int vi = std::stoi(a.get("variant", "1")); + const int n = static_cast(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 << " opacity=" << opacity << " budget=" << budget << (preview ? " [PREVIEW 离屏存图+测fps]"