一、项目简介
本文介绍一个使用 C++17 + Qt 6 + CMake 开发的简单雷达回波信号检测与测距仿真系统。
项目主要模拟雷达接收到多个目标回波后的信号处理过程,并通过窗口能量检测目标,再对目标回波的起始位置进行精定位,最终根据回波延迟计算目标距离,并使用 Qt Widgets 和 QPainter 将结果进行可视化。
整个项目的处理流程如下:
text
目标回波生成
↓
多目标信号叠加
↓
加入高斯噪声
↓
估计噪声能量
↓
计算窗口能量
↓
自适应阈值检测
↓
得到目标候选位置
↓
局部能量精定位
↓
计算目标距离
↓
Qt 可视化
本项目主要用于学习 C++ 信号处理、雷达测距基本原理以及 Qt 自绘图形,并不针对真实雷达系统进行建模。
二、项目使用的技术
开发环境:
- C++17
- Qt 6
- Qt Widgets
- CMake
- MinGW 64-bit
项目结构:
text
RadarSignalSimulator/
├── CMakeLists.txt
├── main.cpp
├── mainwindow.h
├── mainwindow.cpp
├── radar_signal_generator.h
├── radar_signal_generator.cpp
├── signal_widget.h
└── signal_widget.cpp
其中:
text
RadarSignalGenerator
负责信号生成、噪声处理、能量检测和距离计算相关的数据处理。
text
SignalWidget
负责将原始信号、窗口能量和检测结果绘制到 Qt 界面中。
三、雷达测距的基本原理
雷达通过发射电磁波并接收目标反射回来的回波,可以根据回波的传播时间计算目标距离。
假设雷达发射信号后,经过时间 ( τ) 接收到目标回波。
由于电磁波需要经历:
text
雷达 → 目标
目标 → 雷达
两个方向,因此目标距离为:
R=2cτ
其中:
- (R):目标距离
- (c):电磁波传播速度
- ( τ):回波延迟时间
本项目中使用:
c=3×108 m/s
四、模拟雷达回波
首先定义基本参数:
cpp
const int sampleCount = 5000;
const double sampleRate = 10'000'000;
const double carrierFrequency = 1'000'000;
const double pulseWidth = 20e-6;
const double noiseStdDev = 0.15;
也就是:
text
采样点数:5000
采样率:10 MHz
载波频率:1 MHz
脉冲宽度:20 μs
噪声标准差:0.15
然后模拟三个不同距离的目标:
cpp
std::vector<double> signal1 =
generator.generatePulseEcho(
sampleCount,
sampleRate,
carrierFrequency,
pulseWidth,
8000,
1.0,
0.0
);
std::vector<double> signal2 =
generator.generatePulseEcho(
sampleCount,
sampleRate,
carrierFrequency,
pulseWidth,
15000,
0.8,
0.0
);
std::vector<double> signal3 =
generator.generatePulseEcho(
sampleCount,
sampleRate,
carrierFrequency,
pulseWidth,
23000,
0.6,
0.0
);
三个目标分别位于:
text
8 km
15 km
23 km
同时设置不同的目标幅度:
text
目标1:1.0
目标2:0.8
目标3:0.6
用于模拟不同强度的目标回波。
五、根据目标距离计算回波延迟
在信号生成函数中,首先根据目标距离计算回波延迟:
cpp
double delayTime =
2.0 * targetRange / C;
其中:
cpp
C = 3.0e8;
然后转换成采样点:
cpp
int delaySamples =
static_cast<int>(
delayTime * sampleRate
);
例如目标距离为 8 km:
τ=3×1082×8000
得到约:
53.33 μs
在 10 MHz 采样率下:
53.33μs×10MHz≈533.3
因此目标回波大约出现在第 533 个采样点附近。
六、生成带载波的回波
在回波存在的时间范围内生成正弦载波:
cpp
double echo =
targetAmplitude *
std::sin(
2.0 *
PI *
carrierFrequency *
t
);
最终将回波加入对应采样点:
cpp
signal[i] += echo;
因此最终得到的是一个带有一定载波振荡的脉冲回波。
七、加入高斯噪声
真实雷达接收到的信号通常不会是完全干净的。
因此项目中使用 C++ 标准库的随机数工具生成高斯噪声:
cpp
std::normal_distribution<double> noise(
0.0,
noiseStdDev
);
然后在三个目标信号叠加之后统一加入噪声:
cpp
signal[i] =
signal1[i]
+ signal2[i]
+ signal3[i]
+ noise(generator);
最终得到:
text
多目标回波 + 高斯噪声
这也是后续目标检测算法的输入。
八、估计噪声能量
为了避免直接使用一个固定的检测阈值,项目首先利用没有明显目标回波的前 500 个采样点估计噪声能量。
计算方法:
En=N1i=1∑Nxi2
对应代码:
cpp
double energy = 0.0;
for (int i = start;
i < start + count;
++i)
{
energy += signal[i] * signal[i];
}
return energy / count;
这样得到的是噪声平均能量。
例如一次运行得到:
text
Estimated noise energy: 0.0234315
九、使用窗口能量进行目标检测
直接观察带噪声的原始载波信号并不容易判断目标是否存在。
因此项目采用窗口能量作为检测依据。
对于当前采样点附近的一段信号:
E(i)=∑x2
项目使用 50 个采样点作为检测窗口:
cpp
const int detectionWindowSize = 50;
然后计算局部窗口能量。
窗口能量的一个重要特点是:
它可以弱化载波正负振荡带来的影响,更关注这一段信号整体包含多少能量。
因此比直接判断某一个采样点的幅度更加适合当前的目标检测任务。
十、自适应检测阈值
检测阈值根据噪声能量计算:
cpp
const double thresholdFactor = 6.0;
double energyThreshold =
noiseEnergy *
detectionWindowSize *
thresholdFactor;
例如:
text
Estimated noise energy: 0.0234315
Energy threshold: 7.02945
这里没有直接写死:
cpp
energyThreshold = 7;
而是根据当前噪声水平动态计算。
这样做的一个好处是:
当噪声水平发生变化时,检测阈值也会随之变化。
十一、多目标粗检测
计算出窗口能量以后,对整个能量序列进行扫描。
当能量超过检测阈值时,认为进入了一个潜在目标区域。
项目中还允许阈值以下存在一定长度的间隙:
cpp
const int maxGapSamples = 30;
这样可以避免因为噪声导致目标区域被分割成很多小段。
最终得到目标候选位置:
text
Detected candidates:
Candidate: 517
Candidate: 993
Candidate: 1547
这里的 Candidate 并不是最终的目标起始位置,而只是一个粗定位结果。
十二、为什么还需要精定位?
粗检测得到的 Candidate 并不一定等于真正的回波起始位置。
原因之一是窗口能量本身具有一定的时间范围。
例如:
text
原始信号:
回波开始
↓
────────────────╱╲╱╲╱╲────────────
窗口能量:
┌─────────┐
──────────────┘ └──────────
↑
能量区域
窗口会造成一定程度的时间扩展。
因此:
text
Candidate
只能用于确定:
"目标大概在这里。"
接下来还需要进行更加精细的定位。
十三、使用较小窗口进行精定位
粗检测使用:
cpp
const int detectionWindowSize = 50;
精定位则使用更小的窗口:
cpp
const int localizationWindowSize = 10;
对应:
cpp
std::vector<double> localEnergy =
generator.calculateWindowEnergy(
signal,
localizationWindowSize
);
同时使用连续采样点超过阈值作为判断条件:
cpp
const int consecutiveSamples = 3;
也就是说,不是某一个采样点超过阈值就立即认为回波开始,而是要求连续多个采样点满足条件。
十四、Candidate 附近搜索 Echo Start
精定位函数不会直接从 Candidate 往后搜索。
因为窗口能量的中心位置与实际回波起始位置之间可能存在一定偏移。
因此在 Candidate 前面预留一定搜索范围:
cpp
const int backtrackSamples = 50;
搜索范围大致为:
text
Candidate - 50
↓
───────[──────────────]────────
↑ ↑
searchStart searchEnd
然后寻找连续多个采样点超过局部能量阈值的位置。
核心逻辑:
cpp
if (energy[i] > energyThreshold)
{
++count;
if (count >= consecutiveSamples)
{
return i - consecutiveSamples + 1;
}
}
else
{
count = 0;
}
最终得到:
text
Echo start
这一步才作为后续测距的依据。
十五、根据 Echo Start 计算目标距离
得到目标起始采样点以后:
cpp
double delayTime =
static_cast<double>(echoStart)
/ sampleRate;
得到回波延迟时间。
然后使用:
R=2cτ
计算距离:
cpp
double measuredRange =
C * delayTime / 2.0;
最后转换为 km:
cpp
measuredRange / 1000.0
十六、一次运行结果
一次运行得到:
text
Estimated noise energy: 0.0234315
Energy threshold: 7.02945
Local energy threshold: 1.40589
Detected candidates:
Candidate: 517
Candidate: 993
Candidate: 1547
Echo start: 531
Measured range: 7.965 km
Echo start: 999
Measured range: 14.985 km
Echo start: 1538
Measured range: 23.07 km
目标真实距离为:
text
8 km
15 km
23 km
测量结果与真实距离存在一定误差,这是由于当前模拟器采用离散采样,并且目标起始位置最终需要映射到整数采样点。
十七、距离分辨率
当前采样率为:
Fs=10MHz
因此采样间隔:
Δt=Fs1=0.1μs
对应的距离变化为:
ΔR=2Fsc
代入:
ΔR=2×1073×108=15m
因此当前系统一个采样点对应约:
text
15 m
这也解释了为什么最终测距结果不会总是精确等于:
text
8.000 km
15.000 km
23.000 km
十八、Qt 信号可视化
信号处理完成后,使用 Qt Widgets 中的 QPainter 进行绘制。
界面主要显示两部分:

