feat(gpr3dv): 移植精确坐标/轨迹/世界网格(CGCS2000)+测绘级逐线世界对齐建体

复制 CoordinateTransform/TrajectoryCalculator/CScanGridder/PosParser(逐字节一致)进
external/gpr3dviewer;新增 Gpr3dvSurveyVolumeBridge 按 CGCS2000+逐道GPS轨迹建世界对齐体;
gpr_poc 加 build-survey-all/view-survey-all(各体自带世界origin,精确就位跟GPS弯)。
This commit is contained in:
gaozheng 2026-06-25 10:41:02 +08:00
parent ccd3040a67
commit f713c15366
13 changed files with 1925 additions and 3 deletions

View File

@ -23,6 +23,15 @@ add_library(geopro_gpr3dv STATIC
Rd3Parser.cpp
RadarProcessor.cpp
PerformanceLogger.cpp
# P8 3DGPRViewerdiff
# CoordinateTransform CGCS2000 - 3° Qt
# TrajectoryCalculator RTK GPS + 线 CGCS
# CScanGridder IDW C-scan
# PosParser .pos / center.ccc GPS
CoordinateTransform.cpp
TrajectoryCalculator.cpp
CScanGridder.cpp
PosParser.cpp
third_party/kissfft/kiss_fft.c
third_party/kissfft/kiss_fftr.c
)

336
external/gpr3dviewer/CScanGridder.cpp vendored Normal file
View File

@ -0,0 +1,336 @@
#include "CScanGridder.h"
#include <QHash>
#include <QPair>
#include <QtGlobal>
#include <algorithm>
#include <cmath>
#include <limits>
namespace {
struct CScanSamplePoint {
double x = 0.0;
double y = 0.0;
float amplitude = 0.0f;
int line = -1;
int channel = -1;
int trace = -1;
};
struct BinKey {
int x = 0;
int y = 0;
bool operator==(const BinKey &other) const
{
return x == other.x && y == other.y;
}
};
uint qHash(const BinKey &key, uint seed = 0)
{
return ::qHash(key.x, seed) ^ (::qHash(key.y, seed + 0x9e3779b9U) << 1);
}
static double distanceSquared(double ax, double ay, double bx, double by)
{
const double dx = ax - bx;
const double dy = ay - by;
return dx * dx + dy * dy;
}
static int gridIndex(int x, int y, int width)
{
return y * width + x;
}
static QVector<float> gaussianKernel(double sigma)
{
sigma = std::max(0.1, sigma);
const int radius = std::max(1, static_cast<int>(std::ceil(sigma * 3.0)));
QVector<float> kernel(radius * 2 + 1);
double sum = 0.0;
for (int i = -radius; i <= radius; ++i) {
const double v = std::exp(-(i * i) / (2.0 * sigma * sigma));
kernel[i + radius] = static_cast<float>(v);
sum += v;
}
if (sum > 0.0) {
for (float &v : kernel) v = static_cast<float>(v / sum);
}
return kernel;
}
static void smoothMasked(CScanGridResult &grid, double sigma)
{
if (!grid.valid || grid.width <= 0 || grid.height <= 0) return;
const QVector<float> kernel = gaussianKernel(sigma);
const int radius = kernel.size() / 2;
const int count = grid.width * grid.height;
QVector<float> tmpValues(count, 0.0f);
QVector<float> tmpWeights(count, 0.0f);
for (int y = 0; y < grid.height; ++y) {
for (int x = 0; x < grid.width; ++x) {
double sum = 0.0;
double weight = 0.0;
for (int k = -radius; k <= radius; ++k) {
const int sx = x + k;
if (sx < 0 || sx >= grid.width) continue;
const int idx = gridIndex(sx, y, grid.width);
if (!grid.validMask[idx]) continue;
const double w = kernel[k + radius];
sum += grid.values[idx] * w;
weight += w;
}
const int outIdx = gridIndex(x, y, grid.width);
if (weight > 1e-6) {
tmpValues[outIdx] = static_cast<float>(sum / weight);
tmpWeights[outIdx] = static_cast<float>(weight);
}
}
}
QVector<float> outValues(count, 0.0f);
QVector<unsigned char> outMask(count, 0);
for (int y = 0; y < grid.height; ++y) {
for (int x = 0; x < grid.width; ++x) {
double sum = 0.0;
double weight = 0.0;
for (int k = -radius; k <= radius; ++k) {
const int sy = y + k;
if (sy < 0 || sy >= grid.height) continue;
const int idx = gridIndex(x, sy, grid.width);
if (tmpWeights[idx] <= 1e-6f) continue;
const double w = kernel[k + radius] * tmpWeights[idx];
sum += tmpValues[idx] * w;
weight += w;
}
const int outIdx = gridIndex(x, y, grid.width);
if (weight > 1e-4) {
outValues[outIdx] = static_cast<float>(sum / weight);
outMask[outIdx] = 1;
}
}
}
grid.values = std::move(outValues);
grid.validMask = std::move(outMask);
}
static float percentile(QVector<float> values, double percent)
{
if (values.isEmpty()) return 0.0f;
std::sort(values.begin(), values.end());
percent = std::clamp(percent, 0.0, 100.0);
const double pos = percent / 100.0 * (values.size() - 1);
const int lo = static_cast<int>(std::floor(pos));
const int hi = static_cast<int>(std::ceil(pos));
if (lo == hi) return values[lo];
const double t = pos - lo;
return static_cast<float>(values[lo] * (1.0 - t) + values[hi] * t);
}
static void updateDisplayRange(CScanGridResult &grid, double lowPercent, double highPercent)
{
QVector<float> validValues;
validValues.reserve(grid.values.size());
for (int i = 0; i < grid.values.size(); ++i) {
if (i < grid.validMask.size() && grid.validMask[i]) {
validValues.append(grid.values[i]);
}
}
if (validValues.isEmpty()) {
grid.displayMin = 0.0f;
grid.displayMax = 1.0f;
return;
}
grid.displayMin = percentile(validValues, lowPercent);
grid.displayMax = percentile(validValues, highPercent);
if (std::abs(grid.displayMax - grid.displayMin) < 1e-6f) {
grid.displayMax = grid.displayMin + 1.0f;
}
}
} // namespace
CScanGridResult CScanGridder::build(const QVector<SurveyLine> &surveyLines, CScanGridOptions options)
{
CScanGridResult result;
options.cellSizeM = std::max(0.005, options.cellSizeM);
options.searchRadiusM = std::max(options.cellSizeM, options.searchRadiusM);
options.idwPower = std::max(0.1, options.idwPower);
options.smoothingSigmaCells = std::clamp(options.smoothingSigmaCells, 0.1, 5.0);
if (options.clipLowPercent >= options.clipHighPercent) {
options.clipLowPercent = 2.0;
options.clipHighPercent = 98.0;
}
options.maxGridWidth = std::max(64, options.maxGridWidth);
options.maxGridHeight = std::max(64, options.maxGridHeight);
options.maxGridCells = std::max(4096, options.maxGridCells);
QVector<CScanSamplePoint> samples;
double minX = std::numeric_limits<double>::max();
double minY = std::numeric_limits<double>::max();
double maxX = std::numeric_limits<double>::lowest();
double maxY = std::numeric_limits<double>::lowest();
for (int li = 0; li < surveyLines.size(); ++li) {
const SurveyLine &line = surveyLines[li];
if (!line.hasValidTrajectories()) continue;
const GPRDataModel &data = line.data;
const int channelCount = std::min(static_cast<int>(line.channelTrajectories.size()), data.getChannelCount());
const int traceCount = data.getTraceCountPerChannel();
const int sampleCount = data.getSampleCount();
if (channelCount <= 0 || traceCount <= 0 || sampleCount <= 0) continue;
const int sampleIndex = qBound(0, options.sampleIndex, sampleCount - 1);
for (int c = 0; c < channelCount; ++c) {
const QVector<QVector3D> &traj = line.channelTrajectories[c];
const int n = std::min(traceCount, static_cast<int>(traj.size()));
for (int t = 0; t < n; ++t) {
const RadarTrace *trace = data.getTrace(c, t);
if (!trace || sampleIndex >= trace->amplitudes.size()) continue;
const QVector3D &pos = traj[t];
const float amp = static_cast<float>(trace->amplitudes[sampleIndex]);
// Grid X uses CGCS easting (Y), grid Y uses CGCS northing (X),
// so the generated C-scan image aligns with map screen axes.
samples.append({pos.y(), pos.x(), amp, li, c, t});
minX = std::min(minX, static_cast<double>(pos.y()));
minY = std::min(minY, static_cast<double>(pos.x()));
maxX = std::max(maxX, static_cast<double>(pos.y()));
maxY = std::max(maxY, static_cast<double>(pos.x()));
}
}
}
if (samples.isEmpty() || maxX <= minX || maxY <= minY) {
return result;
}
result.dataMinX = minX;
result.dataMinY = minY;
result.dataMaxX = maxX;
result.dataMaxY = maxY;
minX -= options.searchRadiusM;
minY -= options.searchRadiusM;
maxX += options.searchRadiusM;
maxY += options.searchRadiusM;
double cellSize = options.cellSizeM;
int width = static_cast<int>(std::ceil((maxX - minX) / cellSize)) + 1;
int height = static_cast<int>(std::ceil((maxY - minY) / cellSize)) + 1;
while ((width > options.maxGridWidth || height > options.maxGridHeight
|| static_cast<qint64>(width) * height > options.maxGridCells) && cellSize < 10.0) {
cellSize *= 1.5;
width = static_cast<int>(std::ceil((maxX - minX) / cellSize)) + 1;
height = static_cast<int>(std::ceil((maxY - minY) / cellSize)) + 1;
}
if (width <= 0 || height <= 0 || width > options.maxGridWidth || height > options.maxGridHeight
|| static_cast<qint64>(width) * height > options.maxGridCells) {
return result;
}
result.valid = true;
result.originX = minX;
result.originY = minY;
result.cellSizeM = cellSize;
result.width = width;
result.height = height;
const int cellCount = width * height;
result.values.fill(0.0f, cellCount);
result.validMask.fill(0, cellCount);
result.nearestLine.fill(-1, cellCount);
result.nearestChannel.fill(-1, cellCount);
result.nearestTrace.fill(-1, cellCount);
const double binSize = options.searchRadiusM;
QHash<BinKey, QVector<int>> bins;
bins.reserve(samples.size());
for (int i = 0; i < samples.size(); ++i) {
const CScanSamplePoint &p = samples[i];
const BinKey key{static_cast<int>(std::floor((p.x - minX) / binSize)),
static_cast<int>(std::floor((p.y - minY) / binSize))};
bins[key].append(i);
}
const double radiusSq = options.searchRadiusM * options.searchRadiusM;
const int searchBins = std::max(1, static_cast<int>(std::ceil(options.searchRadiusM / binSize)));
for (int y = 0; y < height; ++y) {
const double cy = minY + y * cellSize;
const int by = static_cast<int>(std::floor((cy - minY) / binSize));
for (int x = 0; x < width; ++x) {
const double cx = minX + x * cellSize;
const int bx = static_cast<int>(std::floor((cx - minX) / binSize));
double sum = 0.0;
double weightSum = 0.0;
double bestDistSq = std::numeric_limits<double>::max();
int bestIndex = -1;
bool exact = false;
float exactValue = 0.0f;
for (int oy = -searchBins; oy <= searchBins; ++oy) {
for (int ox = -searchBins; ox <= searchBins; ++ox) {
const BinKey key{bx + ox, by + oy};
auto it = bins.constFind(key);
if (it == bins.constEnd()) continue;
for (int sampleIdx : it.value()) {
const CScanSamplePoint &p = samples[sampleIdx];
const double d2 = distanceSquared(cx, cy, p.x, p.y);
if (d2 > radiusSq) continue;
if (d2 < bestDistSq) {
bestDistSq = d2;
bestIndex = sampleIdx;
}
if (d2 < 1e-8) {
exact = true;
exactValue = p.amplitude;
bestIndex = sampleIdx;
break;
}
const double w = 1.0 / std::pow(std::sqrt(d2), options.idwPower);
sum += p.amplitude * w;
weightSum += w;
}
if (exact) break;
}
if (exact) break;
}
const int idx = gridIndex(x, y, width);
if (exact || weightSum > 0.0) {
result.values[idx] = exact ? exactValue : static_cast<float>(sum / weightSum);
result.validMask[idx] = 1;
if (bestIndex >= 0) {
const CScanSamplePoint &p = samples[bestIndex];
result.nearestLine[idx] = p.line;
result.nearestChannel[idx] = p.channel;
result.nearestTrace[idx] = p.trace;
}
}
}
}
if (options.smoothingEnabled) {
smoothMasked(result, options.smoothingSigmaCells);
}
updateDisplayRange(result, options.clipLowPercent, options.clipHighPercent);
return result;
}

