- BG_GATE_ENABLE: frame peak over its median background (sar3d_background); the GUI scales the frame with bg_gate_gain and its cloud threshold. - SAR_CLEAN_ENABLE: CLEAN of a one-frame aperture on a 4x10x4 cm search grid over the whole scene; objects in their own scale (sar3d_objects). - SAR_MARK_ENABLE: boxes by class around object groups (sar3d_marks), with the legend under the views. - SAR_VOXEL_M as three steps X/Y/Z with the notebook defaults 0.025/0.02/0.02. - GPU backend: image/download/load/project API, per-plane block reduction for the maximum, depth gate without FMA; sar3d host code built with -ffp-contract=off. - References regenerated from notebook cells 1 and 5, with new CLEAN scenes and a synthetic box-rule case; docs updated.
123 lines
4.9 KiB
Python
123 lines
4.9 KiB
Python
"""Live processing settings model and atomic JSON writer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass, field
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from python_app.file_io import atomic_write_json
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ProcessingLiveConfig:
|
|
"""Runtime-adjustable processing parameters shared with data processor."""
|
|
|
|
processor_mode: str = "pass_through"
|
|
bscan_axis: str = "abs"
|
|
bscan_cut_m: float = 0.824
|
|
bscan_max_depth_m: float = 1.0
|
|
bscan_gain: float = 1.0
|
|
bscan_start_freq_mhz: float = 100.0
|
|
bscan_stop_freq_mhz: float = 8800.0
|
|
tx_geometry: list[dict] | None = None
|
|
rx_geometry: list[dict] | None = None
|
|
gpr_input_positions: list[int] | None = None
|
|
gpr_output_positions: list[int] | None = None
|
|
gpr_min_depth_m: float = 2.0
|
|
gpr_max_depth_m: float = 14.0
|
|
gpr_range_comp_power: float = 0.1
|
|
gpr_object_min_frac: float = 0.7
|
|
gpr_score_mode: str = "combined"
|
|
# Backprojection intra-sweep speed-correction mode: "int_minus" (full
|
|
# correction) or "int_focus" (focusing residual only).
|
|
gpr_motion_mode: str = "int_minus"
|
|
gpr_max_detected_objects_to_draw: int = 5
|
|
gpr_draw_top_m_objects: int = 2
|
|
gpr_object_approach_min_frames: int = 3
|
|
gpr_speed_m_s: float = 0.0
|
|
gpr_look_angle_deg: float = 0.0
|
|
gpr_direction_sign: float = 1.0
|
|
gpr_start_freq_mhz: float = 3000.0
|
|
gpr_stop_freq_mhz: float = 6000.0
|
|
gpr_background_subtract_enabled: bool = True
|
|
gpr_background_mean_count: int = 10
|
|
gpr_remove_sidelobe_objects_enabled: bool = True
|
|
gpr_bp_ground_depth_m: float = 1.25
|
|
pair_depth_equalize: bool = True
|
|
# Visible X/Z window (metres). The locator and the desktop plot both clip
|
|
# detected objects to this window, so the socket broadcasts only what is shown.
|
|
gpr_visible_x_min_m: float = -2.0
|
|
gpr_visible_x_max_m: float = 2.0
|
|
gpr_visible_z_min_m: float = 0.0
|
|
gpr_visible_z_max_m: float = 14.0
|
|
# 3D SAR (sar3d processor) parameters, named after the reference notebook
|
|
# Multi_depthnorm_clean.ipynb (SAR_FRAME_RADIUS, BP_GROUND_DEPTH_M, ...).
|
|
# The band, depth gate, antenna selection, speed and look angle are the
|
|
# shared gpr_* fields.
|
|
sar3d_frame_radius: int = 0
|
|
sar3d_voxel_x_m: float = 0.025 # SAR_VOXEL_M, the voxel steps along X, Y and Z
|
|
sar3d_voxel_y_m: float = 0.02
|
|
sar3d_voxel_z_m: float = 0.02
|
|
sar3d_bp_ground_depth_m: float = 1.25
|
|
sar3d_comp_range_power: float = 0.3
|
|
sar3d_use_compensation: bool = True
|
|
sar3d_normalize_each_frame: bool = True
|
|
sar3d_x_size_m: float = 0.0
|
|
sar3d_y_min_m: float = -0.5
|
|
sar3d_y_max_m: float = 2.0
|
|
sar3d_z_size_m: float = 0.0
|
|
sar3d_bg_gate_enabled: bool = True # BG_GATE_ENABLE: peak-to-background ratio for the display
|
|
sar3d_clean_enabled: bool = True # SAR_CLEAN_ENABLE: CLEAN of a one-frame aperture into objects
|
|
# 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
|
|
reprocess_current_result: bool = True
|
|
history_clear_before_ns: int = 0
|
|
history_removed: list[dict[str, int]] = field(default_factory=list)
|
|
settings_revision: int = 0
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Normalize optional list fields to concrete integer lists."""
|
|
if self.gpr_input_positions is None:
|
|
self.gpr_input_positions = []
|
|
else:
|
|
self.gpr_input_positions = [int(value) for value in self.gpr_input_positions]
|
|
if self.gpr_output_positions is None:
|
|
self.gpr_output_positions = []
|
|
else:
|
|
self.gpr_output_positions = [int(value) for value in self.gpr_output_positions]
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
"""Convert live config to JSON-serializable dictionary."""
|
|
optional_geometry = {"tx_geometry", "rx_geometry"}
|
|
return {
|
|
name: value for name, value in asdict(self).items()
|
|
if name not in optional_geometry or value is not None
|
|
}
|
|
|
|
|
|
class ProcessingLiveConfigWriter:
|
|
"""Atomic writer for processing live-config file."""
|
|
|
|
def __init__(self, config_path: Path) -> None:
|
|
"""Create writer targeting `config_path`."""
|
|
self._config_path = config_path
|
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
@property
|
|
def path(self) -> Path:
|
|
"""Return destination config path."""
|
|
return self._config_path
|
|
|
|
def write(self, config: ProcessingLiveConfig) -> Path:
|
|
"""Atomically write config by temp-file replace."""
|
|
# Hot path: rewritten on every live knob change, so keep this at DEBUG.
|
|
logger.debug(
|
|
"Writing live processing config (mode=%s) to %s",
|
|
config.processor_mode,
|
|
self._config_path,
|
|
)
|
|
return atomic_write_json(self._config_path, config.to_dict())
|