其中:
- 蓝色曲线:原始雷达回波
- 红色曲线:窗口能量
- 绿色竖线:检测到的目标位置
- X 轴:目标距离
- 检测阈值:窗口能量判断目标的依据
十九、为什么使用 QPainter?
Qt 的 QPainter 可以直接在 QWidget 上进行二维绘图。
例如绘制信号曲线:
cpp
QPainterPath path;
path.moveTo(x, y);
path.lineTo(x, y);
painter.drawPath(path);
通过将采样点转换成屏幕坐标:
cpp
double xRatio =
static_cast<double>(i) /
static_cast<double>(m_signal.size() - 1);
double x =
left +
xRatio * plotWidth;
就可以把:
text
采样点
映射到:
text
Qt 窗口坐标
这也是本项目中比较重要的一部分 Qt 实践。
二十、目标位置可视化
检测到目标以后,将 echoStarts 传递给 SignalWidget:
cpp
m_signalWidget->setEchoStarts(
echoStarts,
sampleRate
);
然后根据采样点计算屏幕上的 X 坐标:
cpp
double xRatio =
static_cast<double>(echoStart) /
static_cast<double>(
m_signal.size() - 1
);
double x =
left +
xRatio * plotWidth;
最后绘制竖线:
cpp
painter.drawLine(
x,
signalTop,
x,
signalTop + signalHeight
);
这样程序检测到的目标就能够直接显示在波形上。
二十一、项目中的几个关键设计
这个项目虽然规模不大,但在实现过程中有几个值得注意的问题。
1. 粗检测和精定位没有使用同一个窗口
粗检测:
text
50 samples
精定位:
text
10 samples
原因是两者目的不同。
粗检测更加关注:
是否存在目标?
精定位更加关注:
目标究竟从哪里开始?
因此没有必要让两者使用完全相同的参数。
2. 不直接使用原始幅度判断回波开始
如果直接判断:
cpp
std::abs(signal[i]) > threshold
可能受到载波相位影响。
例如正弦波:
text
/\
/ \
────/────\────/────\────
/ \ /
即使目标回波已经存在,某些采样点仍然可能非常接近 0。
因此本项目采用局部能量进行精定位,减少了载波相位对判断的影响。
3. 不使用固定阈值
项目没有简单使用:
cpp
energy > 5
而是根据噪声估计:
cpp
energyThreshold =
noiseEnergy *
windowSize *
thresholdFactor;
这样可以让检测阈值与噪声水平建立联系。
二十二、项目最终效果
目前程序已经能够完成:
text
① 生成多个目标回波
② 模拟高斯噪声
③ 估计噪声能量
④ 计算窗口能量
⑤ 自适应阈值检测
⑥ 检测多个目标
⑦ 对目标进行精定位
⑧ 根据回波延迟计算距离
⑨ Qt 绘制原始信号
⑩ Qt 绘制窗口能量
⑪ 在波形上显示目标位置
⑫ 显示距离刻度
从程序运行结果来看,可以正确检测出 8 km、15 km 和 23 km 附近的三个模拟目标。
二十三、项目总结
通过这个项目,我把一个简单的雷达测距问题拆分成了几个相对独立的模块:
text
信号模型
↓
噪声模型
↓
能量检测
↓
目标定位
↓
距离估计
↓
图形显示
在 C++ 部分,主要实践了:
std::vectorstd::randomstd::normal_distribution- STL 算法
- C++ 类与成员函数
- C++17
在 Qt 部分,主要实践了:
- Qt Widgets
QMainWindowQWidget- 信号与槽
QPainterQPainterPath- 自定义
paintEvent()
在算法方面,则实践了:
- 离散采样
- 回波延迟
- 窗口能量
- 噪声能量估计
- 自适应阈值
- 粗检测
- 精定位
- 雷达测距
这个项目属于一个简化的雷达信号处理仿真,并不能代表实际雷达系统中的完整处理流程。后续如果继续扩展,可以进一步加入 FFT、距离谱、脉冲压缩、距离-多普勒处理以及二维矩阵数据的可视化等内容。
对于目前这个版本来说,已经完成了从**"模拟雷达信号"到"检测目标并测量距离"再到"Qt 可视化"**的完整流程。