From 7329750c0c19b3d5c838284fbf053fd551290aba Mon Sep 17 00:00:00 2001 From: awe Date: Fri, 25 Sep 2026 17:12:18 +0300 Subject: [PATCH] sar3d: fixed default scene window X [-2,2], Y [-0.5,2], Z [0,8] m The grid bounds are now explicit config (sar3d_x/y/z_min/max_m) instead of being derived from the antenna span and the depth gate. The depth mask still zeroes voxels outside [min_depth, max_depth], so the wider Z window just shows the masked wedge. The bound fields are double: the parity tool recomputes the notebook-derived bounds (notebook_grid_bounds) and passes them through the live config, and a float roundtrip shifted the axes by ~1e-7 m, flipping depth-mask knife-edge voxels against the reference npz. Parity on frames 0034-0036 unchanged (1.3e-6, axes bit-match). Co-Authored-By: Claude Fable 5 --- .../gpr_cpp/examples/standalone_bp.cpp | 7 ++- .../gpr_cpp/include/gpr/config.hpp | 11 ++-- .../gpr_cpp/include/gpr/geometry.hpp | 15 +++-- .../gpr_cpp/src/geometry.cpp | 60 ++++++++++--------- .../include/processing_live_config.hpp | 18 ++++-- .../src/processing_live_config.cpp | 18 +++++- .../processors/src/sar3d_processor.cpp | 7 ++- .../processing/tools/sar3d_parity_main.cpp | 48 ++++++++++++--- .../live_processing_mixin.py | 5 +- .../app_window_config/profile_io_mixin.py | 10 +++- .../app_window_config/state_builders.py | 5 +- .../sections/processing_section.py | 39 +++++++++--- python_app/models/gui_profile_codec.py | 33 ++++++++-- python_app/models/gui_profile_schema.py | 5 +- .../orchestration/live_processing_config.py | 15 +++-- python_app/scripts/replay_smoke_run.py | 5 +- 16 files changed, 220 insertions(+), 81 deletions(-) diff --git a/data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp b/data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp index f39677b..e89ddf0 100644 --- a/data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp +++ b/data_acq_and_processing/gpr_cpp/examples/standalone_bp.cpp @@ -135,8 +135,11 @@ int main(int argc, char** argv) { frames.push_back(ApertureFrame{a.get(), offs[f], wts[f]}); } - // полнообластная ground-сетка (как одиночный 3D в ноутбуке) - Grid grid = build_full_region_grid(geom, bp, sar, mot.pitch_deg); + // ноутбучные границы сцены (размах антенн ±2 м, полоса глубин) вместо + // фиксированного окна по умолчанию + notebook_grid_bounds(geom, bp, sar, mot.pitch_deg, 2.0, + sar.x_min_m, sar.x_max_m, sar.z_min_m, sar.z_max_m); + Grid grid = build_full_region_grid(sar); printf("сетка %dx%dx%d, счёт объёма...\n", grid.NX(), grid.NY(), grid.NZ()); VolumeRaw vol = engine.aperture_volume(frames, grid); diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/config.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/config.hpp index e4367b3..a2cf966 100644 --- a/data_acq_and_processing/gpr_cpp/include/gpr/config.hpp +++ b/data_acq_and_processing/gpr_cpp/include/gpr/config.hpp @@ -94,12 +94,15 @@ struct SARConfig { bool use_compensation = true; // Высота НИЖНЕГО ряда антенн над землёй; Y=0 сцены — поверхность земли. double ground_depth_m = 1.25; - // Полнообластная сетка: X — размах антенн ±x_margin_m, Y — высота над - // землёй, Z — полоса, покрывающая радарные глубины [min,max]_depth на всех - // высотах Y (см. build_full_region_grid). - double x_margin_m = 2.0; + // Явные границы сцены [м]: X — поперёк, Y — высота над землёй, Z — вдаль. + // Воксели с радарной глубиной вне [min_depth, max_depth] всё равно + // обнуляются маской, так что Z-окно шире полосы глубин безопасно. + double x_min_m = -2.0; + double x_max_m = 2.0; double y_min_m = -0.5; double y_max_m = 2.0; + double z_min_m = 0.0; + double z_max_m = 8.0; std::array voxel_m{0.025, 0.02, 0.02}; // шаг (X, Y, Z) // Финализация объёма: нормировка без верхних/нижних norm_edge_margin_m по Y // (там ложный гребень постоянной дальности), сглаживание по (Z,X) в каждом diff --git a/data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp b/data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp index 156c7b1..cd26194 100644 --- a/data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp +++ b/data_acq_and_processing/gpr_cpp/include/gpr/geometry.hpp @@ -22,11 +22,16 @@ std::vector frame_weights(const std::vector& offsets, doubl // y = (-ground_depth + depth*sin(pitch)) / cos(pitch) (порт bp_plane_y). double ground_plane_y(double depth, double ground_depth_m, double pitch_rad); -// Полнообластная сетка сцены (порт sar_build_full_region_grid): -// X — размах антенн ±x_margin, Y — [y_min, y_max] над землёй, Z — полоса, -// покрывающая радарные глубины [min_depth, max_depth] на всех высотах Y. -Grid build_full_region_grid(const GeometryConfig&, const BPConfig&, const SARConfig&, - double pitch_deg); +// Сетка сцены по явным границам SARConfig (x/y/z_min..max, шаг voxel_m); +// оси — linspace c числом точек round(span/step)+1, как в ноутбуке. +Grid build_full_region_grid(const SARConfig&); + +// Ноутбучные границы (sar_build_full_region_grid): X — размах антенн ±margin, +// Z — полоса, покрывающая радарные глубины [min_depth, max_depth] на всех +// высотах Y. Используются parity-инструментом для сверки с эталонными npz. +void notebook_grid_bounds(const GeometryConfig&, const BPConfig&, const SARConfig&, + double pitch_deg, double x_margin_m, + double& x_min, double& x_max, double& z_min, double& z_max); // point_to_scene: оси радара → сцена (поворот на pitch + подъём на ground_depth, // если ground; Y=0 сцены — земля). diff --git a/data_acq_and_processing/gpr_cpp/src/geometry.cpp b/data_acq_and_processing/gpr_cpp/src/geometry.cpp index 06558dd..8012416 100644 --- a/data_acq_and_processing/gpr_cpp/src/geometry.cpp +++ b/data_acq_and_processing/gpr_cpp/src/geometry.cpp @@ -58,8 +58,7 @@ double ground_plane_y(double depth, double ground_depth_m, double pitch_rad) { return (-ground_depth_m + depth * std::sin(pitch_rad)) / std::cos(pitch_rad); } -Grid build_full_region_grid(const GeometryConfig& geom, const BPConfig& bp, const SARConfig& sar, - double pitch_deg) { +Grid build_full_region_grid(const SARConfig& sar) { // numpy: linspace(lo, hi, max(round(span/step)+1, 2)); nearbyint = банковское // округление python round() auto axis = [](double lo, double hi, double step, std::vector& out) { @@ -67,36 +66,39 @@ Grid build_full_region_grid(const GeometryConfig& geom, const BPConfig& bp, cons out.resize(n); for (int i = 0; i < n; ++i) out[i] = lo + (hi - lo) * i / (double)(n - 1); }; - double x_lo = geom.x_tx[0], x_hi = geom.x_tx[0]; - for (double x : geom.x_tx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); } - for (double x : geom.x_rx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); } - x_lo -= sar.x_margin_m; - x_hi += sar.x_margin_m; - Grid g; - axis(x_lo, x_hi, sar.voxel_m[0], g.x); + axis(sar.x_min_m, sar.x_max_m, sar.voxel_m[0], g.x); axis(sar.y_min_m, sar.y_max_m, sar.voxel_m[1], g.y); - if (sar.coord_mode_ground) { - // Радарная z линейна по Z сцены при фиксированной высоте: - // z_rad(Y, Z=0) = -(Y - ground_depth)*sin(p), z = (depth - z_rad)/cos(p). - double p = pitch_deg * M_PI / 180.0; - double cos_p = std::cos(p), sin_p = std::sin(p); - double z_lo = 0.0, z_hi = 0.0; - bool first = true; - for (double y : {sar.y_min_m, sar.y_max_m}) { - for (double depth : {bp.min_depth, bp.max_depth}) { - double z_rad0 = -(y - sar.ground_depth_m) * sin_p; - double z = (depth - z_rad0) / cos_p; - if (first) { z_lo = z_hi = z; first = false; } - z_lo = std::min(z_lo, z); - z_hi = std::max(z_hi, z); - } - } - axis(z_lo, z_hi, sar.voxel_m[2], g.z); - } else { - axis(bp.min_depth, bp.max_depth, sar.voxel_m[2], g.z); - } + axis(sar.z_min_m, sar.z_max_m, sar.voxel_m[2], g.z); return g; } +void notebook_grid_bounds(const GeometryConfig& geom, const BPConfig& bp, const SARConfig& sar, + double pitch_deg, double x_margin_m, + double& x_min, double& x_max, double& z_min, double& z_max) { + double x_lo = geom.x_tx[0], x_hi = geom.x_tx[0]; + for (double x : geom.x_tx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); } + for (double x : geom.x_rx) { x_lo = std::min(x_lo, x); x_hi = std::max(x_hi, x); } + x_min = x_lo - x_margin_m; + x_max = x_hi + x_margin_m; + + // Радарная z линейна по Z сцены при фиксированной высоте: + // z_rad(Y, Z=0) = -(Y - ground_depth)*sin(p), z = (depth - z_rad)/cos(p). + double p = pitch_deg * M_PI / 180.0; + double cos_p = std::cos(p), sin_p = std::sin(p); + double z_lo = 0.0, z_hi = 0.0; + bool first = true; + for (double y : {sar.y_min_m, sar.y_max_m}) { + for (double depth : {bp.min_depth, bp.max_depth}) { + double z_rad0 = -(y - sar.ground_depth_m) * sin_p; + double z = (depth - z_rad0) / cos_p; + if (first) { z_lo = z_hi = z; first = false; } + z_lo = std::min(z_lo, z); + z_hi = std::max(z_hi, z); + } + } + z_min = z_lo; + z_max = z_hi; +} + } // namespace gpr diff --git a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp index 7172594..a0dfe2b 100644 --- a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp +++ b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp @@ -103,12 +103,18 @@ struct ProcessingLiveConfig { float sar3d_pitch_deg = 17.5F; // Height of the LOWER antenna row above the ground; scene Y=0 is the ground. float sar3d_ground_depth_m = 1.25F; - // Full-region grid: X spans the antennas +- x_margin, Y is height above the - // ground, Z covers radar depths [gpr_min_depth, gpr_max_depth] over the Y - // range. Voxel pitch per axis. - float sar3d_x_margin_m = 2.0F; - float sar3d_y_min_m = -0.5F; - float sar3d_y_max_m = 2.0F; + // Scene grid bounds (metres): X across, Y height above the ground, Z along + // the ground away from the radar. Voxels whose radar depth falls outside + // [gpr_min_depth, gpr_max_depth] are zeroed by the mask regardless. + // Doubles, not floats: the parity tool feeds the notebook-derived bounds + // through here and a float roundtrip shifts the grid by ~1e-7 m, flipping + // knife-edge voxels of the depth mask against the reference volumes. + double sar3d_x_min_m = -2.0; + double sar3d_x_max_m = 2.0; + double sar3d_y_min_m = -0.5; + double sar3d_y_max_m = 2.0; + double sar3d_z_min_m = 0.0; + double sar3d_z_max_m = 8.0; float sar3d_voxel_x_m = 0.025F; float sar3d_voxel_y_m = 0.02F; float sar3d_voxel_z_m = 0.02F; diff --git a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp index 6551ee7..eca2b23 100644 --- a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp +++ b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp @@ -393,9 +393,6 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s std::pair{"sar3d_edge_frame_weight", &config.sar3d_edge_frame_weight}, std::pair{"sar3d_pitch_deg", &config.sar3d_pitch_deg}, std::pair{"sar3d_ground_depth_m", &config.sar3d_ground_depth_m}, - std::pair{"sar3d_x_margin_m", &config.sar3d_x_margin_m}, - std::pair{"sar3d_y_min_m", &config.sar3d_y_min_m}, - std::pair{"sar3d_y_max_m", &config.sar3d_y_max_m}, std::pair{"sar3d_voxel_x_m", &config.sar3d_voxel_x_m}, std::pair{"sar3d_voxel_y_m", &config.sar3d_voxel_y_m}, std::pair{"sar3d_voxel_z_m", &config.sar3d_voxel_z_m}, @@ -411,6 +408,21 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s *target = static_cast(found->get()); } } + for (const auto& [key, target] : { + std::pair{"sar3d_x_min_m", &config.sar3d_x_min_m}, + std::pair{"sar3d_x_max_m", &config.sar3d_x_max_m}, + std::pair{"sar3d_y_min_m", &config.sar3d_y_min_m}, + std::pair{"sar3d_y_max_m", &config.sar3d_y_max_m}, + std::pair{"sar3d_z_min_m", &config.sar3d_z_min_m}, + std::pair{"sar3d_z_max_m", &config.sar3d_z_max_m}, + }) { + if (const auto found = root.find(key); found != root.end()) { + if (!found->is_number()) { + throw std::runtime_error(std::string("processing.") + key + " must be number"); + } + *target = found->get(); + } + } if (const auto found = root.find("sar3d_tx_per_segment"); found != root.end()) { config.sar3d_tx_per_segment = parse_u32_number(*found, "processing.sar3d_tx_per_segment"); if (config.sar3d_tx_per_segment == 0U) { diff --git a/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp b/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp index ab8c722..4bf0d7e 100644 --- a/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp +++ b/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp @@ -155,9 +155,12 @@ struct Selection { sar.frame_radius = static_cast(std::max(0U, live_config.sar3d_frame_radius)); sar.edge_frame_weight = static_cast(live_config.sar3d_edge_frame_weight); sar.ground_depth_m = static_cast(live_config.sar3d_ground_depth_m); - sar.x_margin_m = static_cast(live_config.sar3d_x_margin_m); + sar.x_min_m = static_cast(live_config.sar3d_x_min_m); + sar.x_max_m = static_cast(live_config.sar3d_x_max_m); sar.y_min_m = static_cast(live_config.sar3d_y_min_m); sar.y_max_m = static_cast(live_config.sar3d_y_max_m); + sar.z_min_m = static_cast(live_config.sar3d_z_min_m); + sar.z_max_m = static_cast(live_config.sar3d_z_max_m); sar.voxel_m = { static_cast(live_config.sar3d_voxel_x_m), static_cast(live_config.sar3d_voxel_y_m), @@ -570,7 +573,7 @@ auto Sar3dProcessor::process_collection( state_->engine_fp = engine_fp; } - const gpr::Grid grid = gpr::build_full_region_grid(geom, bp, sar, motion.pitch_deg); + const gpr::Grid grid = gpr::build_full_region_grid(sar); const gpr::VolumeRaw raw = state_->engine->aperture_volume(frames, grid); // Full notebook finalization: |acc/cnt|, depth gate, normalization // without the Y-edge planes, Gaussian smoothing over (Z, X), clip. diff --git a/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp b/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp index cf60047..e480e28 100644 --- a/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp +++ b/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp @@ -27,6 +27,8 @@ #include +#include "gpr/config.hpp" +#include "gpr/geometry.hpp" #include "processing_live_config.hpp" #include "run_config.hpp" #include "sar3d_processor.hpp" @@ -85,7 +87,11 @@ NpyArray load_npy(const std::string& path) { } // Live config matching the reference notebook run (npz meta parameters). -auto build_live_config() -> radar::processing::ProcessingLiveConfig { +// The default program grid is a fixed window (X -2..2, Z 0..8), while the +// notebook derives its bounds from the antenna span and the depth gate — so +// the notebook bounds are recomputed here and passed explicitly. +auto build_live_config(const radar::config::RunConfig& run_config) + -> radar::processing::ProcessingLiveConfig { radar::processing::ProcessingLiveConfig live{}; live.processor_mode = "sar3d"; live.gpr_start_freq_mhz = 2000.0F; @@ -96,6 +102,32 @@ auto build_live_config() -> radar::processing::ProcessingLiveConfig { live.gpr_look_angle_deg = 17.5F; live.gpr_direction_sign = 1.0F; live.gpr_apply_freq_phase_correction = true; + + gpr::GeometryConfig geom{}; + geom.input_idx.clear(); + geom.output_idx.clear(); + geom.rx_positions.clear(); + geom.tx_positions.clear(); + for (const auto& entry : run_config.gpr.rx_geometry) { + geom.input_idx.push_back(static_cast(entry.input_pos)); + geom.rx_positions.push_back({entry.x_m, entry.y_m, entry.z_m}); + } + for (const auto& entry : run_config.gpr.tx_geometry) { + geom.output_idx.push_back(static_cast(entry.output_pos)); + geom.tx_positions.push_back({entry.x_m, entry.y_m, entry.z_m}); + } + geom.finalize(); + gpr::BPConfig bp{}; + bp.min_depth = live.gpr_min_depth_m; + bp.max_depth = live.gpr_max_depth_m; + gpr::SARConfig sar{}; + double x_min = 0.0, x_max = 0.0, z_min = 0.0, z_max = 0.0; + gpr::notebook_grid_bounds(geom, bp, sar, live.gpr_look_angle_deg, /*x_margin_m=*/2.0, + x_min, x_max, z_min, z_max); + live.sar3d_x_min_m = x_min; + live.sar3d_x_max_m = x_max; + live.sar3d_z_min_m = z_min; + live.sar3d_z_max_m = z_max; return live; } @@ -240,7 +272,13 @@ int main(int argc, char** argv) { } const auto center_index = static_cast(center_it - snapshots.begin()); - auto live = build_live_config(); + const auto run_config = radar::config::load_run_config(root + "/config_profile.json"); + if (run_config.gpr.tx_geometry.empty() || run_config.gpr.rx_geometry.empty()) { + std::printf("FAIL: config_profile.json has no gpr geometry\n"); + return 1; + } + + auto live = build_live_config(run_config); live.sar3d_volume_dump_dir = dump_dir; const auto radius = static_cast(live.sar3d_frame_radius); if (center_index < radius || center_index + radius >= snapshots.size()) { @@ -248,12 +286,6 @@ int main(int argc, char** argv) { return 1; } - const auto run_config = radar::config::load_run_config(root + "/config_profile.json"); - if (run_config.gpr.tx_geometry.empty() || run_config.gpr.rx_geometry.empty()) { - std::printf("FAIL: config_profile.json has no gpr geometry\n"); - return 1; - } - // The processor centers the aperture `radius` frames before the newest // collection, so feeding [center-r .. center+r] in order reproduces the // symmetric aperture around `center`. diff --git a/python_app/gui/controllers/app_window_config/live_processing_mixin.py b/python_app/gui/controllers/app_window_config/live_processing_mixin.py index 627e625..b5745fd 100644 --- a/python_app/gui/controllers/app_window_config/live_processing_mixin.py +++ b/python_app/gui/controllers/app_window_config/live_processing_mixin.py @@ -93,9 +93,12 @@ _WEB_LIVE_SCHEMA = [ ("sar3d_pitch_override_enabled", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_override_enabled")), ("sar3d_pitch_deg", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_deg")), ("sar3d_ground_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_ground_depth_m")), - ("sar3d_x_margin_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_margin_m")), + ("sar3d_x_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_min_m")), + ("sar3d_x_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_max_m")), ("sar3d_y_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_min_m")), ("sar3d_y_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_max_m")), + ("sar3d_z_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_z_min_m")), + ("sar3d_z_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_z_max_m")), ("sar3d_voxel_x_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_x_m")), ("sar3d_voxel_y_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_y_m")), ("sar3d_voxel_z_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_z_m")), diff --git a/python_app/gui/controllers/app_window_config/profile_io_mixin.py b/python_app/gui/controllers/app_window_config/profile_io_mixin.py index ee01bef..2c84d8d 100644 --- a/python_app/gui/controllers/app_window_config/profile_io_mixin.py +++ b/python_app/gui/controllers/app_window_config/profile_io_mixin.py @@ -353,9 +353,12 @@ class AppWindowConfigProfileIOMixin: self._sar3d_pitch_override_enabled, self._sar3d_pitch_deg, self._sar3d_ground_depth_m, - self._sar3d_x_margin_m, + self._sar3d_x_min_m, + self._sar3d_x_max_m, self._sar3d_y_min_m, self._sar3d_y_max_m, + self._sar3d_z_min_m, + self._sar3d_z_max_m, self._sar3d_voxel_x_m, self._sar3d_voxel_y_m, self._sar3d_voxel_z_m, @@ -570,9 +573,12 @@ class AppWindowConfigProfileIOMixin: self._sar3d_pitch_override_enabled.setChecked(bool(sar3d_state.pitch_override_enabled)) self._sar3d_pitch_deg.setValue(float(sar3d_state.pitch_deg)) self._sar3d_ground_depth_m.setValue(float(sar3d_state.ground_depth_m)) - self._sar3d_x_margin_m.setValue(float(sar3d_state.x_margin_m)) + self._sar3d_x_min_m.setValue(float(sar3d_state.x_min_m)) + self._sar3d_x_max_m.setValue(float(sar3d_state.x_max_m)) self._sar3d_y_min_m.setValue(float(sar3d_state.y_min_m)) self._sar3d_y_max_m.setValue(float(sar3d_state.y_max_m)) + self._sar3d_z_min_m.setValue(float(sar3d_state.z_min_m)) + self._sar3d_z_max_m.setValue(float(sar3d_state.z_max_m)) self._sar3d_voxel_x_m.setValue(float(sar3d_state.voxel_x_m)) self._sar3d_voxel_y_m.setValue(float(sar3d_state.voxel_y_m)) self._sar3d_voxel_z_m.setValue(float(sar3d_state.voxel_z_m)) diff --git a/python_app/gui/controllers/app_window_config/state_builders.py b/python_app/gui/controllers/app_window_config/state_builders.py index b7c61dc..f9a47ee 100644 --- a/python_app/gui/controllers/app_window_config/state_builders.py +++ b/python_app/gui/controllers/app_window_config/state_builders.py @@ -441,9 +441,12 @@ class AppWindowConfigStateBuildersMixin: pitch_override_enabled=bool(self._sar3d_pitch_override_enabled.isChecked()), pitch_deg=float(self._sar3d_pitch_deg.value()), ground_depth_m=float(self._sar3d_ground_depth_m.value()), - x_margin_m=float(self._sar3d_x_margin_m.value()), + x_min_m=float(self._sar3d_x_min_m.value()), + x_max_m=float(self._sar3d_x_max_m.value()), y_min_m=float(self._sar3d_y_min_m.value()), y_max_m=float(self._sar3d_y_max_m.value()), + z_min_m=float(self._sar3d_z_min_m.value()), + z_max_m=float(self._sar3d_z_max_m.value()), voxel_x_m=float(self._sar3d_voxel_x_m.value()), voxel_y_m=float(self._sar3d_voxel_y_m.value()), voxel_z_m=float(self._sar3d_voxel_z_m.value()), diff --git a/python_app/gui/controllers/sections/processing_section.py b/python_app/gui/controllers/sections/processing_section.py index fd7eb8e..8c1bf24 100644 --- a/python_app/gui/controllers/sections/processing_section.py +++ b/python_app/gui/controllers/sections/processing_section.py @@ -685,12 +685,29 @@ def build_processing_group(owner) -> QGroupBox: "Height of the lower antenna row above the ground (m); scene Y=0 is the ground." ) - owner._sar3d_x_margin_m = QDoubleSpinBox() - owner._sar3d_x_margin_m.setDecimals(2) - owner._sar3d_x_margin_m.setRange(0.0, 20.0) - owner._sar3d_x_margin_m.setSingleStep(0.1) - owner._sar3d_x_margin_m.setValue(float(sar3d_defaults.x_margin_m)) - owner._sar3d_x_margin_m.setToolTip("Grid X margin beyond the antenna span (m).") + owner._sar3d_x_min_m = QDoubleSpinBox() + owner._sar3d_x_min_m.setDecimals(2) + owner._sar3d_x_min_m.setRange(-50.0, 50.0) + owner._sar3d_x_min_m.setSingleStep(0.1) + owner._sar3d_x_min_m.setValue(float(sar3d_defaults.x_min_m)) + + owner._sar3d_x_max_m = QDoubleSpinBox() + owner._sar3d_x_max_m.setDecimals(2) + owner._sar3d_x_max_m.setRange(-50.0, 50.0) + owner._sar3d_x_max_m.setSingleStep(0.1) + owner._sar3d_x_max_m.setValue(float(sar3d_defaults.x_max_m)) + + owner._sar3d_z_min_m = QDoubleSpinBox() + owner._sar3d_z_min_m.setDecimals(2) + owner._sar3d_z_min_m.setRange(-50.0, 50.0) + owner._sar3d_z_min_m.setSingleStep(0.1) + owner._sar3d_z_min_m.setValue(float(sar3d_defaults.z_min_m)) + + owner._sar3d_z_max_m = QDoubleSpinBox() + owner._sar3d_z_max_m.setDecimals(2) + owner._sar3d_z_max_m.setRange(-50.0, 50.0) + owner._sar3d_z_max_m.setSingleStep(0.1) + owner._sar3d_z_max_m.setValue(float(sar3d_defaults.z_max_m)) owner._sar3d_y_min_m = QDoubleSpinBox() owner._sar3d_y_min_m.setDecimals(2) @@ -789,9 +806,12 @@ def build_processing_group(owner) -> QGroupBox: owner._sar3d_pitch_override_enabled, ("Pitch deg", owner._sar3d_pitch_deg), ("Ground depth m", owner._sar3d_ground_depth_m), - ("X margin m", owner._sar3d_x_margin_m), + ("X min m", owner._sar3d_x_min_m), + ("X max m", owner._sar3d_x_max_m), ("Y min m", owner._sar3d_y_min_m), ("Y max m", owner._sar3d_y_max_m), + ("Z min m", owner._sar3d_z_min_m), + ("Z max m", owner._sar3d_z_max_m), ("Voxel X m", owner._sar3d_voxel_x_m), ("Voxel Y m", owner._sar3d_voxel_y_m), ("Voxel Z m", owner._sar3d_voxel_z_m), @@ -895,7 +915,10 @@ def build_processing_group(owner) -> QGroupBox: owner._sar3d_pitch_override_enabled.toggled.connect(owner._on_processing_live_settings_changed) owner._sar3d_pitch_deg.valueChanged.connect(owner._on_processing_live_settings_changed) owner._sar3d_ground_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed) - owner._sar3d_x_margin_m.valueChanged.connect(owner._on_processing_live_settings_changed) + owner._sar3d_x_min_m.valueChanged.connect(owner._on_processing_live_settings_changed) + owner._sar3d_x_max_m.valueChanged.connect(owner._on_processing_live_settings_changed) + owner._sar3d_z_min_m.valueChanged.connect(owner._on_processing_live_settings_changed) + owner._sar3d_z_max_m.valueChanged.connect(owner._on_processing_live_settings_changed) owner._sar3d_y_min_m.valueChanged.connect(owner._on_processing_live_settings_changed) owner._sar3d_y_max_m.valueChanged.connect(owner._on_processing_live_settings_changed) owner._sar3d_voxel_x_m.valueChanged.connect(owner._on_processing_live_settings_changed) diff --git a/python_app/models/gui_profile_codec.py b/python_app/models/gui_profile_codec.py index 40d73a0..866bc54 100644 --- a/python_app/models/gui_profile_codec.py +++ b/python_app/models/gui_profile_codec.py @@ -551,10 +551,16 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: gui.processing.sar3d.ground_depth_m, "gui.processing.sar3d", ), - x_margin_m=_optional_float( + x_min_m=_optional_float( sar3d_object, - "x_margin_m", - gui.processing.sar3d.x_margin_m, + "x_min_m", + gui.processing.sar3d.x_min_m, + "gui.processing.sar3d", + ), + x_max_m=_optional_float( + sar3d_object, + "x_max_m", + gui.processing.sar3d.x_max_m, "gui.processing.sar3d", ), y_min_m=_optional_float( @@ -569,6 +575,18 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: gui.processing.sar3d.y_max_m, "gui.processing.sar3d", ), + z_min_m=_optional_float( + sar3d_object, + "z_min_m", + gui.processing.sar3d.z_min_m, + "gui.processing.sar3d", + ), + z_max_m=_optional_float( + sar3d_object, + "z_max_m", + gui.processing.sar3d.z_max_m, + "gui.processing.sar3d", + ), voxel_x_m=_optional_float( sar3d_object, "voxel_x_m", @@ -675,8 +693,12 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: raise ValueError("gui.processing.sar3d.edge_frame_weight must be within [0, 1]") if min(gui.processing.sar3d.voxel_x_m, gui.processing.sar3d.voxel_y_m, gui.processing.sar3d.voxel_z_m) <= 0.0: raise ValueError("gui.processing.sar3d.voxel_x/y/z_m must each be > 0") + if gui.processing.sar3d.x_max_m <= gui.processing.sar3d.x_min_m: + raise ValueError("gui.processing.sar3d.x_max_m must be > x_min_m") if gui.processing.sar3d.y_max_m <= gui.processing.sar3d.y_min_m: raise ValueError("gui.processing.sar3d.y_max_m must be > y_min_m") + if gui.processing.sar3d.z_max_m <= gui.processing.sar3d.z_min_m: + raise ValueError("gui.processing.sar3d.z_max_m must be > z_min_m") if gui.processing.sar3d.ground_depth_m < 0.0: raise ValueError("gui.processing.sar3d.ground_depth_m must be >= 0") if gui.processing.sar3d.tx_per_segment < 1: @@ -849,9 +871,12 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]: "pitch_override_enabled": gui.processing.sar3d.pitch_override_enabled, "pitch_deg": gui.processing.sar3d.pitch_deg, "ground_depth_m": gui.processing.sar3d.ground_depth_m, - "x_margin_m": gui.processing.sar3d.x_margin_m, + "x_min_m": gui.processing.sar3d.x_min_m, + "x_max_m": gui.processing.sar3d.x_max_m, "y_min_m": gui.processing.sar3d.y_min_m, "y_max_m": gui.processing.sar3d.y_max_m, + "z_min_m": gui.processing.sar3d.z_min_m, + "z_max_m": gui.processing.sar3d.z_max_m, "voxel_x_m": gui.processing.sar3d.voxel_x_m, "voxel_y_m": gui.processing.sar3d.voxel_y_m, "voxel_z_m": gui.processing.sar3d.voxel_z_m, diff --git a/python_app/models/gui_profile_schema.py b/python_app/models/gui_profile_schema.py index c0f7f66..7b3dd84 100644 --- a/python_app/models/gui_profile_schema.py +++ b/python_app/models/gui_profile_schema.py @@ -148,9 +148,12 @@ class GuiSar3dStateModel: pitch_override_enabled: bool = False pitch_deg: float = 17.5 ground_depth_m: float = 1.25 - x_margin_m: float = 2.0 + x_min_m: float = -2.0 + x_max_m: float = 2.0 y_min_m: float = -0.5 y_max_m: float = 2.0 + z_min_m: float = 0.0 + z_max_m: float = 8.0 voxel_x_m: float = 0.025 voxel_y_m: float = 0.02 voxel_z_m: float = 0.02 diff --git a/python_app/orchestration/live_processing_config.py b/python_app/orchestration/live_processing_config.py index 0691abc..87e45d4 100644 --- a/python_app/orchestration/live_processing_config.py +++ b/python_app/orchestration/live_processing_config.py @@ -82,11 +82,15 @@ class ProcessingLiveConfig: sar3d_pitch_deg: float = 17.5 # Height of the lower antenna row above the ground (scene Y=0 = ground). sar3d_ground_depth_m: float = 1.25 - # Full-region grid: X spans the antennas +- margin, Y is height above the - # ground, Z covers the radar depth gate over the Y range. - sar3d_x_margin_m: float = 2.0 + # Scene grid bounds (metres): X across, Y height above the ground, Z along + # the ground away from the radar. Voxels outside the radar depth gate are + # zeroed by the mask regardless of the Z window. + sar3d_x_min_m: float = -2.0 + sar3d_x_max_m: float = 2.0 sar3d_y_min_m: float = -0.5 sar3d_y_max_m: float = 2.0 + sar3d_z_min_m: float = 0.0 + sar3d_z_max_m: float = 8.0 sar3d_voxel_x_m: float = 0.025 sar3d_voxel_y_m: float = 0.02 sar3d_voxel_z_m: float = 0.02 @@ -171,9 +175,12 @@ class ProcessingLiveConfig: "sar3d_pitch_override_enabled": bool(self.sar3d_pitch_override_enabled), "sar3d_pitch_deg": float(self.sar3d_pitch_deg), "sar3d_ground_depth_m": float(self.sar3d_ground_depth_m), - "sar3d_x_margin_m": float(self.sar3d_x_margin_m), + "sar3d_x_min_m": float(self.sar3d_x_min_m), + "sar3d_x_max_m": float(self.sar3d_x_max_m), "sar3d_y_min_m": float(self.sar3d_y_min_m), "sar3d_y_max_m": float(self.sar3d_y_max_m), + "sar3d_z_min_m": float(self.sar3d_z_min_m), + "sar3d_z_max_m": float(self.sar3d_z_max_m), "sar3d_voxel_x_m": float(self.sar3d_voxel_x_m), "sar3d_voxel_y_m": float(self.sar3d_voxel_y_m), "sar3d_voxel_z_m": float(self.sar3d_voxel_z_m), diff --git a/python_app/scripts/replay_smoke_run.py b/python_app/scripts/replay_smoke_run.py index 2d56ab6..50bf745 100644 --- a/python_app/scripts/replay_smoke_run.py +++ b/python_app/scripts/replay_smoke_run.py @@ -89,9 +89,12 @@ def _sar3d_live_config(profile_path: Path) -> ProcessingLiveConfig: ) live.sar3d_pitch_deg = float(sar3d.get("pitch_deg", live.sar3d_pitch_deg)) live.sar3d_ground_depth_m = float(sar3d.get("ground_depth_m", live.sar3d_ground_depth_m)) - live.sar3d_x_margin_m = float(sar3d.get("x_margin_m", live.sar3d_x_margin_m)) + live.sar3d_x_min_m = float(sar3d.get("x_min_m", live.sar3d_x_min_m)) + live.sar3d_x_max_m = float(sar3d.get("x_max_m", live.sar3d_x_max_m)) live.sar3d_y_min_m = float(sar3d.get("y_min_m", live.sar3d_y_min_m)) live.sar3d_y_max_m = float(sar3d.get("y_max_m", live.sar3d_y_max_m)) + live.sar3d_z_min_m = float(sar3d.get("z_min_m", live.sar3d_z_min_m)) + live.sar3d_z_max_m = float(sar3d.get("z_max_m", live.sar3d_z_max_m)) live.sar3d_voxel_x_m = float(sar3d.get("voxel_x_m", live.sar3d_voxel_x_m)) live.sar3d_voxel_y_m = float(sar3d.get("voxel_y_m", live.sar3d_voxel_y_m)) live.sar3d_voxel_z_m = float(sar3d.get("voxel_z_m", live.sar3d_voxel_z_m))