本篇博文主要工作是基于开源数据集**《简单野外背景条件下地面雷达对"低慢小"无人机探测数据集》** 做算法复现,涉及到基本的文件读取 、数据解析 、雷达信号处理 以及特征提取等工作。
内容包含完整的数据、代码以及可视化结果,以供各位读者参考、学习。数据集论文封面如下图所示。

该数据集来自《信号处理》期刊,读者可直接前往信号处理期刊官网的下载专区下载全部数据集。如下图所示,进入信号处理期刊官网,点击"数据集"下拉栏选项,选择"无人机探测数据集",

滑到页面最下方,即可点击下载全部数据。

下载好的数据集如下图所示。

以Data3为例,进入数据文件夹后,含有truth_data和raw,如下图所示。

truth_data是无人机的真实定位信息,即机载GPS的数据,前面是数据录取时间,中间的"1"表示此时此刻只有一架无人机位于探测范围内,后面则分别是当前无人机的距离、方位角以及俯仰角,如下图所示。

文件夹raw如下图所示,raw中则是雷达原始回波数据,需要进一步做数据解析以及初步的信号处理后才能得到有意义的数据,这部分后面说。

总的来说,该数据集是采用X波段1发8收数字阵列雷达在通航机场采集的,目标包括固定翼无人机以及多旋翼无人机,并包含相应的精确GPS真值标注信息。雷达参数信息如下:

