☰
分水岭算法实现果树图像分割与无人机路径自动生成
2026/10/10 17:07:06 网站建设 项目流程

简介:这份资源面向计算机视觉与无人机应用方向的学习者,提供一套基于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_distance=15, footprint_size=3): """ 果树图像分割: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, iterations=2) binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2) # 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_distance=min_distance, labels=binary) markers = np.zeros(dist.shape, dtype=np.int32) for i, (y, x) in enumerate(coords, start=1): 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, dtype=np.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, gsd=0.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_info

gsd是地面分辨率,正射图一般有,没有就用已知距离标定。航高公式里的 1.8 是经验系数,喷幅窄的机型可以降到 1.5,但别低于 1.2,否则漏喷。这个表直接决定后面航点的 Z 值。

3.2 路径生成:覆盖式还是避障式

路径生成有两种常见做法。覆盖式是把所有质心按最近邻或扫描线排序,飞机依次飞过每个点,适合喷药。避障式是在质心之间插入障碍物检测,适合果园里有电线杆或棚架的场景。我一般先用扫描线排序,因为果树种植通常成行,扫描线路径短、转弯少。

def generate_path_scanline(tree_info, row_threshold=50): """ 扫描线路径生成:按行分组,行内按x排序,行间交替方向 row_threshold: 行分组阈值,单位像素 """ # 按y坐标分组 tree_info_sorted = sorted(tree_info, key=lambda 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, key=lambda t: t['cx'], reverse=(i % 2 == 1)) path.extend(row_sorted) return path

row_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, gsd=0.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 倍。如果还碎,在距离变换前加一次高斯模糊,sigma=2到3,把局部起伏抹平。

4.2 相邻树冠合并,漏喷中间区域

现象:两棵挨得近的树被分成一个区域,质心落在两树中间,喷头覆盖不到两侧。原因:min_distance太大,或者 ExG 二值化后两棵树连成一片,距离变换只有一个峰。解决:降低min_distance,同时在二值化后加一次腐蚀,iterations=1,把连接处断开。如果还不行,改用基于标记的分水岭,手动在每棵树中心点一个种子,但这就失去自动化意义了,只适合小面积校正。

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_rate=1.0, ref_diameter=200): """ 按冠幅添加变率喷施流量 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凭感觉设,结果一片果园分割出两倍树冠,路径飞完发现漏了一半。后来改成先算冠幅再定参数,就稳了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询