examples/a2/algo/heatmap/heatmap_processor.py

examples/a2/algo/heatmap/heatmap_processor.py#

  1# Copyright (c) Acconeer AB, 2024-2026
  2# All rights reserved
  3
  4from __future__ import annotations
  5
  6import argparse
  7
  8import numpy as np
  9
 10from PySide6.QtGui import QTransform
 11
 12import pyqtgraph as pg
 13
 14import acconeer.exptool as et
 15from acconeer.exptool import a2
 16from acconeer.exptool.a2.algo.heatmap.conventional import (
 17    HeatmapConfig,
 18    HeatmapProcessor,
 19    ProcessorResult,
 20)
 21
 22
 23SENSOR_CONFIG = a2.SensorConfig.single_subsweep(
 24    # SensorConfig params.
 25    sweeps_per_frame=1,
 26    sweep_rate=None,
 27    # Subsweep params.
 28    start_point=60,
 29    num_points=34,
 30    step_length=12,
 31    hwaas_max=100,
 32    hwaas_min=4,
 33    hwaas_exponent=4,
 34    tx_antenna_mask=0b111,
 35    rx_antenna_mask=0b1111,
 36    profile=a2.Profile.PROFILE_1,
 37)
 38
 39UPDATE_RATE = 20
 40
 41HEATMAP_CONFIG = HeatmapConfig(
 42    num_az_angles=61,
 43    az_angle_range=(-60, 60),
 44    num_el_angles=1,
 45    el_angle_range=(0, 0),
 46    covariance_matrix_time_constant_s=0.20,
 47    high_pass_filter_time_constant_s=5.0,
 48    antenna_array=a2.AntennaArray.AP212,
 49    heatmap_mode=HeatmapConfig.HeatmapMode.MOVEMENT,  # or DISTANCE
 50)
 51
 52DEGREE_SYMBOL = "\u00b0"
 53BASE_STEP_LENGTH = 2.5e-3
 54
 55
 56class PGUpdater:
 57    """
 58    Minimal version of plotting from exploration tool.
 59    """
 60
 61    def __init__(self, session_config, heatmap_config) -> None:
 62        self._config: a2.SessionConfig | None = session_config
 63        self.heatmap_config = heatmap_config
 64
 65    def setup(self, win):
 66        self.viridis_heatmap_image_item = pg.ImageItem()
 67        self.viridis_heatmap_image_item.setLookupTable(et.utils.pg_mpl_cmap("viridis"))
 68
 69        self.viridis_heatmap_plot_item = win.addPlot(row=0, col=0, title="Heatmap Visualization")
 70        self.viridis_heatmap_plot_item.getAxis("left").setLabel("Distance", units="m")
 71        self.viridis_heatmap_plot_item.getAxis("bottom").setLabel("Az angle", units=DEGREE_SYMBOL)
 72        self.viridis_heatmap_colorbar = self.viridis_heatmap_plot_item.addColorBar(
 73            self.viridis_heatmap_image_item, colorMap="viridis"
 74        )
 75
 76        self.viridis_heatmap_plot_item.clear()
 77        self.viridis_heatmap_plot_item.addItem(self.viridis_heatmap_image_item)
 78        self.viridis_heatmap_plot_smooth_max = et.utils.SmoothMax(
 79            self._config.update_rate, tau_decay=0.1, tau_grow=0.1
 80        )
 81
 82        start_point = self._config.sensor_config.subsweep.start_point
 83        step_length = self._config.sensor_config.subsweep.step_length
 84
 85        self.viridis_heatmap_image_item.setTransform(
 86            QTransform()
 87            .translate(-self.heatmap_config.az_span / 2, start_point * BASE_STEP_LENGTH)
 88            .scale(
 89                self.heatmap_config.az_span / len(self.heatmap_config.az_angles),
 90                step_length * BASE_STEP_LENGTH,
 91            )
 92        )
 93
 94    def update(self, processor_result: ProcessorResult) -> None:
 95        """Plotting is achieved by repeated calls to update with new processorresults"""
 96
 97        if processor_result.heatmap is None:
 98            return
 99        heatmap = processor_result.heatmap[:, 0, :]
100        max_val = np.max(heatmap)
101
102        max_heat = self.viridis_heatmap_plot_smooth_max.update(max_val)
103        image = np.nan_to_num(heatmap)
104        self.viridis_heatmap_colorbar.setLevels((0, max_heat))
105        self.viridis_heatmap_image_item.updateImage(image, levels=(0, max_heat))
106
107
108def main():
109    parser = argparse.ArgumentParser()
110    parser.add_argument(
111        "--output-file",
112        default="data.h5",
113        help="path to h5 file to store the radar data",
114    )
115
116    args = parser.parse_args()
117
118    output_file = args.output_file
119
120    with (
121        a2.Client.open(
122            # Leaving this comment as is will automatically search for an
123            # EVK connected via USB. Otherwise, one of the following should be specified:
124            #
125            # ip_address="<ip address of host (like a RPi). E.g. 192.168.XXX.YYY>",
126            # or
127            # serial_port="<serial port of module. E.g. COM3 or /dev/ttyUSBx for Window/Linux>",
128            # or
129            # usb_device=True,
130            # or
131            # mock=True,
132        ) as client
133    ):
134        processor = HeatmapProcessor(SENSOR_CONFIG.subsweep, HEATMAP_CONFIG, UPDATE_RATE)
135
136        session_config = a2.SessionConfig([SENSOR_CONFIG], update_rate=UPDATE_RATE)
137        client.setup_session(session_config)
138
139        pg_updater = PGUpdater(session_config, HEATMAP_CONFIG)
140        pg_process = et.PGProcess(pg_updater)
141        pg_process.start()
142
143        # Here we create and attach a H5Recorder to the Client.
144        # The H5Recorder will sample all frames retrieved in the client.
145        # Once the "with"-block have been exited, the H5Recorder will
146        # wrap up and close the file.
147        with a2.H5Recorder(output_file, client, mode="w"):
148            client.start_session()
149
150            interrupt_handler = et.utils.ExampleInterruptHandler()
151            print("Press Ctrl-C to end session")
152
153            while not interrupt_handler.got_signal:
154                # Client will send its Results to the H5Recorder.
155                result = client.get_next(format=a2.Client.ResultFormat.SOLE)
156
157                # Post-process radar data and get the heatmap result
158                processor_result = processor.process(result)
159
160                # Make sure we have a heatmap to plot
161                if processor_result.heatmap is None:
162                    continue
163
164                try:
165                    # Plot the heatmap
166                    pg_process.put_data(processor_result)
167                except et.PGProccessDiedException:
168                    break
169
170            print("Disconnecting...")
171            pg_process.close()
172            client.stop_session()
173
174
175if __name__ == "__main__":
176    main()

View this example on GitHub: acconeer/acconeer-python-exploration