Introduction to Radar: Part 2

How to detect objects with the world’s most powerful sensor

Isaac Berrios
10 min readJun 12, 2024

This post will show you how to process raw Radar data to detect objects. In particular, we will learn how to obtain something called the Range-Doppler matrix which shows the range and velocity of different detected objects. The Radar estimates three parameters of distant objects: Range, Velocity, and Angle; we will focus on Range and Velocity and leave Angle for part 3. If you haven’t already, take a look at part 1 since that covers the basic concepts we will implement in this post. I believe it’s best to learn by example, so the data we will use comes from the RaDICaL dataset, and we are using the 50 frame sample file located here which contains an indoor scene. If you would like to follow along, the Jupyter notebook is located on GitHub.

Photo by Johnny McClung on Unsplash

Getting started

First, let’s load the sample data from the h5 file:

import h5py

data_dict = {}
with h5py.File(DATA_PATH, 'r') as h5_obj:

for key in h5_obj.keys():
print(key, 'shape: ', h5_obj[key].shape)
data_dict.update({key : np.asarray(h5_obj[key])})

The Radical dataset comes with a Python library called Radical SDK, but we will avoid using external libraries except for numpy and h5py and do everything from scratch.

Now we have the data stored in a dictionary, let’s go ahead and access a random frame of raw Radar data.

adc_data = data_dict['radar'][28, ...]

What exactly is this data?

We call this ADC data because it is the output of the Analog to Digital Converter (ADC) in the Radar. Go ahead and inspect the data, the complex values allow us to conveniently store magnitude and phase information in a Cartesian manner. This is important because the Radar transmits sinusoidal chirps and the reflections might have phase shifts that correspond to a moving object which we can detect. See Doppler Effect for more details.

How did we get this data? (Optional)

The chirp signal is generated in the Local Oscillator (LO) which sends it to a Power Amplifier (PA) and then to the Tx antenna for transmission. The reflected signal is received by the Rx antenna and amplified by a Low Noise Amplifier (LNA). This is signal is then mixed with two versions of the transmitted signal one is an exact copy and the other is shifted 90° out of phase. The two resulting Intermediate Frequency (IF) signals are called In-Phase and Quadrature or I/Q, which constitute a complex number of the form: I + jQ. The benefit of processing the signals in this manner is that we can obtain a higher Signal to Noise (SNR) ratio which helps improve Radar performance. See this for more details.

Figure 1. Basic FMCW Radar Architecture. Source.

Process Range

Before we go all out and compute the Range, let’s inspect the data dimensions:

adc_data.shape
# (32, 8, 304)

Our data is 3D and is sometimes referred to as the Radar cube. The first dimension is the number of chirps in a Radar frame, this is also the number of Doppler Velocity bins we have. The second dimension is the number of virtual receive antennas, in this case we have a Uniform Linear Array (ULA) of 8 receive antennas. The third dimension is the number of ADC samples which is also the number of range bins.

  • Number of ADC samples → Number of Range Bins
  • Number of Chirps → Number of Doppler Velocity Bins

Let’s go ahead and process the Range data, to do this we take the FFT across all of the ADC samples. We can display the Range Cube data by summing across all the antennas, then take the log of the magnitude for better visualization in Decibels (dB).

range_cube = np.fft.fft(adc_data, axis=2).transpose(2, 1, 0)

plt.plot(10*np.log10(np.abs( range_bin_chirps)))
plt.title("Range bins across all chirps in the frame")
plt.xlabel("Range Bins")
plt.ylabel("Magnitude");
Figure 2. Range bin values from each chirp (32 total). Source: Author.

Here’s our first glimpse of processed data, this provides use with a range estimate for each chirp transmitted in the frame. The large spike on the left, is due to clutter returns from the chirps bouncing off of the floor close to the Radar, the secondary spike below 100 corresponds to people walking, and the large spikes around 200 are from the metal doors. Metal objects have a higher reflectivity for Radars due to the Radio Waves that they use, see RCS for more.

Figure 3. Left: Image of frame 28. Right: Range Bin values of frame 28. Source: Author.

We are doing what early Radar operators used to do, they would look at Range returns on something called the A-scope and manually look at the peaks and determine which ones were targets. Now we have better Statistical Based methods of detection that alleviate the Radar operator of manual work. Despite of this, it can still be a good exercise to understand how the Radar works.

Process Doppler Velocities

To process Doppler Velocity, we will take the FFT over the chirps dimension, this allows us to discover the phase changes that occur from chirp-to-chirp.

