简介:面向车载毫米波雷达信号处理与目标检测学习者,这份资源围绕静止目标(静态杂波)滤除这一关键问题,提供配套代码与实测ADC回波数据,适合正在研究动目标检测、杂波抑制或Matlab仿真实践的读者。压缩包共3个文件,体积约248KB:MATLAB脚本实现了从预处理到滤波的完整流程,txt文档对算法原理和使用步骤作补充说明,mat文件则为验证算法效果提供原始数据支撑。已有1793人学习下载。内容上,脚本可作为直接运行的起点,配合Data_ADC.mat可完成从去直流、增益校正到静态杂波抑制的全过程复现;txt中关于LMS、自适应滤波、频域滤波等方法的拆解,也能帮助理解不同策略在车载场景下的适用性。对希望深入掌握雷达静态目标滤除实现细节的读者而言,这份小体积资源具有较高的参考与动手实践价值。
1. 静态杂波滤除:车载毫米波雷达点云里那 90% 的“假目标”怎么去掉
做过车载毫米波雷达目标检测的人都有这种体验:一帧点云出来,密密麻麻全是回波,但真正值得跟踪的动目标只有零星几个。护栏、路牌、静止车辆、桥墩,这些静止目标反射强度往往比行人还高,直接送进跟踪器会把航迹搅得一团糟。这套配套代码解决的就是这个问题——用 Matlab 实现一条完整的静态杂波滤除链路,从原始 ADC 回波到距离-多普勒图,再做 CFAR 检测、自速度估计和静止带剔除,最后输出只剩运动目标的点云列表。适合正在做车载雷达算法验证的工程师,也适合拿真实雷达数据练手的信号处理方向学生。
2. 架构与数据链路:从 ADC 回波到距离-多普勒图
2.1 工程目录与各文件职责
这份资源不是零散脚本,是按工程习惯组织的完整链路。拿到手先看目录,文件职责非常清晰:雷达参数配置独立成一个脚本,信号处理各阶段各占一个函数,主脚本负责串联,数据文件单独放一个目录。这样改参数不用翻代码,换数据不用动逻辑。
radar_config.m % 雷达参数配置,所有可调参数集中在这里 rangeDopplerMap.m % 快时间FFT + 慢时间FFT,生成距离-多普勒矩阵 caCfar2D.m % 二维恒虚警检测,输出目标掩膜 estimateSelfVelocity.m % 自速度估计,用于确定静止带中心 filterStatic.m % 静止带滤除,输出动目标列表 run_demo_single.m % 单帧演示入口 run_demo_sequence.m % 连续帧演示入口 data/single_frame.mat % 单帧原始回波(256 x 128 复数矩阵) data/seq_01.mat % 连续 20 帧回波数据主流程是单向数据流:原始回波进rangeDopplerMap得到距离-多普勒矩阵,caCfar2D在这个矩阵上做检测得到候选目标,estimateSelfVelocity从多普勒维投影估计自车速度,最后filterStatic把静止带内的候选点剔除。每一步的输出都是下一步的输入,接口干净,单独抽出来也能复用。
2.2 FMCW 参数设计与数据格式
代码包的雷达参数按 77GHz 车载 FMCW 毫米波雷达的典型配置设定。这些参数不是随便填的,它们决定了距离分辨率和速度分辨率,直接影响后面静止带宽度和 CFAR 窗口的设计。
% radar_config.m % 77GHz 车载 FMCW 雷达典型参数 cfg.fs = 10e6; % ADC采样率 10 MHz cfg.slope = 30e12; % 调频斜率 30 MHz/us,换算为 Hz/s cfg.txOn = 100e-6; % 单个chirp时长 100 us cfg.nSample = 256; % 每chirp采样点数,对应快时间维 cfg.nChirp = 128; % 一帧内chirp数,对应慢时间维 cfg.c = 3e8; % 光速 cfg.fc = 77e9; % 中心频率 cfg.lambda = cfg.c / cfg.fc; % 波长约 3.9 mm cfg.rangeRes = cfg.c / (2 * cfg.slope * cfg.txOn); % 距离分辨率 0.05 m cfg.velRes = cfg.lambda / (2 * cfg.nChirp * cfg.txOn); % 速度分辨率约 0.15 m/s cfg.vBand = 0.4; % 静止带半宽,单位 m/s,可调各参数含义这里说清楚:fs决定最大探测距离,slope和txOn一起决定带宽进而决定距离分辨率,nChirp决定速度分辨率和最大不模糊速度。代码包里提供的single_frame.mat就是一个 256×128 的复数矩阵,行是快时间采样点(距离维),列是慢时间 chirp 序号(多普勒维),直接喂给处理函数即可。
2.3 距离-多普勒矩阵的生成代码
距离-多普勒矩阵是整个信号处理的核心中间产物,后面 CFAR 检测、自速度估计、静止带滤除全部在它上面做。生成过程分两步:先在快时间维做 FFT 解调距离,再在慢时间维做 FFT 测速。
function rd = rangeDopplerMap(adcData, cfg) % 输入: adcData - [nSample x nChirp] 复数基带回波矩阵 % 输出: rd - [nSample x nChirp] 距离-多普勒谱(未经取模) % 快时间维加窗,降低距离维旁瓣 winR = hann(cfg.nSample, 'periodic'); rd = fft(adcData .* winR, cfg.nSample, 1); % 慢时间维加窗,降低多普勒维旁瓣 winD = hann(cfg.nChirp, 'periodic'); rd = fft(rd .* winD, cfg.nChirp, 2); % fftshift 把多普勒 0 频移到矩阵中间,方便后续按速度索引 rd = fftshift(rd, 2); end逻辑说明:距离 FFT 必须在前,因为 FMCW 雷达的距离信息调制在差拍频率上,必须先解调出来;多普勒 FFT 是对同一距离单元跨 chirp 做,所以维度选 2。两个维度都加汉宁窗,是为了压低旁瓣——这一点在强静止目标旁边有弱运动目标时尤其重要,后面避坑章节会专门讲。fftshift把多普勒频率轴重排成从负到正,这样矩阵中间列对应 0 速,左半部分是靠近雷达的运动目标,右半部分是远离的运动目标,静止目标就落在中间带上。
得到rd矩阵之后,用db(abs(rd))转成 dB 功率谱就能可视化。代码包里run_demo_single.m会画出滤除前后的对比图:左边是原始距离-多普勒谱,右边是滤除静止带后的谱图,能直观看到中间那条亮带被抠掉,只剩两侧的运动目标亮点。
3. 核心算法拆解:CFAR 检测、自速度估计与静止带滤除
3.1 二维 CA-CFAR:参考单元与保护单元的取舍
CFAR 的目标是在噪声和杂波背景下自适应地找目标,核心思想是用目标周围的单元估计本地噪声功率,再和被测单元比较,超过门限就判定为目标。这套代码用的是最经典的二维 CA-CFAR。
function mask = caCfar2D(rdPower, cfgCfar) % 输入: rdPower - 距离-多普勒功率谱(线性功率,非dB) % cfgCfar - 结构体,含训练单元/保护单元/虚警概率 % 输出: mask - 逻辑矩阵,1表示检测到目标 [NR, ND] = size(rdPower); mask = false(NR, ND); edgeR = cfgCfar.trainR + cfgCfar.guardR; edgeD = cfgCfar.trainD + cfgCfar.guardD; for r = edgeR+1 : NR-edgeR for d = edgeD+1 : ND-edgeD % 取距离维训练单元,注意避开保护单元 refWin = rdPower(r-cfgCfar.trainR : r+cfgCfar.trainR, ... d-cfgCfar.guardD : d+cfgCfar.guardD); % 实际工程用滑窗累加,这里是示意 noise = mean(refWin(:), 'omitnan'); thresh = cfgCfar.alpha * noise; if rdPower(r, d) > thresh mask(r, d) = true; end end end end逻辑说明:对每个待测单元,在距离维取左右各trainR个训练单元,在多普勒维取上下各trainD个训练单元,组成参考窗口;保护单元紧贴待测单元,防止目标能量泄漏进参考窗口拉高噪声估计。门限系数alpha由虚警概率反推,公式是alpha = N * (Pfa^(-1/N) - 1),其中 N 是训练单元总数。代码包里参数给的是距离维训练单元 8、保护单元 4,多普勒维训练单元 8、保护单元 2,虚警概率 1e-4。这个组合在大多数路况下都不错,如果发现漏检就调小虚警概率,发现虚警多就调大训练单元数。
这段循环写法方便理解原理,但跑 256×128 矩阵会有明显的性能焦虑。工程上我一般用二维卷积把参考窗求和向量化,或者用积分图(integral image)把滑窗均值降到 O(1) 复杂度,连续帧处理时帧率能翻几倍。
3.2 自速度估计:静止带的中心不在 0 频
很多第一次接触静止目标滤除的人会犯一个直觉性错误:把多普勒 0 频附近当成静止带。但车载雷达装在运动的车上,路边的静止护栏相对雷达其实有径向速度——这个速度等于自车速度在目标方向上的投影。如果直接以 0 频为中心滤除,运动目标会被误删,静止目标反而滤不干净。
function vSelf = estimateSelfVelocity(rd, cfg) % 输入: rd - 距离-多普勒谱(复数,未取模) % 输出: vSelf - 估计的自车速度,单位 m/s % 取近距离单元的多普勒维投影 % 近距离地面反射、车体反射能量最强,对应速度即自车速度 proj = sum(abs(rd(1:20, :)), 1); [~, idx] = max(proj); % 将峰值索引换算为速度 % fftshift 后速度轴: 左边负速度,右边正速度 vSelf = (idx - cfg.nChirp/2) * cfg.velRes; end逻辑说明:近距离单元(前 20 个距离门)主要被地面、保险杠、前车尾部的强反射占据,这些反射体相对雷达的速度就是自车速度(径向分量近似等于车速)。取多普勒维投影找能量峰值,峰值位置换算成速度,就得到了这一帧的自车速度估计。参数说明:1:20是距离单元范围,取决于雷达安装位置和近距离盲区,实际装车后需要标定,有些方案取前 10 个单元,有些取 30 个,看地面反射强度。idx - nChirp/2是因为fftshift之后索引 1 对应负最大速度,索引 nChirp/2+1 对应 0 速。
需要提一句,这只是自速度估计的朴素做法,胜在简单稳定,够用;更鲁棒的方案是用多普勒维直方图投票,或者和车辆 CAN 总线车速融合,代码包保留了这个接口,你可以替换实现。
3.3 静止带滤除与杂波图更新
有了自车速度和 CFAR 检测结果,滤除逻辑就非常直接了:速度在vSelf ± vBand范围内的检测点全部标记为静止目标,剩下的就是运动目标。
function dets = filterStatic(rdPower, mask, cfg, vSelf) % 输入: rdPower - 距离-多普勒功率谱(dB) % mask - CFAR检测掩膜 % cfg - 雷达参数 % vSelf - 当前帧自车速度 % 输出: dets - [N x 2] 运动目标列表,每行为 [距离, 速度] % 多普勒轴速度向量,与fftshift后的列索引对应 N = cfg.nChirp; velAxis = ((1:N) - N/2) * cfg.velRes; % 静止带判定:以自车速度为中心,宽度为 2*vBand staticMask = abs(velAxis - vSelf) <= cfg.vBand; % 剔除静止带内的检测点,保留运动目标 motionMask = mask; motionMask(:, staticMask) = false; % 提取目标列表 [rIdx, dIdx] = find(motionMask); dets = [cfg.rangeRes * (rIdx - 1), velAxis(dIdx).']; end逻辑说明:staticMask是一个行向量,标记哪些多普勒单元属于静止带,比如自车速度 10 m/s、静止带半宽 0.4 m/s,那么 9.6 到 10.4 m/s 对应的列全被置为 false。find把剩余目标的行列索引取出来,换算成物理距离和速度。参数说明:vBand取 0.4 是经验值,要考虑速度分辨率(约 0.15 m/s)+ 静止目标多普勒泄漏 + 自速度估计误差,太小会漏滤静止目标,太大会误删缓慢运动目标。行人横穿速度约 1.2 m/s,自行车约 3 m/s,所以 0.4 通常安全。
连续帧处理时,代码包还演示了杂波图的进阶玩法:把每一帧的静止带累积到一张staticMap上,多帧累积后超过阈值的单元视为长期静止杂波,逐帧直接剔除。这样可以滤掉那些偶尔在静止带边缘跳变的检测点,比单帧硬切稳定得多。
4. 运行指南:单帧调试与连续帧验证
4.1 环境与数据准备
运行环境要求不苛刻,Matlab R2021a 及以上版本即可,不需要额外的工具箱——FFT、hann都是基础函数,find、mean这些更是标配。代码包里没有依赖 Phased Array Toolbox,信号处理链路全部手写,这对想看懂每一行逻辑的人来说反而是好事。
拿到代码后第一步是检查data目录下有single_frame.mat和seq_01.mat,然后打开run_demo_single.m,确认当前目录在工程根目录下。如果之前没用过复数矩阵数据,先看一眼数据维度:
% 验证数据格式 load('data/single_frame.mat'); whos adcData % 期望输出 256x128 complex double如果adcData是 int16 类型,记得先转 double 再做 FFT,否则fft会按整数运算截断,小信号直接丢精度。这一点在代码包的读取函数里已经做了保护,但你换自己的数据时一定要检查。
4.2 单帧调试流程
单帧演示是最快的上手路径,把整个处理链跑通只需要三步:
% run_demo_single.m 核心调用 run('radar_config.m'); load('data/single_frame.mat'); % Step 1: 生成距离-多普勒谱 rd = rangeDopplerMap(double(adcData), cfg); rdPowerDb = db(abs(rd)); % Step 2: CFAR 检测(输入需要线性功率) mask = caCfar2D(abs(rd).^2, cfg.cfar); % Step 3: 自速度估计 + 静止带滤除 vSelf = estimateSelfVelocity(rd, cfg); dets = filterStatic(rdPowerDb, mask, cfg, vSelf);跑完会在命令行打印自车速度估计值和检测到的运动目标数量,同时弹出两张图:左边是原始距离-多普勒谱,右边是滤除静止目标后的谱图。判断跑没跑对的标准很简单——原始谱中间应该有一条横贯的亮带,这就是静止目标集中区域;滤除后的图上这条带应该消失,两侧的运动目标亮点保留,目标列表里的距离值应该和图上亮点位置一致。
如果目标列表为空,优先检查vSelf是否合理。打印一下估计值,如果自车速度显示为 0 而工况明明是行进中,说明近距离投影找峰值找错了位置,把estimateSelfVelocity里取前 20 个距离单元改成前 10 个试试。
4.3 连续帧验证与指标
单帧跑通只是起点,真正验证滤除效果要看连续帧的稳定性。run_demo_sequence.m对seq_01.mat的 20 帧数据循环处理,每帧输出目标列表并保存到工作区变量allDets,同时计算两个指标:每帧目标数和相邻帧目标跳变率。
连续帧最容易暴露的问题是目标闪烁——同一辆车在这帧被当成动目标保留,下一帧因为多普勒跨了一个 bin 就被滤掉。代码包里用杂波图累积 + 阈值判定来平滑,staticMap每帧叠加静止带标记,超过帧数一半的单元直接硬滤除:
% 连续帧杂波图累积 for k = 1:20 rdK = rangeDopplerMap(data(:, :, k), cfg); vSelfK = estimateSelfVelocity(rdK, cfg); staticMap = staticMap + (abs(velAxis - vSelfK) <= cfg.vBand); end % 超过阈值视为长期静止杂波,直接剔除 persistentMask = staticMap >= 10;参数说明:阈值 10 表示 20 帧里有至少一半帧把该单元判定为静止,这个值决定滤除的保守程度。调高阈值会保留更多可疑目标,调低阈值会滤得更干净但误删概率上升。处理真实路测数据时,一般先看一帧的检测数量,再按虚警率反推阈值。
5. 避坑与调试:静止目标滤除中最常见的 5 个坑
5.1 参数设置类踩坑
坑一:运动目标被一起滤掉
现象:滤除后的点云里,行人、自行车这些明显的运动目标消失了,只剩空荡荡的谱图。原因:静止带中心用了 0 速,没做自速度补偿。自车以 10 m/s 行驶时,路边静止护栏的多普勒速度就是 10 m/s,0 速附近反而是相对雷达静止的跟随车辆。解决:先跑estimateSelfVelocity估计自车速度,再用vSelf ± vBand作为静止带,而不是0 ± vBand。这是整套代码里最值得记住的一条。
坑二:弱运动目标被强静止目标旁瓣盖住
现象:CFAR 检测把静止目标周围一大片区域都标记为目标,运动目标即使存在也被旁瓣淹没。原因:距离维只加了汉宁窗,旁瓣抑制能力有限,强反射静止目标在距离维展宽。解决:把快时间窗换成切比雪夫窗,旁瓣抑制 40dB 起步:
winR = chebwin(cfg.nSample, 40);切比雪夫窗允许你直接指定旁瓣电平,比汉宁窗的固定 -31dB 灵活。加窗后 CFAR 掩膜里的静止目标区域会明显收窄。这类问题调参时最容易让人怀疑是 CFAR 参数不对,实际上问题出在前面加窗环节——调试时按链路顺序排查,不要先动 CFAR 的虚警率。
坑三:静止带宽度怎么调都别扭
现象:vBand调到 0.3 时静止目标滤不干净,调到 0.6 时缓慢运动的行人被误删。原因:速度分辨率约 0.15 m/s,加上自速度估计误差和静止目标的多普勒展宽,宽度的可调区间本来就窄。解决:不要只调宽度,配合杂波图累积使用。单帧硬门限留 0.4,同时用多帧staticMap剔除跳变点,双管齐下比单方向调参数有效得多。
5.2 数据与工程类踩坑
坑四:换了自己的雷达数据,处理结果全乱
现象:下载了代码包,把自己的雷达数据替换进去,出来的距离-多普勒谱完全不是预期形状。原因:雷达参数没同步修改。代码包里的radar_config.m是 77GHz 典型参数,你的雷达如果采样率、chirp 数、调频斜率不一样,距离分辨率、速度分辨率全变,后续所有门限都失去意义。解决:拿到新数据先核对参数表,重点确认fs、slope、txOn、nSample、nChirp五项。参数如果和实际不符,距离轴和速度轴的单位换算全错,检测结果自然是乱的。
坑五:同一目标在连续帧里一会出现一会消失
现象:单帧调试效果很好,一跑连续帧,目标数量剧烈抖动,同一辆车这帧有下帧没有。原因:目标的多普勒速度落在静止带边界附近,帧与帧之间因为车辆微动或多普勒分辨率限制,在带内带外跳变。解决:对检测点先做聚类,把 CFAR 掩膜中的连通域合并成一个目标点,再基于目标中心速度判定是否滤除。代码包里提供clusterDets函数,用最简单的 8 邻域连通域标记,聚类后再按目标中心速度过滤,比逐点硬切稳定得多——这个改动对行人、自行车这类多点目标尤其明显。
6. 二次开发:把滤除模块接进自己的点云跟踪链路
6.1 接口设计:一个函数完成单帧滤除
代码包里的处理链是分步函数,但实际工程接入时,你通常希望一个调用完成整帧处理。按代码包的接口风格,封装成下面这样最顺手:
function dets = processFrame(adcData, cfg, varargin) % 单帧处理完整入口,返回运动目标列表 rd = rangeDopplerMap(double(adcData), cfg); mask = caCfar2D(abs(rd).^2, cfg.cfar); vSelf = estimateSelfVelocity(rd, cfg); dets = filterStatic(db(abs(rd)), mask, cfg, vSelf); end这样接进你现有的检测-跟踪循环只需要替换一行:原来调detectTargets的地方改成调processFrame。注意filterStatic返回的是[距离, 速度]两列,如果你还要角度信息,要么在rangeDopplerMap之前先做阵列测角,要么把角度作为第三列补进目标列表——多数跟踪器要求目标格式至少包含距离、速度、角度三项。
6.2 改造点:换雷达平台和提升鲁棒性
换雷达平台是这套代码最常遇到的二次开发场景。从 77GHz 换成 24GHz 时,波长从 3.9mm 变成 12.5mm,速度分辨率会变差三倍多,vBand要相应放宽;换成 4D 毫米波雷达时,数据从二维矩阵变成距离-多普勒-俯仰-方位四维张量,caCfar2D需要扩展成三维滑窗,训练单元和保护单元的数量也要重新标定。我的建议是:先保持二维 CFAR 只处理距离-多普勒切片,俯仰和方位维单独做峰值提取,这样改动量最小。
另外一个值得做的改造是自速度估计的鲁棒性增强。朴素峰值法在急刹车、大转弯时可能失效,常见做法是拿多普勒维投影的前三个峰值做加权平均,或者把估计值做时间维滤波(一阶低通即可),避免单帧跳变直接带偏静止带。
% 一阶低通平滑自速度 vSelfSmooth = 0.7 * vSelfSmooth + 0.3 * vSelf;时间常数按帧率调,20fps 时 0.7/0.3 的系数响应够快又不会抖动。
6.3 验证方法:拿什么标准评判滤除效果
改完代码不能只看几帧图觉得“看起来不错”,要量化验证。我的习惯是准备一段标注过真值的路测数据,每帧标注运动目标的位置和速度,然后跑三个指标:
| 指标 | 含义 | 达标参考 |
|---|---|---|
| 运动目标保留率 | 真值中的运动目标有多少还在点云里 | 不低于 95% |
| 静止目标残留率 | 滤除后还剩多少静止回波点 | 低于 5% |
| 目标闪烁率 | 相邻帧同一目标消失再出现的比例 | 低于 3% |
单帧调参时看静止目标残留率,连续帧验证时盯运动目标保留率和闪烁率。这些指标在代码包里对应run_demo_sequence.m末尾的计算代码,直接复用就行。
整套代码最让我觉得值的地方,是把“滤除静止”这件事从直觉变成了可量化、可调试、可替换模块的工程链路。从那以后我每次拿到新的雷达数据集,都强制先跑一遍单帧链路确认参数匹配,再上连续帧验证闪烁率,这套流程救过我不少次因为盲目调参而浪费的半天时间。希望帮到你。
本文还有配套的精品资源,点击获取