我要做一个计算机的毕业设计,基于这个摘要给我列举下系统大纲,需要用到的技术
創建於:2026年3月3日
創建於:2026年3月3日
下面给你一个基于摘要的一站式雷达试验数据处理系统的毕业设计大纲(系统功能大纲 + 论文/文档大纲都能直接用),并列出实现过程中常用的技术选型。你可以按自己的工作量删减模块。
目标:面向雷达试验数据,实现“采集 → 解析 → 清洗/标注 → 滤波/下采样等处理 → 版本化管理 → 降维可视化展示”的一站式平台。
数据来源:多格式离线文件 + UDP实时数据流。
2.1 离线文件导入
2.2 UDP实时接收
按摘要里提到的能力组织成“处理流水线/工艺链”会很像工程系统:
5.1 统计滤波
5.2 半径滤波(邻域/半径去噪)
5.3 最小距离滤波
5.4 下采样
5.5 处理参数配置与复现
7.1 常规图表
7.2 降维可视化
摘要提到 JFreeChart 做展示:如果你前端也要交互强,建议前端用 ECharts/Chart.js,后端保留 JFreeChart 用于生成报表图(PNG/PDF)也很合理。
如果你愿意,我也可以把上面模块进一步细化成:
数据采集、数据处理以及数据可视化三个模块,给我一个具体的实现方案
下面给你一套能直接开干的实现方案,只覆盖你说的三个模块:** 数据采集 / 数据处理 / 数据可视化**。我按“总体架构 → 数据流 → 后端实现细节(类/表/接口)→ 前端实现细节 → 关键算法落地”来写,你照着做基本就能完成毕业设计并且好答辩。
技术建议
核心数据流(统一成“数据集 Dataset + 版本 Version”)
把任何格式都解析成统一的 SampleRecord(Map/JSON 形式),避免你后面处理逻辑写三套。
SampleRecord(建议)
ts:时间戳(毫秒或微秒)features:数值特征(比如距离/速度/方位/幅度等)meta:补充字段(雷达型号、通道号、批次号等)valid:是否有效(默认 true)你不需要一开始就固定字段很死,先用“动态字段 + 字段映射表”更适合摘要里的“格式不统一”。
POST /api/ingest/file(multipart)/data/raw/{datasetId}/{fileId}/xxx{ 或 [ 开始;XLSX 是 zip 头)CsvParser(Apache Commons CSV / OpenCSV)ExcelParser(Apache POI)JsonParser(Jackson)Version v0,记录:输入文件、字段映射、样本数量、缺失率等摘要统计方案1:MySQL 直接存样本(适合数据量不大,开发简单)
sample_record 表:id, version_id, ts, data_json, valid方案2:样本落盘为 Parquet/CSV + DB 存索引(适合数据量较大,加分)
/data/version/{versionId}/data.csv 或 parquet毕设一般我建议方案1,除非你明确数据量上百万行以上。
UdpReceiver:DatagramSocket/Netty 监听端口BlockingQueue<UdpPacket>:接收线程只负责入队UdpConsumerWorker(1~N 个):从队列取包 → 解析 → 写入存储(批量)UdpSessionManager:管理当前“采集会话”(开始/停止/超时)UdpBinaryDecoderPOST /api/ingest/udp/start(参数:端口、会话名、持续时间/停止条件)POST /api/ingest/udp/stopGET /api/ingest/udp/status(队列长度、接收速率、丢包估计、已写入条数)你要把“统计滤波 / 半径滤波 / 最小距离滤波 / 下采样”做成可组合的步骤:
Pipeline = Step1 -> Step2 -> ... -> StepNname、参数、输入version、输出version、日志、统计ProcessingStep(接口)
StepResult apply(SampleReader reader, StepContext ctx)StatFilterStepRadiusFilterStepMinDistanceFilterStepDownsampleStepPipelineExecutor
SampleReader / SampleWriter
VersionService
POST /api/process/run → 返回 jobIdPipelineExecutorGET /api/process/job/{jobId} 查询进度/状态versionId实现可以用
@Async + ThreadPoolTaskExecutor,job 状态存 MySQL/Redis 都行。
用途:平滑噪声、去异常点(按字段)
field:处理哪个字段(如 range/velocity)windowSize:滑窗大小(如 5/11)mode:mean / medianoutlierRule:3sigma / iqr(只做剔除,不替换)valid=false 或替换为窗口统计值输出:新版本 + 统计(剔除多少、均方差变化等)
用途:剔除“孤立点”(你摘要里那种“半径滤波”)
fields: 用于计算距离的字段集合(如 [range, azimuth, velocity])radius rminNeighbors k:半径内至少要有 k 个邻居,否则判为噪声< r 的数量输出:标记 valid=false 的点 + 新版本
用途:基于最近邻距离的离群点剔除
fieldsdistanceThreshold dtopPercent:剔除最近邻距离最大的前 x%nnDist(i)nnDist(i) > d → invalid用途:减小数据量、提高可视化性能
mode: every_n / random / time_bucketn:每 n 取 1bucketMs:按时间分桶聚合(每桶取均值/最大值)every_n 最容易time_bucket 最适合雷达时序数据展示(答辩很好讲)每次处理生成一条 Version:
versionIdparentVersionIdpipelineJson(步骤+参数)createdBy / createdAtinputStats / outputStatsdataPath 或 recordCount这样你就能做:
GET /api/version/{id}/summary
返回:GET /api/version/{id}/samples?fields=ts,range,velocity&limit=5000
返回:ts-range 折线图range-velocity 散点图GET /api/version/{id}/hist?field=range&bins=50
后端返回 bins 统计,前端画直方图POST /api/version/{id}/pca
参数:fields=[range, velocity, azimuth, snr]dim=2
后端:/data/version/{id}/pca_2d.json 或表 dim_resultGET /api/version/{id}/pca/result
返回二维点:x, y, label, valid
ECharts scatter:t-SNE/UMAP 如果你想做,可以放成“可选增强功能”,但 PCA 足够完成摘要里的“降维展示”。
前端点“导出报告”:
POST /api/report/version/{id}
后端用 JFreeChart 生成:如果时间紧,就按这个做:
如果你告诉我两点信息,我可以把接口、表结构、以及每个 step 的参数 JSON 直接给你定死到“可复制进代码”的程度:
java语言,对于那几个处理算法怎么实现
下面我按你摘要里提到的 4 个处理算法(统计滤波 / 半径滤波 / 最小距离滤波 / 下采样)给出Java 可直接落地的实现思路 + 可用代码骨架。默认你数据已经解析成一组“样本点”,每个点有若干数值字段(range/velocity/azimuth…)和一个 valid 标记。
javaimport java.util.*; public class SamplePoint { public long ts; // 时间戳,可选 public Map<String, Double> f; // 动态数值特征:field -> value public boolean valid = true; // 是否有效(被滤除则置 false) public String label; // 可选:人工标注标签 public SamplePoint(long ts, Map<String, Double> f) { this.ts = ts; this.f = f; } public Double get(String key) { return f.get(key); } public void set(String key, double v) { f.put(key, v); } }
一些通用工具函数(距离/取字段向量):
javapublic class MathUtil { public static double euclidean(SamplePoint a, SamplePoint b, List<String> fields) { double s = 0.0; for (String k : fields) { double va = a.get(k); double vb = b.get(k); double d = va - vb; s += d * d; } return Math.sqrt(s); } }
适合时序信号平滑。实现要点:
ts 排序valid=true 的点参与统计(你也可以都参与)javaimport java.util.*; public class StatFilters { // 对 field 做滑窗均值平滑:windowSize 必须为奇数更直观(比如 5/11) public static void movingAverage(List<SamplePoint> pts, String field, int windowSize) { pts.sort(Comparator.comparingLong(p -> p.ts)); int n = pts.size(); double[] x = new double[n]; for (int i = 0; i < n; i++) x[i] = pts.get(i).get(field); double[] prefix = new double[n + 1]; for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + x[i]; int half = windowSize / 2; for (int i = 0; i < n; i++) { int l = Math.max(0, i - half); int r = Math.min(n - 1, i + half); double sum = prefix[r + 1] - prefix[l]; double mean = sum / (r - l + 1); pts.get(i).set(field, mean); } } }
中值对尖峰噪声更稳,但朴素实现 O(n·w log w) 也够毕设(窗口一般不大)。
javapublic static void movingMedian(List<SamplePoint> pts, String field, int windowSize) { pts.sort(Comparator.comparingLong(p -> p.ts)); int n = pts.size(); int half = windowSize / 2; for (int i = 0; i < n; i++) { int l = Math.max(0, i - half); int r = Math.min(n - 1, i + half); double[] win = new double[r - l + 1]; for (int j = l; j <= r; j++) win[j - l] = pts.get(j).get(field); Arrays.sort(win); double med = win[win.length / 2]; pts.get(i).set(field, med); } }
定义:对点 i,统计距离 < r 的邻居数(不含自身也行),若 < minNeighbors 则 valid=false。
朴素 O(n²) 实现最直观、最好写,数据量到几万可能慢;毕设一般 OK。想加速再上 KD-Tree(后面给建议)。
javaimport java.util.*; public class RadiusFilter { public static int apply(List<SamplePoint> pts, List<String> fields, double radius, int minNeighbors) { int n = pts.size(); int removed = 0; boolean[] keep = new boolean[n]; Arrays.fill(keep, true); for (int i = 0; i < n; i++) { if (!pts.get(i).valid) { keep[i] = false; continue; } int cnt = 0; for (int j = 0; j < n; j++) { if (i == j) continue; if (!pts.get(j).valid) continue; double d = MathUtil.euclidean(pts.get(i), pts.get(j), fields); if (d < radius) cnt++; if (cnt >= minNeighbors) break; } if (cnt < minNeighbors) keep[i] = false; } for (int i = 0; i < n; i++) { if (!keep[i] && pts.get(i).valid) { pts.get(i).valid = false; removed++; } } return removed; } }
参数怎么选(答辩能讲)
radius:按你字段量纲决定,建议先做标准化(例如各字段 z-score),这样 radius 更好设(如 0.5~2.0)。minNeighbors:一般 3~10。定义:对每个点 i,计算其最近邻距离 nnDist(i);若 nnDist(i) > threshold 则认为是离群点。
朴素 O(n²):
javaimport java.util.*; public class MinDistanceFilter { public static int apply(List<SamplePoint> pts, List<String> fields, double threshold) { int n = pts.size(); double[] nn = new double[n]; Arrays.fill(nn, Double.POSITIVE_INFINITY); for (int i = 0; i < n; i++) { if (!pts.get(i).valid) continue; for (int j = 0; j < n; j++) { if (i == j) continue; if (!pts.get(j).valid) continue; double d = MathUtil.euclidean(pts.get(i), pts.get(j), fields); if (d < nn[i]) nn[i] = d; } } int removed = 0; for (int i = 0; i < n; i++) { if (pts.get(i).valid && nn[i] > threshold) { pts.get(i).valid = false; removed++; } } return removed; } // 另一种更稳:按百分位阈值剔除,比如剔除 nnDist 最大的 topPercent public static int applyTopPercent(List<SamplePoint> pts, List<String> fields, double topPercent) { int n = pts.size(); List<Double> dists = new ArrayList<>(); double[] nn = new double[n]; Arrays.fill(nn, Double.POSITIVE_INFINITY); for (int i = 0; i < n; i++) { if (!pts.get(i).valid) continue; for (int j = 0; j < n; j++) { if (i == j) continue; if (!pts.get(j).valid) continue; double d = MathUtil.euclidean(pts.get(i), pts.get(j), fields); if (d < nn[i]) nn[i] = d; } if (Double.isFinite(nn[i])) dists.add(nn[i]); } dists.sort(Double::compareTo); int idx = (int)Math.floor((1.0 - topPercent) * (dists.size() - 1)); idx = Math.max(0, Math.min(idx, dists.size() - 1)); double thr = dists.get(idx); int removed = 0; for (int i = 0; i < n; i++) { if (pts.get(i).valid && nn[i] > thr) { pts.get(i).valid = false; removed++; } } return removed; } }
javaimport java.util.*; public class Downsample { public static List<SamplePoint> everyN(List<SamplePoint> pts, int n) { if (n <= 1) return new ArrayList<>(pts); List<SamplePoint> out = new ArrayList<>(); for (int i = 0; i < pts.size(); i++) { if (i % n == 0) out.add(pts.get(i)); } return out; } }
javapublic static List<SamplePoint> randomKeep(List<SamplePoint> pts, double keepRate, long seed) { Random r = new Random(seed); List<SamplePoint> out = new ArrayList<>(); for (SamplePoint p : pts) { if (r.nextDouble() < keepRate) out.add(p); } return out; }
每 bucketMs 一个桶,对桶内字段做均值聚合,输出更平滑、点数更少:
javapublic static List<SamplePoint> timeBucketMean(List<SamplePoint> pts, long bucketMs, List<String> fields) { pts.sort(Comparator.comparingLong(p -> p.ts)); List<SamplePoint> out = new ArrayList<>(); if (pts.isEmpty()) return out; long start = pts.get(0).ts; long curBucket = start / bucketMs; Map<String, Double> sum = new HashMap<>(); int cnt = 0; long bucketTs = pts.get(0).ts; for (SamplePoint p : pts) { long b = p.ts / bucketMs; if (b != curBucket) { // flush if (cnt > 0) { Map<String, Double> feat = new HashMap<>(); for (String f : fields) feat.put(f, sum.getOrDefault(f, 0.0) / cnt); out.add(new SamplePoint(bucketTs, feat)); } // reset curBucket = b; sum.clear(); cnt = 0; bucketTs = p.ts; } if (!p.valid) continue; for (String f : fields) sum.put(f, sum.getOrDefault(f, 0.0) + p.get(f)); cnt++; } if (cnt > 0) { Map<String, Double> feat = new HashMap<>(); for (String f : fields) feat.put(f, sum.getOrDefault(f, 0.0) / cnt); out.add(new SamplePoint(bucketTs, feat)); } return out; }
一个简单 Step 接口:
javapublic interface ProcessingStep { String name(); void apply(List<SamplePoint> points); }
举例把半径滤波封装:
javaimport java.util.*; public class RadiusFilterStep implements ProcessingStep { private final List<String> fields; private final double radius; private final int minNeighbors; public RadiusFilterStep(List<String> fields, double radius, int minNeighbors) { this.fields = fields; this.radius = radius; this.minNeighbors = minNeighbors; } @Override public String name() { return "radius_filter"; } @Override public void apply(List<SamplePoint> points) { RadiusFilter.apply(points, fields, radius, minNeighbors); } }
然后 Pipeline:
javapublic class Pipeline { private final List<ProcessingStep> steps = new ArrayList<>(); public Pipeline add(ProcessingStep s) { steps.add(s); return this; } public void run(List<SamplePoint> pts) { for (ProcessingStep s : steps) s.apply(pts); } }
如果数据量上来(> 5万点)朴素 O(n²) 会慢,你有两个常见升级路线:
如果你告诉我你“fields 维度是 2/3 还是更多、数据规模是多少”,我可以把 网格哈希版半径滤波/最近邻 也给你一份完整 Java 实现(非常适合毕设:代码不长、速度提升明显、解释清楚)。
你现在的数据特征字段大概有哪些?(比如 range/velocity/azimuth/snr)以及单批次大概多少点(1万/10万/100万)?我好给你把 radius/threshold 的推荐设定方式和“是否需要加速”的实现版本定下来。
数据加速讲的详细点
下面把“半径滤波 / 最近邻最小距离滤波”在数据量变大时怎么加速讲透,并给你两套很适合 Java 毕设落地的方案(从简单到加分),附关键代码骨架。
半径滤波和最近邻距离滤波,朴素写法都是:
当 n=50,000 时,约 25 亿次距离计算,Java 很容易就卡死。
真实系统里常这么做:
time_bucket / every_n 把点数从 50万降到 5万或更少好处:实现简单、收益巨大;缺点:可能损失细节(但毕设完全可接受)。
你用多个字段算欧氏距离时,各字段量纲不同(比如 range=几千,velocity=几十),会导致距离被某一个字段支配。 建议做 z-score:
标准化后:
两条路:
把空间按 cellSize 切成很多小格子(cell)。
每个点只和自己所在格子 + 周围相邻格子的点做比较,而不是全量比较。
如果 cellSize = radius:
3^d 个格子(d 是维度)
所以网格哈希最适合 2~3 维(雷达数据常拿 range + azimuth + velocity 或 range + doppler)。
例如:
fields = [range, velocity](2D,最快)fields = [range, azimuth, velocity](3D)先把参与距离计算的字段标准化后再建网格。
对每个点:
cellCoord[k] = floor(value[k] / cellSize)javaimport java.util.*; import java.util.stream.*; public class GridIndex { private final List<String> fields; private final double cellSize; private final Map<CellKey, List<Integer>> buckets = new HashMap<>(); private final List<SamplePoint> pts; public GridIndex(List<SamplePoint> pts, List<String> fields, double cellSize) { this.pts = pts; this.fields = fields; this.cellSize = cellSize; build(); } private void build() { for (int i = 0; i < pts.size(); i++) { if (!pts.get(i).valid) continue; CellKey key = cellKeyOf(pts.get(i)); buckets.computeIfAbsent(key, k -> new ArrayList<>()).add(i); } } private CellKey cellKeyOf(SamplePoint p) { int d = fields.size(); long[] c = new long[d]; for (int k = 0; k < d; k++) { double v = p.get(fields.get(k)); c[k] = (long)Math.floor(v / cellSize); } return new CellKey(c); } public List<Integer> candidateIndices(SamplePoint p) { CellKey base = cellKeyOf(p); List<CellKey> neighbors = base.neighbors(); // 3^d 个邻格 ArrayList<Integer> out = new ArrayList<>(); for (CellKey nk : neighbors) { List<Integer> ids = buckets.get(nk); if (ids != null) out.addAll(ids); } return out; } // ---------- CellKey ---------- public static class CellKey { final long[] c; final int hash; public CellKey(long[] c) { this.c = c; this.hash = Arrays.hashCode(c); } @Override public boolean equals(Object o) { return (o instanceof CellKey) && Arrays.equals(c, ((CellKey)o).c); } @Override public int hashCode() { return hash; } public List<CellKey> neighbors() { List<CellKey> res = new ArrayList<>(); long[] cur = Arrays.copyOf(c, c.length); dfs(res, cur, 0); return res; } private void dfs(List<CellKey> res, long[] cur, int idx) { if (idx == cur.length) { res.add(new CellKey(Arrays.copyOf(cur, cur.length))); return; } long orig = cur[idx]; for (long delta = -1; delta <= 1; delta++) { cur[idx] = orig + delta; dfs(res, cur, idx + 1); } cur[idx] = orig; } } }
把“遍历所有 j”换成“遍历候选邻格里的点”。
javapublic class RadiusFilterFast { public static int apply(List<SamplePoint> pts, List<String> fields, double radius, int minNeighbors) { GridIndex gi = new GridIndex(pts, fields, radius); // cellSize=radius int removed = 0; for (int i = 0; i < pts.size(); i++) { SamplePoint pi = pts.get(i); if (!pi.valid) continue; int cnt = 0; for (int j : gi.candidateIndices(pi)) { if (i == j) continue; SamplePoint pj = pts.get(j); if (!pj.valid) continue; double d = MathUtil.euclidean(pi, pj, fields); if (d < radius) { cnt++; if (cnt >= minNeighbors) break; } } if (cnt < minNeighbors) { pi.valid = false; removed++; } } return removed; } }
复杂度直觉:
从 O(n²) 变成约 O(n * k),其中 k 是“附近候选点数量”。如果数据分布比较均匀,k 通常远小于 n。
同理,每个点只在候选邻格里找最近邻。
javapublic class MinDistanceFilterFast { public static int apply(List<SamplePoint> pts, List<String> fields, double threshold) { GridIndex gi = new GridIndex(pts, fields, threshold); // cellSize≈threshold 经验上可行 int removed = 0; for (int i = 0; i < pts.size(); i++) { SamplePoint pi = pts.get(i); if (!pi.valid) continue; double best = Double.POSITIVE_INFINITY; for (int j : gi.candidateIndices(pi)) { if (i == j) continue; SamplePoint pj = pts.get(j); if (!pj.valid) continue; double d = MathUtil.euclidean(pi, pj, fields); if (d < best) best = d; } if (best > threshold) { pi.valid = false; removed++; } } return removed; } }
注意:最近邻用 cellSize=threshold 只是工程上常用近似设定。更稳的方式:
你可以:
KD-Tree 支持:
如果你想走 KD-Tree,我建议你把 fields 固定为 2D/3D,代码量还能控制住;要不然写通用维度 KD-Tree 容易翻车。
半径滤波/最近邻滤波每个点的计算相对独立,适合并行:
valid(数据竞争)
toInvalidate[i]=true/false,最后统一应用伪代码:
javaboolean[] drop = new boolean[n]; IntStream.range(0, n).parallel().forEach(i -> { // 只读 pts,计算 drop[i] }); // 最后单线程更新 valid for (int i=0; i<n; i++) if (drop[i]) pts.get(i).valid=false;
如果你把样本写 MySQL:
如果你把参与距离计算的字段告诉我(2D/3D?)以及单批次数据量级(1万/10万/100万),我可以进一步把:
统计滤波(SOR)、半径滤波(ROR)、最小距离滤波(MD)和下采样(VG)
统计滤波(SOR)、半径滤波(ROR)、最小距离滤波(MD)和下采样(VG)
下面这四个名字基本就是点云/雷达点集里常见的处理链:SOR(Statistical Outlier Removal)统计离群滤波、ROR(Radius Outlier Removal)半径离群滤波、MD(Minimum Distance)最小距离滤波、VG(Voxel Grid)体素下采样。我按“定义→参数→Java实现要点→复杂度/加速”把你能直接写进代码/论文的版本给出来。
你把每个样本点当成 d 维向量(通常 2~3 维最合适):
[range, azimuth, velocity] 或 [x, y, z]javastatic double dist(SamplePoint a, SamplePoint b, List<String> fields) { double s = 0; for (String f : fields) { double d = a.get(f) - b.get(f); s += d * d; } return Math.sqrt(s); }
强烈建议:对 fields 做 z-score 标准化后再做 SOR/ROR/MD(参数更好设、解释更清楚)。
对每个点 ,找到其 k 个近邻,计算平均距离:
对所有点的 计算全局均值 和标准差 ,若:
则认为 是离群点,剔除(置 invalid)。
k:10~50(常用 20)alpha:0.5~2.0(常用 1.0)javastatic int sor(List<SamplePoint> pts, List<String> fields, int k, double alpha) { int n = pts.size(); double[] meanK = new double[n]; for (int i = 0; i < n; i++) { if (!pts.get(i).valid) { meanK[i] = Double.NaN; continue; } // 维护k个最小距离:简单做法用数组收集再部分排序 double[] best = new double[k]; java.util.Arrays.fill(best, Double.POSITIVE_INFINITY); for (int j = 0; j < n; j++) { if (i == j || !pts.get(j).valid) continue; double d = dist(pts.get(i), pts.get(j), fields); // 插入到best里(k很小,O(k)插入就行) for (int t = 0; t < k; t++) { if (d < best[t]) { for (int u = k - 1; u > t; u--) best[u] = best[u - 1]; best[t] = d; break; } } } double sum = 0; int cnt = 0; for (double v : best) if (Double.isFinite(v)) { sum += v; cnt++; } meanK[i] = (cnt == 0) ? Double.POSITIVE_INFINITY : (sum / cnt); } // 全局mu、sigma double mu = 0; int m = 0; for (double v : meanK) if (Double.isFinite(v)) { mu += v; m++; } mu /= Math.max(1, m); double var = 0; for (double v : meanK) if (Double.isFinite(v)) var += (v - mu) * (v - mu); double sigma = Math.sqrt(var / Math.max(1, m)); double thr = mu + alpha * sigma; int removed = 0; for (int i = 0; i < n; i++) { if (pts.get(i).valid && meanK[i] > thr) { pts.get(i).valid = false; removed++; } } return removed; }
SOR 的瓶颈是 kNN 查询:朴素 O(n²)。想快:
给定半径 ,若点 的半径邻域内(距离 < r)的邻居数小于阈值 :
则认为 为离群点,剔除。
radius r:标准化后常用 0.5~2.0;未标准化需按量纲调minNeighbors k:3~10这比 KD-Tree 好写很多,而且在 2~3 维很快。
思路:cellSize=r,把点放入格子,只检查周围 3^d 个格子候选。
javastatic int rorWithGrid(List<SamplePoint> pts, List<String> fields, double r, int minNeighbors) { // 1) 建桶 Map<String, java.util.ArrayList<Integer>> bucket = new java.util.HashMap<>(); int d = fields.size(); java.util.function.Function<SamplePoint, long[]> cell = p -> { long[] c = new long[d]; for (int i = 0; i < d; i++) c[i] = (long) Math.floor(p.get(fields.get(i)) / r); return c; }; java.util.function.Function<long[], String> key = c -> java.util.Arrays.toString(c); for (int i = 0; i < pts.size(); i++) { if (!pts.get(i).valid) continue; bucket.computeIfAbsent(key.apply(cell.apply(pts.get(i))), k -> new java.util.ArrayList<>()).add(i); } // 2) 逐点统计邻居 boolean[] drop = new boolean[pts.size()]; for (int i = 0; i < pts.size(); i++) { SamplePoint pi = pts.get(i); if (!pi.valid) continue; long[] base = cell.apply(pi); int cnt = 0; // 枚举邻格:3^d int[] delta = new int[d]; java.util.Arrays.fill(delta, -1); while (true) { long[] nb = base.clone(); for (int t = 0; t < d; t++) nb[t] += delta[t]; var list = bucket.get(key.apply(nb)); if (list != null) { for (int j : list) { if (i == j) continue; if (!pts.get(j).valid) continue; if (dist(pi, pts.get(j), fields) < r) { cnt++; if (cnt >= minNeighbors) break; } } } if (cnt >= minNeighbors) break; // delta++(三进制进位:-1,0,1) int pos = d - 1; while (pos >= 0) { if (delta[pos] < 1) { delta[pos]++; break; } delta[pos] = -1; pos--; } if (pos < 0) break; } if (cnt < minNeighbors) drop[i] = true; } int removed = 0; for (int i = 0; i < pts.size(); i++) if (pts.get(i).valid && drop[i]) { pts.get(i).valid = false; removed++; } return removed; }
对每个点 ,计算最近邻距离:
若 则认为为离群点剔除。
threshold T:标准化后常用 1.0~3.0(具体看数据疏密)T(或 T/2 更稳)实现方式跟 ROR 很像:对每点在候选集合里找最小距离,若没找到候选或最小距离过大则剔除。
如果你要我把 MD 的“逐圈扩展邻格直到找到近邻(更稳)”版补全,我也能给你,但核心逻辑就是:先查 3^d 邻格,没有候选就扩大到 5^d、7^d… 直到找到。
用体素边长 leafSize 划分空间,所有落在同一体素的点用一个代表点替代(常见取质心):
javastatic List<SamplePoint> voxelGrid(List<SamplePoint> pts, List<String> fields, double leaf) { int d = fields.size(); class Acc { double[] sum = new double[d]; int cnt = 0; long tsSum = 0; void add(SamplePoint p) { for (int i = 0; i < d; i++) sum[i] += p.get(fields.get(i)); tsSum += p.ts; cnt++; } SamplePoint toPoint() { java.util.Map<String, Double> f = new java.util.HashMap<>(); for (int i = 0; i < d; i++) f.put(fields.get(i), sum[i] / cnt); return new SamplePoint(tsSum / Math.max(1, cnt), f); } } Map<String, Acc> map = new HashMap<>(); for (SamplePoint p : pts) { if (!p.valid) continue; long[] v = new long[d]; for (int i = 0; i < d; i++) v[i] = (long)Math.floor(p.get(fields.get(i)) / leaf); String key = java.util.Arrays.toString(v); map.computeIfAbsent(key, k -> new Acc()).add(p); } List<SamplePoint> out = new ArrayList<>(map.size()); for (Acc a : map.values()) out.add(a.toPoint()); return out; }
工程上一般这样串:
如果你把 fields 具体是哪几个(例如 [range, azimuth, velocity] 还是 [x,y,z])和 单批次点数规模(1万/10万/100万)告诉我,我可以直接给你一套“参数默认值 + 标准化代码 + 网格哈希的稳健版(逐圈扩展)MD/SOR kNN 查询实现”,基本就是可直接抄进项目的最终版本。
前端样本数据手动修正怎么实现
下面给你一个**“前端手动修正样本数据”**的可落地方案(Vue + ElementPlus + ECharts),包含:交互设计、前后端数据结构、接口、并发控制、以及关键实现细节。按这个做,答辩时也很好讲“人工复核/可追溯/版本化”。
建议把手动修正拆成 3 类(从易到难):
valid=false/true我下面的方案以 1+2 为主(毕业设计性价比最高)。
强烈建议:原始版本只读,手动修正产生一个 修正版 Version(或 overlay patch),这样你能讲清楚可追溯。
A. 直接生成新版本(简单直观)
B. 修正补丁 overlay(更工程、更省空间)
毕业设计我建议:B(patch),因为:
最好解析入库时给每条样本一个 sample_id(自增/雪花都行)。
sample_patch
idversion_id(对哪个版本做修正)sample_idop_type:SET_VALID / UPDATE_FIELDSpatch_json:例如 { "valid": false } 或 { "range": 123.4, "velocity": 5.6 }created_by, created_at批量修正就是插多条 patch,或者 patch_json 支持批处理(建议多条更清晰、可审计)。
页面分 3 区:
valid=false 的点用另一条 series 显示(灰/空心)ECharts 支持 brush 选中散点,拿到选中的 dataIndex,然后映射回 sampleId。
关键:你的每个点数据结构要带 sampleId:
js// 每个点传给 echarts: [x, y, sampleId] data: points.map(p => [p.range, p.velocity, p.sampleId])
brush 事件里获取选中点:
jschart.on('brushSelected', (params) => { const selected = []; params.batch[0].selected.forEach(sel => { sel.dataIndex.forEach(idx => { const item = option.series[sel.seriesIndex].data[idx]; selected.push(item[2]); // sampleId }); }); selectedSampleIds.value = Array.from(new Set(selected)); });
setCurrentRow)GET /api/version/{vid}/samples?fields=ts,range,velocity,valid&limit=5000&offset=0
后端做法:
POST /api/version/{vid}/patch
请求体:
json{ "op": "SET_VALID", "sampleIds": [1,2,3], "payload": { "valid": false } }
或者字段修正:
json{ "op": "UPDATE_FIELDS", "sampleIds": [1,2,3], "payload": { "range": 123.4, "velocity": 5.6 } }
POST /api/version/{vid}/patch/transform
json{ "sampleIds": [1,2,3], "transform": [ { "field": "range", "op": "ADD", "value": 1.5 }, { "field": "velocity", "op": "MUL", "value": 0.98 } ] }
这样前端不用自己算,避免浮点误差和被篡改参数;后端也能记录审计。
DELETE /api/version/{vid}/patch?patchIds=...
或者 POST /api/version/{vid}/patch/undoLast
至少做一个轻量的乐观锁:
patch_revision(每次 patch +1)ifRevision请求示例:
json{ "op":"SET_VALID", "sampleIds":[1,2,3], "payload":{"valid":false}, "ifRevision": 12 }
雷达/点云数据很容易几十万点,前端渲染会卡。
建议:
limit=5000~20000 点用于可视化(可叠加 VG 下采样)如果你要支持“时间区间修正”,就别传所有 sampleId:
完成这四步,你的“手动修正”就已经像一个真实系统了。
如果你告诉我你可视化主要用哪两个字段做散点(比如 range-velocity 还是 x-y),以及你的样本是不是有 sampleId(或者只能靠 ts+序号 定位),我可以把:
有个问题,点云数据量都是上万,手动检查修改这个需求似乎不太合理
确实不合理——“逐点手动检查/修改上万点”在工程上几乎不会这么做。真实需求通常是:** 人工只做“少量复核 + 批量规则修正 + 重点区域精修”**。你可以把“手动修正”设计成下面这种更符合场景、也更容易答辩的方案。
把“手动检查修改”改成:
这样就合理了:人不是处理所有点,人是“审核与纠偏”。
前端默认只展示:
全量点只在你缩放到局部、或点选某段时间/某个区域时再加载(分页/按窗口加载)。
框选区域修正(brush)
在散点图里框选一片区域 → 批量置 invalid 或批量加偏移/设定值
✅ 最符合“点云人工清理”
条件过滤修正(规则)
UI 给一个“条件构造器”(简单版也行):
range > 5000 AND snr < 3velocity in [a,b]
然后点击“应用修正到符合条件的点”
✅ 实际工程最常用时间段/索引区间修正(雷达时序特别常见)
拖一个时间滑块选 [t1,t2] → 批量修正
✅ 很好讲“试验段落异常/设备切换导致的数据段剔除”
后端在处理时为每个点算一个异常分数 score(你已有算法就能产出):
meanKDist 越大越异常前端提供“异常点队列”:
这是关键:别让前端把几万个 id 发回去。
POST /api/version/{vid}/patch/byRegionjson{ "fields": ["range","velocity"], "polygon": [[x1,y1],[x2,y2],...], "op": "SET_VALID", "payload": {"valid": false} }
POST /api/version/{vid}/patch/byFilterjson{ "filter": "range > 5000 AND snr < 3", "op": "SET_VALID", "payload": {"valid": false} }
POST /api/version/{vid}/patch/byTimejson{ "tsFrom": 1700000000000, "tsTo": 1700000300000, "op": "UPDATE_FIELDS", "payload": {"velocity": 0.0} }
后端执行:
/samples?tsFrom&tsTo&limit=...如果你愿意,我可以按你现在的数据字段(比如你用 range-velocity 还是 x-y-z)给你画一套前端页面结构 + ECharts brush/zoom 联动 + 条件构造器最简实现(直接可抄到 Vue 里)。你现在可视化主要用哪几个字段做散点?(2D 还是 3D)
可不可以在处理的时候标记异常值,然后可以根据这个条件进行排序选择,人工就只用处理异常值高的
可以,而且这是点云/雷达数据里最合理、最工程化的“人机协同”做法:** 算法处理时给每个点打异常分数(score)和原因(reason)**,前端把点按 score 排序,只让人工复核 Top-N 或超过阈值的一小部分点,再做批量修正/确认。
下面给你一套能直接实现的方案。
anomalyScore:异常分数(越大越异常)anomalyReasons:异常原因列表(SOR/ROR/MD 哪些触发)anomalyFlags:位标记(便于筛选)reviewStatus:人工状态(UNREVIEWED / CONFIRMED_BAD / CONFIRMED_GOOD)
valid仍然保留:你可以让算法先“不直接删”,而是先标记,人工确认后再置 invalid(更稳,答辩也好讲)。
SOR里每个点都有 meanKDist(i)(k近邻平均距离)
做法:
thr = mu + α * sigmascore_sor = max(0, (meanKDist - thr) / (sigma + eps))ROR得到每点邻居数 neighborCount(i)
neighborCount < minNeighbors 就异常score_ror = max(0, (minNeighbors - neighborCount) / minNeighbors)MD得到最近邻距离 nnDist(i)
score_md = max(0, (nnDist - T) / (T + eps))T 再算分数最简单也够用:
anomalyScore = w1*score_sor + w2*score_ror + w3*score_md
权重建议:w1=0.5, w2=0.3, w3=0.2(可在配置里调)同时把触发原因写进去:
reasons += ["SOR"] if score_sor > 0reasons += ["ROR"] if neighborCount < minNeighborsreasons += ["MD"] if score_md > 0A. 直接写到样本表里(简单)
anomaly_score DOUBLEanomaly_flags INTanomaly_reasons JSON/TEXTreview_status TINYINTB. 单独一张结果表(更干净)
sample_quality(version_id, sample_id, anomaly_score, flags, reasons, review_status, metrics_json)
metrics_json 可以存 meanKDist/nnDist/neighborCount,前端展示很直观GET /api/version/{vid}/anomalies?minScore=1.0&sort=score_desc&limit=200&offset=0GET /api/version/{vid}/anomalies?reason=SOR&limit=200GET /api/version/{vid}/anomalies?reviewStatus=UNREVIEWED返回里包含:
左侧:异常列表(按分数排序、分页、筛选)
右侧:图表定位 + 局部窗口 + 批量操作
典型流程:
UNREVIEWED + score desc Top200除了 TopN,再加一个“随机抽样 100 点”用于评估误报率/漏报率,报告里很好写。
建议你的处理任务输出:
流水线顺序建议:
如果你告诉我你点的维度是 2D 还是 3D(比如 [range, velocity] 或 [x,y,z])以及你希望“自动剔除”还是“先标记再人工确认”,我可以把:
anomaly_flags 位定义