test new version

This commit is contained in:
awe
2026-09-16 13:22:21 +03:00
parent b3339d101c
commit 16ccafff69
17 changed files with 213 additions and 37 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
202609144
# IPython # IPython
profile_default/ profile_default/
ipython_config.py ipython_config.py
+6 -6
View File
@@ -119,24 +119,24 @@ TARGETS := \
$(BIN_DIR)/data_preprocessor \ $(BIN_DIR)/data_preprocessor \
$(BIN_DIR)/data_processor $(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 # On-demand parity check of the sar3d processor against the gpr_cpp
# standalone_bp example (same .npy dataset). Not part of `all`. # standalone_bp example (same .npy dataset). Not part of `all`.
SAR3D_PARITY_OBJS := $(addprefix $(BUILD_DIR)/, \ SAR3D_PARITY_OBJS := $(addprefix $(BUILD_DIR)/, \
data_acq_and_processing/processing/tools/sar3d_parity_main.o \ data_acq_and_processing/processing/tools/sar3d_parity_main.o \
data_acq_and_processing/processing/processors/src/sar3d_processor.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 sar3d_parity: $(BIN_DIR)/sar3d_parity
$(BIN_DIR)/sar3d_parity: $(SAR3D_PARITY_OBJS) $(GPR_CPP_LIB) $(BIN_DIR)/sar3d_parity: $(SAR3D_PARITY_OBJS) $(GPR_CPP_LIB)
@mkdir -p $(BIN_DIR) @mkdir -p $(BIN_DIR)
$(CXX) $(SAR3D_PARITY_OBJS) -o $@ $(DATA_PROCESSOR_LDFLAGS) $(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) $(BIN_DIR)/sweep_orchestrator: $(SWEEP_ORCH_OBJS)
@mkdir -p $(BIN_DIR) @mkdir -p $(BIN_DIR)
$(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS) $(CXX) $(SWEEP_ORCH_OBJS) -o $@ $(ORCH_LDFLAGS)
@@ -105,6 +105,10 @@ struct ProcessingLiveConfig {
std::uint32_t sar3d_grid_nz = 160U; std::uint32_t sar3d_grid_nz = 160U;
bool sar3d_use_compensation = true; bool sar3d_use_compensation = true;
bool sar3d_normalize_pairs = 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 // 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 // keeps using `gpr_speed_m_s` from this file. Mirrored from the GUI's
// "ignore socket speed" checkbox. // "ignore socket speed" checkbox.
@@ -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_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_y_m", &config.sar3d_half_width_y_m},
std::pair{"sar3d_half_width_z_m", &config.sar3d_half_width_z_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 (const auto found = root.find(key); found != root.end()) {
if (!found->is_number()) { if (!found->is_number()) {
@@ -457,6 +457,15 @@ auto Sar3dProcessor::process_collection(
// Finalization (port of gpr_core sar_finish_volume, minus smoothing): // Finalization (port of gpr_core sar_finish_volume, minus smoothing):
// |acc/(cnt+eps)|, ground-coordinate depth gate, normalization to peak. // |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 nx = volume.NX;
const int ny = volume.NY; const int ny = volume.NY;
const int nz = volume.NZ; const int nz = volume.NZ;
@@ -464,8 +473,36 @@ auto Sar3dProcessor::process_collection(
const double cos_p = std::cos(pitch_rad); const double cos_p = std::cos(pitch_rad);
const double sin_p = std::sin(pitch_rad); const double sin_p = std::sin(pitch_rad);
const double theta_deg = std::clamp(static_cast<double>(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<double> 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<std::size_t>(nx));
for (int iu = 0; iu < nx; ++iu) {
u_axis[static_cast<std::size_t>(iu)] = u_min + (u_max - u_min) * iu / static_cast<double>(nx - 1);
}
}
const int nu = static_cast<int>(u_axis.size());
const double u_start = u_axis.front();
const double inv_du = nu > 1 ? static_cast<double>(nu - 1) / (u_axis.back() - u_start) : 0.0;
std::vector<float> projection_xz(static_cast<std::size_t>(nz) * nx, 0.0F); std::vector<float> projection_xz(static_cast<std::size_t>(nz) * nx, 0.0F);
std::vector<float> projection_xy(static_cast<std::size_t>(ny) * nx, 0.0F); std::vector<float> projection_bottom(static_cast<std::size_t>(ny) * nu, 0.0F);
float peak = 0.0F; float peak = 0.0F;
for (int iy = 0; iy < ny; ++iy) { for (int iy = 0; iy < ny; ++iy) {
for (int iz = 0; iz < nz; ++iz) { for (int iz = 0; iz < nz; ++iz) {
@@ -475,6 +512,8 @@ auto Sar3dProcessor::process_collection(
continue; continue;
} }
const std::size_t row = (static_cast<std::size_t>(iy) * nz + iz) * nx; const std::size_t row = (static_cast<std::size_t>(iy) * nz + iz) * nx;
const std::size_t bottom_row = static_cast<std::size_t>(iy) * nu;
const double z_sin_t = grid.z[iz] * sin_t;
for (int ix = 0; ix < nx; ++ix) { for (int ix = 0; ix < nx; ++ix) {
const std::size_t voxel = row + static_cast<std::size_t>(ix); const std::size_t voxel = row + static_cast<std::size_t>(ix);
const float value = std::abs(volume.acc[voxel]) / const float value = std::abs(volume.acc[voxel]) /
@@ -482,8 +521,16 @@ auto Sar3dProcessor::process_collection(
peak = std::max(peak, value); peak = std::max(peak, value);
auto& xz_cell = projection_xz[static_cast<std::size_t>(iz) * nx + ix]; auto& xz_cell = projection_xz[static_cast<std::size_t>(iz) * nx + ix];
xz_cell = std::max(xz_cell, value); xz_cell = std::max(xz_cell, value);
auto& xy_cell = projection_xy[static_cast<std::size_t>(iy) * nx + ix]; int bin = ix;
xy_cell = std::max(xy_cell, value); if (side_view) {
bin = iz;
} else if (!front_view) {
const double u = grid.x[ix] * cos_t + z_sin_t;
bin = static_cast<int>(std::lround((u - u_start) * inv_du));
bin = std::clamp(bin, 0, nu - 1);
}
auto& bottom_cell = projection_bottom[bottom_row + static_cast<std::size_t>(bin)];
bottom_cell = std::max(bottom_cell, value);
} }
} }
} }
@@ -491,7 +538,7 @@ auto Sar3dProcessor::process_collection(
for (auto& value : projection_xz) { for (auto& value : projection_xz) {
value /= peak; value /= peak;
} }
for (auto& value : projection_xy) { for (auto& value : projection_bottom) {
value /= peak; value /= peak;
} }
} }
@@ -500,7 +547,7 @@ auto Sar3dProcessor::process_collection(
build_image_payload("sar3d_proj_xz", grid.x, grid.z, std::move(projection_xz)) build_image_payload("sar3d_proj_xz", grid.x, grid.z, std::move(projection_xz))
); );
results.collection_payloads.push_back( 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{}; ipc::ResultPayload peak_payload{};
peak_payload.processing_name = "sar3d_peak"; peak_payload.processing_name = "sar3d_peak";
@@ -164,7 +164,11 @@ auto load_collection(
return 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; std::size_t argmax = 0U;
float best = -1.0F; float best = -1.0F;
for (std::size_t index = 0U; index < payload.image_values.size(); ++index) { 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 col = argmax % nx;
const auto row = argmax / nx; const auto row = argmax / nx;
std::printf( 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(), payload.processing_name.c_str(),
static_cast<double>(best), static_cast<double>(best),
col_axis_name,
static_cast<double>(payload.image_x_axis[col]) * 100.0, static_cast<double>(payload.image_x_axis[col]) * 100.0,
row_axis_name, row_axis_name,
static_cast<double>(payload.image_y_axis[row]) * 100.0 static_cast<double>(payload.image_y_axis[row]) * 100.0
@@ -253,10 +258,26 @@ int main(int argc, char** argv) {
} }
} }
for (const auto& payload : result.collection_payloads) { for (const auto& payload : result.collection_payloads) {
if (payload.kind != radar::ipc::ResultKind::ImageF32) { if (payload.kind == radar::ipc::ResultKind::ImageF32 && payload.processing_name == "sar3d_proj_xz") {
continue; 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<const radar::ipc::PreprocessedCollection>(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<double>(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"); std::printf("OK: sar3d processor path completed\n");
return 0; return 0;
@@ -13,6 +13,7 @@ from PyQt6.QtWidgets import (
) )
from python_app.gui.runtime.constraints import validate_processing_mode_constraints 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 from python_app.orchestration.live_processing_config import ProcessingLiveConfig
_GPR_MODES = ("gpr", "legacy_gpr") _GPR_MODES = ("gpr", "legacy_gpr")
@@ -99,6 +100,7 @@ _WEB_LIVE_SCHEMA = [
("sar3d_grid_nz", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nz")), ("sar3d_grid_nz", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nz")),
("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")), ("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")),
("sar3d_normalize_pairs", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_pairs")), ("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} _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. # Switch-position CSV text -> integer lists.
values["gpr_input_positions"] = self._parse_csv_int_list(values["gpr_input_positions"]) 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"]) 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. # 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"] 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) values["pass_through_y_min_db"] = min(y_min_db, y_max_db)
@@ -10,6 +10,7 @@ from pathlib import Path
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QFileDialog 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.gui_profile_model import GuiProfileModel
from python_app.models.run_config_model import RunConfigModel from python_app.models.run_config_model import RunConfigModel
from python_app.orchestration.preprocess_assets import ( 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_grid_nz.setValue(int(sar3d_state.grid_nz))
self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation)) self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation))
self._sar3d_normalize_pairs.setChecked(bool(sar3d_state.normalize_pairs)) 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._save_count.setValue(int(gui_state.data_actions.save_count))
self._record_count.setValue(int(gui_state.data_actions.record_count)) self._record_count.setValue(int(gui_state.data_actions.record_count))
@@ -25,6 +25,7 @@ from python_app.orchestration.preprocess_assets import (
VISIBLE_PREPROCESS_ASSET_KEYS, VISIBLE_PREPROCESS_ASSET_KEYS,
preprocess_asset_model, 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 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()), grid_nz=int(self._sar3d_grid_nz.value()),
use_compensation=bool(self._sar3d_use_compensation.isChecked()), use_compensation=bool(self._sar3d_use_compensation.isChecked()),
normalize_pairs=bool(self._sar3d_normalize_pairs.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( data_actions=GuiDataActionsStateModel(
@@ -1,14 +1,18 @@
"""3D SAR projection rendering helpers. """3D SAR projection rendering helpers.
The sar3d processor publishes two normalized max-projections of the imaged 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_xz`` — top view: X (m) horizontal, Z/range (m) vertical
- ``sar3d_proj_xy`` — top view: X (m) horizontal, Y (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 plus ``sar3d_peak`` (ScalarF32), the volume peak before normalization. Both
projections are drawn on their own PlotWidgets stacked in one plot-stack page 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 from __future__ import annotations
@@ -17,23 +21,39 @@ from PyQt6.QtCore import QRectF
import numpy as np import numpy as np
import pyqtgraph as pg 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 from python_app.models.dataset_model import ResultCollection
class AppWindowSar3dPlotMixin: class AppWindowSar3dPlotMixin:
"""Renders 3D SAR volume projections published by the sar3d processor.""" """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: def _configure_sar3d_plot_axes(self) -> None:
"""Apply persistent axis labels and view settings to both projection plots.""" """Apply persistent axis labels and view settings to both projection plots."""
for plot, y_label in ( _bottom_title, bottom_x_label = self._sar3d_bottom_view_labels()
(self._sar3d_xz_plot, "Range Z"), for plot, x_label, y_label in (
(self._sar3d_xy_plot, "Cross-range Y"), (self._sar3d_xz_plot, "X", "Range Z"),
(self._sar3d_xy_plot, bottom_x_label, "Height Y"),
): ):
plot_item = plot.getPlotItem() plot_item = plot.getPlotItem()
plot_item.showAxis("left", show=True) plot_item.showAxis("left", show=True)
plot_item.showAxis("bottom", show=True) plot_item.showAxis("bottom", show=True)
plot_item.setClipToView(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") plot.setLabel("left", y_label, units="m")
view_box = plot.getViewBox() view_box = plot.getViewBox()
view_box.invertY(False) view_box.invertY(False)
@@ -69,8 +89,9 @@ class AppWindowSar3dPlotMixin:
self._sar3d_xz_image_item.hide() self._sar3d_xz_image_item.hide()
if self._sar3d_xy_image_item is not None: if self._sar3d_xy_image_item is not None:
self._sar3d_xy_image_item.hide() self._sar3d_xy_image_item.hide()
self._sar3d_xz_plot.setTitle("3D SAR side view (X-Z)") bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels()
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)") 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: def _draw_sar3d_projection(self, plot: pg.PlotWidget, image_item: pg.ImageItem, payload) -> bool:
"""Draw one normalized projection payload onto its plot; return success.""" """Draw one normalized projection payload onto its plot; return success."""
@@ -98,10 +119,10 @@ class AppWindowSar3dPlotMixin:
return True return True
def _draw_sar3d_projections(self, collection: ResultCollection) -> bool: 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) 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) bottom_payload = self._collection_payload_by_name(collection, "sar3d_proj_bottom", kind=3)
if xz_payload is None and xy_payload is None: if xz_payload is None and bottom_payload is None:
self._clear_sar3d_plot() self._clear_sar3d_plot()
return False return False
@@ -109,13 +130,15 @@ class AppWindowSar3dPlotMixin:
plot.setUpdatesEnabled(False) plot.setUpdatesEnabled(False)
try: try:
self._ensure_sar3d_plot_items() 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( xz_drawn = xz_payload is not None and self._draw_sar3d_projection(
self._sar3d_xz_plot, self._sar3d_xz_image_item, xz_payload self._sar3d_xz_plot, self._sar3d_xz_image_item, xz_payload
) )
xy_drawn = xy_payload is not None and self._draw_sar3d_projection( bottom_drawn = bottom_payload is not None and self._draw_sar3d_projection(
self._sar3d_xy_plot, self._sar3d_xy_image_item, xy_payload 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() self._clear_sar3d_plot()
return False return False
@@ -123,8 +146,9 @@ class AppWindowSar3dPlotMixin:
peak_suffix = "" peak_suffix = ""
if peak_payload is not None: if peak_payload is not None:
peak_suffix = f" | peak={float(peak_payload.scalar_value):.3g}" peak_suffix = f" | peak={float(peak_payload.scalar_value):.3g}"
self._sar3d_xz_plot.setTitle(f"3D SAR side view (X-Z){peak_suffix}") bottom_title, _bottom_x_label = self._sar3d_bottom_view_labels()
self._sar3d_xy_plot.setTitle("3D SAR top view (X-Y)") self._sar3d_xz_plot.setTitle(f"3D SAR top view (X-Z){peak_suffix}")
self._sar3d_xy_plot.setTitle(bottom_title)
finally: finally:
for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot): for plot in (self._sar3d_xz_plot, self._sar3d_xy_plot):
plot.setUpdatesEnabled(True) plot.setUpdatesEnabled(True)
@@ -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.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.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: 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 = QCheckBox("Normalize Tx/Rx pairs")
owner._sar3d_normalize_pairs.setChecked(bool(sar3d_defaults.normalize_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( sar3d_page = _build_processing_mode_page(
owner._processing_mode_pages, owner._processing_mode_pages,
[ [
@@ -730,6 +746,7 @@ def build_processing_group(owner) -> QGroupBox:
("Grid Nx", owner._sar3d_grid_nx), ("Grid Nx", owner._sar3d_grid_nx),
("Grid Ny", owner._sar3d_grid_ny), ("Grid Ny", owner._sar3d_grid_ny),
("Grid Nz", owner._sar3d_grid_nz), ("Grid Nz", owner._sar3d_grid_nz),
("Bottom view", owner._sar3d_bottom_view),
owner._sar3d_use_compensation, owner._sar3d_use_compensation,
owner._sar3d_normalize_pairs, 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_grid_nz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_use_compensation.toggled.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_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()) owner._set_processing_mode_page(owner._processing_mode.currentText())
return group return group
+30
View File
@@ -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]
+7
View File
@@ -578,6 +578,12 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
gui.processing.sar3d.normalize_pairs, gui.processing.sar3d.normalize_pairs,
"gui.processing.sar3d", "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"}: 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, "grid_nz": gui.processing.sar3d.grid_nz,
"use_compensation": gui.processing.sar3d.use_compensation, "use_compensation": gui.processing.sar3d.use_compensation,
"normalize_pairs": gui.processing.sar3d.normalize_pairs, "normalize_pairs": gui.processing.sar3d.normalize_pairs,
"bottom_view_deg": gui.processing.sar3d.bottom_view_deg,
}, },
}, },
"data_actions": { "data_actions": {
+3
View File
@@ -155,6 +155,9 @@ class GuiSar3dStateModel:
grid_nz: int = 160 grid_nz: int = 160
use_compensation: bool = True use_compensation: bool = True
normalize_pairs: 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) @dataclass(slots=True)
@@ -87,6 +87,9 @@ class ProcessingLiveConfig:
sar3d_grid_nz: int = 160 sar3d_grid_nz: int = 160
sar3d_use_compensation: bool = True sar3d_use_compensation: bool = True
sar3d_normalize_pairs: 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 # When true, the C++ data_processor ignores socket-supplied vlc updates
# and keeps using `gpr_speed_m_s` from this file. # and keeps using `gpr_speed_m_s` from this file.
ignore_socket_speed: bool = False ignore_socket_speed: bool = False
@@ -163,6 +166,7 @@ class ProcessingLiveConfig:
"sar3d_grid_nz": int(self.sar3d_grid_nz), "sar3d_grid_nz": int(self.sar3d_grid_nz),
"sar3d_use_compensation": bool(self.sar3d_use_compensation), "sar3d_use_compensation": bool(self.sar3d_use_compensation),
"sar3d_normalize_pairs": bool(self.sar3d_normalize_pairs), "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), "ignore_socket_speed": bool(self.ignore_socket_speed),
"reprocess_current_result": bool(self.reprocess_current_result), "reprocess_current_result": bool(self.reprocess_current_result),
"history_command_seq": int(self.history_command_seq), "history_command_seq": int(self.history_command_seq),
+2 -1
View File
@@ -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_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_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_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 return live
@@ -207,7 +208,7 @@ def main() -> int:
ids_monotonic = False ids_monotonic = False
last_id = result.collection_id last_id = result.collection_id
names = {p.processing_name: p for p in result.collection_payloads} 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 sar3d_results += 1
peak_payload = names.get("sar3d_peak") peak_payload = names.get("sar3d_peak")
if peak_payload is not None: if peak_payload is not None:
@@ -191,7 +191,8 @@
"grid_ny": 120, "grid_ny": 120,
"grid_nz": 160, "grid_nz": 160,
"use_compensation": true, "use_compensation": true,
"normalize_pairs": true "normalize_pairs": true,
"bottom_view_deg": 0.0
} }
}, },
"preprocess_dialog": { "preprocess_dialog": {