range_doppler = np.fft.fftshift(np.fft.fft(range_cube, axis=2), axes=2)
range_doppler_psd = 10*np.log10( np.abs(range_doppler)**2 ).sum(axis=1).T

_, ax = plt.subplots(1, 1, figsize=(20, 5))
ax.imshow(range_doppler_psd)
ax.set_title("Range Doppler Power Spectrum")
ax.set_xlabel("Range Bins")
ax.set_ylabel("Doppler Bins");

# set Doppler Bin Ticks
ax.set_yticks([0, 8, 16, 24, 32], [16, 8, 0, -8, -16]);
Figure 4. Range Doppler over all antennas. Source: Author.

Once we compute the complex Range Doppler cube, we compute the Power Spectral Density by taking the square of it’s magnitude. We sum across all receive antennas to gather all the information in a single 2D matrix. The vertical axis shows the Doppler Velocity bins which range from -16 to 16, objects moving closer will be positive while objects moving away will be negative. The horizontal axis shows the same Range bins the we saw previously. In figure 4 we can really see that the Range information has been cleaned up, the two people around range bin 100 are clearly broken out in range and the highly reflective doors are broken out around range bin 200. Another observation is that the clutter returns in the first ~5 range bins are confined to the 0-Doppler range bin, which is what we expect because the clutter in this scenario is generated from the floor. Speaking of the 0-Doppler bins, why are they all so high? It’s because most of the Radar returns have 0 — Velocity. So this tells us that the noise levels will be higher for different for 0-Doppler Bin compared to the rest of the Doppler Bins, let’s take another look to gain some intuition.

_, ax = plt.subplots(1, 2, figsize=(15, 5))
ax[0].plot(range_doppler_psd.T); # plot range hits from each Doppler Bin
ax[0].set_title("Range bins for each Doppler")
ax[1].set_xlabel("Range Bins")
ax[1].set_ylabel("Relative Magnitude");
ax[1].plot(range_doppler_psd.sum(axis=0)); # Sum Range hits across Doppler Bins
ax[1].set_title("Range bins summed across Doppler")
ax[1].set_xlabel("Range Bins")
ax[1].set_ylabel("Relative Magnitude");
Figure 5. Left: Range hits for each Doppler Frequency. Right: Range hit summed across all Doppler frequencies. Source: Author.

The plot on the right shows the range hits from each Doppler Bin, the pink plot is the 0 Doppler bin. Notice how the large clutter return close to 0 is only in the 0 Doppler bin. We can also see potential hidden targets (around range bin 125) that are not apparent in the Doppler bin due to its high noise level. This is another benefit of Radar, it’s inherently good at identifying Moving Targets (This is a whole sub-field of Radar: MTI). The right plot shows the range hits summed across all Doppler bins, notice how the objects are more clearly distinguished and the clutter return at the 0 Doppler bin is insignificant. In the next section, we will learn how to apply real units to this so we can make actual measurements.

Adding Real Units

Now we will add real units to see what exactly the Radar is measuring, all we need to do is compute both the Range and Doppler/Velocity resolutions. We will need the Radar config file for this data which is located here, the config file tells us how the Radar was programmed/configured. We will heavily leverage this piece of Texas Instruments documentation which describes the different Radar parameters and settings used to configure the Radar. Let’s go ahead and read the desired fields from the config file.

Yes, we have range and Doppler resolutions in the config file, but we will learn how to compute these from scratch given our Radar config parameters.

# not the brst way, but it gets the job done
with open(CONFIG_PATH, 'r') as f:
for line in f.readlines():
if 'profileCfg' in line:
line = line.split(' ')

start_freq = float(line[2]) # GHz
idle_time = float(line[3]) # usec
ramp_end_time = float(line[5]) # usec
chirp_slope = float(line[8]) # MHz/usec
num_adc_samples = float(line[10]) # unitless
adc_sample_rate = float(line[11]) # Msps

elif 'channelCfg' in line:
line = line.split(' ')

# number of receive antennas
rx_bin = bin(int(line[1])).zfill(4)
num_rx = len([i for i in rx_bin if i == '1'])

# number of transmit antennas
tx_bin = bin(int(line[2])).zfill(4)
num_tx = len([i for i in tx_bin if i == '1'])


# Number of Rx Antennas: 4
# Number of Tx Antennas: 2
# Start Frequency: 77.0 GHz
# Idle Time: 58.0 usec
# Ramp End Time: 40.0 usec
# Chirp Slope: 100.0 MHz/usec
# Number of ADC Samples: 304.0
# ADC sample rate: 9499.0 Msps