48
external/gpr3dviewer/CScanGridder.h vendored Normal file
View File

@ -0,0 +1,48 @@
#ifndef CSCANGRIDDER_H
#define CSCANGRIDDER_H
#include "GPRDataModel.h"
#include <QVector>
struct CScanGridOptions {
int sampleIndex = 0;
double cellSizeM = 0.05;
double searchRadiusM = 0.50;
double idwPower = 2.0;
bool smoothingEnabled = true;
double smoothingSigmaCells = 0.9;
int maxGridWidth = 2048;
int maxGridHeight = 2048;
int maxGridCells = 250000;
double clipLowPercent = 2.0;
double clipHighPercent = 98.0;
};
struct CScanGridResult {
bool valid = false;
double originX = 0.0;
double originY = 0.0;
double dataMinX = 0.0;
double dataMinY = 0.0;
double dataMaxX = 0.0;
double dataMaxY = 0.0;
double cellSizeM = 0.05;
int width = 0;
int height = 0;
QVector<float> values;
QVector<unsigned char> validMask;
QVector<int> nearestLine;
QVector<int> nearestChannel;
QVector<int> nearestTrace;
float displayMin = 0.0f;
float displayMax = 1.0f;
};
class CScanGridder
{
public:
static CScanGridResult build(const QVector<SurveyLine> &surveyLines, CScanGridOptions options);
};
#endif // CSCANGRIDDER_H

View File

@ -0,0 +1,219 @@
#include "CoordinateTransform.h"
#include <cmath>
// 静态辅助:预计算的子午线弧长系数
static inline double computeA0(double e2) {
return 1.0 + 3.0/4.0 * e2 + 45.0/64.0 * e2*e2 + 175.0/256.0 * e2*e2*e2
+ 11025.0/16384.0 * e2*e2*e2*e2;
}
static inline double computeA2(double e2) {
return 3.0/4.0 * e2 + 15.0/16.0 * e2*e2 + 525.0/512.0 * e2*e2*e2
+ 2205.0/2048.0 * e2*e2*e2*e2;
}
static inline double computeA4(double e2) {
return 15.0/64.0 * e2*e2 + 105.0/256.0 * e2*e2*e2
+ 2205.0/4096.0 * e2*e2*e2*e2;
}
static inline double computeA6(double e2) {
return 35.0/512.0 * e2*e2*e2 + 315.0/2048.0 * e2*e2*e2*e2;
}
static inline double computeA8(double e2) {
return 315.0/16384.0 * e2*e2*e2*e2;
}
int CoordinateTransform::detectZoneFromCgcsY(double cgcsY)
{
// 中国范围内 CGCS2000 坐标 Y 通常为正值且包含带号
// 格式zone * 1,000,000 + 500,000 + easting
double absY = std::abs(cgcsY);
if (absY > 1e6) {
int zone = static_cast<int>(std::floor(absY / 1e6));
if (zone >= 1 && zone <= 60)
return zone;
}
// 如果无法推断,返回 0表示需要手动指定
return 0;
}
double CoordinateTransform::centralMeridianFromZone(int zone)
{
// 3度带中央经线 = 带号 * 3°
return zone * 3.0;
}
double CoordinateTransform::meridianArc(double latRad)
{
const double e2 = E2;
const double a = A;
const double A0 = computeA0(e2);
const double A2 = computeA2(e2);
const double A4 = computeA4(e2);
const double A6 = computeA6(e2);
const double A8 = computeA8(e2);
double s2 = std::sin(2.0 * latRad);
double s4 = std::sin(4.0 * latRad);
double s6 = std::sin(6.0 * latRad);
double s8 = std::sin(8.0 * latRad);
return a * (1.0 - e2) * (A0 * latRad - A2/2.0 * s2 + A4/4.0 * s4
- A6/6.0 * s6 + A8/8.0 * s8);
}
double CoordinateTransform::meridianArcInverse(double x)
{
const double e2 = E2;
const double a = A;
const double A0 = computeA0(e2);
const double A2 = computeA2(e2);
const double A4 = computeA4(e2);
const double A6 = computeA6(e2);
const double A8 = computeA8(e2);
const double c0 = a * (1.0 - e2) * A0;
double bf = x / c0;
for (int i = 0; i < 8; ++i) {
double s2 = std::sin(2.0 * bf);
double s4 = std::sin(4.0 * bf);
double s6 = std::sin(6.0 * bf);
double s8 = std::sin(8.0 * bf);
double fx = a * (1.0 - e2) * (A0 * bf - A2/2.0 * s2 + A4/4.0 * s4
- A6/6.0 * s6 + A8/8.0 * s8) - x;
double fpx = a * (1.0 - e2) * (A0 - A2 * std::cos(2.0 * bf)
+ A4 * std::cos(4.0 * bf)
- A6 * std::cos(6.0 * bf)
+ A8 * std::cos(8.0 * bf));
double delta = fx / fpx;
bf -= delta;
if (std::abs(delta) < 1e-12)
break;
}
return bf;
}
void CoordinateTransform::cgcs2000ToWgs84(double cgcsX, double cgcsY, int zone,
double &latRad, double &lonRad)
{
const double a = A;
const double e2 = E2;
const double e12 = E12;
// 提取东偏移(去掉带号和 500km 常数)
double easting = cgcsY;
if (zone > 0) {
easting = cgcsY - zone * 1e6 - FALSE_EASTING;
} else {
easting = cgcsY - FALSE_EASTING;
}
double northing = cgcsX;
double l0 = centralMeridianFromZone(zone) * DEG2RAD;
// 底点纬度
double bf = meridianArcInverse(northing);
double sinBf = std::sin(bf);
double cosBf = std::cos(bf);
double tanBf = std::tan(bf);
double Nf = a / std::sqrt(1.0 - e2 * sinBf * sinBf);
double etaf2 = e12 * cosBf * cosBf;
double tf = tanBf;
double Vf2 = 1.0 + etaf2;
double y = easting;
double y2 = y * y;
double y4 = y2 * y2;
double y6 = y4 * y2;
double Nf2 = Nf * Nf;
double Nf4 = Nf2 * Nf2;
double Nf6 = Nf4 * Nf2;
// 纬度修正
double dB = tf / Vf2 * (
y2 / (2.0 * Nf2) * (1.0 + etaf2)
- y4 / (24.0 * Nf4) * (5.0 + 3.0 * tf*tf + 6.0 * etaf2
- 6.0 * tf*tf * etaf2
- 3.0 * etaf2*etaf2
- 9.0 * tf*tf * etaf2*etaf2)
+ y6 / (720.0 * Nf6) * (61.0 + 90.0 * tf*tf + 45.0 * tf*tf*tf*tf)
);
latRad = bf - dB;
// 经差
double dl = y / (Nf * cosBf) * (
1.0
- y2 / (6.0 * Nf2) * (1.0 + 2.0 * tf*tf + etaf2)
+ y4 / (120.0 * Nf4) * (5.0 + 28.0 * tf*tf + 24.0 * tf*tf*tf*tf
+ 6.0 * etaf2 + 8.0 * tf*tf * etaf2)
- y6 / (5040.0 * Nf6) * (61.0 + 662.0 * tf*tf + 1320.0 * tf*tf*tf*tf
+ 720.0 * tf*tf*tf*tf*tf*tf)
);
lonRad = l0 + dl;
}
void CoordinateTransform::wgs84ToCgcs2000(double latRad, double lonRad, int zone,
double &cgcsX, double &cgcsY)
{
const double a = A;
const double e2 = E2;
const double e12 = E12;
double l0 = centralMeridianFromZone(zone) * DEG2RAD;
double l = lonRad - l0;
double sinB = std::sin(latRad);
double cosB = std::cos(latRad);
double tanB = std::tan(latRad);
double N = a / std::sqrt(1.0 - e2 * sinB * sinB);
double t2 = tanB * tanB;
double t4 = t2 * t2;
double eta2 = e12 * cosB * cosB;
double eta4 = eta2 * eta2;
double l2 = l * l;
double l4 = l2 * l2;
double l6 = l4 * l2;
// 子午线弧长
double X = meridianArc(latRad);
// 北坐标
cgcsX = X + N / 2.0 * sinB * cosB * l2 * (
1.0 + l2 / 12.0 * (5.0 - t2 + 9.0 * eta2 + 4.0 * eta4)
+ l4 / 360.0 * (61.0 - 58.0 * t2 + t4)
);
// 东坐标(自然值,不含带号和 500km
double yNatural = N * cosB * l * (
1.0 + l2 / 6.0 * (1.0 - t2 + eta2)
+ l4 / 120.0 * (5.0 - 18.0 * t2 + t4 + 14.0 * eta2 - 58.0 * t2 * eta2)
);
// 加上带号和 500km 常数
if (zone > 0) {
cgcsY = zone * 1e6 + FALSE_EASTING + yNatural;
} else {
cgcsY = FALSE_EASTING + yNatural;
}
}
void CoordinateTransform::wgs84ToWebMercator(double latRad, double lonRad, double &mx, double &my)
{
mx = EARTH_RADIUS * lonRad;
my = EARTH_RADIUS * std::log(std::tan(PI / 4.0 + latRad / 2.0));
}
void CoordinateTransform::webMercatorToWgs84(double mx, double my, double &latRad, double &lonRad)
{
lonRad = mx / EARTH_RADIUS;
latRad = 2.0 * std::atan(std::exp(my / EARTH_RADIUS)) - PI / 2.0;
}

