Update GPR and SAR3D processing

This commit is contained in:
Ayzen
2026-10-08 16:24:01 +03:00
parent 390015ec39
commit 3575ca68f5
67 changed files with 1187 additions and 2581 deletions
@@ -46,12 +46,7 @@ class DataProcessor {
// value exists, so the speed from the live config stays in force.
[[nodiscard]] auto socket_speed(const ProcessingLiveConfig& live_config) const -> std::optional<double>;
// Convert one processed collection into a locator filter spec, derived
// from the live config and current processor mode.
[[nodiscard]] auto build_locator_filter(const ProcessingLiveConfig& live_config) const
-> radar::locator::FilterParams;
void publish_locator(const ipc::ResultCollection& collection, const ProcessingLiveConfig& live_config);
void publish_locator(const ipc::ResultCollection& collection);
const config::RunConfig& config_;
config::RunConfig processing_config_;
@@ -10,19 +10,6 @@
namespace radar::processing {
enum class LegacyGprMode {
Point,
Extended,
};
enum class LegacyGprReferenceMode {
// t_ref = midpoint between the first and last event centers.
FrameCenter,
// t_ref = center of the first event. Useful when motion offsets should be
// accumulated from frame start, e.g. for tagging frames by their head time.
FirstTxEvent,
};
struct HistoryTarget {
std::uint64_t collection_id = 0;
std::uint64_t capture_start_ns = 0;
@@ -36,15 +23,13 @@ struct ProcessingLiveConfig {
float bscan_gain = 1.0F;
float bscan_start_freq_mhz = 100.0F;
float bscan_stop_freq_mhz = 8800.0F;
LegacyGprMode legacy_gpr_mode = LegacyGprMode::Point;
// Absent in legacy live files: use the initial run_config geometry and medium.
// Absent geometry overrides use the initial run_config geometry.
std::optional<config::GprConfig> gpr{};
std::vector<std::uint32_t> gpr_input_positions{};
std::vector<std::uint32_t> gpr_output_positions{};
float gpr_min_depth_m = 2.0F;
float gpr_max_depth_m = 14.0F;
float gpr_range_comp_power = 0.1F;
float gpr_comp_power = 0.2F;
// BP object-detection stop level, as a fraction of the global peak; peaks
// below it are not objects.
float gpr_object_min_frac = 0.7F;
@@ -54,29 +39,15 @@ struct ProcessingLiveConfig {
std::string gpr_motion_mode = "int_minus";
float gpr_speed_m_s = 0.0F;
float gpr_look_angle_deg = 0.0F;
// Motion model parameters for the legacy GPR pipeline. The direction sign
// selects which way later Tx-events appear deeper (+1) or shallower (-1).
// Intra-sweep phase correction compensates the motion that happens *inside*
// one Tx-sweep before the IFFT — it is independent of the per-pair coarse
// tau shift and can be disabled without affecting the rest of the pipeline.
// The reference mode picks the anchor used to compute dt_ref per event.
float gpr_direction_sign = 1.0F;
bool gpr_apply_freq_phase_correction = true;
LegacyGprReferenceMode gpr_reference_mode = LegacyGprReferenceMode::FrameCenter;
float gpr_snr_thresh = 4.5F;
float gpr_snr_comp_max = 25.0F;
float gpr_start_freq_mhz = 3000.0F;
float gpr_stop_freq_mhz = 6000.0F;
bool gpr_background_subtract_enabled = true;
std::uint32_t gpr_background_mean_count = 10U;
bool gpr_remove_sidelobe_objects_enabled = true;
// BP image is computed in the y=imaging_plane_y_m slice of the 3D grid.
// Default 0 keeps legacy 1D antenna layouts imaging in the antenna plane.
float gpr_imaging_plane_y_m = 0.0F;
// Coherent BP object visibility (window + the draw limits below) is applied in
// the processor itself, with no score threshold. Only legacy GPR thresholds,
// on a pair count.
float legacy_gpr_min_visible_pair_count = 0.0F;
// Ground plane in radar coordinates, at this height and the configured pitch.
float gpr_bp_ground_depth_m = 1.25F;
bool pair_depth_equalize = true;
std::uint32_t gpr_max_detected_objects_to_draw = 0;
std::uint32_t gpr_draw_top_m_objects = 0;
// Cross-frame approach filter: an object is shown only once it persists as a
@@ -89,7 +60,7 @@ struct ProcessingLiveConfig {
float gpr_visible_z_min_m = 0.0F;
float gpr_visible_z_max_m = 14.0F;
// 3D SAR (sar3d processor) parameters, named after the reference notebook
// Multi_antenna_3D_clean_v1.ipynb (SAR_FRAME_RADIUS, BP_GROUND_DEPTH_M, ...).
// Multi_antenna_demonstrate_depthnorm.ipynb (SAR_FRAME_RADIUS, BP_GROUND_DEPTH_M, ...).
// The band, depth gate, antenna selection, speed and look angle are the
// shared gpr_* fields above.
std::uint32_t sar3d_frame_radius = 0U;
@@ -99,6 +70,10 @@ struct ProcessingLiveConfig {
float sar3d_comp_range_power = 0.3F;
bool sar3d_use_compensation = true;
bool sar3d_normalize_each_frame = true;
float sar3d_x_size_m = 0.0F;
float sar3d_y_min_m = -0.5F;
float sar3d_y_max_m = 2.0F;
float sar3d_z_size_m = 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.
@@ -22,10 +22,15 @@ namespace {
constexpr const char* kDefaultProcessorMode = "pass_through";
constexpr std::size_t kBscanReplayWindow = 50U;
[[nodiscard]] auto replay_history_limit(const config::RunConfig& config) -> std::size_t {
[[nodiscard]] auto processing_history_limit(const config::RunConfig& config, const ProcessingLiveConfig& live) -> std::size_t {
const auto preprocessed_capacity = static_cast<std::size_t>(std::max<std::uint32_t>(1U, config.rings.preprocessed.capacity));
const auto results_capacity = static_cast<std::size_t>(std::max<std::uint32_t>(1U, config.rings.results.capacity));
return std::min({preprocessed_capacity, results_capacity, kBscanReplayWindow});
std::size_t required = 1U;
if (live.processor_mode == "gpr" || live.processor_mode == "sar3d") {
if (live.gpr_background_subtract_enabled) { required += live.gpr_background_mean_count; }
if (live.processor_mode == "sar3d") { required += live.sar3d_frame_radius; }
}
return std::max(required, std::min({preprocessed_capacity, results_capacity, kBscanReplayWindow}));
}
void publish_result_collection(
@@ -68,7 +73,6 @@ DataProcessor::DataProcessor(
void DataProcessor::run(const std::atomic<bool>& stop_requested) {
std::vector<std::uint8_t> bytes{};
std::vector<ipc::PreprocessedCollection> preprocessed_history{};
const std::size_t history_limit = replay_history_limit(config_);
std::uint64_t last_replayed_revision = live_config_loader_.revision();
runtime::CollectionErrors errors;
std::uint64_t oversized_results = 0;
@@ -92,6 +96,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
processing_config_.gpr = live_config.gpr.value_or(config_.gpr);
}
auto& processor = resolve_processor(live_config);
const auto history_limit = processing_history_limit(config_, live_config);
const bool socket_speed_dirty = current_socket_speed != last_reprocessed_socket_speed;
const auto was_removed = [&](const ipc::PreprocessedCollection& collection) {
@@ -107,6 +112,9 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
};
if (live_revision != last_replayed_revision || socket_speed_dirty) {
std::erase_if(preprocessed_history, was_removed);
if (preprocessed_history.size() > history_limit) {
preprocessed_history.erase(preprocessed_history.begin(), preprocessed_history.end() - history_limit);
}
if (!live_config.reprocess_current_result) {
// Socket-fed speed updates should affect only future preprocessed collections,
@@ -120,7 +128,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
live_config
);
publish_result_collection(replay_result, results_ring_, oversized_results);
publish_locator(replay_result, live_config);
publish_locator(replay_result);
}
} else if (!preprocessed_history.empty()) {
const auto replay_result = process_collection(
@@ -130,7 +138,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
live_config
);
publish_result_collection(replay_result, results_ring_, oversized_results);
publish_locator(replay_result, live_config);
publish_locator(replay_result);
}
last_replayed_revision = live_revision;
//record the speed we just reprocessed with so an
@@ -156,8 +164,8 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
preprocessed.capture_start_ns <= preprocessed_history.back().capture_start_ns;
if (!fed_back) {
preprocessed_history.push_back(std::move(preprocessed));
while (preprocessed_history.size() > history_limit) {
preprocessed_history.erase(preprocessed_history.begin());
if (preprocessed_history.size() > history_limit) {
preprocessed_history.erase(preprocessed_history.begin(), preprocessed_history.end() - history_limit);
}
}
const auto& current = fed_back ? preprocessed : preprocessed_history.back();
@@ -175,7 +183,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
);
publish_result_collection(result_collection, results_ring_, oversized_results);
if (!fed_back) {
publish_locator(result_collection, live_config);
publish_locator(result_collection);
}
errors.progress();
continue;
@@ -237,40 +245,13 @@ auto DataProcessor::socket_speed(const ProcessingLiveConfig& live_config) const
return locator_server_->latest_socket_speed();
}
auto DataProcessor::build_locator_filter(const ProcessingLiveConfig& live_config) const
-> radar::locator::FilterParams {
const std::string requested_mode =
live_config.processor_mode.empty() ? default_processor_mode_ : live_config.processor_mode;
radar::locator::FilterParams filter{};
if (requested_mode == "legacy_gpr") {
// Legacy GPR emits its objects unfiltered, so the socket applies the legacy
// rule here: clip to the visible window and threshold on the pair count. The
// GUI disables "draw top N" for legacy, so we skip it on the wire to match.
filter.visible_bounds = radar::locator::VisibleBounds{
.x_min = live_config.gpr_visible_x_min_m,
.x_max = live_config.gpr_visible_x_max_m,
.z_min = live_config.gpr_visible_z_min_m,
.z_max = live_config.gpr_visible_z_max_m,
};
filter.min_score = live_config.legacy_gpr_min_visible_pair_count;
filter.draw_limits.reset();
}
// Coherent BP already emits the FINAL visible object set from the processor
// (window + N/M, no score threshold), so the
// socket forwards it verbatim. The default FilterParams passes everything through
// (it only drops non-finite rows), keeping the filtering logic in one place.
return filter;
}
void DataProcessor::publish_locator(
const ipc::ResultCollection& collection,
const ProcessingLiveConfig& live_config
const ipc::ResultCollection& collection
) {
if (locator_server_ == nullptr || !locator_server_->is_running()) {
if (locator_server_ == nullptr) {
return;
}
locator_server_->publish(collection, build_locator_filter(live_config));
locator_server_->publish(collection);
}
auto create_default_processors() -> ProcessorRegistry {
@@ -287,10 +268,6 @@ auto create_default_processors() -> ProcessorRegistry {
auto processor = std::make_unique<GprProcessor>();
processors.emplace(processor->name(), std::move(processor));
}
{
auto processor = std::make_unique<LegacyGprProcessor>();
processors.emplace(processor->name(), std::move(processor));
}
{
auto processor = std::make_unique<Sar3dProcessor>();
processors.emplace(processor->name(), std::move(processor));
@@ -19,28 +19,6 @@ namespace {
using Json = nlohmann::json;
[[nodiscard]] auto parse_legacy_gpr_mode(const std::string& value, const std::string& field_name) -> LegacyGprMode {
if (value == "point") {
return LegacyGprMode::Point;
}
if (value == "extended") {
return LegacyGprMode::Extended;
}
throw std::runtime_error(field_name + " must be one of: point, extended");
}
[[nodiscard]] auto parse_legacy_gpr_reference_mode(
const std::string& value, const std::string& field_name
) -> LegacyGprReferenceMode {
if (value == "frame_center") {
return LegacyGprReferenceMode::FrameCenter;
}
if (value == "first_tx_event") {
return LegacyGprReferenceMode::FirstTxEvent;
}
throw std::runtime_error(field_name + " must be one of: frame_center, first_tx_event");
}
[[nodiscard]] auto parse_gpr_score_mode(const std::string& value, const std::string& field_name) -> std::string {
if (value == "peak" || value == "combined") {
return value;
@@ -55,24 +33,6 @@ using Json = nlohmann::json;
throw std::runtime_error(field_name + " must be one of: int_minus, int_focus");
}
void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::string& value) {
if (value == "backprojection") {
return;
}
if (value == "legacy_point") {
config.legacy_gpr_mode = LegacyGprMode::Point;
} else if (value == "legacy_extended") {
config.legacy_gpr_mode = LegacyGprMode::Extended;
} else {
throw std::runtime_error(
"processing.gpr_algorithm must be one of: backprojection, legacy_point, legacy_extended"
);
}
if (config.processor_mode.empty() || config.processor_mode == "gpr") {
config.processor_mode = "legacy_gpr";
}
}
[[nodiscard]] auto parse_f32(const Json& value, const std::string& field_name) -> float {
if (!value.is_number()) {
throw std::runtime_error(field_name + " must be number");
@@ -192,11 +152,8 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
if (const auto found = root.find("settings_revision"); found != root.end()) {
config.settings_revision = parse_u64_number(*found, "processing.settings_revision");
}
if (root.contains("relative_permittivity") || root.contains("tx_geometry") || root.contains("rx_geometry")) {
if (root.contains("tx_geometry") || root.contains("rx_geometry")) {
config.gpr = run_config.gpr;
if (const auto found = root.find("relative_permittivity"); found != root.end()) {
config.gpr->relative_permittivity = parse_f32(*found, "processing.relative_permittivity");
}
if (const auto found = root.find("tx_geometry"); found != root.end()) {
config.gpr->tx_geometry = parse_geometry(*found, "output_pos", &config::GprTxGeometry::output_pos);
}
@@ -206,25 +163,16 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
config::validate_gpr_config(*config.gpr, run_config);
}
assign_optional(root, "processor_mode", config.processor_mode);
if (!config.processor_mode.empty() && config.processor_mode != "pass_through"
&& config.processor_mode != "bscan" && config.processor_mode != "gpr" && config.processor_mode != "sar3d") {
throw std::runtime_error("Unsupported processing.processor_mode: " + config.processor_mode);
}
assign_optional(root, "bscan_axis", config.bscan_axis);
assign_optional(root, "bscan_cut_m", config.bscan_cut_m);
assign_optional(root, "bscan_max_depth_m", config.bscan_max_depth_m);
assign_optional(root, "bscan_gain", config.bscan_gain);
assign_optional(root, "bscan_start_freq_mhz", config.bscan_start_freq_mhz);
assign_optional(root, "bscan_stop_freq_mhz", config.bscan_stop_freq_mhz);
if (const auto found = root.find("legacy_gpr_mode"); found != root.end()) {
if (!found->is_string()) {
throw std::runtime_error("processing.legacy_gpr_mode must be string");
}
config.legacy_gpr_mode =
parse_legacy_gpr_mode(found->get<std::string>(), "processing.legacy_gpr_mode");
}
if (const auto found = root.find("gpr_algorithm"); found != root.end()) {
if (!found->is_string()) {
throw std::runtime_error("processing.gpr_algorithm must be string");
}
apply_legacy_gpr_algorithm_alias(config, found->get<std::string>());
}
if (const auto found = root.find("gpr_input_positions"); found != root.end()) {
config.gpr_input_positions = parse_u32_array(*found, "processing.gpr_input_positions");
}
@@ -235,7 +183,6 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
assign_optional(root, "gpr_max_depth_m", config.gpr_max_depth_m);
assign_optional(root, "gpr_range_comp_power", config.gpr_range_comp_power);
assign_optional(root, "gpr_object_min_frac", config.gpr_object_min_frac);
assign_optional(root, "gpr_comp_power", config.gpr_comp_power);
if (const auto found = root.find("gpr_score_mode"); found != root.end()) {
if (!found->is_string()) {
throw std::runtime_error("processing.gpr_score_mode must be string");
@@ -251,16 +198,6 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
assign_optional(root, "gpr_speed_m_s", config.gpr_speed_m_s);
assign_optional(root, "gpr_look_angle_deg", config.gpr_look_angle_deg);
assign_optional(root, "gpr_direction_sign", config.gpr_direction_sign);
assign_optional(root, "gpr_apply_freq_phase_correction", config.gpr_apply_freq_phase_correction);
if (const auto found = root.find("gpr_reference_mode"); found != root.end()) {
if (!found->is_string()) {
throw std::runtime_error("processing.gpr_reference_mode must be string");
}
config.gpr_reference_mode =
parse_legacy_gpr_reference_mode(found->get<std::string>(), "processing.gpr_reference_mode");
}
assign_optional(root, "gpr_snr_thresh", config.gpr_snr_thresh);
assign_optional(root, "gpr_snr_comp_max", config.gpr_snr_comp_max);
assign_optional(root, "gpr_start_freq_mhz", config.gpr_start_freq_mhz);
assign_optional(root, "gpr_stop_freq_mhz", config.gpr_stop_freq_mhz);
assign_optional(root, "gpr_background_subtract_enabled", config.gpr_background_subtract_enabled);
@@ -268,7 +205,8 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
config.gpr_background_mean_count = parse_u32_number(*found, "processing.gpr_background_mean_count");
}
assign_optional(root, "gpr_remove_sidelobe_objects_enabled", config.gpr_remove_sidelobe_objects_enabled);
assign_optional(root, "gpr_imaging_plane_y_m", config.gpr_imaging_plane_y_m);
assign_optional(root, "gpr_bp_ground_depth_m", config.gpr_bp_ground_depth_m);
assign_optional(root, "pair_depth_equalize", config.pair_depth_equalize);
for (const auto& [key, target] : {
std::pair{"gpr_visible_x_min_m", &config.gpr_visible_x_min_m},
std::pair{"gpr_visible_x_max_m", &config.gpr_visible_x_max_m},
@@ -277,7 +215,6 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
}) {
assign_optional(root, key, *target);
}
assign_optional(root, "legacy_gpr_min_visible_pair_count", config.legacy_gpr_min_visible_pair_count);
if (const auto found = root.find("gpr_max_detected_objects_to_draw"); found != root.end()) {
config.gpr_max_detected_objects_to_draw =
parse_u32_number(*found, "processing.gpr_max_detected_objects_to_draw");
@@ -297,6 +234,10 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
std::pair{"sar3d_voxel_m", &config.sar3d_voxel_m},
std::pair{"sar3d_bp_ground_depth_m", &config.sar3d_bp_ground_depth_m},
std::pair{"sar3d_comp_range_power", &config.sar3d_comp_range_power},
std::pair{"sar3d_x_size_m", &config.sar3d_x_size_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_size_m", &config.sar3d_z_size_m},
}) {
assign_optional(root, key, *target);
}
@@ -306,6 +247,10 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
}) {
assign_optional(root, key, *target);
}
if (config.sar3d_x_size_m < 0.0F || config.sar3d_z_size_m < 0.0F ||
config.sar3d_y_max_m <= config.sar3d_y_min_m) {
throw std::runtime_error("SAR scene sizes must be non-negative and SAR_Y_RANGE_M must increase");
}
assign_optional(root, "ignore_socket_speed", config.ignore_socket_speed);
assign_optional(root, "reprocess_current_result", config.reprocess_current_result);
if (const auto found = root.find("history_clear_before_ns"); found != root.end()) {