Data3测试目标为大疆M300型四旋翼双叶片无人机,距离波门为0(即0-2500m测距范围),PRT为1000us,即PRF为1000Hz,采集时长共6s(48个文件,每个文件0.125s),采集时间为2023年7月30日上午9点04分04秒起,至上午9点04分10秒阶数,GPS信息:方位角149.97度度,俯仰角8.10度,距离212m,目标悬停在30m高度上,距离校正量为-227.66m。
先读取数据:
Matlab
%% 数据读取
Path = ['C:\Users\14150\Desktop\低空监视\数据集\' ...
'简单野外背景条件下地面雷达对"低慢小"无人机探测数据集\' ...
'数据集\Data3\raw\'];
posi = 40;
num = 8;
data_total = file_process(Path, posi, num);
% 输出读取的回波文件名
data_total.name;
% 输出读取的回波数据
data_total.value;
接下来配置雷达参数,并完成脉冲压缩和静态杂波抑制,得到时间距离图,并与GPS真值进行相互验证。
Matlab
%% 脉冲压缩 √
% 雷达参数
nChannel = 8; % 通道数
nChirps = 128; % 慢时间回波数
nSamples = 8192; % 快时间采样点数
Bandwidth = 120e6; % 带宽
SamplePoints = 8192; % 采样点数
Periodicity = 1000e-6; % chirp持续时间1ms(含idle time)或533us
Tchirps = 2.0835e-6; % 脉冲持续时间
SampleRate = 491.52e6; % 采样率
fSlope = Bandwidth/Tchirps; % 调频斜率
nFrames = 1; % 帧数,共1帧,每帧128个chirps
fStart = 3.13344e9; % 扫频起始频率
nChirpLoops = 1; % 无chirp内循环
PRT = Periodicity/nChirpLoops; % 脉冲重复时间,由于无chirp内循环,所以与Periodicity相同
Ttotal = nChirps*Periodicity; % 帧持续时间为128ms
lambda = 3e8/fStart; % 信号波长
d = lambda/2; % 阵元间距
PRF = 1/PRT; % 脉冲重复频率
% 性能参数
% 距离分辨率
dR = 3e8/(2*Bandwidth);
% 最大探测距离
rangeMax = ((nSamples/SampleRate)*3e8)/2;
% 速度分辨率
dv = 3e8/fStart/2/nChirps/Periodicity;
% 最大可测速度
velocityMax = 3e8/4/fStart/Periodicity;
% 若真值数据为:2023-07-30 21:18:20.00 1 250.41 203.67 7.24
% 则表示:该时刻存在目标,斜距为250.41m,方位角为203.67°,俯仰角为7.24°
data_raw = zeros(nChannel, nChirps, nSamples);
nSelectedFrames = num; % 累积帧数
for i = 1:1:nSelectedFrames
data = data_total(i);
data_tmp = data.value;
data_raw = cat(2,data_raw,data_tmp);
end
% 这里data_raw即多帧合成的长时积累的雷达数据立方体
data_raw = data_raw(:, nChirps+1:end, :);
% 数据重排:快×慢×通道数
complex_data = permute(data_raw(:,:,:),[3 2 1]);
% DBF数字波束形成实现 2025-07-21
theta_deg = 8.1; % 目标俯仰角度(也可通过DOA估计得到)
theta_rad = theta_deg*pi/180; % 角度转为弧度
% 构造ULA导向矢量
element_idx = 0:(nChannel-1);
steer_vec = exp(-1j*2*pi*(d/lambda)*sin(theta_rad) * element_idx);
% 权重归一化df
weights = steer_vec.'/nChannel;
bfData = zeros(nSamples, nChirps); % 初始化输出矩阵
% 对每个chirp进行空域滤波(加权求和)
for k = 1:nChirps*nSelectedFrames
slice = squeeze(complex_data(:, k, :));
bfData(:, k) = slice * weights; % 波束形成结果
end
% 使用频域方法实现匹配滤波求时延,获取时间距离图矩阵
t_chirps = linspace(0, Tchirps, Tchirps*SampleRate)';% 时间向量
ref_chirp = exp(1i*pi*fSlope*t_chirps.^2); % 理想发射信号(单边基带)
Hf = conj(fft(ref_chirp,nSamples));
hamming_RT = hamming(nSamples); % FFT窗函数
for i = 1:1:nChirps*nSelectedFrames
temp_RT_data = bfData(:,i).*hamming_RT; % 信号加窗
X_fft = fft(temp_RT_data, nSamples, 1); % 对每个chirp做FFT
RTmap(:,i) = ifft(X_fft.*Hf, nSamples, 1); % 匹配滤波输出即时间距离图
end
% 距离坐标轴表示(纵轴)
rax = (0:nSamples-1)*(rangeMax/nSamples);
% 时间坐标轴表示(横轴)
tax = (0:nChirps*nSelectedFrames-1)*PRT;
% 均值相消滤波
% background = mean(RTmap,2);
% Data_Range_filtOut = RTmap - background;
% MTI滤波
Data_Range_filtOut = myMTIFilter(RTmap,1);
% 绘制初步处理后的R-T图
figure;
plot_RTmap(tax,rax,abs(Data_Range_filtOut),"Range-Time Map (remove clutter by mean and MTI) ");
得到的时间距离图如下。

目标所在距离门为450m,经距离校正后为223m,真值为212m,误差小于5%。
接下来沿慢时间维做FFT,得到距离多普勒图。
Matlab
%% 距离多普勒 R-D map √
Data_Velocity = fftshift(fft(Data_Range_filtOut, nSamples, 2), 2);
RD_y = rax;
RD_x = linspace(-velocityMax,velocityMax,nSamples);
figure;
imagesc(RD_x,RD_y,10*log10(abs(Data_Velocity)));
% shading interp;
colorbar;colormap jet;
clim([40,60]);
set(gca,'ydir','normal');
xlabel('Velocity (s)','HorizontalAlignment', 'center');
ylabel('Range (m)','HorizontalAlignment', 'center');
title("Range-Doppler Map");
结果如下:

零频部分已被MTI抑制,速度方向单边存在四个两点,对应四个旋翼。
将目标距离门内的信号沿多普勒轴提取出来,结果如下。
Matlab
figure;
plot(linspace(-PRF/2,PRF/2,8192),sum(abs(Data_Velocity(1425:1445,:))),LineWidth=1.0);
xlabel('Frequency (Hz)','HorizontalAlignment', 'center');
ylabel('Amplitude','HorizontalAlignment', 'center');grid on;

可以看到,在零频的任意一边(这里以左边为例),峰值之间频差为89Hz,对应的即N*fr,N为单个旋翼叶片数量,fr为叶片转速,大疆M300叶片数量为2,由此可得转速fr为44.5Hz(44.5r/s),约为2670rpm。
为更好地提取出距离多普勒图中的周期关系,接下来对RDM中距离门内的信号进行倒谱分析,结果如下。
Matlab
%% 倒谱分析 2025-07-22 √
Xf = sum(Data_Velocity(1425:1480,:)); % 目标距离门内的微多普勒频谱
P = abs(Xf).^2; % 功率谱
logP = log(P); % 取对数功率谱
cepstrum = real(ifft(logP)); % 计算倒谱
N = length(cepstrum); % quefrency轴(单位:s)
quef = (0:N-1)/PRF;
% 可视化
figure;
plot(quef, abs(cepstrum),LineWidth=1.5);
xlabel('Quefrency (s)');
ylabel('Amplitude');
title('Power Cepstrum');
axis([0 0.25 0 2]);
grid on;

由倒谱图可得,距离多普勒中的周期分量约为0.011s,即90.9Hz,对应的转速即为45.45r/s。
最后针对距离门内的信号做时频分析,具体为:取时间距离图目标距离门内的信号沿距离轴叠加后,做窗长为16的STFT(窗长越短时间分辨率越好,我们后续需要在时间方向上提取经过桨叶正弦调制后的微多普勒信号,所以在时间分辨率-频率分辨率这一trade off问题上优先时间分辨率,16点窗长对应的时间分辨率约为0.021s),结果如下。

局部放大后,正弦调制轮廓明显,由于M300为四旋翼双叶片无人机,所以一个周期对应两个正弦包络,如图所示。

由此可得单叶片转速为1/0.023≈43.47r/s,前后结果相一致。
完整代码如下:
Matlab
%% 01 paper 简单野外背景条件下地面雷达对"低慢小"无人机探测数据集 算法复现
% 2025-07-17 Start
% 2025-07-23 RT、RD、Micro-Doppler and Cepstrum Analysis has completed.
% 2025-08-03 CA-CFAR、STFT has completed.
% 2025-08-10 在距离维、速度维压缩时分别加入了汉宁窗
% 根据雷达回波数据求【叶片数量】、【叶片旋转速率】以及【叶片长度】
% 大疆M300为四轴双叶片无人机
% Author:zhchen
clear;clc;
%% 数据读取
Path = ['..\复现数据集\' ...
'简单野外背景条件下地面雷达对"低慢小"无人机探测数据集\' ...
'数据集\Data3\raw\'];
posi = 40;
num = 8;
data_total = file_process(Path, posi, num);
% 输出读取的回波文件名
data_total.name;
% 输出读取的回波数据
data_total.value;
%% 脉冲压缩 √
% 雷达参数
nChannel = 8; % 通道数
nChirps = 128; % 慢时间回波数
nSamples = 8192; % 快时间采样点数
Bandwidth = 120e6; % 带宽
SamplePoints = 8192; % 采样点数
Periodicity = 1000e-6; % chirp持续时间1ms(含idle time)或533us
Tchirps = 2.0835e-6; % 脉冲持续时间
SampleRate = 491.52e6; % 采样率
fSlope = Bandwidth/Tchirps; % 调频斜率
nFrames = 1; % 帧数,共1帧,每帧128个chirps
fStart = 3.13344e9; % 扫频起始频率
nChirpLoops = 1; % 无chirp内循环
PRT = Periodicity/nChirpLoops; % 脉冲重复时间,由于无chirp内循环,所以与Periodicity相同
Ttotal = nChirps*Periodicity; % 帧持续时间为128ms
lambda = 3e8/fStart; % 信号波长
d = lambda/2; % 阵元间距
PRF = 1/PRT; % 脉冲重复频率
% 性能参数
% 距离分辨率
dR = 3e8/(2*Bandwidth);
% 最大探测距离
rangeMax = ((nSamples/SampleRate)*3e8)/2;
% 速度分辨率
dv = 3e8/fStart/2/nChirps/Periodicity;
% 最大可测速度
velocityMax = 3e8/4/fStart/Periodicity;
% 若真值数据为:2023-07-30 21:18:20.00 1 250.41 203.67 7.24
% 则表示:该时刻存在目标,斜距为250.41m,方位角为203.67°,俯仰角为7.24°
data_raw = zeros(nChannel, nChirps, nSamples);
nSelectedFrames = num; % 累积帧数
for i = 1:1:nSelectedFrames
data = data_total(i);
data_tmp = data.value;
data_raw = cat(2,data_raw,data_tmp);
end
% 这里data_raw即多帧合成的长时积累的雷达数据立方体
data_raw = data_raw(:, nChirps+1:end, :);
% 数据重排:快×慢×通道数
complex_data = permute(data_raw(:,:,:),[3 2 1]);
% DBF数字波束形成实现 2025-07-21
theta_deg = 8.1; % 目标俯仰角度(也可通过DOA估计得到)
theta_rad = theta_deg*pi/180; % 角度转为弧度
% 构造ULA导向矢量
element_idx = 0:(nChannel-1);
steer_vec = exp(-1j*2*pi*(d/lambda)*sin(theta_rad) * element_idx);
% 权重归一化df
weights = steer_vec.'/nChannel;
bfData = zeros(nSamples, nChirps); % 初始化输出矩阵
% 对每个chirp进行空域滤波(加权求和)
for k = 1:nChirps*nSelectedFrames
slice = squeeze(complex_data(:, k, :));
bfData(:, k) = slice * weights; % 波束形成结果
end
% 使用频域方法实现匹配滤波求时延,获取时间距离图矩阵
t_chirps = linspace(0, Tchirps, Tchirps*SampleRate)';% 时间向量
ref_chirp = exp(1i*pi*fSlope*t_chirps.^2); % 理想发射信号(单边基带)
Hf = conj(fft(ref_chirp,nSamples));
hamming_RT = hann(nSamples); % FFT窗函数
for i = 1:1:nChirps*nSelectedFrames
temp_RT_data = bfData(:,i).*hamming_RT; % 信号加窗
X_fft = fft(temp_RT_data, nSamples, 1); % 对每个chirp做FFT
RTmap(:,i) = ifft(X_fft.*Hf, nSamples, 1); % 匹配滤波输出即时间距离图
end
% 距离坐标轴表示(纵轴)
rax = (0:nSamples-1)*(rangeMax/nSamples);
% 时间坐标轴表示(横轴)
tax = (0:nChirps*nSelectedFrames-1)*PRT;
% 均值相消滤波
% background = mean(RTmap,2);
% Data_Range_filtOut = RTmap - background;
% MTI滤波
Data_Range_filtOut = myMTIFilter(RTmap,1);
% 绘制初步处理后的R-T图
figure;
plot_RTmap(tax,rax,abs(Data_Range_filtOut),"Range-Time Map (remove clutter by mean and MTI) ");
%% 距离多普勒 R-D map √
w = hann(nChirps*num); % 多普勒窗
w = w / mean(w); % 能量归一
Data_Range_filtOut_win = Data_Range_filtOut.*w'; % 加窗
Data_Velocity = fftshift(fft(Data_Range_filtOut_win, nSamples, 2), 2);
RD_y = rax;
RD_x = linspace(-velocityMax,velocityMax,nSamples);
% 绘制RDM
figure;
imagesc(RD_x,RD_y,10*log10(abs(Data_Velocity)));
colorbar;colormap jet;
clim([40,60]);
set(gca,'ydir','normal');
xlabel('Velocity (s)','HorizontalAlignment', 'center');
ylabel('Range (m)','HorizontalAlignment', 'center');
title("Range-Doppler Map");
%% 多普勒频谱分析 √
figure;
plot(linspace(-PRF/2,PRF/2,8192),sum(abs(Data_Velocity(1425:1445,:))),LineWidth=1.0);
xlabel('Frequency (Hz)','HorizontalAlignment', 'center');
ylabel('Amplitude','HorizontalAlignment', 'center');grid on;
%% 倒谱分析 2025-07-22 √
Xf = sum(Data_Velocity(1425:1480,:)); % 目标距离门内的微多普勒频谱
P = abs(Xf).^2; % 功率谱
logP = log(P); % 取对数功率谱
cepstrum = real(ifft(logP)); % 计算倒谱
N = length(cepstrum); % quefrency轴(单位:s)
quef = (0:N-1)/PRF;
% 可视化
figure;
plot(quef, abs(cepstrum),LineWidth=1.5);
xlabel('Quefrency (s)');
ylabel('Amplitude');
title('Power Cepstrum');
axis([0 0.25 0 2]);
grid on;
%% CA-CFAR 2025-07-22 √
inputRDM = abs(Data_Velocity); % 输入距离多普勒图
Pfa = 1e-6; % 虚警率
regCellnums = 8; % 单侧保护单元
refCellnums = 48; % 单侧参考单元
Ntrain = 2*refCellnums; % 参考单元总数量
Tca = Ntrain*(Pfa^(-1/Ntrain)-1); % 门限因子
% 沿速度维计算CA-CFAR
for i = 1:1:size(inputRDM,1)
for j = 1+regCellnums+refCellnums:1:size(inputRDM,2)-regCellnums-refCellnums
Zca = (sum(inputRDM(i,j-regCellnums-refCellnums:j-regCellnums-1)) + sum(inputRDM(i,j+regCellnums+1:j+regCellnums+refCellnums)))/Ntrain;
T = Zca*Tca;
if inputRDM(i,j)>T
inputRDM(i,j) = 1;
else
inputRDM(i,j) = 0;
end
end
end
inputRDM(:,1:regCellnums+refCellnums) = 0;
inputRDM(:,size(inputRDM,2)-regCellnums-refCellnums:size(inputRDM,2)) = 0;
figure;
imagesc(RD_x,RD_y,inputRDM);
colorbar;
clim([0,1]);
set(gca,'ydir','normal');
xlabel('velocity (s)','HorizontalAlignment', 'center');
ylabel('Range (m)','HorizontalAlignment', 'center');
title("Range-Doppler Map after velocity-range-2D-CA-CFAR");
%% STFT时频分析 2025-08-03 √
% 均值相消滤波
background = mean(RTmap,2);
STFT_Data = RTmap - background;
% 提取目标距离门内回波信号
slice = STFT_Data(1420:1470,:);
sliceSum = sum(slice,1);
% 参数定义
Nfft = nSamples; % 频率分辨率,直接取信号长度即可
windowLength = 16; % 窗函数长度,对应时间分辨率,16对应的分辨率约为0.016s
window = hamming(windowLength); % 信号加窗(Hamming)
noverlap = windowLength-1; % 滑窗长度
% STFT变换
[S,F,T] = stft(sliceSum, PRF, Window = window, OverlapLength = noverlap, FFTLength = Nfft);
S = abs(S);
% 显示时频图
figure;
imagesc(T, F, 10*log10(S));
xlabel('Time (s)');
ylabel('Doppler Frequency (Hz)');
title('Micro-Doppler Signature (STFT)');
colormap jet; colorbar;clim([10,58]);
%% 时频分析参数测试GUI 2025-08-04 √
stft_window_gui(RTmap, PRF, nSamples);
stft_explorer_gui
%% 合并图床 2025-08-04 √
merge_figures();
相关的其他函数文件如下:
(1)file_process.m:
Matlab
%% 读取原始回波数据
function data_total = file_process(Path, posi, num)
% 输入:Path ------原始回波所在文件夹的路径
% posi ------从文件夹第几个位置开始读
% num ------一次读取的回波文件数
% 输出:data_total ------回波数据结构体
% data_total.name ------回波路径字段
% data_total.value ------回波数据字段
N_channel = 8;
N_chirp = 128;
N_sample = 8192;
File = dir(fullfile(Path,'*.raw')); % 显示文件夹下所有符合后缀名为.raw文件的完整信息
FileNames = {File(posi:end).name}'; % 所有.raw文件的名字,转换为n行1列
for i = 1:num
temp_name = strcat(Path,FileNames{i});
fid = fopen(temp_name,'rb');
d = fread(fid,'int16');
idx = 0;
data = zeros(N_channel, N_chirp, N_sample);
for q = 1:N_channel
for p = 1:N_chirp
data(q, p, :)= 1i*d(idx+1 : 2 : idx+N_sample*2) + d(idx+2 : 2 : idx+N_sample*2);
idx = idx + N_sample*2;
end
end
fclose(fid);
data_total(i).name = temp_name;
data_total(i).value = data;
end
end
(2)merge_figures.m:
Matlab
function merge_figures(nRows, nCols)
% merge_figures_exact 合并当前所有已打开的figure到一个窗口,保留原图所有显示属性
%
% 用法:
% merge_figures_exact() % 自动布局
% merge_figures_exact(2, 3) % 指定2行3列布局
%
% ZHChen 2025-08-04
% 找到所有figure(按创建顺序)
figs = findall(0, 'Type', 'figure');
figs = flipud(figs);
if isempty(figs)
warning('没有检测到任何打开的figure.');
return;
end
nFig = numel(figs);
% 自动布局
if nargin < 2
nRows = ceil(sqrt(nFig));
nCols = ceil(nFig / nRows);
end
% 新窗口
mergedFig = figure('Name', 'Merged Figures (Exact)', 'NumberTitle', 'off');
for k = 1:nFig
% 创建子图区域
axNew = subplot(nRows, nCols, k, 'Parent', mergedFig);
% 找原figure中的axes
axOld = findall(figs(k), 'Type', 'axes');
if isempty(axOld)
continue;
end
% 如果原figure里有多个axes,这里可以选择第一个或做更多处理
axOld = axOld(1);
% 把原axes的内容完整复制到新axes
copyobj(allchild(axOld), axNew);
% 复制坐标轴属性
axNew.XLim = axOld.XLim;
axNew.YLim = axOld.YLim;
axNew.ZLim = axOld.ZLim;
% 复制颜色范围和映射
axNew.CLim = axOld.CLim;
colormap(axNew, colormap(axOld));
% 复制标签与标题
axNew.XLabel.String = axOld.XLabel.String;
axNew.YLabel.String = axOld.YLabel.String;
axNew.ZLabel.String = axOld.ZLabel.String;
axNew.Title.String = axOld.Title.String;
% 复制视角(如果是3D)
axNew.View = axOld.View;
% 保留刻度
axNew.XTick = axOld.XTick;
axNew.YTick = axOld.YTick;
axNew.ZTick = axOld.ZTick;
% 保留刻度标签
axNew.XTickLabel = axOld.XTickLabel;
axNew.YTickLabel = axOld.YTickLabel;
axNew.ZTickLabel = axOld.ZTickLabel;
% 复制颜色条(如果有)
cbOld = findall(figs(k), 'Type', 'colorbar');
if ~isempty(cbOld)
cbNew = colorbar(axNew);
cbNew.Limits = cbOld.Limits;
cbNew.Ticks = cbOld.Ticks;
cbNew.TickLabels = cbOld.TickLabels;
cbNew.Label.String = cbOld.Label.String;
end
end
end
(3)myMTIFilter.m:
Matlab
function data_mti = myMTIFilter(data, order, method)
% ZHChen 2025-07-22
% 通用MTI滤波器:支持任意阶差分,保持输出尺寸不变
%
% 输入:
% data : 输入的回波数据,一般为时间距离图
% order : 差分阶数(正整数,通常为1或2或3)
% method : 边界处理方式('zero' | 'repeat' | 'mirror')
%
% 输出:
% data_mti : 与原始数据同尺寸的滤波结果
if nargin < 3
method = 'zero'; % 默认边界补零
end
[N_range, N_chirp] = size(data);
data_mti = zeros(N_range, N_chirp); % 初始化输出
% 差分计算(输出尺寸为 N_range × (N_chirp - order))
diff_data = diff(data, order, 2);
% 恢复尺寸:补齐左侧边界(order列)
switch lower(method)
case 'zero'
pad = zeros(N_range, order);
case 'repeat'
pad = repmat(diff_data(:,1), 1, order); % 重复第一列
case 'mirror'
pad = flip(diff_data(:,1:order), 2); % 镜像填充
otherwise
error('未知边界补偿方式');
end
% 拼接,保持原始尺寸
data_mti = [pad, diff_data];
end
(4)plot_RTmap:
Matlab
function isPlot = plot_RTmap(x_axis, y_axis, RTdata, figureTitle)
% 绘制时间-距离图
% x_axis -> 横轴坐标表示
% y_axis -> 纵轴坐标表示
% RTdata -> 需要绘制的RT图数据
% isPlot -> 函数执行标志
% figureTitle -> 图名
% 2025-02-18 by ZHChen
% ----function code part----%
imagesc(x_axis, y_axis, RTdata);
shading interp;
colorbar;
colormap('hot');
xlabel('Time (s)','HorizontalAlignment', 'center');
ylabel('Range (m)','HorizontalAlignment', 'center');
title(figureTitle);
set(gca,'ydir','normal');
isPlot = 1;
end
(5)rdm_to_ram:
Matlab
function [RAM, theta_axis_deg] = rdm_to_ram(RDM, lambda, d, varargin)
% 将整张RDM[nRange x nDoppler x nChannel]转换为RAM[nRange x NfftAng]
% 通过沿通道维做FFT得到角度谱,并在多普勒维进行聚合以消去多普勒维度
%
% 必填:
% RDM : 复数三维数组 [nRange x nDoppler x nChannel]
% lambda : 工作波长
% d : 阵元间距
%
% 可选参数(以'Name',Value传入):
% 'NfftAng' : 角度FFT点数, 默认为max(256, 2^nextpow2(nChannel))
% 'Window' : 通道窗函数句柄或向量, 默认hann
% 'AggMode' : 多普勒聚合方式: 'sum'(非相干求和, 默认), 'max'(投影取最大),
% 'coh'(相干求和后取幅度)
% 'DopplerSel' : 选取的多普勒索引向量, 默认1:nDoppler
% 'BlockSize' : 多普勒分块大小, 默认min(256,nDoppler)
% 'Power' : 输出幅度类型: 'mag'(幅度, 默认) 或 'pow'(功率)
%
% 返回:
% RAM : [nRange x NfftAng] 距离--方位图
% theta_axis_deg : [1 x NfftAng] 方位角坐标(度)
%================ 参数解析 =================
[nR, nD, nC] = size(RDM);
p = inputParser;
addParameter(p,'NfftAng', max(256, 2^nextpow2(nC)));
addParameter(p,'Window', @hann); % 可传入函数句柄或长度为nC的向量
addParameter(p,'AggMode','sum'); % 'sum' | 'max' | 'coh'
addParameter(p,'DopplerSel', 1:nD);
addParameter(p,'BlockSize', min(256,nD));
addParameter(p,'Power','mag'); % 'mag' | 'pow'
parse(p,varargin{:});
NfftAng = p.Results.NfftAng;
winSpec = p.Results.Window;
aggMode = lower(p.Results.AggMode);
dopSel = p.Results.DopplerSel(:).';
blk = p.Results.BlockSize;
powMode = lower(p.Results.Power);
% 窗函数
if isa(winSpec,'function_handle')
w = winSpec(nC);
else
w = winSpec(:);
assert(numel(w)==nC,'Window长度需等于通道数nChannel');
end
w = w / mean(w); % 归一化,减少幅度偏置
% 角度轴(通用d、lambda公式): 2*pi*f = 2*pi*d*sin(theta)/lambda => sin(theta) = (lambda/d)*f
f_axis = (-NfftAng/2 : NfftAng/2-1) / NfftAng;
u_axis = (lambda/d) * f_axis;
u_axis = max(min(u_axis,1),-1);
theta_axis_deg = asind(u_axis);
%================ 预分配与聚合器 =================
switch aggMode
case 'sum'
RAM = zeros(nR, NfftAng, 'single'); % 非相干求和
case 'max'
RAM = -inf(nR, NfftAng, 'single'); % 最大投影
case 'coh'
RAM = complex(zeros(nR, NfftAng, 'single')); % 相干累加(复数)
otherwise
error('未知AggMode: %s', aggMode);
end
%================ 多普勒分块处理 =================
nSel = numel(dopSel);
for k = 1:blk:nSel
idx = dopSel(k : min(k+blk-1, nSel)); % 当前块的多普勒索引
% 取出该块数据并在通道维加窗,得到[nRange x nBlk x nChannel]
X = RDM(:, idx, :);
for m = 1:nC
X(:,:,m) = X(:,:,m) * w(m);
end
% 沿通道维做零填充FFT+fftshift,得到角度响应: [nRange x nBlk x NfftAng]
A = fftshift(fft(X, NfftAng, 3), 3);
% 将[nRange x nBlk x NfftAng]视作矩阵做聚合
switch aggMode
case 'sum'
% 非相干求和: 对多普勒维的幅度/功率求和
if strcmp(powMode,'pow')
Pblk = sum(abs(A).^2, 2, 'native'); % [nRange x 1 x NfftAng]
else
Pblk = sum(abs(A), 2, 'native'); % [nRange x 1 x NfftAng]
end
Pblk = squeeze(Pblk); % [nRange x NfftAng]
RAM = RAM + single(Pblk);
case 'max'
% 投影取最大: 多普勒维取最大幅度或功率
if strcmp(powMode,'pow')
Pblk = squeeze(max(abs(A).^2, [], 2)); % [nRange x NfftAng]
else
Pblk = squeeze(max(abs(A), [], 2)); % [nRange x NfftAng]
end
RAM = max(RAM, single(Pblk));
case 'coh'
% 相干累加: 多普勒维先相干求和,再取幅度/功率
Cblk = squeeze(sum(A, 2)); % [nRange x NfftAng] 复数
RAM = RAM + single(Cblk);
end
end
% 相干模式下将复数累加结果转为幅度或功率
if strcmp(aggMode,'coh')
if strcmp(powMode,'pow')
RAM = abs(RAM).^2;
else
RAM = abs(RAM);
end
end
end
(6)stft_explorer_gui.m:
Matlab
function stft_explorer_gui
% STFT参数探索GUI(R2024a兼容版)
% 依赖:Signal Processing Toolbox
%============ 主界面与布局 ============%
f = uifigure('Name','STFT参数探索器','Position',[100 80 1400 820]);
mainGL = uigridlayout(f,[1 2]);
mainGL.ColumnWidth = {480, '1x'};
mainGL.RowHeight = {'1x'};
% 左侧控制面板
pnl = uipanel(mainGL,'Title','参数','FontWeight','bold');
pnl.Scrollable = 'on';
% 给足够多的行,RowHeight使用'fit'
NROWS = 70;
glL = uigridlayout(pnl,[NROWS 2]);
glL.RowHeight = repmat({'fit'},1,NROWS);
glL.ColumnWidth = {190, 260};
% 右侧显示面板
right = uipanel(mainGL,'Title','结果','FontWeight','bold');
glR = uigridlayout(right,[1 1]);
tabs = uitabgroup(glR);
tabWave = uitab(tabs,'Title','时域波形');
axWave = uiaxes(tabWave,'Position',[10 10 930 720]);
tabSpec = uitab(tabs,'Title','STFT时频图');
axSpec = uiaxes(tabSpec,'Position',[10 10 930 720]);
%====== 行号管理器 ======%
r = 1;
function h = addLabel(txt)
h = uilabel(glL,'Text',txt);
h.Layout.Row = r; h.Layout.Column = 1;
end
function h = addNum(val)
h = uieditfield(glL,'numeric','Value',val);
h.Layout.Row = r; h.Layout.Column = 2;
end
function h = addTxt(val)
h = uieditfield(glL,'text','Value',val);
h.Layout.Row = r; h.Layout.Column = 2;
end
function h = addDD(items, val)
h = uidropdown(glL,'Items',items,'Value',val);
h.Layout.Row = r; h.Layout.Column = 2;
end
function h = addCB(val)
h = uicheckbox(glL,'Value',val);
h.Layout.Row = r; h.Layout.Column = 2;
end
function h = addSlider(vmin,vmax,val)
addLabel('重叠百分比');
h = uislider(glL,'Limits',[vmin vmax],'Value',val);
h.Layout.Row = r; h.Layout.Column = [1 2];
end
%============ 控件:信号与采样相关 ============%
addLabel('信号类型'); ddSig = addDD({'正弦','多音','线性调频','AM','FM','脉冲串LFM'},'线性调频'); r=r+1;
addLabel('采样率Hz'); edtFs = addNum(48e3); r=r+1;
addLabel('时长s'); edtDur = addNum(2); r=r+1;
addLabel('SNRdB'); edtSNR = addNum(40); r=r+1;
addLabel('f0Hz/载频'); edtF0 = addNum(1000); r=r+1;
addLabel('f1Hz/上限'); edtF1 = addNum(8000); r=r+1;
addLabel('调制频率fmHz'); edtFm = addNum(10); r=r+1;
addLabel('调制指数/频偏Hz'); edtBeta = addNum(200); r=r+1;
addLabel('多音Hz(逗号分隔)'); edtMulti = addTxt('800,1500,3000'); r=r+1;
addLabel('脉冲占空比0-1'); edtDuty = addNum(0.2); r=r+1;
%============ 控件:STFT参数 ============%
addLabel('窗类型'); ddWin = addDD({'Hamming','Hann','Rect','Blackman','Kaiser'},'Hamming'); r=r+1;
addLabel('Kaiserβ'); edtKaiser = addNum(8); edtKaiser.Editable='off'; r=r+1;
addLabel('窗长N'); edtWin = addNum(256); r=r+1;
sldOv = addSlider(0,95,75); r=r+1;
addLabel('FFT点数Nfft'); edtNfft = addNum(1024); r=r+1;
addLabel('幅值刻度'); ddScale = addDD({'dB','线性'},'dB'); r=r+1;
addLabel('Colormap'); ddCmap = addDD({'jet','parula','turbo','hot','gray','cool'},'jet'); r=r+1;
addLabel('Colorbar'); cbCbar = addCB(true); r=r+1;
addLabel('CLim自动'); cbAutoCL= addCB(true); r=r+1;
addLabel('CLim最小'); edtCLmin= addNum(-120); r=r+1;
addLabel('CLim最大'); edtCLmax= addNum(-20); r=r+1;
addLabel('参数变更即更新'); cbLive = addCB(true); r=r+1;
% 按钮
btnGen = uibutton(glL,'Text','生成信号并分析');
btnGen.Layout.Row = r; btnGen.Layout.Column = [1 2]; r=r+1;
btnSave = uibutton(glL,'Text','保存时频图PNG');
btnSave.Layout.Row = r; btnSave.Layout.Column = [1 2]; r=r+1;
lblStat = uilabel(glL,'Text','就绪');
lblStat.Layout.Row = r; lblStat.Layout.Column = [1 2]; r=r+1;
% 颜色条句柄
cbh = [];
% 事件绑定
btnGen.ButtonPushedFcn = @(~,~)runOnce();
btnSave.ButtonPushedFcn = @(~,~)saveFig();
allCtrls = [ddSig, edtFs, edtDur, edtSNR, edtF0, edtF1, edtFm, edtBeta, edtMulti, edtDuty, ...
ddWin, edtKaiser, edtWin, sldOv, edtNfft, ddScale, ddCmap, cbCbar, cbAutoCL, edtCLmin, edtCLmax, cbLive];
for k = 1:numel(allCtrls)
if isprop(allCtrls(k),'ValueChangedFcn')
allCtrls(k).ValueChangedFcn = @(~,~)onAnyChange();
end
end
ddWin.ValueChangedFcn = @(~,~)onWinChange();
% 初次运行
runOnce();
%============ 内部函数 ============%
function onAnyChange()
onWinChange();
if cbLive.Value
runOnce();
end
end
function onWinChange()
if strcmp(ddWin.Value,'Kaiser')
edtKaiser.Editable = 'on';
else
edtKaiser.Editable = 'off';
end
end
function runOnce()
try
% 参数
fs = max(1, edtFs.Value);
dur = max(0.01, edtDur.Value);
N = max(2, round(fs*dur));
snr = edtSNR.Value;
f0 = edtF0.Value;
f1 = edtF1.Value;
fm = edtFm.Value;
beta = edtBeta.Value;
duty = min(max(edtDuty.Value,0.01),0.95);
winLen = max(4, round(edtWin.Value));
ovPct = min(max(sldOv.Value,0),95);
nfft = max(8, 2^nextpow2(round(edtNfft.Value)));
sclDb = strcmp(ddScale.Value,'dB');
cmap = ddCmap.Value;
useCbar = cbCbar.Value;
autoCL = cbAutoCL.Value;
climMin = edtCLmin.Value;
climMax = edtCLmax.Value;
if nfft < winLen
nfft = 2^nextpow2(winLen);
edtNfft.Value = nfft;
end
% 生成信号
t = (0:N-1).'/fs;
x = genSignal(ddSig.Value, t, f0, f1, fm, beta, duty, edtMulti.Value);
% 加噪
x = awgnSafe(x, snr);
% 窗
w = makeWindow(ddWin.Value, winLen, edtKaiser.Value);
% 重叠样本
noverlap = min(winLen-1, max(0, round(winLen*ovPct/100)));
% 计算STFT
[S,F,T] = stft(x, fs, 'Window', w, 'OverlapLength', noverlap, 'FFTLength', nfft);
A = abs(S);
if sclDb
M = 20*log10(A+eps);
else
M = A;
end
% 时域
plot(axWave, t, x, 'LineWidth', 1.0);
xlabel(axWave,'Time(s)'); ylabel(axWave,'Amplitude'); grid(axWave,'on');
title(axWave, sprintf('时域波形 N=%d, fs=%.0fHz', N, fs));
% 时频
imagesc(axSpec, T, F, M);
axSpec.YDir = 'normal';
xlabel(axSpec,'Time(s)'); ylabel(axSpec,'Frequency(Hz)');
title(axSpec, sprintf('STFT 窗=%s N=%d, 重叠=%d, Nfft=%d', ddWin.Value, winLen, noverlap, nfft));
colormap(axSpec, cmap);
% colorbar
if useCbar
if isempty(cbh) || ~isvalid(cbh)
cbh = colorbar(axSpec);
end
cbh.Visible = 'on';
else
if ~isempty(cbh) && isvalid(cbh)
cbh.Visible = 'off';
end
end
% CLim
if autoCL
if sclDb
p = prctile(M(:),[2 98]);
axSpec.CLim = [p(1) p(2)];
else
axSpec.CLimMode = 'auto';
end
else
if climMax <= climMin
climMax = climMin + 1;
edtCLmax.Value = climMax;
end
axSpec.CLim = [climMin, climMax];
end
drawnow;
lblStat.Text = sprintf('完成,T×F=%dx%d', numel(T), numel(F));
catch ME
lblStat.Text = ['错误: ' ME.message];
warning(ME.message);
end
end
function saveFig()
[file,path] = uiputfile({'*.png'},'保存时频图为');
if isequal(file,0), return; end
ftmp = figure('Visible','off');
axc = copyobj(axSpec, ftmp);
set(axc,'Units','normalized','Position',[0.13 0.11 0.775 0.815]);
colormap(axc, colormap(axSpec));
% 若当前显式有colorbar,复制一个
axcColorbarNeeded = true;
if ~isempty(cbh) && isvalid(cbh) && strcmp(cbh.Visible,'off')
axcColorbarNeeded = false;
end
if axcColorbarNeeded
colorbar(axc);
end
exportgraphics(ftmp, fullfile(path,file));
close(ftmp);
lblStat.Text = '已保存PNG';
end
%============ 信号与窗函数 ============%
function x = genSignal(kind, t, f0, f1, fm, beta, duty, multiStr)
switch kind
case '正弦'
x = cos(2*pi*f0*t);
case '多音'
freqList = parseFreqList(multiStr);
x = zeros(size(t));
for kk = 1:numel(freqList)
x = x + cos(2*pi*freqList(kk)*t);
end
x = x/numel(freqList);
case '线性调频'
x = chirp(t, f0, t(end), f1, 'linear', 0);
case 'AM'
m = cos(2*pi*fm*t);
x = (1+min(max(beta,0),1).*m).*cos(2*pi*f0*t);
case 'FM'
% beta视为频偏Hz
kf = beta;
intm = cumsum(cos(2*pi*fm*t))/numel(t)*(t(end)-t(1));
x = cos(2*pi*f0*t + 2*pi*kf*intm);
case '脉冲串LFM'
xchirp = chirp(t, f0, t(end), f1, 'linear', 0);
prf = max(1, floor(1/ max(0.05, t(end)/10) ));
gate = double(mod(t*prf,1) < duty);
x = xchirp .* gate;
otherwise
x = cos(2*pi*f0*t);
end
x = x / max(1e-12, max(abs(x)));
end
function w = makeWindow(wtype, Nw, beta)
switch wtype
case 'Hamming', w = hamming(Nw,'periodic');
case 'Hann', w = hann(Nw,'periodic');
case 'Rect', w = rectwin(Nw);
case 'Blackman', w = blackman(Nw,'periodic');
case 'Kaiser', w = kaiser(Nw, beta);
otherwise, w = hamming(Nw,'periodic');
end
end
function xnoisy = awgnSafe(x, snrdb)
if iscolumn(x), x = x.'; end
P = mean(abs(x).^2);
if P<=0, xnoisy = x.'; return; end
snrLin = 10^(snrdb/10);
N0 = P/snrLin;
n = sqrt(N0/2)*(randn(size(x))+1i*randn(size(x)));
xnoisy = real(x + n);
xnoisy = xnoisy.';
end
function v = parseFreqList(str)
try
parts = regexp(str, '[,,;; ]+', 'split');
v = cellfun(@str2double, parts);
v = v(isfinite(v) & v>0);
if isempty(v), v = [500 1000 2000]; end
catch
v = [500 1000 2000];
end
end
end
(7)stft_window_gui.m:
Matlab
function stft_window_gui(RTmap, PRF, nSamples, rangeIdx)
% STFT时频分析结果岁窗口长度参数变化过程的可视化GUI
% 使用方法(直接调用即可):
% stft_window_gui(RTmap, PRF, nSamples); % 默认rangeIdx=[1420 1470]
% stft_window_gui(RTmap, PRF, nSamples, [iStart iEnd]); % 指定距离门区间
% 该函数目的是为观察窗口长度参数(时间分辨率)变化对时频分析结果的影响
% RTmap为经过脉压处理后得到的时间距离图
% PRF为脉冲重复频率
% nSamples为每个脉冲的采样点数
% rangeIdx为距离门索引区间,即无人机目标对应的距离门区间
%
% ZHChen 2025-08-04
if nargin < 4
rangeIdx = [1420 1470];
end
validateattributes(RTmap, {'numeric'}, {'2d', 'nonempty'}, mfilename, 'RTmap');
validateattributes(PRF, {'numeric'}, {'scalar', 'positive'}, mfilename, 'PRF');
validateattributes(nSamples, {'numeric'}, {'scalar', 'integer', '>=', 1}, mfilename, 'nSamples');
validateattributes(rangeIdx, {'numeric'}, {'vector', 'numel', 2, 'integer', 'positive'}, mfilename, 'rangeIdx');
iStart = rangeIdx(1); iEnd = rangeIdx(2);
if iStart < 1 || iEnd > size(RTmap,1) || iStart >= iEnd
error('rangeIdx越界或无效');
end
% 预处理:均值相消与距离门内求和
background = mean(RTmap, 2);
STFT_Data = RTmap - background;
slice = STFT_Data(iStart:iEnd, :);
sliceSum = sum(slice, 1);
sliceLen = numel(sliceSum);
% GUI搭建
f = uifigure('Name', 'STFT窗口长度可视化', 'Position', [100 100 1000 600]);
ax = uiaxes(f, 'Position', [60 90 700 480]);
ax.XLabel.String = 'Time(s)';
ax.YLabel.String = 'Doppler Frequency(Hz)';
% 窗口长度取值范围与初值
minWin = max(4, 2*ceil(PRF/PRF)); % 下限设为4点
maxWin = min(1024, sliceLen); % 上限不超过信号长度
initWin = min(16, maxWin);
% 滑块
sld = uislider(f, ...
'Position', [80 60 680 3], ...
'Limits', [minWin maxWin], ...
'Value', initWin);
sld.MajorTicks = unique([minWin, round(linspace(minWin, maxWin, 6)), maxWin]);
sld.Tooltip = 'Window Length';
% 数值输入框
uilabel(f, 'Position', [770 480 200 22], 'Text', 'Window Length');
edt = uieditfield(f, 'numeric', ...
'Limits', [minWin maxWin], ...
'RoundFractionalValues', true, ...
'Value', initWin, ...
'Position', [770 455 120 22]);
% 重叠策略选择
uilabel(f, 'Position', [770 410 200 22], 'Text', 'Overlap策略');
ddOverlap = uidropdown(f, 'Items', {'winLen-1', '50%重叠', '25%重叠', '自定义数值'}, ...
'Value', 'winLen-1', ...
'Position', [770 385 160 22]);
% 自定义重叠输入
uilabel(f, 'Position', [770 350 200 22], 'Text', '自定义Overlap');
edtOverlap = uieditfield(f, 'numeric', ...
'Limits', [0 maxWin-1], ...
'Value', max(initWin-1, 0), ...
'Editable', 'off', ...
'Position', [770 325 120 22]);
% 颜色映射与动态范围
uilabel(f, 'Position', [770 280 200 22], 'Text', 'Colormap');
ddCmap = uidropdown(f, 'Items', {'jet', 'parula', 'turbo', 'hot', 'gray'}, ...
'Value', 'jet', ...
'Position', [770 255 120 22]);
uilabel(f, 'Position', [770 220 200 22], 'Text', 'CLim[dB]最小');
edtCLmin = uieditfield(f, 'numeric', 'Value', 10, 'Position', [770 195 120 22]);
uilabel(f, 'Position', [900 220 200 22], 'Text', 'CLim[dB]最大');
edtCLmax = uieditfield(f, 'numeric', 'Value', 58, 'Position', [900 195 120 22]);
% 导出按钮
btnSave = uibutton(f, 'push', ...
'Text', '保存当前图像', ...
'Position', [770 140 160 28]);
% 状态文本
lbl = uilabel(f, 'Position', [60 20 900 22], 'Text', '');
% 初次绘制
doUpdate(initWin);
% 交互联动
sld.ValueChangingFcn = @(src,evt) sliderChanging(evt);
sld.ValueChangedFcn = @(src,evt) sliderChanged();
edt.ValueChangedFcn = @(src,evt) editChanged();
ddOverlap.ValueChangedFcn = @(src,evt) overlapChanged();
edtOverlap.ValueChangedFcn = @(src,evt) doUpdate(round(sld.Value));
ddCmap.ValueChangedFcn = @(src,evt) applyCmap();
edtCLmin.ValueChangedFcn = @(src,evt) applyClim();
edtCLmax.ValueChangedFcn = @(src,evt) applyClim();
btnSave.ButtonPushedFcn = @(src,evt) saveImage();
% 内部函数:统一更新
function doUpdate(winLen)
winLen = clampWin(round(winLen));
sld.Value = winLen;
edt.Value = winLen;
noverlap = computeOverlap(winLen);
Nfft = nSamples;
w = hamming(winLen);
[S,F,T] = stft(sliceSum, PRF, 'Window', w, 'OverlapLength', noverlap, 'FFTLength', Nfft);
Sabs = abs(S);
SdB = 10*log10(Sabs + eps);
imagesc(ax, T, F, SdB);
set(ax, 'YDir', 'normal');
ax.XLabel.String = 'Time(s)';
ax.YLabel.String = 'Doppler Frequency(Hz)';
% 配色与动态范围
applyCmap();
applyClim();
% 时间分辨率与频率分辨率提示
dt = winLen/PRF;
df = PRF/Nfft;
ax.Title.String = sprintf('Micro-Doppler Signature 窗长%d, 时间分辨率%.3g s, 频率分辨率%.3g Hz', winLen, dt, df);
% lbl.Text = sprintf('rangeIdx=[%d,%d], PRF=%.3f Hz, Nfft=%d, Overlap=%d样本', iStart, iEnd, PRF, Nfft, noverlap);
end
% 计算重叠
function noverlap = computeOverlap(winLen)
switch ddOverlap.Value
case 'winLen-1'
noverlap = max(winLen-1, 0);
case '50%重叠'
noverlap = max(round(0.5*winLen), 0);
case '25%重叠'
noverlap = max(round(0.25*winLen), 0);
otherwise
noverlap = clampOverlap(round(edtOverlap.Value), winLen);
end
edtOverlap.Limits = [0 max(winLen-1,0)];
if strcmp(ddOverlap.Value, '自定义数值')
edtOverlap.Editable = 'on';
else
edtOverlap.Editable = 'off';
edtOverlap.Value = noverlap;
end
end
% 事件:滑动时实时更新
function sliderChanging(evt)
doUpdate(evt.Value);
end
% 事件:滑动结束
function sliderChanged()
doUpdate(sld.Value);
end
% 事件:编辑框修改
function editChanged()
doUpdate(edt.Value);
end
% 事件:重叠策略修改
function overlapChanged()
doUpdate(sld.Value);
end
% 应用配色
function applyCmap()
try
colormap(ax, ddCmap.Value);
catch
colormap(ax, 'jet');
end
end
% 应用动态范围
function applyClim()
vmin = edtCLmin.Value;
vmax = edtCLmax.Value;
if vmax <= vmin
vmax = vmin + 1;
edtCLmax.Value = vmax;
end
caxis(ax, [vmin vmax]);
colorbar(ax);
end
% 保存图像
function saveImage()
[file, path] = uiputfile({'*.png';'*.jpg';'*.tif'}, '保存图像为');
if isequal(file,0)
return;
end
frame = getframe(ax);
imwrite(frame.cdata, fullfile(path, file));
end
% 工具函数:范围约束
function w = clampWin(w)
w = max(min(round(w), maxWin), minWin);
w = max(min(w, sliceLen), minWin);
end
function ov = clampOverlap(ov, w)
ov = max(min(ov, max(w-1,0)), 0);
end
end