
简介本资源是一套基于MATLAB实现Voronoi图生成的完整算法代码包面向计算几何初学者、高校数学建模与科学计算课程学习者以及需要深入理解Delaunay三角化与Voronoi图内在关联的工程实践者。资源通过9个核心.m函数如DT2、VD1/VD2、findNeighborTriangles、triangleCircumCircle等完整复现了从点集输入、Delaunay三角剖分、外接圆判定到Voronoi边构建的全过程辅以2张结果图VD1结果图.png、VD2结果图.png直观验证算法正确性。压缩包共11个文件总大小仅131KB轻量易用适合教学演示、算法调试与原理验证。目前已有5928人学习下载读者可直接运行代码观察Voronoi区域划分逻辑掌握边界线段生成机制与邻接关系判定方法并对比两种实现VD1与VD2在鲁棒性或效率上的差异是理解计算几何底层原理不可多得的实操范例。1. Voronoi图不是“花里胡哨的装饰图”它在传感器布点、路径规划、空间插值中是真·硬需求Matlab一行命令就能生成但90%的人卡在边界裁剪和多边形导出这一步你手头有一组散乱的监测点坐标比如土壤湿度探头、基站位置、无人机航拍采样点想快速知道每个点“实际影响范围”有多大——不是简单画个圆而是按最近邻原则划分连续空间。这就是Voronoi图的核心价值它把平面自动切成互不重叠的凸多边形每个多边形内任意一点到其对应生成点的距离都比到其他任何生成点更近。Matlab原生支持voronoi和voronoin但问题来了默认输出只是线段集合没有闭合多边形边界无限延伸无法直接用于面积计算或GIS导入更别说导出为Shp、GeoJSON或带属性的CSV了。我见过太多项目在仿真阶段用plot(voronoi(x,y))草草了事一到部署就发现——这图根本没法进下游系统。本文不讲数学推导只聚焦一个目标用Matlab生成可落地的、带完整边界约束的、能导出为标准地理格式的Voronoi多边形。适合正在做空间分析、无线网络优化、地质建模或机器人覆盖路径设计的工程师尤其当你需要把结果喂给QGIS、ArcGIS或Python的geopandas时。2. 从散点到闭合多边形用voronoinpolyshape构建可裁剪的Voronoi区域Voronoi图的本质是Delaunay三角剖分的对偶图。Matlab里有两个核心函数voronoi画线和voronoin算顶点。前者只能画图后者返回完整的几何拓扑——这才是我们真正要抓的“源数据”。关键在于voronoin输出的是高维空间的顶点索引关系而我们要的是二维平面上每个生成点对应的、有明确边界的闭合多边形。下面这段代码就是最小可行闭环% 假设你的生成点坐标是 Nx2 矩阵 points points [1.2, 3.5; 4.1, 2.8; 2.9, 6.3; 5.7, 4.9; 3.8, 1.2]; % 步骤1调用 voronoin 获取 Voronoi 顶点和区域索引 [V, C] voronoin(points); % 步骤2过滤掉无穷远区域C{i}为空或含Inf索引的区域 valid_regions {}; for i 1:length(C) if ~isempty(C{i}) all(isfinite(V(C{i}, :))) valid_regions{end1} V(C{i}, :); end end % 步骤3将每个有效区域转为 polyshape 对象自动处理闭合、自相交等 voronoi_polygons cell(size(valid_regions)); for i 1:length(valid_regions) % 关键polyshape会自动闭合首尾并修复数值误差导致的微小缺口 voronoi_polygons{i} polyshape(valid_regions{i}); end逻辑说明voronoin(points)返回两个输出V是所有Voronoi顶点坐标M×2矩阵C是元胞数组C{i}表示第i个生成点对应的顶点索引向量。但原始C里包含大量Inf索引——代表该区域延伸至无穷远必须剔除。polyshape是Matlab R2017b后引入的空间对象它不只是“画图”而是承载了完整的几何语义面积、周长、布尔运算、裁剪能力。用它封装每个区域后续所有操作才有意义。参数说明points必须是双精度浮点数矩阵列数2x,y若输入含重复点或共线三点voronoin会报错需提前用unique(points,rows)去重V的行数通常远大于points的行数这是正常现象——Voronoi顶点数与生成点数呈O(n)关系但常数因子较大。3. 边界裁剪为什么你的Voronoi图总“破框而出”用intersect强制限定在研究区域内默认Voronoi图没有地理边界概念——它假设平面无限延展。但现实中你的传感器只布在某块农田里你的基站只覆盖某个城区你的采样点全在某条河流流域内。不裁剪导出的多边形就包含无效大区域面积计算失真GIS软件加载崩溃。裁剪不是简单画个矩形框xlim/ylim而是用polyshape的布尔交集运算把每个Voronoi区域和你的研究区多边形做intersect。这里的关键是研究区多边形必须是闭合、无自相交、且坐标系与生成点一致的polyshape。% 定义研究区例如一个不规则多边形经纬度或投影坐标均可 study_area polyshape([0 0 6 6 0], [0 8 8 0 0]); % 矩形示例实际可用shaperead读取 % 步骤1确保 study_area 和 voronoi_polygons 坐标系一致单位、投影 % 若 points 是WGS84经纬度study_area 也必须是经纬度若已投影为米制两者都需米制 % 步骤2对每个Voronoi多边形执行裁剪 clipped_polygons cell(size(voronoi_polygons)); for i 1:length(voronoi_polygons) if isscalar(voronoi_polygons{i}) ~isempty(voronoi_polygons{i}) % intersect 返回空 polyshape 表示无交集该生成点在研究区外 clipped intersect(voronoi_polygons{i}, study_area); clipped_polygons{i} clipped; else clipped_polygons{i} polyshape(); % 空多边形占位 end end % 步骤3可视化验证可选 figure; plot(study_area, FaceColor, none, EdgeColor, k, LineWidth, 1.5); hold on; colors lines(length(clipped_polygons)); for i 1:length(clipped_polygons) if ~isempty(clipped_polygons{i}) plot(clipped_polygons{i}, FaceColor, colors(i,:), FaceAlpha, 0.4, EdgeColor, w, LineWidth, 0.8); end end plot(points(:,1), points(:,2), r*, MarkerSize, 10, LineWidth, 2); title(裁剪后的Voronoi图每个区域严格限定在研究区内); xlabel(X坐标); ylabel(Y坐标);逻辑说明intersect(A,B)是polyshape的内置方法它精确计算两个多边形的重叠部分返回新的polyshape。即使Voronoi区域原本是无限延伸的只要它和study_area有交集intersect就能返回一个有限、闭合、拓扑正确的多边形。注意如果某个生成点完全在study_area外intersect返回空polyshape()需在后续导出时跳过。参数说明study_area可以是任意复杂形状用shaperead(boundary.shp)读取Shapefile后通过polyshape(shp.X, shp.Y)转换若研究区是规则矩形用[xmin xmax xmax xmin]和[ymin ymin ymax ymax]构造即可FaceAlpha设为0.4是为了看清重叠关系生产环境可设为0.6~0.8plot(points)加星号是为了确认生成点是否都在研究区内——如果某个点被裁掉对应多边形会消失这是正常行为。4. 导出为标准地理格式从MATLAB到QGIS/Python无缝衔接的3种落地方式生成图只是第一步真正投入工程要用。Matlab本身不原生支持Shapefile或GeoJSON但通过writematrix、jsonencode和shapewrite需Mapping Toolbox可实现三类主流导出。重点不是“能不能导”而是“导出的数据能否被下游工具直接识别、无需手动修数据”。下面给出经实测QGIS 3.28、geopandas 0.12、ArcGIS Pro 3.0验证的方案4.1 导出为带坐标的CSV最通用零依赖适用于需要快速查看坐标、导入Excel做统计、喂给Python脚本做二次计算。% 准备导出数据每行一个Voronoi区域含生成点ID、面积、顶点坐标列表 csv_data {}; for i 1:length(clipped_polygons) p clipped_polygons{i}; if ~isempty(p) % 获取顶点坐标自动闭合首尾相同 [xv, yv] vertices(p); % 转为字符串x1,y1;x2,y2;...;xn,yn coords_str strjoin(string(xv) , string(yv), ;); area_val area(p); csv_data{end1, :} {i, area_val, coords_str}; end end % 写入CSV表头id,area,coordinates writematrix([ID,Area,Coordinates; csv_data], voronoi_export.csv);关键细节vertices(p)返回polyshape的所有顶点按顺时针或逆时针顺序排列且首尾自动闭合即第一行和最后一行坐标相同这是GIS软件识别闭合多边形的硬性要求strjoin(..., ;)用分号分隔点对避免CSV逗号冲突writematrix比csvwrite更鲁棒支持字符串混合写入。4.2 导出为GeoJSONWeb GIS和Python首选适用于集成到Leaflet地图、用geopandas读取、上传到在线GIS平台。% 构建GeoJSON FeatureCollection结构 features {}; for i 1:length(clipped_polygons) p clipped_polygons{i}; if ~isempty(p) [xv, yv] vertices(p); % GeoJSON要求坐标为[y,x]顺序纬度在前且为数组嵌套 coords_cell arrayfun((a,b) {[b,a]}, yv, xv, UniformOutput, false); coords_array vertcat(coords_cell{:}); feature struct(... type, Feature, ... properties, struct(id, i, area, area(p)), ... geometry, struct(type, Polygon, coordinates, {coords_array})); features{end1} feature; end end geojson_obj struct(type, FeatureCollection, features, features); json_str jsonencode(geojson_obj); fid fopen(voronoi.geojson, w); fprintf(fid, %s, json_str); fclose(fid);关键细节GeoJSON规范强制要求坐标为[longitude, latitude]即[x,y]但很多Web库如Leaflet习惯[lat,lng]所以此处按标准写[y,x]arrayfun生成坐标数组vertcat拼成Nx2矩阵jsonencode自动处理转义和格式比手拼字符串安全百倍。4.3 导出为Shapefile传统GIS软件刚需适用于QGIS/ArcGIS桌面端直接打开、做空间连接、叠加遥感影像。% 需Mapping Toolbox检查ver(map) if isempty(ver(map)) error(Mapping Toolbox未安装请先安装); end % 构造Shapefile字段必须含Geometry多边形和属性字段 S struct(); S.Geometry {}; S.ID []; S.Area []; for i 1:length(clipped_polygons) p clipped_polygons{i}; if ~isempty(p) [xv, yv] vertices(p); % Shapefile要求每个环独立单环多边形直接赋值 S.Geometry{end1} struct(X, xv, Y, yv); % 注意转置为列向量 S.ID(end1) i; S.Area(end1) area(p); end end % 写入Shapefile shapewrite(S, voronoi_shapefile.shp);关键细节shapewrite要求Geometry字段是结构体元胞数组每个元素含X和Y字段且为列向量xvID和Area必须是数值向量不能是cellShapefile不支持中文路径文件名用英文导出后务必用QGIS打开检查——若多边形显示为“空心”或“错位”大概率是坐标顺序或闭合问题。5. 避坑指南Voronoi生成中5个血泪经验换来的必踩雷区与解法Voronoi图看似简单但Matlab实现中隐藏着多个数值计算和拓扑逻辑陷阱。以下是我在线上系统部署、客户交付、跨平台联调中反复翻车后总结的5条硬核避坑记录每一条都对应真实故障场景5.1 现象voronoin报错“Input is not full rank”原因输入点集存在共线、共面2D下即三点共线或重复点。voronoin底层调用Qhull对退化构型极度敏感。解决在调用前强制去重并扰动微小噪声points unique(points, rows); % 去重 if size(points,1) 2 % 对每个点加1e-8量级随机扰动不影响业务精度但破除退化 points points 1e-8 * randn(size(points)); end5.2 现象导出的Shapefile在QGIS中显示为“空心多边形”或“线条”原因shapewrite要求X和Y必须是严格列向量且顶点顺序必须闭合首尾相同。若vertices(p)返回的坐标未闭合或转置错误Shapefile解析器会丢弃整个多边形。解决导出前显式闭合并验证[xv, yv] vertices(p); if ~isequal(xv(1), xv(end)) || ~isequal(yv(1), yv(end)) xv [xv; xv(1)]; yv [yv; yv(1)]; % 强制闭合 end S.Geometry{end1} struct(X, xv, Y, yv); % 确保是列向量5.3 现象裁剪后某些区域面积为0或极小如1e-15原因数值精度误差导致intersect返回退化多边形如三点共线area()计算为0。解决添加面积阈值过滤避免下游误用area_val area(p); if area_val 1e-6 % 根据你的坐标单位设阈值米制用1e-6经纬度用1e-12 continue; % 跳过该区域 end5.4 现象GeoJSON在Leaflet中显示错位所有多边形挤在赤道附近原因坐标顺序写反。GeoJSON规范是[longitude, latitude]即[x,y]但Leaflet示例常写[lat,lng]导致你按示例改了顺序却违反标准。解决严格遵循RFC 7946导出时保持[x,y]并在Leaflet中用L.geoJSON(data, {crs: L.CRS.EPSG4326})显式声明坐标系。5.5 现象polyshape创建失败报错“Vertices must be finite”原因voronoin返回的V中含Inf或NaN但你在过滤C时只检查了索引没检查V本身是否含非法值。解决双重过滤在构建valid_regions前清洗V% 清洗V移除含Inf/NaN的行 valid_V_idx all(isfinite(V), 2); V_clean V(valid_V_idx, :); % 重构C将原C{i}中指向非法行的索引替换为[]再过滤空C{i} C_clean cell(size(C)); for i 1:length(C) if ~isempty(C{i}) idx C{i}; idx idx(idx size(V_clean,1)); % 只保留合法索引 if ~isempty(idx) C_clean{i} idx; end end end6. 进阶技巧批量处理动态更新——让Voronoi图真正活起来生成静态图只是起点。真实项目中点位会增删如新增传感器、坐标会漂移如GPS定位误差、研究区会调整如农田扩建。与其每次手动跑脚本不如构建一个可复用、可维护、可嵌入工作流的Voronoi生成模块。我给自己实验室写的voronoi_pipeline.m就基于以下三个技巧已稳定运行两年支撑了17个不同尺度的空间分析任务。6.1 把流程封装成函数支持配置驱动不写死路径和参数用结构体传入配置让同一份代码适配不同项目function generate_voronoi(config) % config 是结构体含以下字段 % config.points_path - CSV路径含x,y列 % config.boundary_path - Shapefile或CSV边界路径 % config.output_dir - 输出目录 % config.area_threshold - 面积过滤阈值 % config.export_formats - {csv,geojson,shp} % 1. 读取点数据 points readmatrix(config.points_path); % 2. 读取边界支持CSV和SHP if endsWith(config.boundary_path, .shp) shp shaperead(config.boundary_path); study_area polyshape(shp.X, shp.Y); else bnd readmatrix(config.boundary_path); study_area polyshape(bnd(:,1), bnd(:,2)); end % 3. 执行核心流程调用前面章节的代码 [V, C] voronoin(points); % ...中间步骤省略同前文... % 4. 按配置导出 if ismember(csv, config.export_formats) writematrix(...); % 同前 end if ismember(geojson, config.export_formats) jsonencode(...); % 同前 end % 其他格式... end % 调用示例 cfg struct(points_path, sensor_points.csv, ... boundary_path, farm_boundary.shp, ... output_dir, results/, ... area_threshold, 1e-6, ... export_formats, {csv,geojson}); generate_voronoi(cfg);6.2 用timer实现动态更新当点位实时变化时如果点坐标来自串口、MQTT或数据库轮询可设置定时器每30秒刷新一次t timer(ExecutionMode, fixedRate, Period, 30, ... TimerFcn, (~,~) update_voronoi_from_db()); start(t); function update_voronoi_from_db() % 从数据库读最新点位 conn database(mydb,,); points fetch(conn, SELECT x,y FROM sensors); close(conn); % 重新生成并覆盖输出 generate_voronoi(struct(points_path, temp_points.csv, ...)); % 将points写入temp_points.csv后调用 end6.3 用parfor加速大批量点集处理当你要为100个不同时间步长、或100个不同参数组合生成Voronoi图时parfor能显著提速% 假设param_list是100个配置结构体的元胞数组 parpool; % 启动并行池 parfor i 1:length(param_list) generate_voronoi(param_list{i}); % 每个worker独立运行 end delete(gcp(nocreate)); % 清理我的血泪教训是别在parfor里用shapewrite——它内部有文件锁会导致竞争失败。解决方案是parfor只生成polyshape对象并保存为.mat最后用单线程统一导出。另外voronoin本身不支持GPU别白费力气加gpuArray。现在回头看当年为一个农业物联网项目写的第一版Voronoi脚本只有37行连裁剪都没有。后来加裁剪、加导出、加异常处理、加配置化……最终变成423行的voronoi_pipeline.m。它不再是个“图”而是一个空间分析的基础设施。如果你也在做类似的事希望这篇笔记帮你少走半年弯路。希望帮到你。本文还有配套的精品资源点击获取