The Idle Time is the dead/idle time between the end of the previous chirp to the next chirp. The Ramp End Time is the time from the start of the Chirp Ramp until the end of the ramp. The total chirp time interval is the sum of these two values. Here’s a survey of the parameters we will be dealing with.

Compute Range Resolution

The range resolution equation is actually simple:

We just need to compute the chirp Bandwidth B, which depends on the length and slope of the chirp. An example of this relationship is shown below.

Figure 5. Example of a chirp of length Tc with slope S and Bandwidth B.

You might think we can use the total chirp time as the length of the chirp, however it isn’t exactly this straight forward, to process the range returns we need to consider how the Radar is listening to the received signal. When the Radar receives reflected signals, it’s ADC will digitize it, but it’s not always actively doing this. We need to find it’s active sampling window/period and use that as our chirp length, see “ADC Sampling Window” in orange below.

Figure 7. Typical FMCW Chirp with annotated ADC Sampling Window and Bandwidth. Source.

Thankfully there’s a simple way to do this, we don’t need to know when it goes active we just need to know how long it’s active. So we just multiply the ADC sampling rate with the total number of ADC samples to get the total length of the ADC Sampling Window/Period, which is the precise time interval that the Radar listens for reflections from each chirp. Next, we multiply the chirp slope (MHz/usec) by the Sampling Window/Period (1/msec) to get the total bandwidth. The code is much easier than the explanation.

# speed of wave propagation
c = 299792458 # m/s

# compute ADC sample period T_c in msec
adc_sample_period = 1 / adc_sample_rate * num_adc_samples # msec

# next compute the Bandwidth in GHz
bandwidth = adc_sample_period * chirp_slope # GHz

# Coompute range resolution in meters
range_resolution = c / (2 * (bandwidth * 1e9)) # meters

range_resolution
# 0.04683764076549342

To compute the max range, all we do is multiply the range resolution by the maximum number of range bins:

max_range = range_resolution * num_adc_samples

Compute Doppler/Velocity Resolution

Now let’s compute the Doppler/Velocity resolution, for brevity we will just refer to this as Doppler resolution from now on. The formula for this was derived in part 1, where N is the total number of chirps and λ is the center wavelength of the transmit chirp.

Unlike the Range resolution we can compute this directly, but we need to clarify the total number of chirps N. This Radar config actually uses two transmit antennas, each one emitting 32 chirps for a total of 64 chirps. The Radar uses Time Division Multiplexing (TDM) to make this work, and there is actually some more processing called Doppler Deinterleaving to obtain the chirps in the true order of transmission before processing. This usually occurs locally on the Radar chips before we get the I/Q data, see this or this for more details.

num_chirps = adc_data.shape[0]

# compute center frequency in GHz
center_freq = (77 + bandwidth/2) # GHz

# compute center wavelength
lmbda = c/(center_freq * 1e9) # meters

# interval for an entire chirp including deadtime
chirp_interval = (ramp_end_time + idle_time) * 1e-6 # usec

doppler_resolution = lmbda / (2 * num_chirps * num_tx * chirp_interval)

doppler_resolution
# 0.3040613230233833

To compute the max doppler, we need to make sure to divide by the number of transmitters.

max_doppler = num_chirps * doppler_resolution / 2 # m/s

Let’s go ahead and display the Range-Doppler matrix with real units.

ranges = np.arange(0, max_range + range_resolution, range_resolution)
dopplers = np.arange(-max_doppler, max_doppler + doppler_resolution, doppler_resolution)

range_ticks = np.arange(0, len(ranges), len(ranges)//10)
range_tick_labels = ranges[::len(ranges)//10].round(2)

doppler_ticks = np.arange(0, len(dopplers), len(dopplers)//10)
doppler_tick_labels = dopplers[::len(dopplers)//10][::-1].round(2)

fig, ax = plt.subplots(1, 1, figsize=(25, 2))
ax.imshow(range_doppler_psd)
fig.suptitle("Range Doppler Power Spectrum")
ax.set_xlabel("Range (meters)")
ax.set_ylabel("Doppler Velocity (m/sec)");

# apply Range and Doppler labels
ax.set_xticks(range_ticks, range_tick_labels);

ax.set_yticks(doppler_ticks, doppler_tick_labels);
Figure 8. Range-Doppler matrix with real units. Source: Author.

Conclusion

We’ve learned how to process Range and Doppler information which just amounts to FFT operations across different dimensions of the Radar data cube. We have also learned how to convert the binned values to real units that would be useful for a real system. In the next post, we will learn how to process Angle data.

References

--

--