diff --git a/.gitignore b/.gitignore index 7839ad8..deb8359 100644 --- a/.gitignore +++ b/.gitignore @@ -86,7 +86,7 @@ target/ # Jupyter Notebook .ipynb_checkpoints - +202609144 # IPython profile_default/ ipython_config.py diff --git a/Makefile b/Makefile index a7c04af..1f4f621 100644 --- a/Makefile +++ b/Makefile @@ -119,24 +119,24 @@ TARGETS := \ $(BIN_DIR)/data_preprocessor \ $(BIN_DIR)/data_processor +.PHONY: all clean kamil_adc_collector sar3d_parity + +all: $(TARGETS) + +kamil_adc_collector: $(BIN_DIR)/kamil_adc_collector + # On-demand parity check of the sar3d processor against the gpr_cpp # standalone_bp example (same .npy dataset). Not part of `all`. SAR3D_PARITY_OBJS := $(addprefix $(BUILD_DIR)/, \ data_acq_and_processing/processing/tools/sar3d_parity_main.o \ data_acq_and_processing/processing/processors/src/sar3d_processor.o) -.PHONY: all clean kamil_adc_collector sar3d_parity - sar3d_parity: $(BIN_DIR)/sar3d_parity $(BIN_DIR)/sar3d_parity: $(SAR3D_PARITY_OBJS) $(GPR_CPP_LIB) @mkdir -p $(BIN_DIR) $(CXX) $(SAR3D_PARITY_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS) -all: $(TARGETS) - -kamil_adc_collector: $(BIN_DIR)/kamil_adc_collector - $(BIN_DIR)/sweep_orchestrator: $(SWEEP_ORCH_OBJS) @mkdir -p $(BIN_DIR) $(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS) 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 64089ad..d55a605 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 @@ -105,6 +105,10 @@ struct ProcessingLiveConfig { std::uint32_t sar3d_grid_nz = 160U; bool sar3d_use_compensation = true; bool sar3d_normalize_pairs = true; + // Bottom-view projection angle around the vertical Y axis, degrees: + // 0 = front view (X horizontal), 90 = side view (Z horizontal), + // intermediate values project along the rotated horizontal direction. + float sar3d_bottom_view_deg = 0.0F; // When true, the data_processor ignores socket-supplied `vlc` updates and // keeps using `gpr_speed_m_s` from this file. Mirrored from the GUI's // "ignore socket speed" checkbox. 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 05a4ea5..2b236ca 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 @@ -396,6 +396,7 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s std::pair{"sar3d_half_width_x_m", &config.sar3d_half_width_x_m}, std::pair{"sar3d_half_width_y_m", &config.sar3d_half_width_y_m}, std::pair{"sar3d_half_width_z_m", &config.sar3d_half_width_z_m}, + std::pair{"sar3d_bottom_view_deg", &config.sar3d_bottom_view_deg}, }) { if (const auto found = root.find(key); found != root.end()) { if (!found->is_number()) { 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 fd6609f..90af580 100644 --- a/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp +++ b/data_acq_and_processing/processing/processors/src/sar3d_processor.cpp @@ -457,6 +457,15 @@ auto Sar3dProcessor::process_collection( // Finalization (port of gpr_core sar_finish_volume, minus smoothing): // |acc/(cnt+eps)|, ground-coordinate depth gate, normalization to peak. + // + // Two projections match the legacy render conventions (X across, + // Z range/"вдаль", Y up): + // - top view `sar3d_proj_xz`: max over Y, axes X (horizontal) / Z; + // - bottom view `sar3d_proj_bottom`: vertical axis is always Y, the + // horizontal axis u is the ground direction rotated by + // `sar3d_bottom_view_deg` around Y: 0° = front view (u = X, max + // along Z), 90° = side view (u = Z, max along X), intermediate + // angles project along the rotated perpendicular. const int nx = volume.NX; const int ny = volume.NY; const int nz = volume.NZ; @@ -464,8 +473,36 @@ auto Sar3dProcessor::process_collection( const double cos_p = std::cos(pitch_rad); const double sin_p = std::sin(pitch_rad); + const double theta_deg = std::clamp(static_cast(live_config.sar3d_bottom_view_deg), 0.0, 90.0); + const double theta_rad = theta_deg * M_PI / 180.0; + const double cos_t = std::cos(theta_rad); + const double sin_t = std::sin(theta_rad); + // Exact-axis fast paths keep the front/side views on the native grid. + const bool front_view = theta_deg <= 1e-9; + const bool side_view = theta_deg >= 90.0 - 1e-9; + std::vector u_axis{}; + if (front_view) { + u_axis = grid.x; + } else if (side_view) { + u_axis = grid.z; + } else { + const double u_a = grid.x.front() * cos_t + grid.z.front() * sin_t; + const double u_b = grid.x.front() * cos_t + grid.z.back() * sin_t; + const double u_c = grid.x.back() * cos_t + grid.z.front() * sin_t; + const double u_d = grid.x.back() * cos_t + grid.z.back() * sin_t; + const double u_min = std::min(std::min(u_a, u_b), std::min(u_c, u_d)); + const double u_max = std::max(std::max(u_a, u_b), std::max(u_c, u_d)); + u_axis.resize(static_cast(nx)); + for (int iu = 0; iu < nx; ++iu) { + u_axis[static_cast(iu)] = u_min + (u_max - u_min) * iu / static_cast(nx - 1); + } + } + const int nu = static_cast(u_axis.size()); + const double u_start = u_axis.front(); + const double inv_du = nu > 1 ? static_cast(nu - 1) / (u_axis.back() - u_start) : 0.0; + std::vector projection_xz(static_cast(nz) * nx, 0.0F); - std::vector projection_xy(static_cast(ny) * nx, 0.0F); + std::vector projection_bottom(static_cast(ny) * nu, 0.0F); float peak = 0.0F; for (int iy = 0; iy < ny; ++iy) { for (int iz = 0; iz < nz; ++iz) { @@ -475,6 +512,8 @@ auto Sar3dProcessor::process_collection( continue; } const std::size_t row = (static_cast(iy) * nz + iz) * nx; + const std::size_t bottom_row = static_cast(iy) * nu; + const double z_sin_t = grid.z[iz] * sin_t; for (int ix = 0; ix < nx; ++ix) { const std::size_t voxel = row + static_cast(ix); const float value = std::abs(volume.acc[voxel]) / @@ -482,8 +521,16 @@ auto Sar3dProcessor::process_collection( peak = std::max(peak, value); auto& xz_cell = projection_xz[static_cast(iz) * nx + ix]; xz_cell = std::max(xz_cell, value); - auto& xy_cell = projection_xy[static_cast(iy) * nx + ix]; - xy_cell = std::max(xy_cell, value); + int bin = ix; + if (side_view) { + bin = iz; + } else if (!front_view) { + const double u = grid.x[ix] * cos_t + z_sin_t; + bin = static_cast(std::lround((u - u_start) * inv_du)); + bin = std::clamp(bin, 0, nu - 1); + } + auto& bottom_cell = projection_bottom[bottom_row + static_cast(bin)]; + bottom_cell = std::max(bottom_cell, value); } } } @@ -491,7 +538,7 @@ auto Sar3dProcessor::process_collection( for (auto& value : projection_xz) { value /= peak; } - for (auto& value : projection_xy) { + for (auto& value : projection_bottom) { value /= peak; } } @@ -500,7 +547,7 @@ auto Sar3dProcessor::process_collection( build_image_payload("sar3d_proj_xz", grid.x, grid.z, std::move(projection_xz)) ); results.collection_payloads.push_back( - build_image_payload("sar3d_proj_xy", grid.x, grid.y, std::move(projection_xy)) + build_image_payload("sar3d_proj_bottom", u_axis, grid.y, std::move(projection_bottom)) ); ipc::ResultPayload peak_payload{}; peak_payload.processing_name = "sar3d_peak"; 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 2637f25..9fa77c8 100644 --- a/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp +++ b/data_acq_and_processing/processing/tools/sar3d_parity_main.cpp @@ -164,7 +164,11 @@ auto load_collection( return collection; } -void print_projection_argmax(const radar::ipc::ResultPayload& payload, const char* row_axis_name) { +void print_projection_argmax( + const radar::ipc::ResultPayload& payload, + const char* col_axis_name, + const char* row_axis_name +) { std::size_t argmax = 0U; float best = -1.0F; for (std::size_t index = 0U; index < payload.image_values.size(); ++index) { @@ -177,9 +181,10 @@ void print_projection_argmax(const radar::ipc::ResultPayload& payload, const cha const auto col = argmax % nx; const auto row = argmax / nx; std::printf( - "%s: max=%.6f at X=%.1f cm, %s=%.1f cm\n", + "%s: max=%.6f at %s=%.1f cm, %s=%.1f cm\n", payload.processing_name.c_str(), static_cast(best), + col_axis_name, static_cast(payload.image_x_axis[col]) * 100.0, row_axis_name, static_cast(payload.image_y_axis[row]) * 100.0 @@ -253,10 +258,26 @@ int main(int argc, char** argv) { } } for (const auto& payload : result.collection_payloads) { - if (payload.kind != radar::ipc::ResultKind::ImageF32) { - continue; + if (payload.kind == radar::ipc::ResultKind::ImageF32 && payload.processing_name == "sar3d_proj_xz") { + print_projection_argmax(payload, "X", "Z"); + } + } + + // Bottom view across the selectable angles: the target's u coordinate must + // move smoothly from its X (front, 0 deg) to its Z (side, 90 deg). + const auto last_previous = + std::span(history.data(), history.size() - 1U); + for (const float angle_deg : {0.0F, 30.0F, 60.0F, 90.0F}) { + auto live_angle = live; + live_angle.sar3d_bottom_view_deg = angle_deg; + const auto angle_result = + processor.process_collection(run_config, history.back(), last_previous, live_angle); + for (const auto& payload : angle_result.collection_payloads) { + if (payload.kind == radar::ipc::ResultKind::ImageF32 && payload.processing_name == "sar3d_proj_bottom") { + std::printf("bottom @ %2.0f deg -> ", static_cast(angle_deg)); + print_projection_argmax(payload, "u", "Y"); + } } - print_projection_argmax(payload, payload.processing_name == "sar3d_proj_xz" ? "Z" : "Y"); } std::printf("OK: sar3d processor path completed\n"); return 0; 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 2cc212f..0b410c3 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 @@ -13,6 +13,7 @@ from PyQt6.QtWidgets import ( ) from python_app.gui.runtime.constraints import validate_processing_mode_constraints +from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label from python_app.orchestration.live_processing_config import ProcessingLiveConfig _GPR_MODES = ("gpr", "legacy_gpr") @@ -99,6 +100,7 @@ _WEB_LIVE_SCHEMA = [ ("sar3d_grid_nz", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nz")), ("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")), ("sar3d_normalize_pairs", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_pairs")), + ("sar3d_bottom_view_deg", "3D SAR", ("sar3d",), _attr("_sar3d_bottom_view")), ] _WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA} @@ -285,6 +287,10 @@ class AppWindowLiveProcessingMixin: # Switch-position CSV text -> integer lists. values["gpr_input_positions"] = self._parse_csv_int_list(values["gpr_input_positions"]) values["gpr_output_positions"] = self._parse_csv_int_list(values["gpr_output_positions"]) + # Bottom-view combo label -> projection angle in degrees. + values["sar3d_bottom_view_deg"] = sar3d_bottom_view_deg_from_label( + str(values["sar3d_bottom_view_deg"]) + ) # Order-normalize the pass-through Y range so a reversed entry still works. y_min_db, y_max_db = values["pass_through_y_min_db"], values["pass_through_y_max_db"] values["pass_through_y_min_db"] = min(y_min_db, y_max_db) 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 91ad953..8fbd169 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 @@ -10,6 +10,7 @@ from pathlib import Path from PyQt6.QtCore import QSignalBlocker from PyQt6.QtWidgets import QFileDialog +from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_label_from_deg from python_app.models.gui_profile_model import GuiProfileModel from python_app.models.run_config_model import RunConfigModel from python_app.orchestration.preprocess_assets import ( @@ -571,6 +572,10 @@ class AppWindowConfigProfileIOMixin: self._sar3d_grid_nz.setValue(int(sar3d_state.grid_nz)) self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation)) self._sar3d_normalize_pairs.setChecked(bool(sar3d_state.normalize_pairs)) + self._set_combo_current_text( + self._sar3d_bottom_view, + sar3d_bottom_view_label_from_deg(float(sar3d_state.bottom_view_deg)), + ) self._save_count.setValue(int(gui_state.data_actions.save_count)) self._record_count.setValue(int(gui_state.data_actions.record_count)) 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 aef709b..e2b8b09 100644 --- a/python_app/gui/controllers/app_window_config/state_builders.py +++ b/python_app/gui/controllers/app_window_config/state_builders.py @@ -25,6 +25,7 @@ from python_app.orchestration.preprocess_assets import ( VISIBLE_PREPROCESS_ASSET_KEYS, preprocess_asset_model, ) +from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label from python_app.storage.npz_store import radar_key_from_config @@ -447,6 +448,9 @@ class AppWindowConfigStateBuildersMixin: grid_nz=int(self._sar3d_grid_nz.value()), use_compensation=bool(self._sar3d_use_compensation.isChecked()), normalize_pairs=bool(self._sar3d_normalize_pairs.isChecked()), + bottom_view_deg=sar3d_bottom_view_deg_from_label( + self._sar3d_bottom_view.currentText() + ), ), ), data_actions=GuiDataActionsStateModel( diff --git a/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py b/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py index dbfa9e4..2c5253d 100644 --- a/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py +++ b/python_app/gui/controllers/app_window_plot/sar3d_plot_mixin.py @@ -1,14 +1,18 @@ """3D SAR projection rendering helpers. The sar3d processor publishes two normalized max-projections of the imaged -volume as collection-level ImageF32 payloads: +volume as collection-level ImageF32 payloads, matching the legacy render +conventions (X across, Z range/"вдаль", Y up): -- ``sar3d_proj_xz`` — side view: X (m) horizontal, Z/range (m) vertical; -- ``sar3d_proj_xy`` — top view: X (m) horizontal, Y (m) vertical; +- ``sar3d_proj_xz`` — top view: X (m) horizontal, Z/range (m) vertical + (max over the height Y), like the classic GPR heatmap; +- ``sar3d_proj_bottom`` — switchable bottom view: height Y (m) vertical, the + horizontal axis is the ground direction rotated by ``sar3d_bottom_view_deg`` + around Y (0° = front view X, 90° = side view Z, 30°/60° in between); plus ``sar3d_peak`` (ScalarF32), the volume peak before normalization. Both projections are drawn on their own PlotWidgets stacked in one plot-stack page -(side view on top), mirroring how the pass-through page stacks its two plots. +(top view on top), mirroring how the pass-through page stacks its two plots. """ from __future__ import annotations @@ -17,23 +21,39 @@ from PyQt6.QtCore import QRectF import numpy as np import pyqtgraph as pg +from python_app.gui.runtime.sar3d_views import sar3d_bottom_view_deg_from_label from python_app.models.dataset_model import ResultCollection class AppWindowSar3dPlotMixin: """Renders 3D SAR volume projections published by the sar3d processor.""" + def _sar3d_bottom_view_labels(self) -> tuple[str, str]: + """Return (plot title, horizontal axis label) for the active bottom view. + + The plot surfaces are created before the processing section builds the + bottom-view combo, so fall back to the front view until it exists. + """ + combo = getattr(self, "_sar3d_bottom_view", None) + degrees = sar3d_bottom_view_deg_from_label(combo.currentText()) if combo is not None else 0.0 + if degrees <= 1e-9: + return "3D SAR front view (X-Y)", "X" + if degrees >= 90.0 - 1e-9: + return "3D SAR side view (Z-Y)", "Range Z" + return f"3D SAR view @ {degrees:g}\N{DEGREE SIGN}", f"u({degrees:g}\N{DEGREE SIGN})" + def _configure_sar3d_plot_axes(self) -> None: """Apply persistent axis labels and view settings to both projection plots.""" - for plot, y_label in ( - (self._sar3d_xz_plot, "Range Z"), - (self._sar3d_xy_plot, "Cross-range Y"), + _bottom_title, bottom_x_label = self._sar3d_bottom_view_labels() + for plot, x_label, y_label in ( + (self._sar3d_xz_plot, "X", "Range Z"), + (self._sar3d_xy_plot, bottom_x_label, "Height Y"), ): plot_item = plot.getPlotItem() plot_item.showAxis("left", show=True) plot_item.showAxis("bottom", show=True) plot_item.setClipToView(True) - plot.setLabel("bottom", "X", units="m") + plot.setLabel("bottom", x_label, units="m") plot.setLabel("left", y_label, units="m") view_box = plot.getViewBox() view_box.invertY(False) @@ -69,8 +89,9 @@ class AppWindowSar3dPlotMixin: self._sar3d_xz_image_item.hide() if self._sar3d_xy_image_item is not None: self._sar3d_xy_image_item.hide() - self._sar3d_xz_plot.setTitle("3D SAR side view (X-Z)") - self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)") + bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels() + self._sar3d_xz_plot.setTitle("3D SAR top view (X-Z)") + self._sar3d_xy_plot.setTitle(bottom_title) def _draw_sar3d_projection(self, plot: pg.PlotWidget, image_item: pg.ImageItem, payload) -> bool: """Draw one normalized projection payload onto its plot; return success.""" @@ -98,10 +119,10 @@ class AppWindowSar3dPlotMixin: return True def _draw_sar3d_projections(self, collection: ResultCollection) -> bool: - """Draw the latest collection-level 3D SAR side and top projections.""" + """Draw the latest collection-level 3D SAR top and bottom projections.""" xz_payload = self._collection_payload_by_name(collection, "sar3d_proj_xz", kind=3) - xy_payload = self._collection_payload_by_name(collection, "sar3d_proj_xy", kind=3) - if xz_payload is None and xy_payload is None: + bottom_payload = self._collection_payload_by_name(collection, "sar3d_proj_bottom", kind=3) + if xz_payload is None and bottom_payload is None: self._clear_sar3d_plot() return False @@ -109,13 +130,15 @@ class AppWindowSar3dPlotMixin: plot.setUpdatesEnabled(False) try: self._ensure_sar3d_plot_items() + # Axis labels track the bottom-view selector (payload axes change with it). + self._configure_sar3d_plot_axes() xz_drawn = xz_payload is not None and self._draw_sar3d_projection( self._sar3d_xz_plot, self._sar3d_xz_image_item, xz_payload ) - xy_drawn = xy_payload is not None and self._draw_sar3d_projection( - self._sar3d_xy_plot, self._sar3d_xy_image_item, xy_payload + bottom_drawn = bottom_payload is not None and self._draw_sar3d_projection( + self._sar3d_xy_plot, self._sar3d_xy_image_item, bottom_payload ) - if not xz_drawn and not xy_drawn: + if not xz_drawn and not bottom_drawn: self._clear_sar3d_plot() return False @@ -123,8 +146,9 @@ class AppWindowSar3dPlotMixin: peak_suffix = "" if peak_payload is not None: peak_suffix = f" | peak={float(peak_payload.scalar_value):.3g}" - self._sar3d_xz_plot.setTitle(f"3D SAR side view (X-Z){peak_suffix}") - self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)") + bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels() + self._sar3d_xz_plot.setTitle(f"3D SAR top view (X-Z){peak_suffix}") + self._sar3d_xy_plot.setTitle(bottom_title) finally: for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot): plot.setUpdatesEnabled(True) diff --git a/python_app/gui/controllers/sections/processing_section.py b/python_app/gui/controllers/sections/processing_section.py index 10eaf3e..161d59a 100644 --- a/python_app/gui/controllers/sections/processing_section.py +++ b/python_app/gui/controllers/sections/processing_section.py @@ -20,6 +20,10 @@ from PyQt6.QtWidgets import ( from python_app.gui.controllers.app_window_config.state_builders import GUI_SAVE_HISTORY_LIMIT from python_app.gui.controllers.sections.layout_helpers import FormRow, build_two_column_form_widget +from python_app.gui.runtime.sar3d_views import ( + SAR3D_BOTTOM_VIEW_CHOICES, + sar3d_bottom_view_label_from_deg, +) def _format_geometry_row(position: int, x_m: float, y_m: float, z_m: float) -> str: @@ -706,6 +710,18 @@ def build_processing_group(owner) -> QGroupBox: owner._sar3d_normalize_pairs = QCheckBox("Normalize Tx/Rx pairs") owner._sar3d_normalize_pairs.setChecked(bool(sar3d_defaults.normalize_pairs)) + owner._sar3d_bottom_view = QComboBox() + owner._sar3d_bottom_view.addItems([label for label, _deg in SAR3D_BOTTOM_VIEW_CHOICES]) + owner._set_combo_current_text( + owner._sar3d_bottom_view, + sar3d_bottom_view_label_from_deg(float(sar3d_defaults.bottom_view_deg)), + ) + owner._sar3d_bottom_view.setToolTip( + "Bottom plot projection: front view (X-Y), side view (Z-Y) or an " + "intermediate rotation around the vertical axis. The top plot always " + "shows the X-Z top view." + ) + sar3d_page = _build_processing_mode_page( owner._processing_mode_pages, [ @@ -730,6 +746,7 @@ def build_processing_group(owner) -> QGroupBox: ("Grid Nx", owner._sar3d_grid_nx), ("Grid Ny", owner._sar3d_grid_ny), ("Grid Nz", owner._sar3d_grid_nz), + ("Bottom view", owner._sar3d_bottom_view), owner._sar3d_use_compensation, owner._sar3d_normalize_pairs, ], @@ -832,6 +849,7 @@ def build_processing_group(owner) -> QGroupBox: owner._sar3d_grid_nz.valueChanged.connect(owner._on_processing_live_settings_changed) owner._sar3d_use_compensation.toggled.connect(owner._on_processing_live_settings_changed) owner._sar3d_normalize_pairs.toggled.connect(owner._on_processing_live_settings_changed) + owner._sar3d_bottom_view.currentTextChanged.connect(owner._on_processing_live_settings_changed) owner._set_processing_mode_page(owner._processing_mode.currentText()) return group diff --git a/python_app/gui/runtime/sar3d_views.py b/python_app/gui/runtime/sar3d_views.py new file mode 100644 index 0000000..b5dda7c --- /dev/null +++ b/python_app/gui/runtime/sar3d_views.py @@ -0,0 +1,30 @@ +"""Shared mapping for the sar3d bottom-view projection choices. + +The bottom plot of the sar3d mode projects the imaged volume along a +horizontal direction rotated around the vertical Y axis: 0° = front view +(X horizontal), 90° = side view (Z horizontal). One shared mapping keeps the +combo box, the live-config build, the profile persistence and the plot labels +consistent. +""" + +from __future__ import annotations + +SAR3D_BOTTOM_VIEW_CHOICES: tuple[tuple[str, float], ...] = ( + ("front (0°)", 0.0), + ("30°", 30.0), + ("60°", 60.0), + ("side (90°)", 90.0), +) + + +def sar3d_bottom_view_deg_from_label(label: str) -> float: + """Map a bottom-view combo label to its projection angle in degrees.""" + for choice_label, degrees in SAR3D_BOTTOM_VIEW_CHOICES: + if choice_label == label: + return degrees + return 0.0 + + +def sar3d_bottom_view_label_from_deg(degrees: float) -> str: + """Map a projection angle to the nearest bottom-view combo label.""" + return min(SAR3D_BOTTOM_VIEW_CHOICES, key=lambda choice: abs(choice[1] - float(degrees)))[0] diff --git a/python_app/models/gui_profile_codec.py b/python_app/models/gui_profile_codec.py index 69b0c7c..2e73211 100644 --- a/python_app/models/gui_profile_codec.py +++ b/python_app/models/gui_profile_codec.py @@ -578,6 +578,12 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel: gui.processing.sar3d.normalize_pairs, "gui.processing.sar3d", ), + bottom_view_deg=_optional_float( + sar3d_object, + "bottom_view_deg", + gui.processing.sar3d.bottom_view_deg, + "gui.processing.sar3d", + ), ), ) if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr", "legacy_gpr", "sar3d"}: @@ -799,6 +805,7 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]: "grid_nz": gui.processing.sar3d.grid_nz, "use_compensation": gui.processing.sar3d.use_compensation, "normalize_pairs": gui.processing.sar3d.normalize_pairs, + "bottom_view_deg": gui.processing.sar3d.bottom_view_deg, }, }, "data_actions": { diff --git a/python_app/models/gui_profile_schema.py b/python_app/models/gui_profile_schema.py index a24aa86..70d0036 100644 --- a/python_app/models/gui_profile_schema.py +++ b/python_app/models/gui_profile_schema.py @@ -155,6 +155,9 @@ class GuiSar3dStateModel: grid_nz: int = 160 use_compensation: bool = True normalize_pairs: bool = True + # Bottom-view projection angle around the vertical Y axis, degrees: + # 0 = front view (X-Y), 90 = side view (Z-Y). + bottom_view_deg: float = 0.0 @dataclass(slots=True) diff --git a/python_app/orchestration/live_processing_config.py b/python_app/orchestration/live_processing_config.py index 5b637ec..1b84cf0 100644 --- a/python_app/orchestration/live_processing_config.py +++ b/python_app/orchestration/live_processing_config.py @@ -87,6 +87,9 @@ class ProcessingLiveConfig: sar3d_grid_nz: int = 160 sar3d_use_compensation: bool = True sar3d_normalize_pairs: bool = True + # Bottom-view projection angle around the vertical Y axis, degrees: + # 0 = front view (X horizontal), 90 = side view (Z horizontal). + sar3d_bottom_view_deg: float = 0.0 # When true, the C++ data_processor ignores socket-supplied vlc updates # and keeps using `gpr_speed_m_s` from this file. ignore_socket_speed: bool = False @@ -163,6 +166,7 @@ class ProcessingLiveConfig: "sar3d_grid_nz": int(self.sar3d_grid_nz), "sar3d_use_compensation": bool(self.sar3d_use_compensation), "sar3d_normalize_pairs": bool(self.sar3d_normalize_pairs), + "sar3d_bottom_view_deg": float(self.sar3d_bottom_view_deg), "ignore_socket_speed": bool(self.ignore_socket_speed), "reprocess_current_result": bool(self.reprocess_current_result), "history_command_seq": int(self.history_command_seq), diff --git a/python_app/scripts/replay_smoke_run.py b/python_app/scripts/replay_smoke_run.py index cbe2597..59987ad 100644 --- a/python_app/scripts/replay_smoke_run.py +++ b/python_app/scripts/replay_smoke_run.py @@ -94,6 +94,7 @@ def _sar3d_live_config(profile_path: Path) -> ProcessingLiveConfig: live.sar3d_grid_nz = int(sar3d.get("grid_nz", live.sar3d_grid_nz)) live.sar3d_use_compensation = bool(sar3d.get("use_compensation", live.sar3d_use_compensation)) live.sar3d_normalize_pairs = bool(sar3d.get("normalize_pairs", live.sar3d_normalize_pairs)) + live.sar3d_bottom_view_deg = float(sar3d.get("bottom_view_deg", live.sar3d_bottom_view_deg)) return live @@ -207,7 +208,7 @@ def main() -> int: ids_monotonic = False last_id = result.collection_id names = {p.processing_name: p for p in result.collection_payloads} - if "sar3d_proj_xz" in names and "sar3d_proj_xy" in names: + if "sar3d_proj_xz" in names and "sar3d_proj_bottom" in names: sar3d_results += 1 peak_payload = names.get("sar3d_peak") if peak_payload is not None: diff --git a/run_config_examples/run_config_replay_vna.example.json b/run_config_examples/run_config_replay_vna.example.json index c293da5..5e545eb 100644 --- a/run_config_examples/run_config_replay_vna.example.json +++ b/run_config_examples/run_config_replay_vna.example.json @@ -191,7 +191,8 @@ "grid_ny": 120, "grid_nz": 160, "use_compensation": true, - "normalize_pairs": true + "normalize_pairs": true, + "bottom_view_deg": 0.0 } }, "preprocess_dialog": {