added new filtration and fixed processing parameters
This commit is contained in:
+9
-5
@@ -45,8 +45,10 @@ struct ProcessingLiveConfig {
|
||||
float gpr_min_depth_m = 2.0F;
|
||||
float gpr_max_depth_m = 14.0F;
|
||||
float gpr_range_comp_power = 0.1F;
|
||||
float gpr_angle_comp_power = 0.0F;
|
||||
float gpr_comp_power = 0.2F;
|
||||
// BP object-detection stop level, as a fraction of the global peak (Python
|
||||
// Horns_motion_3libre.py BP_OBJECT_MIN_FRAC); peaks below it are not objects.
|
||||
float gpr_object_min_frac = 0.7F;
|
||||
std::string gpr_score_mode = "combined";
|
||||
// Backprojection intra-sweep speed-correction mode: "int_minus" (full
|
||||
// correction) or "int_focus" (focusing residual only). Mirrors the Python
|
||||
@@ -73,13 +75,15 @@ struct ProcessingLiveConfig {
|
||||
// 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;
|
||||
// Locator filter parameters. Mode-dependent threshold (legacy_gpr uses
|
||||
// `legacy_gpr_min_visible_pair_count`, everything else uses
|
||||
// `gpr_min_visible_score`). Draw limits apply only to non-legacy modes.
|
||||
float gpr_min_visible_score = 0.0F;
|
||||
// Coherent BP object visibility (window + the draw limits below) is applied in
|
||||
// the processor itself, matching Horns_motion_3libre.py — there is NO score
|
||||
// threshold for it. Only legacy GPR still thresholds, on a pair count.
|
||||
float legacy_gpr_min_visible_pair_count = 0.0F;
|
||||
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
|
||||
// motion-consistent track over this many consecutive frames (<= 1 disables it).
|
||||
std::uint32_t gpr_object_approach_min_frames = 3;
|
||||
// Visible X/Z window (metres). The locator clips broadcast objects to this
|
||||
// window so the socket emits only what the desktop plot actually shows.
|
||||
float gpr_visible_x_min_m = -2.0F;
|
||||
|
||||
@@ -65,7 +65,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
std::uint64_t last_applied_history_command_seq = 0;
|
||||
std::uint64_t error_count = 0;
|
||||
std::uint64_t consecutive_errors = 0;
|
||||
// Fix #55: track the socket-fed speed used for the last reprocess so a change
|
||||
// track the socket-fed speed used for the last reprocess so a change
|
||||
// arriving without a live-config revision bump still triggers a reprocess of
|
||||
// the current result (gated below by reprocess_current_result).
|
||||
std::optional<double> last_reprocessed_socket_speed = std::nullopt;
|
||||
@@ -124,7 +124,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
publish_locator(replay_result, live_config);
|
||||
}
|
||||
last_replayed_revision = live_revision;
|
||||
// Fix #55: record the speed we just reprocessed with so an
|
||||
//record the speed we just reprocessed with so an
|
||||
// unchanged socket value does not retrigger every iteration.
|
||||
last_reprocessed_socket_speed = current_socket_speed;
|
||||
}
|
||||
@@ -232,29 +232,23 @@ auto DataProcessor::build_locator_filter(const ProcessingLiveConfig& live_config
|
||||
live_config.processor_mode.empty() ? default_processor_mode_ : live_config.processor_mode;
|
||||
|
||||
radar::locator::FilterParams filter{};
|
||||
// Clip broadcast objects to the same visible X/Z window the desktop plot uses,
|
||||
// so the socket emits only the objects the operator actually sees.
|
||||
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,
|
||||
};
|
||||
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;
|
||||
// The GUI deliberately disables the "draw top N" capping for legacy
|
||||
// GPR, so we also skip it on the wire to match observation semantics.
|
||||
filter.draw_limits.reset();
|
||||
} else {
|
||||
filter.min_score = live_config.gpr_min_visible_score;
|
||||
if (live_config.gpr_max_detected_objects_to_draw > 0U
|
||||
&& live_config.gpr_draw_top_m_objects > 0U) {
|
||||
filter.draw_limits = radar::locator::DrawLimits{
|
||||
.max_detected_objects = live_config.gpr_max_detected_objects_to_draw,
|
||||
.draw_top_objects = live_config.gpr_draw_top_m_objects,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Coherent BP already emits the FINAL visible object set from the processor
|
||||
// (window + N/M, no score threshold — matching Horns_motion_3libre.py), 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -255,11 +255,11 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
|
||||
}
|
||||
config.gpr_range_comp_power = static_cast<float>(found->get<double>());
|
||||
}
|
||||
if (const auto found = root.find("gpr_angle_comp_power"); found != root.end()) {
|
||||
if (const auto found = root.find("gpr_object_min_frac"); found != root.end()) {
|
||||
if (!found->is_number()) {
|
||||
throw std::runtime_error("processing.gpr_angle_comp_power must be number");
|
||||
throw std::runtime_error("processing.gpr_object_min_frac must be number");
|
||||
}
|
||||
config.gpr_angle_comp_power = static_cast<float>(found->get<double>());
|
||||
config.gpr_object_min_frac = static_cast<float>(found->get<double>());
|
||||
}
|
||||
if (const auto found = root.find("gpr_comp_power"); found != root.end()) {
|
||||
if (!found->is_number()) {
|
||||
@@ -355,12 +355,6 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
|
||||
}
|
||||
config.gpr_imaging_plane_y_m = static_cast<float>(found->get<double>());
|
||||
}
|
||||
if (const auto found = root.find("gpr_min_visible_score"); found != root.end()) {
|
||||
if (!found->is_number()) {
|
||||
throw std::runtime_error("processing.gpr_min_visible_score must be number");
|
||||
}
|
||||
config.gpr_min_visible_score = static_cast<float>(found->get<double>());
|
||||
}
|
||||
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},
|
||||
@@ -388,6 +382,10 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
|
||||
config.gpr_draw_top_m_objects =
|
||||
parse_u32_number(*found, "processing.gpr_draw_top_m_objects");
|
||||
}
|
||||
if (const auto found = root.find("gpr_object_approach_min_frames"); found != root.end()) {
|
||||
config.gpr_object_approach_min_frames =
|
||||
parse_u32_number(*found, "processing.gpr_object_approach_min_frames");
|
||||
}
|
||||
if (const auto found = root.find("ignore_socket_speed"); found != root.end()) {
|
||||
if (!found->is_boolean()) {
|
||||
throw std::runtime_error("processing.ignore_socket_speed must be bool");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "object_approach_filter.hpp"
|
||||
#include "processor_interface.hpp"
|
||||
|
||||
namespace radar::processing {
|
||||
@@ -13,6 +14,11 @@ class GprProcessor final : public ProcessorInterface {
|
||||
std::span<const ipc::PreprocessedCollection> previous_collections,
|
||||
const ProcessingLiveConfig& live_config
|
||||
) -> ipc::ResultCollection override;
|
||||
|
||||
private:
|
||||
// Cross-frame "approach" track filter. Persists across collections because the
|
||||
// owning processor instance is long-lived (one per data_processor run).
|
||||
ObjectApproachFilter approach_filter_{};
|
||||
};
|
||||
|
||||
class LegacyGprProcessor final : public ProcessorInterface {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::processing {
|
||||
|
||||
// Temporal "approach" filter for coherent-BP detections — a port of the objects-only
|
||||
// filter in Horns_motion_3libre's demonstrate notebook.
|
||||
//
|
||||
// It keeps only objects that persist as a *motion-consistent track* across at least
|
||||
// `min_frames` consecutive frames: as the radar moves, a real target reappears at a
|
||||
// predictable, shifting (x, z), whereas a one-frame noise spike forms no track and is
|
||||
// dropped. The expected per-frame change in range is `speed * dt * cos(look_angle)`,
|
||||
// where `dt` is the real interval between consecutive frames (so dropped frames and a
|
||||
// varying frame period are handled naturally).
|
||||
//
|
||||
// CAUSAL: unlike the offline notebook (which can look forward over the whole sequence),
|
||||
// this confirms a track by looking *backward* — an object is kept once it ends a track
|
||||
// of `min_frames` frames seen so far. A target therefore first appears after it has
|
||||
// persisted `min_frames` frames; the early frames of its track are not shown
|
||||
// retroactively.
|
||||
//
|
||||
// Stateless across pipeline restarts is approximated by breaking tracks across a large
|
||||
// inter-frame gap (`kMaxFrameGapSeconds`), so a stale history from a previous run cannot
|
||||
// spuriously confirm objects.
|
||||
//
|
||||
// IDEMPOTENT under reprocessing: the data_processor re-runs the last collection (or
|
||||
// replays the whole window) whenever live settings or the socket speed change, with no
|
||||
// new sweep. The history is therefore keyed by `frame_id` (the strictly increasing
|
||||
// collection id): the same id replaces its entry (reprocess of the current frame), a
|
||||
// smaller id rebuilds from scratch (a replay restart), so repeated reprocessing never
|
||||
// duplicates frames or falsely confirms a track.
|
||||
class ObjectApproachFilter {
|
||||
public:
|
||||
struct Point {
|
||||
double x_m;
|
||||
double z_m;
|
||||
};
|
||||
|
||||
// Record `objects` as frame `frame_id` and return, per object, whether it is
|
||||
// confirmed (ends a >= `min_frames` motion-consistent track). `frame_id` is the
|
||||
// collection id (identity/order, survives reprocessing); `frame_time_seconds` is the
|
||||
// frame's wall-clock timestamp (drives the inter-frame interval); `speed_m_s`/
|
||||
// `look_angle_deg` are the live motion estimate. `min_frames <= 1` disables filtering.
|
||||
[[nodiscard]] auto confirm(
|
||||
const std::vector<Point>& objects,
|
||||
std::uint64_t frame_id,
|
||||
double frame_time_seconds,
|
||||
double speed_m_s,
|
||||
double look_angle_deg,
|
||||
std::size_t min_frames
|
||||
) -> std::vector<bool> {
|
||||
record_frame(Frame{frame_id, frame_time_seconds, objects});
|
||||
const std::size_t history_depth = std::max<std::size_t>(min_frames, 1U);
|
||||
while (history_.size() > history_depth) {
|
||||
history_.pop_front();
|
||||
}
|
||||
|
||||
std::vector<bool> confirmed(objects.size(), min_frames <= 1U);
|
||||
if (min_frames <= 1U || history_.size() < min_frames) {
|
||||
return confirmed; // disabled, or not enough history yet to confirm anything
|
||||
}
|
||||
|
||||
const double range_step_per_second = std::abs(speed_m_s) * std::cos(to_radians(look_angle_deg));
|
||||
for (std::size_t object_index = 0U; object_index < objects.size(); ++object_index) {
|
||||
confirmed[object_index] = has_backward_track(objects[object_index], min_frames, range_step_per_second);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
void reset() { history_.clear(); }
|
||||
|
||||
private:
|
||||
struct Frame {
|
||||
std::uint64_t id;
|
||||
double time_seconds;
|
||||
std::vector<Point> objects;
|
||||
};
|
||||
|
||||
// Append a genuinely new frame, replace the current one on reprocessing (same id),
|
||||
// or rebuild from scratch when the id steps backward (a replay restart). This keeps
|
||||
// the history one entry per distinct collection no matter how often settings change.
|
||||
void record_frame(Frame frame) {
|
||||
if (history_.empty() || frame.id > history_.back().id) {
|
||||
history_.push_back(std::move(frame));
|
||||
} else if (frame.id == history_.back().id) {
|
||||
history_.back() = std::move(frame);
|
||||
} else {
|
||||
history_.clear();
|
||||
history_.push_back(std::move(frame));
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed matching tolerances (Horns_motion notebook 0.2 block). Range decreases as the
|
||||
// radar approaches the target, hence the negative Z sign.
|
||||
static constexpr double kXToleranceM = 0.45;
|
||||
static constexpr double kRangeToleranceFraction = 0.85;
|
||||
static constexpr double kRangeToleranceFloorM = 0.12;
|
||||
static constexpr double kRangeSign = -1.0;
|
||||
static constexpr double kMaxFrameGapSeconds = 2.0;
|
||||
|
||||
[[nodiscard]] static auto to_radians(double degrees) -> double {
|
||||
return degrees * (M_PI / 180.0);
|
||||
}
|
||||
|
||||
// Walk back from the current object through the history, matching a motion-consistent
|
||||
// predecessor in each earlier frame. Confirmed iff a full chain of `min_frames` frames
|
||||
// (the current one plus `min_frames - 1` predecessors) is found.
|
||||
[[nodiscard]] auto has_backward_track(
|
||||
const Point& object,
|
||||
std::size_t min_frames,
|
||||
double range_step_per_second
|
||||
) const -> bool {
|
||||
const std::size_t newest = history_.size() - 1U;
|
||||
Point current = object;
|
||||
for (std::size_t step = 1U; step < min_frames; ++step) {
|
||||
const std::size_t earlier_index = newest - step;
|
||||
const Frame& earlier = history_[earlier_index];
|
||||
const double dt = history_[earlier_index + 1U].time_seconds - earlier.time_seconds;
|
||||
if (!(dt > 0.0) || dt > kMaxFrameGapSeconds) {
|
||||
return false; // non-monotonic time, or a gap that breaks the track
|
||||
}
|
||||
const double expected_range_shift = range_step_per_second * dt;
|
||||
const Point* predecessor = match_predecessor(current, earlier.objects, expected_range_shift);
|
||||
if (predecessor == nullptr) {
|
||||
return false;
|
||||
}
|
||||
current = *predecessor;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The best earlier-frame object consistent with `object` having moved by one frame:
|
||||
// its range was larger by `expected_range_shift` (radar since approached), within the
|
||||
// cross-range and range tolerances. Returns nullptr when nothing matches.
|
||||
[[nodiscard]] static auto match_predecessor(
|
||||
const Point& object,
|
||||
const std::vector<Point>& candidates,
|
||||
double expected_range_shift
|
||||
) -> const Point* {
|
||||
const double target_z = object.z_m - (kRangeSign * expected_range_shift);
|
||||
const double range_tolerance =
|
||||
std::max(kRangeToleranceFloorM, kRangeToleranceFraction * expected_range_shift);
|
||||
|
||||
const Point* best = nullptr;
|
||||
double best_cost = std::numeric_limits<double>::infinity();
|
||||
for (const Point& candidate : candidates) {
|
||||
const double dx = std::abs(candidate.x_m - object.x_m);
|
||||
const double dz = std::abs(candidate.z_m - target_z);
|
||||
if (dx > kXToleranceM || dz > range_tolerance) {
|
||||
continue;
|
||||
}
|
||||
const double cost = (dx / kXToleranceM) * (dx / kXToleranceM)
|
||||
+ (dz / range_tolerance) * (dz / range_tolerance);
|
||||
if (cost < best_cost) {
|
||||
best = &candidate;
|
||||
best_cost = cost;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
std::deque<Frame> history_{}; // recent frames, newest at the back; capped to min_frames
|
||||
};
|
||||
|
||||
} // namespace radar::processing
|
||||
+62
-14
@@ -21,7 +21,6 @@ constexpr double kSmoothSigma = 1.5;
|
||||
// same default 'reflect' (half-sample symmetric) extension — see reflect_index.
|
||||
constexpr double kGaussianTruncate = 4.0;
|
||||
constexpr std::size_t kMaxObjects = 10U;
|
||||
constexpr double kObjectMinFrac = 0.7;
|
||||
constexpr double kRegionThresholdFrac = 0.75;
|
||||
constexpr double kSuppressThresholdFrac = 0.20;
|
||||
constexpr double kSuppressRadiusXM = 0.80;
|
||||
@@ -1405,7 +1404,8 @@ void apply_depth_gate(
|
||||
|
||||
[[nodiscard]] auto find_bp_objects(
|
||||
const std::vector<double>& bp_image,
|
||||
const GridDefinition& grid
|
||||
const GridDefinition& grid,
|
||||
double min_frac
|
||||
) -> std::vector<ObjectRecord> {
|
||||
std::vector<ObjectRecord> objects{};
|
||||
if (bp_image.empty() || grid.x_grid.size() < 2U || grid.z_grid.size() < 2U) {
|
||||
@@ -1414,7 +1414,7 @@ void apply_depth_gate(
|
||||
|
||||
std::vector<double> work = bp_image;
|
||||
const double global_peak = max_value(work);
|
||||
const double stop_level = kObjectMinFrac * global_peak;
|
||||
const double stop_level = min_frac * global_peak;
|
||||
if (!(global_peak > 0.0)) {
|
||||
return objects;
|
||||
}
|
||||
@@ -1740,7 +1740,15 @@ void add_bp_score_metrics(
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto output_objects_sorted(
|
||||
// Select the FINAL visible objects exactly as Horns_motion_3libre.py does, so the
|
||||
// processor is the single source of truth: the GUI plot and the locator socket both
|
||||
// consume this set verbatim (no second, duplicated filter). Steps, in order:
|
||||
// 1. drop sidelobe candidates (when enabled), then sort by score (peak tie-break);
|
||||
// 2. clip to the visible X/Z window (display window doubles as an object gate);
|
||||
// 3. apply the N/M draw rule (BP_MAX_DETECTED_OBJECTS_TO_DRAW / BP_DRAW_TOP_M_OBJECTS):
|
||||
// if more than N survive, show none; otherwise keep the top M.
|
||||
// There is deliberately NO score threshold (the Python reference has none).
|
||||
[[nodiscard]] auto select_visible_objects(
|
||||
const std::vector<ObjectRecord>& objects,
|
||||
const ProcessingLiveConfig& live_config
|
||||
) -> std::vector<const ObjectRecord*> {
|
||||
@@ -1750,6 +1758,12 @@ void add_bp_score_metrics(
|
||||
if (live_config.gpr_remove_sidelobe_objects_enabled && object.sidelobe_candidate) {
|
||||
continue;
|
||||
}
|
||||
if (object.x_m < live_config.gpr_visible_x_min_m
|
||||
|| object.x_m > live_config.gpr_visible_x_max_m
|
||||
|| object.z_m < live_config.gpr_visible_z_min_m
|
||||
|| object.z_m > live_config.gpr_visible_z_max_m) {
|
||||
continue;
|
||||
}
|
||||
visible.push_back(&object);
|
||||
}
|
||||
|
||||
@@ -1759,6 +1773,17 @@ void add_bp_score_metrics(
|
||||
}
|
||||
return left->selected_score > right->selected_score;
|
||||
});
|
||||
|
||||
// N/M draw rule (0 on either disables limiting, mirroring the GUI/locator default).
|
||||
const auto max_detected = live_config.gpr_max_detected_objects_to_draw;
|
||||
const auto draw_top = live_config.gpr_draw_top_m_objects;
|
||||
if (max_detected > 0U && draw_top > 0U) {
|
||||
if (visible.size() > max_detected) {
|
||||
visible.clear();
|
||||
} else if (visible.size() > draw_top) {
|
||||
visible.resize(draw_top);
|
||||
}
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
@@ -1815,7 +1840,8 @@ void add_bp_score_metrics(
|
||||
const config::RunConfig& run_config,
|
||||
const ipc::PreprocessedCollection& collection,
|
||||
std::span<const ipc::PreprocessedCollection> previous_collections,
|
||||
const ProcessingLiveConfig& live_config
|
||||
const ProcessingLiveConfig& live_config,
|
||||
ObjectApproachFilter& approach_filter
|
||||
) -> ipc::ResultCollection {
|
||||
ipc::ResultCollection results{};
|
||||
results.collection_id = collection.collection_id;
|
||||
@@ -1833,8 +1859,10 @@ void add_bp_score_metrics(
|
||||
return results;
|
||||
}
|
||||
|
||||
const double velocity_mps =
|
||||
kSpeedOfLightMetersPerSec / std::sqrt(std::max(1e-6, static_cast<double>(run_config.gpr.relative_permittivity)));
|
||||
// Coherent BP fixes the medium to vacuum/air (eps_r = 1), matching the Python
|
||||
// reference Horns_motion_3libre.py (its 0.3 block hardcodes eps_r = 1.0). The
|
||||
// configurable relative_permittivity stays a legacy-GPR-only knob.
|
||||
const double velocity_mps = kSpeedOfLightMetersPerSec;
|
||||
const double start_hz = static_cast<double>(live_config.gpr_start_freq_mhz) * 1'000'000.0;
|
||||
const double stop_hz = static_cast<double>(live_config.gpr_stop_freq_mhz) * 1'000'000.0;
|
||||
const double min_depth_m = static_cast<double>(live_config.gpr_min_depth_m);
|
||||
@@ -1920,7 +1948,7 @@ void add_bp_score_metrics(
|
||||
min_depth_m,
|
||||
max_depth_m,
|
||||
std::max(0.0, static_cast<double>(live_config.gpr_range_comp_power)),
|
||||
std::max(0.0, static_cast<double>(live_config.gpr_angle_comp_power))
|
||||
0.0 // angle compensation fixed off (Python Horns_motion_3libre.py COMP_ANGLE_POWER = 0.0)
|
||||
);
|
||||
if (bp.image.empty()) {
|
||||
return results;
|
||||
@@ -1930,7 +1958,7 @@ void add_bp_score_metrics(
|
||||
const auto incoherent_display_map =
|
||||
normalize_bp_map(bp.incoherent, grid, min_depth_m, max_depth_m, kSmoothSigma);
|
||||
|
||||
auto objects = find_bp_objects(display_map, grid);
|
||||
auto objects = find_bp_objects(display_map, grid, static_cast<double>(live_config.gpr_object_min_frac));
|
||||
add_local_prominence_metrics(objects, display_map, grid, min_depth_m, max_depth_m);
|
||||
add_incoherent_support_metrics(objects, incoherent_display_map, bp.coherence_factor);
|
||||
mark_sidelobe_candidates(objects, selected_traces, selection, imaging_plane_y_m);
|
||||
@@ -1951,14 +1979,34 @@ void add_bp_score_metrics(
|
||||
|
||||
results.collection_payloads.push_back(build_image_payload("gpr_accumulator", grid.x_grid, grid.z_grid, display_map));
|
||||
|
||||
// Final visible set (window + N/M), then the cross-frame approach filter: keep only
|
||||
// objects confirmed as a >= min_frames motion-consistent track (Horns_motion notebook).
|
||||
const auto visible = select_visible_objects(objects, live_config);
|
||||
std::vector<ObjectApproachFilter::Point> visible_points{};
|
||||
visible_points.reserve(visible.size());
|
||||
for (const auto* object : visible) {
|
||||
visible_points.push_back({object->x_m, object->z_m});
|
||||
}
|
||||
const auto confirmed = approach_filter.confirm(
|
||||
visible_points,
|
||||
collection.collection_id,
|
||||
static_cast<double>(collection.monotonic_ns) * 1e-9,
|
||||
static_cast<double>(live_config.gpr_speed_m_s),
|
||||
static_cast<double>(live_config.gpr_look_angle_deg),
|
||||
static_cast<std::size_t>(live_config.gpr_object_approach_min_frames)
|
||||
);
|
||||
|
||||
std::vector<std::vector<float>> point_rows{};
|
||||
point_rows.reserve(objects.size());
|
||||
for (const auto* object : output_objects_sorted(objects, live_config)) {
|
||||
point_rows.reserve(visible.size());
|
||||
for (std::size_t index = 0U; index < visible.size(); ++index) {
|
||||
if (!confirmed[index]) {
|
||||
continue;
|
||||
}
|
||||
point_rows.push_back(
|
||||
{
|
||||
static_cast<float>(object->x_m),
|
||||
static_cast<float>(object->z_m),
|
||||
static_cast<float>(object->selected_score),
|
||||
static_cast<float>(visible[index]->x_m),
|
||||
static_cast<float>(visible[index]->z_m),
|
||||
static_cast<float>(visible[index]->selected_score),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ auto GprProcessor::process_collection(
|
||||
std::span<const ipc::PreprocessedCollection> previous_collections,
|
||||
const ProcessingLiveConfig& live_config
|
||||
) -> ipc::ResultCollection {
|
||||
return process_backprojection_gpr(run_config, collection, previous_collections, live_config);
|
||||
return process_backprojection_gpr(run_config, collection, previous_collections, live_config, approach_filter_);
|
||||
}
|
||||
|
||||
auto LegacyGprProcessor::name() const -> std::string {
|
||||
|
||||
Reference in New Issue
Block a user