View File

@ -0,0 +1,48 @@
#ifndef COORDINATETRANSFORM_H
#define COORDINATETRANSFORM_H
#include <cmath>
class CoordinateTransform {
public:
// CGCS2000 椭球参数
static constexpr double A = 6378137.0; // 长半轴 (m)
static constexpr double F = 1.0 / 298.257222101; // 扁率
static constexpr double E2 = 2.0 * F - F * F; // 第一偏心率平方
static constexpr double E12 = E2 / (1.0 - E2); // 第二偏心率平方
static constexpr double FALSE_EASTING = 500000.0; // 东偏移 (m)
static constexpr double EARTH_RADIUS = 6378137.0; // Web Mercator 地球半径
static constexpr double PI = 3.14159265358979323846;
static constexpr double DEG2RAD = PI / 180.0;
static constexpr double RAD2DEG = 180.0 / PI;
// 从 CGCS2000 东向坐标(Y)自动推断 3° 带带号
// 假设 Y 格式为zone*1e6 + 500000 + easting或纯自然值
static int detectZoneFromCgcsY(double cgcsY);
// 带号 → 中央经线(度)
static double centralMeridianFromZone(int zone);
// CGCS2000 平面坐标 → WGS84 经纬度(弧度)
// cgcsX: 北向坐标(m), cgcsY: 东向坐标(m, 可含带号)
static void cgcs2000ToWgs84(double cgcsX, double cgcsY, int zone,
double &latRad, double &lonRad);
// WGS84 经纬度(弧度) → CGCS2000 平面坐标
static void wgs84ToCgcs2000(double latRad, double lonRad, int zone,
double &cgcsX, double &cgcsY);
// WGS84 经纬度(弧度) ↔ Web Mercator 平面坐标(米)
static void wgs84ToWebMercator(double latRad, double lonRad, double &mx, double &my);
static void webMercatorToWgs84(double mx, double my, double &latRad, double &lonRad);
private:
// 子午线弧长(从赤道到纬度 B 的弧长)
static double meridianArc(double latRad);
// 子午线弧长反解:已知弧长 X求底点纬度 Bf
static double meridianArcInverse(double x);
};
#endif // COORDINATETRANSFORM_H

73
external/gpr3dviewer/PosParser.cpp vendored Normal file
View File

@ -0,0 +1,73 @@
#include "PosParser.h"
#include <QFile>
#include <QTextStream>
#include <QDebug>
#include <QRegularExpression>
bool PosParser::loadPosFile(const QString &posFilePath, QVector<QVector3D> &outPositions) {
outPositions.clear();
QFile file(posFilePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "PosParser: Cannot open pos file:" << posFilePath;
return false;
}
QTextStream in(&file);
QRegularExpression reLineComment("^\\s*%");
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty()) continue;
if (reLineComment.match(line).hasMatch()) continue; // 跳过注释行
QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() < 4) continue;
bool okX = false, okY = false, okZ = false;
// 列顺序: 道号(忽略) X Y Z
double x = parts[1].toDouble(&okX);
double y = parts[2].toDouble(&okY);
double z = parts[3].toDouble(&okZ);
if (okX && okY && okZ) {
outPositions.append(QVector3D(static_cast<float>(x), static_cast<float>(y), static_cast<float>(z)));
}
}
file.close();
qDebug() << "PosParser: Loaded" << outPositions.size() << "GPS points from" << posFilePath;
return !outPositions.isEmpty();
}
bool PosParser::loadCenterCcc(const QString &cccFilePath, double &outLat, double &outLon) {
QFile file(cccFilePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "PosParser: Cannot open center.ccc:" << cccFilePath;
return false;
}
QTextStream in(&file);
QRegularExpression reLineComment("^\\s*%");
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty()) continue;
if (reLineComment.match(line).hasMatch()) continue;
QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() < 2) continue;
bool okLat = false, okLon = false;
outLat = parts[0].toDouble(&okLat);
outLon = parts[1].toDouble(&okLon);
if (okLat && okLon) {
file.close();
return true;
}
}
file.close();
return false;
}

17
external/gpr3dviewer/PosParser.h vendored Normal file
View File

@ -0,0 +1,17 @@
#ifndef POSPARSER_H
#define POSPARSER_H
#include <QString>
#include <QVector>
#include <QVector3D>
class PosParser {
public:
// 加载 .pos 文件4列道号 X Y Z
static bool loadPosFile(const QString &posFilePath, QVector<QVector3D> &outPositions);
// 加载 center.ccc 项目中心坐标2列纬度 经度)
static bool loadCenterCcc(const QString &cccFilePath, double &outLat, double &outLon);
};
#endif // POSPARSER_H

View File

