多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

分水岭算法实现果树图像分割与无人机路径自动生成

分水岭算法实现果树图像分割与无人机路径自动生成 简介这份资源面向计算机视觉与无人机应用方向的学习者提供一套基于MATLAB的完整项目实现先对果树图像做预处理与分水岭算法分割再依据分割结果自动生成无人机飞行路径覆盖从图像处理到路径规划的全流程。压缩包共8个文件约272KB包含6个m脚本与2张jpg示例图脚本分别承担图像分割、路径搜索与整体流程整合等任务图片用于直观展示处理效果结构紧凑、便于按模块阅读与调试。目前已有119人学习下载。读者可借此掌握分水岭算法中预处理、梯度边缘检测、标记点设置与流域合并等关键环节并理解如何将分割出的果树区域作为障碍物结合搜索算法完成航点规划与轨迹平滑。代码注释与示例数据齐全适合作为课程设计、毕业设计或相关课题的参考实现也可在此基础上拓展到其他图像分割与自动化路径规划任务。1. 分水岭算法做果树图像分割从像素到航点这条路能不能走通果园植保无人机飞一趟最怕的不是风大是航线压在树冠上或者漏掉半行。人工在地图上点航点几十亩地要点一下午树冠形状一变还得重来。分水岭算法做果树图像分割再自动生成无人机路径解决的正是这个断层让图像自己告诉飞机该往哪飞。它适合两类人——手里有正射影像或航拍图、想批量出航线的植保从业者以及做农业无人机路径规划、需要一套可复现分割到路径流水线的开发者。核心链路是果树图像分割出树冠掩膜提取质心与冠幅再按覆盖或避障约束生成航点序列。分水岭算法在这里不是万能钥匙它擅长把粘连树冠分开但参数没调好会把一棵树切成三块后面路径就全乱。下面按我实际跑通的顺序拆开讲。2. 分水岭算法分割果树从正射图到树冠掩膜2.1 为什么选分水岭而不是阈值或语义分割阈值法在单株、背景干净的图上够用但果园正射图里树冠互相遮挡、杂草和阴影混在一起阈值一高就丢小树一低就连成一片。语义分割模型精度高但需要标注数据而且换一个果园、换一个季节模型泛化就掉。分水岭算法的优势在于它基于地形学思路把灰度图看成高低起伏的地形树冠中心是山峰树冠边缘是山谷从标记点开始注水水汇合的地方就是边界。这对粘连树冠的拆分特别有效而且不需要训练调参就能适配不同果园。我一般会先用超绿指数ExG把植被增强再转灰度做分水岭。ExG 的公式是 2G - R - B对绿色树冠响应强能压掉土壤和阴影。这一步比直接拿 RGB 转灰度稳得多尤其是午后阴影重的图。2.2 完整分割代码ExG 增强 距离变换 分水岭import cv2 import numpy as np from scipy import ndimage def segment_canopy(image_path, min_distance15, footprint_size3): 果树图像分割ExG增强 - 距离变换 - 分水岭 image_path: 输入正射图路径 min_distance: 距离变换峰值最小间距控制单株最小冠幅 footprint_size: 形态学开运算核大小去噪 img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 1. ExG 植被增强 R img[:,:,0].astype(np.float32) G img[:,:,1].astype(np.float32) B img[:,:,2].astype(np.float32) exg 2*G - R - B exg_norm cv2.normalize(exg, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # 2. 二值化 形态学去噪 _, binary cv2.threshold(exg_norm, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) kernel np.ones((footprint_size, footprint_size), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations2) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations2) # 3. 距离变换找树冠中心 dist cv2.distanceTransform(binary, cv2.DIST_L2, 5) dist_norm cv2.normalize(dist, None, 0, 1.0, cv2.NORM_MINMAX) # 4. 找局部峰值作为标记点 from skimage.feature import peak_local_max coords peak_local_max(dist, min_distancemin_distance, labelsbinary) markers np.zeros(dist.shape, dtypenp.int32) for i, (y, x) in enumerate(coords, start1): markers[y, x] i # 5. 分水岭 markers ndimage.grey_dilation(markers, size(3,3)) img_bgr cv2.cvtColor(img, cv2.COLOR_RGB2BGR) markers cv2.watershed(img_bgr, markers) # 6. 生成树冠掩膜 canopy_mask np.zeros(binary.shape, dtypenp.uint8) canopy_mask[markers 1] 255 return canopy_mask, markers, coords这段代码的逻辑是ExG 把树冠从背景里拉出来Otsu 自动定阈值避免手动调形态学开闭去掉噪点和孔洞距离变换把每棵树变成一个山峰峰值检测找到树冠中心作为分水岭的种子点最后分水岭在种子点之间画边界。min_distance是最关键的参数它决定两棵树中心至少隔多少像素才被当成两棵。设小了一棵大树会被切成多块设大了相邻小树会合并。我一般按图像分辨率来估如果一棵成年果树冠幅在图上约 60 像素min_distance设 20 到 25 比较稳。footprint_size控制去噪强度杂草多的图可以加到 5但别超过 7否则小树冠会被腐蚀掉。2.3 分割结果怎么验证三个必看的指标分割完不能直接拿去生成路径得先验证。我一般看三个东西树冠数量是否和地面实测接近、单株面积分布是否合理、边界有没有明显粘连。数量对不上多半是min_distance或阈值问题面积分布出现双峰说明有些树被合并了边界粘连看掩膜叠加图分水岭线如果穿过树冠内部就是种子点没找对。def validate_segmentation(canopy_mask, markers): 输出分割验证指标 num_trees len(np.unique(markers)) - 2 # 去掉背景和边界 areas [] for label_id in range(2, len(np.unique(markers))): area np.sum(markers label_id) if area 50: # 过滤噪声 areas.append(area) areas np.array(areas) print(f检测株数: {num_trees}) print(f平均冠幅面积: {areas.mean():.1f} 像素) print(f面积标准差: {areas.std():.1f}) print(f面积变异系数: {areas.std()/areas.mean():.2f}) return num_trees, areas变异系数超过 0.5 就要回头查参数正常果园应该在 0.2 到 0.4 之间。这个验证步骤花不了两分钟但能省掉后面路径全错的返工。3. 从树冠掩膜到航点质心提取与路径生成3.1 质心、冠幅和航高怎么算分割出掩膜后每棵树的质心就是它的标签区域重心冠幅用等效圆直径表示。这两个量直接决定航点位置和喷幅覆盖。质心计算用scipy.ndimage.center_of_mass冠幅用面积反推d 2 * sqrt(area / pi)。航高不是固定的冠幅大的树需要飞高一点保证喷幅覆盖但太高又会被风吹散。我一般按冠幅的 1.5 到 2 倍来设航高同时不低于安全高度。from scipy import ndimage import numpy as np def extract_tree_info(markers, gsd0.5): 提取每棵树的质心、冠幅、建议航高 gsd: 地面分辨率单位 cm/pixel tree_info [] for label_id in range(2, markers.max() 1): mask (markers label_id) if mask.sum() 50: continue cy, cx ndimage.center_of_mass(mask) area_px mask.sum() diameter_px 2 * np.sqrt(area_px / np.pi) diameter_cm diameter_px * gsd # 航高按冠幅1.8倍最低3米 flight_height max(3.0, diameter_cm * 1.8 / 100) tree_info.append({ id: label_id, cx: cx, cy: cy, diameter_cm: diameter_cm, flight_height_m: flight_height }) return tree_infogsd是地面分辨率正射图一般有没有就用已知距离标定。航高公式里的 1.8 是经验系数喷幅窄的机型可以降到 1.5但别低于 1.2否则漏喷。这个表直接决定后面航点的 Z 值。3.2 路径生成覆盖式还是避障式路径生成有两种常见做法。覆盖式是把所有质心按最近邻或扫描线排序飞机依次飞过每个点适合喷药。避障式是在质心之间插入障碍物检测适合果园里有电线杆或棚架的场景。我一般先用扫描线排序因为果树种植通常成行扫描线路径短、转弯少。def generate_path_scanline(tree_info, row_threshold50): 扫描线路径生成按行分组行内按x排序行间交替方向 row_threshold: 行分组阈值单位像素 # 按y坐标分组 tree_info_sorted sorted(tree_info, keylambda t: t[cy]) rows [] current_row [tree_info_sorted[0]] for tree in tree_info_sorted[1:]: if abs(tree[cy] - current_row[-1][cy]) row_threshold: current_row.append(tree) else: rows.append(current_row) current_row [tree] rows.append(current_row) # 行内按x排序行间交替 path [] for i, row in enumerate(rows): row_sorted sorted(row, keylambda t: t[cx], reverse(i % 2 1)) path.extend(row_sorted) return pathrow_threshold控制行分组灵敏度种植规整的果园设 30 到 50 像素散种的可以放宽到 80。行间交替方向是为了减少空飞这个细节在几十亩地上能省不少电。3.3 航点转无人机可执行格式路径点算出来后要转成飞控能读的格式。常见的是 MAVLink 的MISSION_ITEM或 KML。我一般先输出 CSV 做检查再转 MAVLink。CSV 里包含序号、纬度、经度、高度、速度、悬停时间。import csv def export_waypoints_csv(path, tree_info, output_csv, origin_lat, origin_lon, gsd0.5): 导出航点CSV像素坐标转经纬度 origin_lat/lon: 图像左上角对应的经纬度 with open(output_csv, w, newline) as f: writer csv.writer(f) writer.writerow([seq, lat, lon, alt_m, speed_ms, hover_s]) for i, tree in enumerate(path): # 像素转经纬度1度约111320米 dx_m tree[cx] * gsd / 100 dy_m tree[cy] * gsd / 100 lat origin_lat - dy_m / 111320 lon origin_lon dx_m / (111320 * np.cos(np.radians(origin_lat))) writer.writerow([i, f{lat:.7f}, f{lon:.7f}, f{tree[flight_height_m]:.1f}, 3.0, 2]) print(f航点已导出: {output_csv}, 共{len(path)}个点)经纬度转换里gsd单位是 cm/pixel转成米要除以 100。纬度方向是减因为图像 y 轴向下。这个转换看着简单但原点搞错整个航线就偏了我一般会先用两个已知点验证。4. 避坑与排查分水岭分割和路径生成里最容易翻车的五件事4.1 树冠被切成碎片路径点暴增现象分割结果里一棵树变成三四个小区域航点数量翻倍。原因min_distance设太小距离变换的峰值检测把一棵树的多个局部高点都当成独立种子。解决先统计单株冠幅像素面积按sqrt(area/pi)估算半径min_distance设为半径的 0.6 到 0.8 倍。如果还碎在距离变换前加一次高斯模糊sigma2到3把局部起伏抹平。4.2 相邻树冠合并漏喷中间区域现象两棵挨得近的树被分成一个区域质心落在两树中间喷头覆盖不到两侧。原因min_distance太大或者 ExG 二值化后两棵树连成一片距离变换只有一个峰。解决降低min_distance同时在二值化后加一次腐蚀iterations1把连接处断开。如果还不行改用基于标记的分水岭手动在每棵树中心点一个种子但这就失去自动化意义了只适合小面积校正。4.3 航高按冠幅算出来忽高忽低飞控频繁变高现象相邻航点高度差超过 2 米飞机爬升下降耗电喷幅也不稳定。原因冠幅检测有噪声小树冠被低估大树冠被高估。解决对flight_height_m做滑动平均窗口 3 到 5 个点或者按行取中位数高度。我一般按行统一高度行内高度差不超过 0.5 米这样飞控稳喷幅也均匀。4.4 经纬度转换后航线整体偏移现象导出的航点在卫星图上整体偏了几米。原因原点经纬度对错了或者gsd单位没统一。解决在图像上找两个已知地物点比如道路交叉口算出实际距离和像素距离的比值反推gsd。原点用图像左上角对应的经纬度不要用中心点。转换完先导 KML 在卫星图上叠一下确认无误再转飞控格式。4.5 分水岭边界线被当成树冠质心偏移现象质心不在树冠中心偏向边界。原因cv2.watershed会把边界标为 -1如果掩膜生成时把 -1 也算进去边界线会拉偏质心。解决生成掩膜时只取markers 1的区域边界线单独处理。验证时把掩膜叠加到原图上看质心应该落在树冠几何中心。5. 进阶技巧用分水岭边界做避障缓冲和变率喷施分水岭算法给出的不只是树冠区域边界线本身也有用。边界线是树冠之间的空隙无人机如果必须穿越行间边界线就是天然的安全通道。我一般会把边界线膨胀 1 到 2 米作为避障缓冲航点如果落在缓冲区内就自动偏移到最近的安全点。这个技巧在果树行距窄、树冠茂密的果园特别实用比单纯按质心飞要安全。另一个进阶用法是变率喷施。每棵树的冠幅不同需要的药量也不同。把冠幅映射到喷头流量大冠幅多喷、小冠幅少喷能省药 15% 到 20%。实现上就是在航点 CSV 里加一列flow_rate飞控支持变量喷施的话直接读。def add_variable_rate(path, base_rate1.0, ref_diameter200): 按冠幅添加变率喷施流量 base_rate: 基准流量 L/min ref_diameter: 参考冠幅 cm对应基准流量 for tree in path: ratio tree[diameter_cm] / ref_diameter tree[flow_rate] round(base_rate * min(max(ratio, 0.5), 1.5), 2) return path流量系数限制在 0.5 到 1.5 倍之间避免极端值导致喷头堵塞或药害。这个表直接写进航点文件飞控端做映射就行。验证整套流程是否可靠我习惯用一个 10 亩左右的小地块做闭环先飞正射跑分割生成路径导 KML 叠图检查再实际飞一次看覆盖。重点看三个数航点数量是否等于树冠数量、航高变化是否平滑、边界缓冲有没有触发偏移。如果这三个都过再放大到整园。这套东西我踩过最深的坑是min_distance凭感觉设结果一片果园分割出两倍树冠路径飞完发现漏了一半。后来改成先算冠幅再定参数就稳了。希望帮到你。本文还有配套的精品资源点击获取
返回列表