@ -0,0 +1,242 @@
#include "TrajectoryCalculator.h"
#include <algorithm>
#include <cmath>
namespace {
double planarDistance(const QVector3D &a, const QVector3D &b)
{
const double dx = static_cast<double>(a.x()) - b.x();
const double dy = static_cast<double>(a.y()) - b.y();
return std::sqrt(dx * dx + dy * dy);
}
QVector3D lerpPoint(const QVector3D &a, const QVector3D &b, double t)
{
return a * static_cast<float>(1.0 - t) + b * static_cast<float>(t);
}
QVector3D catmullRom(const QVector3D &p0, const QVector3D &p1, const QVector3D &p2, const QVector3D &p3, double t)
{
const float tf = static_cast<float>(t);
const float t2 = tf * tf;
const float t3 = t2 * tf;
return 0.5f * ((2.0f * p1) + (-p0 + p2) * tf + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 +
(-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3);
}
} // namespace
double TrajectoryCalculator::headingAt(int traceIdx, const QVector<QVector3D> &gpsPositions)
{
const int n = gpsPositions.size();
if (n <= 1) return 0.0;
if (traceIdx < n - 1) {
double deltaX = gpsPositions[traceIdx + 1].x() - gpsPositions[traceIdx].x(); // 北向变化
double deltaY = gpsPositions[traceIdx + 1].y() - gpsPositions[traceIdx].y(); // 东向变化
return std::atan2(deltaY, deltaX);
} else {
// 最后一点复用前一点方向
double deltaX = gpsPositions[traceIdx].x() - gpsPositions[traceIdx - 1].x();
double deltaY = gpsPositions[traceIdx].y() - gpsPositions[traceIdx - 1].y();
return std::atan2(deltaY, deltaX);
}
}
bool TrajectoryCalculator::computeTrajectories(SurveyLine &line, const SurveyGeometry &geom)
{
const QVector<QVector3D> &gps = line.gpsPositions;
const int nRtk = gps.size();
if (nRtk <= 0) return false;
const int chCount = geom.channelCount;
if (chCount <= 0) return false;
// 通道相对x偏移优先使用头文件CH_X_OFFSETS推导出的逐通道数组旧工程无数组时按首通道对称补齐。
QVector<double> chXRel = geom.channelXRel;
if (chXRel.size() != chCount) {
chXRel.resize(chCount);
const double lastXRel = -geom.ch1XRel;
for (int c = 0; c < chCount; ++c) {
chXRel[c] = (chCount > 1)
? geom.ch1XRel + (lastXRel - geom.ch1XRel) * c / (chCount - 1.0)
: geom.ch1XRel;
}
}
// 预分配 channelTrajectories
line.channelTrajectories.resize(chCount);
for (int c = 0; c < chCount; ++c) {
line.channelTrajectories[c].resize(nRtk);
}
// 遍历每个 RTK 点
for (int i = 0; i < nRtk; ++i) {
double thetaY = headingAt(i, gps);
double thetaX = thetaY + M_PI / 2.0;
// 旋转矩阵 R = [cos(theta_x), cos(theta_y); sin(theta_x), sin(theta_y)]
// 作用将设备相对坐标系x=右, y=前转换为绝对坐标系X=北, Y=东)
double r11 = std::cos(thetaX);
double r12 = std::cos(thetaY);
double r21 = std::sin(thetaX);
double r22 = std::sin(thetaY);
// RTK 相对天线中心的偏移向量(设备坐标系)
double rtkLocalX = geom.rtkOffsetX; // 横向偏移
double rtkLocalY = geom.rtkOffsetY; // 纵向偏移(前方为正)
// 转换为绝对坐标系偏移
double rtkAbsOffsetX = r11 * rtkLocalX + r12 * rtkLocalY;
double rtkAbsOffsetY = r21 * rtkLocalX + r22 * rtkLocalY;
// 天线中心绝对坐标 = RTK - 偏移量
double antX = gps[i].x() - rtkAbsOffsetX;
double antY = gps[i].y() - rtkAbsOffsetY;
double antZ = gps[i].z();
// 遍历每个通道
for (int c = 0; c < chCount; ++c) {
// 通道相对坐标(设备坐标系)
double chLocalX = chXRel[c];
double chLocalY = 0.0; // MATLAB 中 ch_y_rel = zeros(...)
// 转换为绝对坐标系偏移
double chAbsOffsetX = r11 * chLocalX + r12 * chLocalY;
double chAbsOffsetY = r21 * chLocalX + r22 * chLocalY;
// 通道绝对坐标
double chX = antX + chAbsOffsetX;
double chY = antY + chAbsOffsetY;
double chZ = antZ;
line.channelTrajectories[c][i] = QVector3D(static_cast<float>(chX),
static_cast<float>(chY),
static_cast<float>(chZ));
}
}
// 同步更新 GPRDataModel 中 traces 的 position
// 数据存储顺序trace0_ch0, trace0_ch1, ... trace0_chN, trace1_ch0, ...
// 即 globalIdx = traceInChannel * chCount + channel
const int tracesPerChannel = line.data.getTracesPerChannel();
for (int c = 0; c < chCount; ++c) {
for (int t = 0; t < tracesPerChannel && t < nRtk; ++t) {
int globalIdx = t * chCount + c;
if (globalIdx >= 0 && globalIdx < line.data.traces.size()) {
line.data.traces[globalIdx].position = line.channelTrajectories[c][t];
}
}
}
return true;
}
QVector<QVector3D> TrajectoryCalculator::resampleTrajectoryLinear(const QVector<QVector3D> &input, int targetCount)
{
QVector<QVector3D> output;
if (targetCount <= 0 || input.isEmpty()) return output;
if (input.size() == 1 || targetCount == 1) {
output.resize(targetCount);
std::fill(output.begin(), output.end(), input.first());
return output;
}
output.reserve(targetCount);
const double scale = static_cast<double>(input.size() - 1) / qMax(1, targetCount - 1);
for (int i = 0; i < targetCount; ++i) {
const double src = i * scale;
const int i0 = qBound(0, static_cast<int>(std::floor(src)), input.size() - 1);
const int i1 = qBound(0, i0 + 1, input.size() - 1);
output.append(lerpPoint(input[i0], input[i1], src - i0));
}
return output;
}
QVector<QVector3D> TrajectoryCalculator::resampleTrajectorySpline(const QVector<QVector3D> &input, int targetCount)
{
if (input.size() < 4) return resampleTrajectoryLinear(input, targetCount);
QVector<QVector3D> output;
if (targetCount <= 0) return output;
if (targetCount == 1) return QVector<QVector3D>{input.first()};
output.reserve(targetCount);
const double scale = static_cast<double>(input.size() - 1) / qMax(1, targetCount - 1);
for (int i = 0; i < targetCount; ++i) {
const double src = i * scale;
const int i1 = qBound(0, static_cast<int>(std::floor(src)), input.size() - 1);
const int i2 = qBound(0, i1 + 1, input.size() - 1);
const int i0 = qBound(0, i1 - 1, input.size() - 1);
const int i3 = qBound(0, i2 + 1, input.size() - 1);
output.append(catmullRom(input[i0], input[i1], input[i2], input[i3], src - i1));
}
return output;
}
TrajectoryFilterResult TrajectoryCalculator::filterAndInterpolateTrajectory(const QVector<QVector3D> &input,
const TrajectoryFilterOptions &options,
int targetCount)
{
TrajectoryFilterResult result;
const int n = input.size();
result.keepMask = QVector<bool>(n, true);
result.distanceOutlierMask = QVector<bool>(n, false);
result.speedOutlierMask = QVector<bool>(n, false);
result.angleOutlierMask = QVector<bool>(n, false);
if (n <= 0 || targetCount <= 0) return result;
for (int i = 1; i < n; ++i) {
const double dist = planarDistance(input[i - 1], input[i]);
if (options.distanceFilterEnabled && dist > options.maxSegmentDistanceM) {
result.distanceOutlierMask[i] = true;
result.keepMask[i] = false;
}
if (options.speedFilterEnabled && options.traceIntervalSec > 1e-9 && dist / options.traceIntervalSec > options.maxSpeedMps) {
result.speedOutlierMask[i] = true;
result.keepMask[i] = false;
}
}
if (options.angleFilterEnabled) {
const double thresholdRad = options.maxTurnAngleDeg * M_PI / 180.0;
for (int i = 1; i + 1 < n; ++i) {
const double d0 = planarDistance(input[i - 1], input[i]);
const double d1 = planarDistance(input[i], input[i + 1]);
if (d0 < 1e-6 || d1 < 1e-6) continue;
const double ax = input[i].x() - input[i - 1].x();
const double ay = input[i].y() - input[i - 1].y();
const double bx = input[i + 1].x() - input[i].x();
const double by = input[i + 1].y() - input[i].y();
const double cosv = std::clamp((ax * bx + ay * by) / (d0 * d1), -1.0, 1.0);
const double turn = std::acos(cosv);
if (turn > thresholdRad) {
result.angleOutlierMask[i] = true;
result.keepMask[i] = false;
}
}
}
if (options.preserveEndpoints && n > 0) {
result.keepMask[0] = true;
result.keepMask[n - 1] = true;
}
QVector<QVector3D> kept;
kept.reserve(n);
for (int i = 0; i < n; ++i) {
if (result.keepMask.value(i, true)) kept.append(input[i]);
}
if (kept.size() < 2) {
kept = input;
result.warnings.append(QStringLiteral("过滤后有效轨迹点过少,已使用原始轨迹插值。"));
}
if (options.interpolationMode == TrajectoryFilterOptions::InterpolationMode::Spline) {
if (kept.size() < 4) result.warnings.append(QStringLiteral("样条插值点数不足,已回退到线性插值。"));
result.outputPositions = resampleTrajectorySpline(kept, targetCount);
} else {
result.outputPositions = resampleTrajectoryLinear(kept, targetCount);
}
return result;
}

View File

@ -0,0 +1,49 @@
#ifndef TRAJECTORYCALCULATOR_H
#define TRAJECTORYCALCULATOR_H
#include "GPRDataModel.h"
#include "SurveyGeometry.h"
#include <QVector3D>
#include <QStringList>
struct TrajectoryFilterOptions {
bool distanceFilterEnabled = true;
double maxSegmentDistanceM = 1.0;
bool speedFilterEnabled = false;
double maxSpeedMps = 60.0;
double traceIntervalSec = 0.05;
bool angleFilterEnabled = true;
double maxTurnAngleDeg = 60.0;
enum class InterpolationMode { Linear, Spline };
InterpolationMode interpolationMode = InterpolationMode::Linear;
bool preserveEndpoints = true;
bool preserveManualEdits = true;
};
struct TrajectoryFilterResult {
QVector<QVector3D> outputPositions;
QVector<bool> keepMask;
QVector<bool> distanceOutlierMask;
QVector<bool> speedOutlierMask;
QVector<bool> angleOutlierMask;
QStringList warnings;
};
class TrajectoryCalculator {
public:
// 计算第 traceIdx 个 RTK 点的行进方向角(弧度)
// 返回 theta_y = atan2(dY, dX),其中 X=北向, Y=东向
static double headingAt(int traceIdx, const QVector<QVector3D> &gpsPositions);
// 根据 RTK 轨迹和天线几何参数,计算所有通道的绝对坐标
// 结果写入 line.channelTrajectories 和 line.data.traces[].position
static bool computeTrajectories(SurveyLine &line, const SurveyGeometry &geom);
static QVector<QVector3D> resampleTrajectoryLinear(const QVector<QVector3D> &input, int targetCount);
static QVector<QVector3D> resampleTrajectorySpline(const QVector<QVector3D> &input, int targetCount);
static TrajectoryFilterResult filterAndInterpolateTrajectory(const QVector<QVector3D> &input,
const TrajectoryFilterOptions &options,
int targetCount);
};
#endif // TRAJECTORYCALCULATOR_H

View File

@ -9,11 +9,17 @@ target_link_libraries(geopro_io_gpr PUBLIC geopro_core)
# C++17 Qt/VTK AUTOMOC/UIC/RCC
set_target_properties(geopro_io_gpr PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
# gpr3dv 桥接层(P2) vendored gpr3dv(P1) geopro 量化体(BuiltI16)
# gpr3dv 桥接层(P2/P8) vendored gpr3dv geopro 量化体(BuiltI16)
# target(不污染上面的纯 C++17 解析层) geopro_gpr3dv(Qt)+geopro_core
add_library(geopro_gpr3dv_bridge STATIC Gpr3dvVolumeBridge.cpp)
# Gpr3dvVolumeBridge P2 线局部坐标体(X=道/Y=通道/Z=样本origin≈0)
# Gpr3dvSurveyVolumeBridge P8 CGCS2000 世界对齐体(逐道跟 GPS 轨迹)
# GpsTrack(geopro_io_gpr) .gps
add_library(geopro_gpr3dv_bridge STATIC
Gpr3dvVolumeBridge.cpp
Gpr3dvSurveyVolumeBridge.cpp)
target_include_directories(geopro_gpr3dv_bridge PUBLIC ${CMAKE_SOURCE_DIR}/src)
target_compile_features(geopro_gpr3dv_bridge PUBLIC cxx_std_17)
target_link_libraries(geopro_gpr3dv_bridge PUBLIC geopro_core geopro_gpr3dv)
target_link_libraries(geopro_gpr3dv_bridge PUBLIC
geopro_core geopro_gpr3dv geopro_io_gpr)
set_target_properties(geopro_gpr3dv_bridge PROPERTIES
AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)

View File

@ -0,0 +1,270 @@
#include "io/gpr/Gpr3dvSurveyVolumeBridge.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <QString>
#include <QVector>
#include <QVector3D>
#include "CScanGridder.h"
#include "CoordinateTransform.h"
#include "GPRDataModel.h"
#include "IprhParser.h"
#include "RadarProcessor.h"
#include "SurveyGeometry.h"
#include "TrajectoryCalculator.h"
#include "core/model/ScalarVolumeI16.hpp"
#include "io/gpr/GpsTrack.hpp"
namespace geopro::io::gpr {
namespace {
// 全体 traces 平均绝对幅值——「处理是否生效」标量证据(与 P2 桥接同口径)。
double meanAbsAmplitude(const GPRDataModel& model) {
long double sum = 0.0L;
long long count = 0;
for (const RadarTrace& tr : model.traces) {
for (short v : tr.amplitudes) {
sum += static_cast<long double>(v < 0 ? -v : v);
++count;
}
}
return count > 0 ? static_cast<double>(sum / count) : 0.0;
}
// 深度采样间距(米)(timeWindow/samples) ns × 波速(m/ns) / 2(往返)。
// 与 GPRDataModel::calculateDepth 同式。取不到 → 1.0(单位间距)。
double depthSpacingZ(const GPRDataModel::Header& h) {
if (h.samplesPerTrace <= 0 || h.timeWindowNs <= 0.0) return 1.0;
const double timePerSample = h.timeWindowNs / h.samplesPerTrace; // ns
const double vel = h.waveVelocity > 0.0 ? h.waveVelocity : 0.1; // m/ns
const double dz = timePerSample * vel / 2.0;
return dz > 1e-12 ? dz : 1.0;
}
double nowMs(std::chrono::steady_clock::time_point t0) {
const auto t1 = std::chrono::steady_clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}
constexpr double kDeg2Rad = 3.14159265358979323846 / 180.0;
} // namespace
geopro::core::BuiltI16 buildLineVolumeSurvey(const std::string& lineDir,
const std::string& linePrefix,
const std::string& gpsPath,
SurveyBridgeMetrics* metricsOut,
int coarse, double cellSizeM,
double searchRadiusM) {
const int stride = coarse > 1 ? coarse : 1; // 深度下采样步长(≥1)
const QString dir = QString::fromLocal8Bit(lineDir.c_str());
const QString base = QString::fromLocal8Bit(linePrefix.c_str());
// 1) 走 P1 链load → buildVolumeData(处理前) → runPipeline → 再 buildVolumeData。
const auto tLoad = std::chrono::steady_clock::now();
GPRDataModel model;
if (!IprhParser::loadImpulseMultiChannel(dir, base, model)) {
throw std::runtime_error("loadImpulseMultiChannel 失败: " + lineDir + " / " +
linePrefix);
}
model.buildVolumeData();
const double meanBefore = meanAbsAmplitude(model);
const double loadMs = nowMs(tLoad);
const auto tPipe = std::chrono::steady_clock::now();
RadarProcessor::ProcPipeline pipeline;
pipeline.setDefaultFlow();
GPRDataModel processed = RadarProcessor::runPipeline(model, pipeline);
// 零时校正会改 samplesPerTrace且 runPipeline 不重建 volumeData → 必须再建一次。
processed.buildVolumeData();
const double meanAfter = meanAbsAmplitude(processed);
const double pipelineMs = nowMs(tPipe);
// 2) 几何 + GPS→CGCS2000 世界坐标(逐 RTK 点)。
const auto tTraj = std::chrono::steady_clock::now();
// 值拷贝(非引用):下面 processed 会被 std::move 进 SurveyLine之后仍要用 header 算 dz
// 引用会变悬垂(use-after-move)。Header 是小结构(几个 QVector/QString),拷贝代价可忽略。
const GPRDataModel::Header h = processed.header;
const int channels = h.numberOfChannels > 0 ? h.numberOfChannels : 1;
// SurveyGeometry逐通道天线横偏(归一到天线中心),来自头 CH_X/Y_OFFSETS。
SurveyGeometry geom =
SurveyGeometry::fromHeaderOffsets(channels, h.chXOffsets, h.chYOffsets);
// .gps 经纬(度) → CGCS2000 平面(北X, 东Y) 米。带号由首点经度自动定(3°带)。
const geopro::io::gpr::GpsTrack track = geopro::io::gpr::parseGps(gpsPath);
if (track.pts.size() < 2) {
throw std::runtime_error("GPS 轨迹点 <2(无法定位/航向): " + gpsPath);
}
const double lon0Deg = track.pts.front().lon;
// 3°带号 = round(中央经线/3);中央经线最近 3° 倍数。
const int zone = static_cast<int>(std::lround(lon0Deg / 3.0));
const double centralMeridianDeg = CoordinateTransform::centralMeridianFromZone(zone);
geom.cgcsZone = zone;
geom.centralMeridianDeg = centralMeridianDeg;
QVector<QVector3D> gpsCgcs;
gpsCgcs.reserve(static_cast<int>(track.pts.size()));
for (const auto& p : track.pts) {
double cx = 0.0, cy = 0.0; // cx=北(northing), cy=东(easting含带号)
CoordinateTransform::wgs84ToCgcs2000(p.lat * kDeg2Rad, p.lon * kDeg2Rad,
zone, cx, cy);
// SurveyLine.gpsPositions 约定 x=北, y=东, z=高(与 TrajectoryCalculator 一致)。
gpsCgcs.append(QVector3D(static_cast<float>(cx), static_cast<float>(cy),
static_cast<float>(p.elev)));
}
SurveyLine line;
line.data = std::move(processed);
line.geometry = geom;
line.gpsPositions = gpsCgcs;
if (!TrajectoryCalculator::computeTrajectories(line, geom)) {
throw std::runtime_error("computeTrajectories 失败: " + linePrefix);
}
if (!line.hasValidTrajectories()) {
throw std::runtime_error("无有效通道轨迹: " + linePrefix);
}
const double trajMs = nowMs(tTraj);
const int samples = line.data.getSampleCount();
if (samples <= 0) {
throw std::runtime_error("处理后样本数为 0: " + linePrefix);
}
// 3) 逐深度 IDW 世界网格(CScanGridder)→ 堆成世界轴对齐体。
// 全部深度共享同一水平网格(dims 仅由轨迹 AABB + cellSize 定,与深度无关)
// 故用深度 0 的网格几何作体的世界框,逐深度填一层。
const auto tGrid = std::chrono::steady_clock::now();
const QVector<SurveyLine> lines{line};
CScanGridOptions baseOpts;
if (cellSizeM > 0.0) baseOpts.cellSizeM = cellSizeM;
if (searchRadiusM > 0.0) baseOpts.searchRadiusM = searchRadiusM;
// coarse水平格距 ×coarse 省空间(searchRadius 同步放大保证邻域覆盖)。
if (stride > 1) {
baseOpts.cellSizeM *= stride;
baseOpts.searchRadiusM =
std::max(baseOpts.searchRadiusM, baseOpts.cellSizeM * 2.0);
}
// 关闭逐层百分位裁剪对体值无副作用(displayMin/Max 仅显示用,不改 values)
// 平滑保留(与原版 C-scan 一致)。
// 输出深度采样数(coarse 沿深度下采样)nzOut = ceil(samples/stride)。
const int nzOut = (samples + stride - 1) / stride;
// 先建深度 0 网格锁定世界框。
CScanGridOptions opt0 = baseOpts;
opt0.sampleIndex = 0;
const CScanGridResult g0 = CScanGridder::build(lines, opt0);
if (!g0.valid || g0.width <= 0 || g0.height <= 0) {
throw std::runtime_error("CScanGridder 深度0 网格无效(轨迹/数据为空?): " +
linePrefix);
}
const int nx = g0.width; // 东向 easting 格数
const int ny = g0.height; // 北向 northing 格数
const int nz = nzOut; // 深度
const double cellSize = g0.cellSizeM;
const double originX = g0.originX; // CGCS easting(含带号) 米
const double originY = g0.originY; // CGCS northing 米
// 量化区间:先扫所有输出深度网格的有效值(命中 validMask)定 [vmin,vmax]。
// (CScanGridder values 为处理后幅值的 IDW 插值结果,连续浮点。)
double vmin = std::numeric_limits<double>::infinity();
double vmax = -std::numeric_limits<double>::infinity();
// 缓存每层网格(避免二次计算):网格层数 = nzOut每层 nx*ny float明星路约
// 数百×数千×数百 → 单线峰值可控(逐线建,建完即释放)。
std::vector<CScanGridResult> layers;
layers.reserve(static_cast<std::size_t>(nzOut));
std::int64_t filledCells = 0;
for (int zo = 0; zo < nzOut; ++zo) {
const int s = zo * stride;
CScanGridOptions opt = baseOpts;
opt.sampleIndex = std::min(s, samples - 1);
CScanGridResult g =
(zo == 0) ? g0 : CScanGridder::build(lines, opt);
// 守护:所有深度网格 dims 必须与深度0一致(理论恒等);不一致则该层置空跳过。
if (!g.valid || g.width != nx || g.height != ny) {
g.valid = false;
} else {
for (int i = 0; i < g.values.size() && i < g.validMask.size(); ++i) {
if (!g.validMask[i]) continue;
++filledCells;
const double v = static_cast<double>(g.values[i]);
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
}
}
layers.push_back(std::move(g));
}
if (!(vmin <= vmax)) { // 无任何有效值(理论不至)。
vmin = 0.0;
vmax = 0.0;
}
geopro::core::Quant quant;
quant.scale = (vmax > vmin) ? (vmax - vmin) / 64000.0 : 1.0;
quant.offset = 0.5 * (vmin + vmax); // 中点 → 防溢出
// 4) 填体:逐层逐 cell。空 cell(validMask=0)→ kBlank(渲染透明)。
// 轴 X=东(easting), Y=北(northing), Z=深度。
geopro::core::BuiltI16 built;
built.vol = geopro::core::ScalarVolumeI16(nx, ny, nz);
for (int zo = 0; zo < nzOut; ++zo) {
const CScanGridResult& g = layers[static_cast<std::size_t>(zo)];
for (int y = 0; y < ny; ++y) {
for (int x = 0; x < nx; ++x) {
const int gi = y * nx + x;
std::int16_t q = geopro::core::ScalarVolumeI16::kBlank;
if (g.valid && gi < g.values.size() && gi < g.validMask.size() &&
g.validMask[gi]) {
q = quant.toQ(static_cast<double>(g.values[gi]));
}
built.vol.at(x, y, zo) = q;
}
}
}
const double gridMs = nowMs(tGrid);
// 5) 几何origin=CGCS2000 世界米(东, 北, 0)spacing=(cell, cell, dz×stride)。
const double dz = depthSpacingZ(h) * stride;
built.quant = quant;
built.origin = {originX, originY, 0.0};
built.spacing = {cellSize, cellSize, dz};
built.vminPhys = vmin;
built.vmaxPhys = vmax;
if (metricsOut) {
metricsOut->nx = nx;
metricsOut->ny = ny;
metricsOut->nz = nz;
metricsOut->rtkPoints = static_cast<int>(track.pts.size());
metricsOut->channels = channels;
metricsOut->cgcsZone = zone;
metricsOut->centralMeridianDeg = centralMeridianDeg;
metricsOut->meanAbsBefore = meanBefore;
metricsOut->meanAbsAfter = meanAfter;
metricsOut->vminPhys = vmin;
metricsOut->vmaxPhys = vmax;
metricsOut->cellSizeM = cellSize;
metricsOut->dz = dz;
metricsOut->originX = originX;
metricsOut->originY = originY;
metricsOut->filledCells = filledCells;
metricsOut->totalCells = static_cast<std::int64_t>(nx) * ny * nz;
metricsOut->loadMs = loadMs;
metricsOut->pipelineMs = pipelineMs;
metricsOut->trajMs = trajMs;
metricsOut->gridMs = gridMs;
}
return built;
}
} // namespace geopro::io::gpr

View File

@ -0,0 +1,71 @@
#ifndef GEOPRO_IO_GPR_GPR3DVSURVEYVOLUMEBRIDGE_HPP
#define GEOPRO_IO_GPR_GPR3DVSURVEYVOLUMEBRIDGE_HPP
#include <string>
#include "core/algo/GprVolumeBuilder.hpp" // geopro::core::BuiltI16
namespace geopro::io::gpr {
// 测绘级(survey-grade)精确坐标逐线建体桥接(Task P8)。
//
// 与 Gpr3dvVolumeBridge(线局部坐标 X=道/Y=通道/Z=样本origin≈0)不同:
// 本桥接用 vendored 3DGPRViewer 的精确坐标/轨迹/网格代码(算法零改动)
// 让单条测线严格按 CGCS2000 大地坐标、逐通道逐道跟 GPS 轨迹建成【世界轴对齐体】,
// 体的 origin/spacing 为真实 CGCS2000 米,多线共享同一参考系 → view-all 直接按 origin
// 摆放即精确就位(跟路的弯,非起点+航向刚体近似)。
//
// 链路(全程走 P1 原版 + P8 拷入模块,算法零改动)
// IprhParser::loadImpulseMultiChannel(dir, base, model) // 多通道交错 + 头几何
// → model.buildVolumeData() // 处理前立方体(before 统计)
// → RadarProcessor::runPipeline(默认链) // 信号处理(11 步选配)
// → processed.buildVolumeData() // 处理后立方体
// geopro::io::gpr::parseGps(.gps) → 逐 RTK 点经纬(度)
// → CoordinateTransform::wgs84ToCgcs2000 // CGCS2000 平面(北X,东Y) 米
// → SurveyLine.gpsPositions(QVector3D: x=北, y=东, z=高)
// SurveyGeometry::fromHeaderOffsets(头 CH_X/Y_OFFSETS) // 逐通道天线横偏
// TrajectoryCalculator::computeTrajectories(line, geom) // 每通道每道 CGCS 世界坐标(跟弯)
// 逐深度 sampleIndexCScanGridder::build({line}, opts) // 世界坐标 IDW 网格 C-scan
// → 沿深度堆成世界轴对齐规则体(空 cell=kBlank)
// 量化 int16(中点 offset) → BuiltI16origin=CGCS2000 世界米、spacing=(cell,cell,dz)。
//
// 轴映射(geopro 体)X=东向(CGCS easting)、Y=北向(CGCS northing)、Z=深度。
// (与 CScanGridder 同gridX=pos.y()东、gridY=pos.x()北,图像与地图屏幕轴对齐。)
struct SurveyBridgeMetrics {
int nx = 0, ny = 0, nz = 0; // 体维度(东 ×× 深度)
int rtkPoints = 0; // .gps RTK 点数
int channels = 0; // 通道数
int cgcsZone = 0; // CGCS2000 3°带号
double centralMeridianDeg = 0.0; // 中央经线(度)
double meanAbsBefore = 0.0; // 处理前 traces 平均绝对幅值
double meanAbsAfter = 0.0; // 处理后 traces 平均绝对幅值
double vminPhys = 0.0, vmaxPhys = 0.0;
double cellSizeM = 0.0; // 水平格距(米)
double dz = 0.0; // 深度采样间距(米)
double originX = 0.0, originY = 0.0; // CGCS2000 世界 origin(东, 北) 米
std::int64_t filledCells = 0; // 非空体素数(validMask 命中累计)
std::int64_t totalCells = 0; // 总体素数
double loadMs = 0.0; // 读 + 建立方体
double pipelineMs = 0.0; // runPipeline + 再建立方体
double trajMs = 0.0; // GPS→CGCS + 轨迹
double gridMs = 0.0; // 逐深度 IDW 网格 + 量化填体
};
// 走 P8 测绘级链建单线世界对齐体。
// lineDir/linePrefix 同 build-line(如 "D:/Downloads/明星路", "明星路_001")。
// gpsPath 为该线 .gps 绝对路径(经纬度geopro parseGps 解析)。
// coarse(≥1):省空间档。水平格距 ×coarse、深度每 coarse 个采样取 1(保形)coarse≤1 全分辨率。
// cellSizeM/searchRadiusM:基础水平格距/搜索半径(米)0=用 CScanGridder 默认(0.05/0.50)。
// metricsOut 非空时回填维度/世界 origin/量化/耗时(供 CLI 报告,不编造)。
// 失败(加载失败/GPS 无效/体为空) → 抛 std::runtime_error。
geopro::core::BuiltI16 buildLineVolumeSurvey(const std::string& lineDir,
const std::string& linePrefix,
const std::string& gpsPath,
SurveyBridgeMetrics* metricsOut,
int coarse = 1,
double cellSizeM = 0.0,
double searchRadiusM = 0.0);
} // namespace geopro::io::gpr
#endif // GEOPRO_IO_GPR_GPR3DVSURVEYVOLUMEBRIDGE_HPP

View File

@ -34,6 +34,7 @@
#include "core/model/ColorScale.hpp"
#include "core/model/ScalarVolumeI16.hpp"
#include "data/store/ChunkedVolumeStore.hpp"
#include "io/gpr/Gpr3dvSurveyVolumeBridge.hpp"
#include "io/gpr/Gpr3dvVolumeBridge.hpp"
#include "io/gpr/GprSurveyAssembler.hpp"
#include "io/gpr/GpsTrack.hpp"
@ -1060,6 +1061,291 @@ int cmdBuildAll(int argc, char** argv) {
return 0;
}
// ============================================================================
// build-survey-line / build-survey-all测绘级精确坐标逐线世界对齐体(Task P8)
// ============================================================================
//
// 与 build-line(线局部坐标 X=道/Y=通道/Z=样本origin≈0)不同:本路径走 P8 测绘级桥接
// (Gpr3dvSurveyVolumeBridge)——用 vendored 3DGPRViewer 的精确坐标/轨迹/网格代码
// (CoordinateTransform/TrajectoryCalculator/CScanGridder算法零改动),让单线严格按
// CGCS2000 大地坐标、逐通道逐道跟 GPS 轨迹建成【世界轴对齐体】(跟路的弯)。体 meta.origin
// 为真实 CGCS2000 世界米,多线共享同一参考系 → view-survey-all 直接按 origin 摆放即精确就位。
// 按线前缀(如 "明星路_001")在 dir 下找匹配的 .gps(尾段 _NNN 相同)。空串=未找到。
std::string findGpsForPrefix(const std::string& dir, const std::string& prefix) {
const std::size_t us = prefix.find_last_of('_');
if (us == std::string::npos) return "";
const std::string num = prefix.substr(us + 1);
std::error_code ec;
for (const auto& e : fs::directory_iterator(dir, ec)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".gps") continue;
const std::string stem = e.path().stem().string();
const std::size_t s2 = stem.find_last_of('_');
if (s2 != std::string::npos && stem.substr(s2 + 1) == num)
return e.path().string();
}
return "";
}
// 单线测绘级建体核心P8 桥接 → 世界对齐量化体 → 落盘 + 金字塔。
// 异常(加载失败/GPS 无效/体空/退化)由调用方捕获,不在此中断批量。
LineBuildResult buildOneSurveyLine(const std::string& lineDir,
const std::string& linePrefix,
const std::string& gpsPath,
const std::string& out, int levels,
int coarse, double cellSizeM,
double searchRadiusM) {
LineBuildResult r;
r.prefix = linePrefix;
std::cout << "[build-survey-line] lineDir=" << lineDir
<< " linePrefix=" << linePrefix << " gps=" << gpsPath
<< " levels=" << levels << " coarse=" << coarse
<< " cellSize=" << cellSizeM << " out=" << out << "\n";
Stopwatch swBridge;
geopro::io::gpr::SurveyBridgeMetrics bm;
geopro::core::BuiltI16 built = geopro::io::gpr::buildLineVolumeSurvey(
lineDir, linePrefix, gpsPath, &bm, coarse, cellSizeM, searchRadiusM);
const double bridgeMs = swBridge.elapsedMs();
const std::int64_t nx = built.vol.nx(), ny = built.vol.ny(),
nz = built.vol.nz();
r.nx = nx;
r.ny = ny;
r.nz = nz;
if (nx < 2 || ny < 2 || nz < 2) {
r.ok = false;
r.reason = "世界体维度退化(东×北×深=" + std::to_string(nx) + "x" +
std::to_string(ny) + "x" + std::to_string(nz) +
"),无法建可看体,跳过";
std::cerr << "[build-survey-line] " << linePrefix << " " << r.reason << "\n";
return r;
}
const std::int64_t voxels = nx * ny * nz;
const std::int64_t rawBytes = voxels * 2;
const double fillRate =
bm.totalCells > 0
? static_cast<double>(bm.filledCells) / bm.totalCells
: 0.0;
std::cout << "[build-survey-line] 处理前后平均绝对幅值: " << bm.meanAbsBefore
<< "" << bm.meanAbsAfter << "\n";
std::cout << "[build-survey-line] RTK点=" << bm.rtkPoints
<< " 通道=" << bm.channels << " CGCS带号=" << bm.cgcsZone
<< " 中央经线=" << bm.centralMeridianDeg << "°\n";
std::cout << "[build-survey-line] 世界体维度(东×北×深) = " << nx << " x " << ny
<< " x " << nz << " 非空体素=" << bm.filledCells << " ("
<< (fillRate * 100.0) << "%)\n";
std::cout << "[build-survey-line] CGCS2000 世界 origin=(" << std::fixed
<< bm.originX << ", " << bm.originY << ") spacing cell=" << bm.cellSizeM
<< " dz=" << bm.dz << " (m)\n";
fs::create_directories(out);
Stopwatch swWrite;
geopro::data::ChunkedVolumeStore::write(out, built);
const double writeMs = swWrite.elapsedMs();
Stopwatch swPyr;
{
geopro::data::ChunkedVolumeStore store(out);
store.buildPyramid(levels);
}
const double pyrMs = swPyr.elapsedMs();
const std::int64_t dataBytes = storeDataBytes(out);
r.dataBytes = dataBytes;
const double ratio =
dataBytes > 0 ? static_cast<double>(rawBytes) / dataBytes : 0.0;
const double peak = Probe::peakMemMB();
std::cout << "\n=== build-survey-line 指标(测绘级 CGCS2000 世界对齐体)===\n";
std::cout << "桥接耗时(ms) : " << bridgeMs << " (读 " << bm.loadMs
<< " + 处理 " << bm.pipelineMs << " + GPS轨迹 " << bm.trajMs
<< " + 网格量化 " << bm.gridMs << ")\n";
std::cout << "落盘耗时(ms) : " << writeMs << "\n";
std::cout << "金字塔耗时(ms) : " << pyrMs << "\n";
std::cout << "世界体维度 : " << nx << " x " << ny << " x " << nz << "\n";
std::cout << "体素数 : " << voxels << " 非空 " << bm.filledCells
<< " (" << (fillRate * 100.0) << "%)\n";
std::cout << "处理后值域 : [" << bm.vminPhys << ", " << bm.vmaxPhys
<< "] 量化 scale=" << built.quant.scale
<< " offset=" << built.quant.offset << "\n";
std::cout << "CGCS2000 origin: (" << std::fixed << bm.originX << ", "
<< bm.originY << ") (东, 北) 米\n";
std::cout << "世界 spacing : cell=" << bm.cellSizeM << " dz=" << bm.dz
<< " (m)\n";
std::cout << "data.bin(B) : " << dataBytes << " ("
<< dataBytes / (1024.0 * 1024.0) << " MB) 压缩比 " << ratio
<< " x\n";
std::cout << "峰值内存(MB) : " << peak << "\n";
writeMetricLine(
"build-survey-line,prefix=" + linePrefix +
",coarse=" + std::to_string(coarse) + ",nx=" + std::to_string(nx) +
",ny=" + std::to_string(ny) + ",nz=" + std::to_string(nz) +
",voxels=" + std::to_string(voxels) +
",filled=" + std::to_string(bm.filledCells) +
",zone=" + std::to_string(bm.cgcsZone) +
",originX=" + std::to_string(bm.originX) +
",originY=" + std::to_string(bm.originY) +
",cell=" + std::to_string(bm.cellSizeM) + ",dz=" + std::to_string(bm.dz) +
",vmin=" + std::to_string(bm.vminPhys) +
",vmax=" + std::to_string(bm.vmaxPhys) +
",dataB=" + std::to_string(dataBytes) +
",bridgeMs=" + std::to_string(bridgeMs) +
",peakMB=" + std::to_string(peak));
r.ok = true;
return r;
}
int cmdBuildSurveyLine(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.size() < 2) {
std::cerr << "用法: gpr_poc build-survey-line <lineDir> <linePrefix> "
"--out <storeDir> [--levels 3] [--coarse F] [--cell 0.05] "
"[--radius 0.5] [--gps <path>]\n"
"例: gpr_poc build-survey-line \"D:/Downloads/明星路\" "
"明星路_001 --out tmp/survey001 --levels 3 --coarse 4\n";
return 2;
}
const std::string lineDir = a.positional[0];
const std::string linePrefix = a.positional[1];
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
const double cellSizeM = std::stod(a.get("cell", "0"));
const double radiusM = std::stod(a.get("radius", "0"));
std::string gps = a.get("gps", "");
if (gps.empty()) gps = findGpsForPrefix(lineDir, linePrefix);
const std::string out = a.get(
"out", (fs::temp_directory_path() / "gpr_store_survey_line").string());
if (gps.empty()) {
std::cerr << "[build-survey-line] 失败: 未找到 " << linePrefix
<< " 的 .gps(可用 --gps 指定)\n";
return 1;
}
try {
const LineBuildResult r = buildOneSurveyLine(
lineDir, linePrefix, gps, out, levels, coarse, cellSizeM, radiusM);
if (!r.ok) {
std::cerr << "[build-survey-line] 跳过: " << r.reason << "\n";
return 1;
}
return 0;
} catch (const std::exception& e) {
std::cerr << "[build-survey-line] 失败(" << linePrefix << "): " << e.what()
<< "\n";
return 1;
}
}
// build-survey-all发现所有测线 → 逐条走 P8 测绘级路径建世界对齐体到 baseDir/<lineName>/。
// 各线 .gps 自动按尾段 _NNN 匹配;缺 .gps 的线跳过报因。磁盘守护同 build-all。
int cmdBuildSurveyAll(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty() || !a.kv.count("outBase")) {
std::cerr << "用法: gpr_poc build-survey-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--cell 0.05] [--radius 0.5] "
"[--minFreeGB 3]\n"
"例: gpr_poc build-survey-all \"D:/Downloads/明星路\" "
"--outBase tmp/survey_all --levels 3 --coarse 4\n";
return 2;
}
const std::string lineDir = a.positional[0];
const std::string outBase = a.get("outBase", "");
const int levels = std::stoi(a.get("levels", "3"));
const int coarse = std::stoi(a.get("coarse", "1"));
const double cellSizeM = std::stod(a.get("cell", "0"));
const double radiusM = std::stod(a.get("radius", "0"));
const double minFreeGB = std::stod(a.get("minFreeGB", "3"));
// 发现所有测线前缀(扫 *_A<NN>.iprh截 _A<NN> 得前缀),与 build-all 同口径。
std::set<std::string> prefixSet;
std::error_code ec;
for (const auto& e : fs::directory_iterator(lineDir, ec)) {
if (!e.is_regular_file()) continue;
if (e.path().extension().string() != ".iprh") continue;
const std::string name = e.path().filename().string();
const std::size_t pos = name.rfind("_A");
if (pos == std::string::npos) continue;
std::size_t d = pos + 2;
while (d < name.size() && std::isdigit(static_cast<unsigned char>(name[d])))
++d;
if (d == pos + 2) continue;
prefixSet.insert(name.substr(0, pos));
}
if (prefixSet.empty()) {
std::cerr << "[build-survey-all] 未在 " << lineDir
<< " 发现任何测线(*_A<NN>.iprh)\n";
return 1;
}
std::vector<std::string> prefixes(prefixSet.begin(), prefixSet.end());
std::cout << "[build-survey-all] 发现 " << prefixes.size()
<< " 条测线outBase=" << outBase << " levels=" << levels
<< " coarse=" << coarse << " minFreeGB=" << minFreeGB << "\n";
fs::create_directories(outBase, ec);
std::vector<LineBuildResult> results;
bool stoppedByDisk = false;
for (const std::string& prefix : prefixes) {
const double freeGB = freeSpaceGB(outBase);
std::cout << "\n[build-survey-all] --- " << prefix << " --- 剩余磁盘 "
<< freeGB << " GB\n";
if (freeGB >= 0.0 && freeGB < minFreeGB) {
std::cerr << "[build-survey-all] 磁盘守护触发: 剩余 " << freeGB << " GB < "
<< minFreeGB << " GB停止未建 " << prefix << " 及其后。\n";
stoppedByDisk = true;
break;
}
const std::string out = (fs::path(outBase) / prefix).string();
LineBuildResult r;
r.prefix = prefix;
const std::string gps = findGpsForPrefix(lineDir, prefix);
if (gps.empty()) {
r.ok = false;
r.reason = "缺 .gps跳过";
std::cerr << "[build-survey-all] " << prefix << " 缺 .gps跳过\n";
results.push_back(r);
continue;
}
try {
r = buildOneSurveyLine(lineDir, prefix, gps, out, levels, coarse,
cellSizeM, radiusM);
} catch (const std::exception& e) {
r.ok = false;
r.reason = std::string("异常: ") + e.what();
std::cerr << "[build-survey-all] " << prefix << " 失败: " << e.what()
<< "(跳过,继续)\n";
}
results.push_back(r);
}
std::cout << "\n=== build-survey-all 汇总(测绘级 CGCS2000 世界对齐体) ===\n";
int okCount = 0;
std::int64_t totalBytes = 0;
for (const auto& r : results) {
if (r.ok) {
++okCount;
totalBytes += r.dataBytes;
std::cout << " [OK] " << r.prefix << " 维度(东×北×深)=" << r.nx << "x"
<< r.ny << "x" << r.nz << " data="
<< r.dataBytes / (1024.0 * 1024.0) << " MB\n";
} else {
std::cout << " [跳过] " << r.prefix << " 原因=" << r.reason << "\n";
}
}
std::cout << "成功 " << okCount << "/" << prefixes.size()
<< " 条,合计 data=" << totalBytes / (1024.0 * 1024.0 * 1024.0)
<< " GB剩余磁盘 " << freeSpaceGB(outBase) << " GB\n";
if (stoppedByDisk)
std::cout << "注意: 因磁盘守护提前停止,部分测线未建。\n";
return 0;
}
int cmdLoad(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
@ -4013,6 +4299,243 @@ int cmdViewAll(int argc, char** argv) {
return 0;
}
// ============================================================================
// view-survey-all测绘级世界对齐体直接按 CGCS2000 origin 摆放渲染(Task P8)
// ============================================================================
//
// 与 view-all(线局部体 + .gps 起点+航向刚体近似摆放)不同:本命令的体已是测绘级世界对齐
// 体(build-survey-all 产出meta.origin=CGCS2000 世界米,轴 X=东/Y=北/Z=深)。故【无需 .gps、
// 无需航向旋转】——直接按各体 meta.origin 平移摆进同一世界框即精确就位(跟路的弯)。
// 取公共参考原点(全体 origin 最小东/北)平移到近零局部框,保 VTK 数值稳定且相对位置严格不变。
// 由测绘级世界对齐体 + 公共参考原点 + Z 夸张 → vtkVolume(平移到世界位,无旋转)。
vtkSmartPointer<vtkVolume> makeSurveyPlacedVolume(
vtkImageData* img, const geopro::data::StoreMeta& m, double refX,
double refY, double exagg, double& vminOut, double& vmaxOut) {
const GalleryVariant& v = kViewDefaultVariant;
double vmin = m.vminPhys, vmax = m.vmaxPhys;
const ScalarPercentiles pc =
sampleScalarPercentiles(img, m.quant, 0.02, 0.98);
if (pc.samples > 0) {
vmin = pc.lo;
vmax = pc.hi;
}
vminOut = vmin;
vmaxOut = vmax;
const geopro::core::ColorScale cs = pickColor(v.color, vmin, vmax);
GradStats gs;
if (v.useGradientOpacity) gs = sampleGradientMagnitude(img);
vtkSmartPointer<vtkVolumeProperty> prop = makeVariantProperty(
v, m.quant, cs, vmin, vmax, v.maxOpacity,
v.useGradientOpacity ? &gs : nullptr);
// img 的 origin 是 meta.origin(CGCS 世界米,东向 ~4e7 量级)。直接把 image origin 减去公共
// 参考原点落到近零局部框——避免 VTK 内部 float32 在 ~4e7 绝对坐标上丢精度(±1m);相对位置严格不变。
double iorg[3];
img->GetOrigin(iorg);
img->SetOrigin(iorg[0] - refX, iorg[1] - refY, iorg[2]);
vtkNew<vtkSmartVolumeMapper> mapper;
mapper->SetInputData(img);
mapper->SetRequestedRenderMode(vtkSmartVolumeMapper::GPURenderMode);
mapper->SetAutoAdjustSampleDistances(0);
mapper->SetInteractiveAdjustSampleDistances(0);
auto volume = vtkSmartPointer<vtkVolume>::New();
volume->SetMapper(mapper);
volume->SetProperty(prop);
// 体已落近零世界框;仅 Z 夸张(局部深度)。
auto xf = vtkSmartPointer<vtkTransform>::New();
xf->PostMultiply();
xf->Scale(1.0, 1.0, exagg);
volume->SetUserTransform(xf);
return volume;
}
int cmdViewSurveyAll(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
std::cerr << "用法: gpr_poc view-survey-all <storesDir> [--preview] "
"[--exagg 8] [--level 1] [--shotDir <dir>]\n"
"例: gpr_poc view-survey-all tmp/survey_all --preview "
"--exagg 8\n";
return 2;
}
const std::string storesDir = a.positional[0];
const double exagg = std::stod(a.get("exagg", "8"));
const int level = std::stoi(a.get("level", "1"));
const bool preview = a.kv.count("preview") > 0;
const std::string shotDir = a.get("shotDir", storesDir);
std::cout << "[view-survey-all] storesDir=" << storesDir << " exagg=" << exagg
<< " level=" << level
<< (preview ? " [PREVIEW 离屏俯视+斜视出图]" : " [真窗口可交互]")
<< "\n";
std::cout << "[view-survey-all] 离屏闸门复检...\n";
if (cmdOffscreenSmoke() != 0) {
std::cout << "[view-survey-all] 闸门失败,中止。\n";
return 1;
}
// 1) 发现体目录(含 meta.json),按名排序。
std::vector<std::string> storeNames;
for (const auto& e : fs::directory_iterator(storesDir)) {
if (!e.is_directory()) continue;
if (!fs::exists(e.path() / "meta.json")) continue;
storeNames.push_back(e.path().filename().string());
}
std::sort(storeNames.begin(), storeNames.end());
std::cout << "[view-survey-all] 发现体目录数=" << storeNames.size() << "\n";
if (storeNames.empty()) {
std::cerr << "[view-survey-all] 错误: 未发现任何含 meta.json 的体目录\n";
return 1;
}
// 2) 逐体读 meta先定公共参考原点(全体 origin 最小东/北)。
struct SurveyPlaced {
std::string name;
vtkSmartPointer<vtkImageData> img;
geopro::data::StoreMeta meta;
};
std::vector<SurveyPlaced> placed;
double refX = std::numeric_limits<double>::infinity();
double refY = std::numeric_limits<double>::infinity();
for (const std::string& nm : storeNames) {
const std::string storePath = (fs::path(storesDir) / nm).string();
geopro::data::ChunkedVolumeStore store(storePath);
const geopro::data::StoreMeta m = store.meta();
refX = std::min(refX, m.origin[0]);
refY = std::min(refY, m.origin[1]);
const int lv = std::max(0, std::min(level, store.levels() - 1));
vtkSmartPointer<vtkImageData> img = buildLevelImage(store, lv, m);
std::cout << "[view-survey-all] " << nm << " level=" << lv << " 维度="
<< img->GetDimensions()[0] << "x" << img->GetDimensions()[1] << "x"
<< img->GetDimensions()[2] << " CGCS origin=(" << std::fixed
<< m.origin[0] << ", " << m.origin[1] << ")\n";
placed.push_back({nm, img, m});
}
if (placed.empty()) {
std::cerr << "[view-survey-all] 错误: 无可用体\n";
return 1;
}
std::cout << "[view-survey-all] 公共参考原点(最小东/北)=(" << std::fixed << refX
<< ", " << refY << ") (共 " << placed.size() << " 体)\n";
// 3) 同一 renderer 加全部世界对齐体(按 origin-ref 摆放,无旋转)。
const int winW = 1400, winH = 900;
auto rw = preview ? makeOffscreenWindow(winW, winH)
: vtkSmartPointer<vtkRenderWindow>::New();
if (!preview) rw->SetSize(winW, winH);
vtkNew<vtkRenderer> ren;
ren->SetBackground(kViewDefaultVariant.bg[0], kViewDefaultVariant.bg[1],
kViewDefaultVariant.bg[2]);
rw->AddRenderer(ren);
auto capWin = vtkSmartPointer<CapturingOutputWindow>::New();
vtkOutputWindow::SetInstance(capWin);
int added = 0;
for (const SurveyPlaced& sp : placed) {
double vmin = 0, vmax = 0;
vtkSmartPointer<vtkVolume> vol = makeSurveyPlacedVolume(
sp.img.Get(), sp.meta, refX, refY, exagg, vmin, vmax);
ren->AddVolume(vol);
++added;
}
std::cout << "[view-survey-all] 已加入场景体数=" << added << "\n";
ren->ResetCamera();
rw->Render();
if (capWin->textureError()) {
std::cerr << "[view-survey-all] 警告: 检测到 3D 纹理维度错误(某体超 GL 上限)"
"可增大 --level 取更粗层。\n";
}
if (preview) {
fs::create_directories(shotDir);
{ // 俯视(top):看 20 条按真实 CGCS 位置铺成测区(含路的弯)。
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
double fp[3];
cam->GetFocalPoint(fp);
const double* b = ren->ComputeVisiblePropBounds();
const double span = std::max({b[1] - b[0], b[3] - b[2], b[5] - b[4]});
cam->SetPosition(fp[0], fp[1], fp[2] + span * 2.0);
cam->SetFocalPoint(fp[0], fp[1], fp[2]);
cam->SetViewUp(0, 1, 0);
ren->ResetCameraClippingRange();
rw->Render();
const std::string p =
(fs::path(shotDir) / "view-survey-all-top.png").string();
savePng(rw.Get(), p);
std::cout << "[view-survey-all] 俯视图存: " << p << "\n";
}
{ // 斜视(oblique)。
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(35.0);
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
rw->Render();
const std::string p =
(fs::path(shotDir) / "view-survey-all-oblique.png").string();
savePng(rw.Get(), p);
std::cout << "[view-survey-all] 斜视图存: " << p << "\n";
}
rw->Render();
Stopwatch sw;
const int frames = 60;
for (int f = 0; f < frames; ++f) rw->Render();
const double ms = sw.elapsedMs();
const double fps = ms > 0 ? frames * 1000.0 / ms : 0.0;
const vtkIdType nb = countNonBlackPixels(rw.Get(), winW, winH);
std::cout << "\n=== view-survey-all --preview 测区全貌(测绘级世界对齐体)===\n";
std::cout << "参与体数 : " << placed.size() << "\n";
std::cout << "level : " << level << "\n";
std::cout << "exagg(Z) : " << exagg << "\n";
std::cout << "公共参考原点 : (" << std::fixed << refX << ", " << refY
<< ") CGCS2000 米\n";
std::cout << "非黑像素 : " << nb << " / " << (winW * winH) << "\n";
std::cout << "fps(" << frames << "帧连渲) : " << fps << "\n";
std::cout << "俯视图 : "
<< (fs::path(shotDir) / "view-survey-all-top.png").string()
<< "\n";
std::cout << "斜视图 : "
<< (fs::path(shotDir) / "view-survey-all-oblique.png").string()
<< "\n";
writeMetricLine("view-survey-all,bodies=" + std::to_string(placed.size()) +
",level=" + std::to_string(level) +
",exagg=" + std::to_string(exagg) +
",nonBlack=" + std::to_string(nb) +
",fps=" + std::to_string(fps));
return nb > 0 ? 0 : 1;
}
rw->SetWindowName("gpr_poc view-survey-all —— 测绘级 CGCS2000 世界对齐体");
vtkNew<vtkRenderWindowInteractor> iren;
iren->SetRenderWindow(rw);
vtkNew<vtkInteractorStyleTrackballCamera> style;
iren->SetInteractorStyle(style);
ren->ResetCamera();
vtkCamera* cam = ren->GetActiveCamera();
cam->Elevation(35.0);
cam->Azimuth(30.0);
ren->ResetCameraClippingRange();
std::cout << "[view-survey-all] 打开真窗口。左键旋转 / 滚轮缩放 / q 退出。\n";
iren->Initialize();
rw->Render();
iren->Start();
std::cout << "[view-survey-all] 窗口关闭,退出。\n";
return 0;
}
int cmdView(int argc, char** argv) {
const Args a = parseArgs(argc, argv, 2);
if (a.positional.empty()) {
@ -4886,6 +5409,12 @@ void usage() {
"[--levels 3] [--coarse F]\n"
" gpr_poc build-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--minFreeGB 3]\n"
" gpr_poc build-survey-line <lineDir> <linePrefix> --out "
"<storeDir> [--levels 3] [--coarse F] [--cell 0.05] "
"[--radius 0.5] [--gps <path>]\n"
" gpr_poc build-survey-all <lineDir> --outBase <baseDir> "
"[--levels 3] [--coarse F] [--cell 0.05] [--radius 0.5] "
"[--minFreeGB 3]\n"
" gpr_poc load <storeDir>\n"
" gpr_poc selftest\n"
" gpr_poc offscreen-smoke\n"
@ -4902,6 +5431,8 @@ void usage() {
"[--frames 90]\n"
" gpr_poc view-all <storesDir> <gpsDir> [--preview] "
"[--exagg 8] [--level 1] [--spread M] [--shotDir <dir>]\n"
" gpr_poc view-survey-all <storesDir> [--preview] [--exagg 8] "
"[--level 1] [--shotDir <dir>]\n"
" gpr_poc polish <storeDir> [--exagg 8] [--frames 90] "
"[--localBricks 4]\n";
}
@ -4924,6 +5455,8 @@ int main(int argc, char** argv) {
if (cmd == "build-geo") return cmdBuildGeo(argc, argv);
if (cmd == "build-line") return cmdBuildLine(argc, argv);
if (cmd == "build-all") return cmdBuildAll(argc, argv);
if (cmd == "build-survey-line") return cmdBuildSurveyLine(argc, argv);
if (cmd == "build-survey-all") return cmdBuildSurveyAll(argc, argv);
if (cmd == "load") return cmdLoad(argc, argv);
if (cmd == "selftest") return cmdSelftest();
if (cmd == "offscreen-smoke") return cmdOffscreenSmoke();
@ -4936,6 +5469,7 @@ int main(int argc, char** argv) {
if (cmd == "fps-budget") return cmdFpsBudget(argc, argv);
if (cmd == "view") return cmdView(argc, argv);
if (cmd == "view-all") return cmdViewAll(argc, argv);
if (cmd == "view-survey-all") return cmdViewSurveyAll(argc, argv);
if (cmd == "polish") return cmdPolish(argc, argv);
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << "\n";