简介:面向图像分割任务的研究者与学习者,提供一份马铃薯叶片病害分割数据集,覆盖健康、早期枯萎病、枯萎病晚期三种叶片状态,并包含对应的彩色掩膜标签。压缩包共两千个文件,其中图片文件一千九百九十九张,另含一个可视化脚本,整体大小约五十八点二八兆字节。数据集图像分辨率统一为二百五十六乘二百五十六,背景简洁、前景区域丰富且标注良好,适合直接用于分割模型训练与算法验证;掩膜标签采用三通道彩色格式,背景以零标记,目标区域清晰可辨。附带脚本可随机抽取样本,直观展示原始图像、真实标签以及标签叠加效果,便于检查标注质量与快速启动实验。目前已有二百一十五人学习下载,可作为图像分割入门与进阶实践的基准数据。
1. 图像分割遇上马铃薯:一份能直接落地的叶片病害分割数据集
做病害识别时最容易被数据坑到的点,不是模型选型,而是标签的“颗粒度”。目标检测给的是矩形框,可马铃薯早晚疫病的病斑偏偏是不规则块状,边缘还带晕染,框里框外全是背景噪声。这就是为什么现在不少农业视觉项目开始转向图像分割——逐像素判断“病”与“不病”,面积、扩散趋势、病斑占比全部能量化。这份 potato 叶片病害分割数据集,给的正是这种精细到像素的标签,同时附带类别标签文件和一份可视化代码,拿到手不用写转换脚本就能先看清楚标签长什么样。适合正在做植物病害识别、农业检测项目、以及想用分割任务练手但一直缺一套干净农业数据的从业者。它能解决的核心问题就一个:让你把精力从“洗标签”挪到“跑模型”上。
2. 数据集的目录与标签结构:先搞清掩码是怎么存的
2.1 目录层级与文件命名规律
我拿到这份资源的第一步,不是急着跑代码,而是先把整个目录树列出来。分割任务的数据集,最怕的就是掩码与原因对应不上,命名不规律、子目录混乱、掩码格式不统一,这些都是后面所有代码要踩的坑。解压之后第一眼先确认三样东西:原图文件夹、掩码文件夹、类别说明文件是否齐整。
常见做法是下面这套布局,我一般拿到手先拿 tree 命令过一遍:
potato_leaf_seg/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── masks/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt ├── label_info.csv └── visualize.py逻辑说明:images与masks按 train / val / test 对齐,这是分割任务最标准的组织方式。classes.txt写的是全部类别名,label_info.csv一般是每张图的文件名、类别、病斑占比等信息,visualize.py就是配套的可视化脚本。
参数说明:命名规则的常见写法是Potato_Early_Blight_001.jpg这种“作物+病害+序号”的组合。如果你拿到的版本命名不规律,我的建议是先别急着训练,写一段脚本批量重命名,把序号补成四位数,否则后面做数据增强或拼接时文件名排序会乱。
2.2 掩码的编码方式与类别标签含义
分割数据集里掩码常见的存法有三种:PNG 单通道索引图、JPEG 压缩后的二值图、以及 Polygon JSON 多边形坐标。这份 potato 叶片病害数据集用的是 PNG 掩码,属于第一种。单通道 PNG 的好处是每个像素存的是类别索引,不是 RGB 值,画出来的效果是黑色背景上不同的灰度区块。
# 查看掩码图的模式与像素分布(bash 环境) python -c " from PIL import Image import numpy as np mask = Image.open('masks/train/Potato_Early_Blight_001.png') print('mode:', mask.mode) arr = np.array(mask) print('unique values:', np.unique(arr)) print('mask size:', arr.shape) "逻辑说明:这段代码是用来确认掩码的读法。mode: L表示单通道灰度图,unique values应该是一个连续的整数列表,比如[0, 1, 2],分别对应背景、健康叶片、病斑。如果读到的是[0, 255],说明这份掩码是黑白二值图,病斑部分全白。
参数说明:类别索引的排布顺序要看classes.txt。一般0固定是背景,1往后才是实际类别。这份资源里的典型类别是健康叶片、早疫病斑、晚疫病斑三分类。掩码分辨率一般与原图一致,但有的数据集为了省空间会缩到一半,这时候就要在训练 pipeline 里做 resize 对齐。
2.3 标签文件里的统计维度
label_info.csv是容易被忽略但极其有用的文件。里面通常记录了每张图的病斑像素占比、病斑数量分布、图像尺寸这些元信息。我拿到后会先做一次聚合统计,搞清楚数据集的类别平衡和难度分布,这直接决定了训练时要不要做类别加权。
import pandas as pd df = pd.read_csv('label_info.csv') # 按类别统计样本数 print(df['class'].value_counts()) # 看看有没有单张图同时包含多类病斑的情况 multi = df.groupby('image_id').size() print('多标签样本数:', (multi > 1).sum())逻辑说明:第一段 value_counts 让你摸清数据集是否平衡——如果早疫病占了 80%,晚疫病只有 5%,训练出来的模型对晚疫病的 recall 大概率很难看。第二段是检查多标签情况,一张叶片同时感染早疫和晚疫在农业生产中很常见,如果你的数据集里有这类样本,说明掩码里可能出现多个类别非零的区域,这时候用多分类 softmax 输出就不合适,改成 multi-label 或者按病斑区域分别处理更靠谱。
参数说明:如果你的数据分布严重不平衡,我一般会在 loss 里加类别权重,典型配比是class_weight = [1.0, 0.8, 2.5],具体数值根据验证集上的 pixel-IoU 反过来调。
3. 可视化代码:跑通脚本,肉眼确认掩码与图像的对齐程度
3.1 可视化脚本核心逻辑与运行方式
很多人拿到数据集后犯的第一个错误是直接开训,不看掩码是否与叶片边缘对齐。标注工具在导出时偶尔会出坐标偏移,掩码和原图错位半个叶片,这种问题训练阶段损失永远降不到理想值。所以配套的可视化代码不是给你画两张图晒朋友圈的,而是用来做质量抽检。
import cv2 import numpy as np from PIL import Image import matplotlib.pyplot as plt IMG_DIR = 'images/val/' MASK_DIR = 'masks/val/' COLOR_MAP = { 0: [0, 0, 0], # 背景 1: [0, 255, 0], # 健康叶片 2: [0, 0, 255], # 早疫病斑 3: [255, 0, 0], # 晚疫病斑 } def visualize(img_name, save=False): img = cv2.imread(IMG_DIR + img_name) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = np.array(Image.open(MASK_DIR + img_name.replace('.jpg', '.png'))) overlay = np.zeros_like(img) for cls_idx, color in COLOR_MAP.items(): overlay[mask == cls_idx] = color blended = cv2.addWeighted(img, 0.6, overlay, 0.4, 0) plt.figure(figsize=(12, 4)) plt.subplot(1, 3, 1); plt.imshow(img); plt.title('Original') plt.subplot(1, 3, 2); plt.imshow(mask, cmap='gray'); plt.title('Mask') plt.subplot(1, 3, 3); plt.imshow(blended); plt.title('Overlay') plt.show() if save: cv2.imwrite('visual_check/' + img_name, cv2.cvtColor(blended, cv2.COLOR_RGB2BGR)) # 抽检 val 集前 5 张 import os val_imgs = sorted(os.listdir(IMG_DIR))[:5] for name in val_imgs: visualize(name, save=True)逻辑说明:COLOR_MAP里的颜色分配是随意的,但有一个原则——病斑类用红/蓝这类醒目颜色,健康叶片用绿色,这样叠加图上一眼就能看出病斑边界是否贴合叶片边缘。cv2.addWeighted把原图和伪彩色掩码做了融合,透明白度 0.4 是经验值,太高会盖住叶片纹理,太低病斑不够显眼。
参数说明:img_name.replace('.jpg', '.png')这行是掩码文件命名和原图不同后缀时的常规对齐做法。如果你的资源里掩码是_mask后缀,把 replace 改成替换文件名即可。运行前确保visual_check/目录存在,否则cv2.imwrite会静默失败。
3.2 用可视化结果反推标注质量
脚本跑完之后,需要盯着输出图做三个维度的检查。第一个看轮廓贴合度,病斑掩码的边缘应该落在病斑色变的边界上,而不是超出到健康组织里。第二个看类别混淆,早疫病和晚疫病的病斑在视觉上有差异——早疫病斑有同心轮纹,晚疫病斑边缘水渍状,如果掩码把两种病斑混在一个区域里,说明标注员的判定标准有问题。第三个看小目标漏标,有些早期病斑可能只有几个像素大小,掩码上如果完全消失,模型会学成“忽略小病斑”。
提示:强烈建议跑完可视化后,按“掩码面积从大到小”排序抽检 20 张左右的图。大掩码一般标得准,小掩码才是翻车高发区,漏标、擦除不干净都集中在这里。
3.3 可视化代码的扩展用法:把掩码转成 polygons
如果你后续想接 CVAT 做人工修正,或者想把这份数据喂给某些检测框架,PNG 掩码不够灵活,需要转成多边形坐标。这段代码在visualize.py基础上扩展一个函数就行。
import cv2 import numpy as np from PIL import Image def mask_to_polygons(mask_path, min_area=50): mask = np.array(Image.open(mask_path)) polygons = {} for cls_idx in np.unique(mask): if cls_idx == 0: continue binary = (mask == cls_idx).astype(np.uint8) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) polys = [] for contour in contours: area = cv2.contourArea(contour) if area < min_area: continue # 轮廓点压缩到 8 个点以内,防止 polygon 文件过大 epsilon = 0.005 * cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, epsilon, True) polys.append(approx.squeeze().tolist()) polygons[int(cls_idx)] = polys return polygons poly_data = mask_to_polygons('masks/val/Potato_Early_Blight_001.png') print('类别:', list(poly_data.keys()), '每个类别的多边形数:', [len(v) for v in poly_data.values()])逻辑说明:RETR_EXTERNAL只取外轮廓,因为病斑内部有坏死空洞时,内层轮廓反而会干扰训练数据的标注格式。approxPolyDP是轮廓简化,压缩点数能显著减小 polygon 文件的体积,但epsilon参数要控制好——设太大病斑形状会被削成多边形,太小又起不到压缩作用。
参数说明:min_area=50的含义是面积小于 50 像素的轮廓直接丢弃。这个阈值对应的是“训练时也学不会的小病斑”,保留反而会放大噪声。epsilon = 0.005 * arcLength这个系数是经验值,我一般在 0.003 到 0.008 之间调,弧度尖锐的病斑用更小值保形状,平滑大斑用更大值压体积。
4. 避坑排查:马铃薯叶片分割数据集使用中的五个高频陷阱
4.1 掩码和原图尺寸不一致,数据读取时直接崩
现象:训练代码里Image.open加载掩码后,与原图做torch.stack或者np.concatenate时报 shape mismatch。
原因:数据集导出的掩码图部分被图像处理软件自动压缩过分辨率。标注工具在导出时,如果原图是 1920x1080,掩码可能被缩到 960x540 以减小体积,但文件名不变,肉眼看不出来。
解决:在数据加载器里统一加一个 resize 逻辑,而且必须保证原图和掩码使用同一个插值方法。原图用cv2.INTER_LINEAR,掩码用cv2.INTER_NEAREST,因为最近邻插值不会污染类别索引。
def load_pair(img_path, mask_path, target_size=(512, 512)): img = cv2.imread(img_path) img = cv2.resize(img, target_size, interpolation=cv2.INTER_LINEAR) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask = cv2.resize(mask, target_size, interpolation=cv2.INTER_NEAREST) return img, mask4.2 掩码像素值不是从 0 开始
现象:训练时nn.CrossEntropyLoss报错 target 里出现了超出类别数的值,或者可视化时整个图是黑的。
原因:这属于最常见的分割数据坑之一。有些标注工具导出时是按调色板索引写的,掩码像素值可能是 0 和 255,或者背景是 255、前景是 0,方向反了。只要 unique values 不是[0, 1, 2]这种从 0 开始且连续的整数序列,就不能直接喂给模型。
解决:加载掩码后先做一次像素值重映射,把非零且离散的索引值压缩到[0, C]区间。我一般写一个remap_mask函数,在数据加载器里固定调用。
def remap_mask(mask): unique_vals = np.unique(mask) if list(unique_vals) == list(range(len(unique_vals))): return mask mapping = {val: idx for idx, val in enumerate(unique_vals)} remapped = np.zeros_like(mask) for val, idx in mapping.items(): remapped[mask == val] = idx return remapped.astype(np.int64)4.3 类别标签顺序与 classes.txt 不对齐
现象:训练时 loss 正常下降,验证时每个 batch 的像素准确率极高,但可视化预测结果发现病斑区域全标成了背景或相反的病害类型。
原因:数据加载器里读掩码的方式和classes.txt的顺序不一致。比如classes.txt里顺序是 [背景, 健康, 早疫, 晚疫],但掩码里像素值 1 代表早疫。这种不一致在训练阶段不会被发现,因为 loss 照样能算,只是映射关系错了。
解决:训练前强制做一次“标签自检”。从数据集里随机抽 3 张图,打印掩码里每类像素占比,与label_info.csv里的对应记录比对,差异超过 10% 就说明读错图了。
4.4 病斑掩码边缘有 1~2 像素的空洞或白边
现象:可视化叠加图里掩码边缘和叶片原图有明显缝隙,或者掩码边缘有锯齿状的白点。
原因:标注工具导出的掩码经过了形态学平滑或者误差收缩,导致病斑边缘比真实边界小一圈。这类误差在普通分割任务里影响不大,但在需要精确计算病斑面积的农业任务中会让面积统计产生系统性偏差。
解决:在训练数据处理阶段做一次形态学膨胀,把掩码向外扩 1 个像素,抵消标注时的收缩误差。
kernel = np.ones((3, 3), np.uint8) mask_dilated = cv2.dilate(mask, kernel, iterations=1)4.5 训练集里混入了重复或近似重复的图像
现象:训练损失下降很快,但验证集 IoU 始终上不去,甚至出现训练集准确率 > 99%、验证集准确率 < 60% 的极端情况。
原因:数据采集时同一个叶片被拍了好几张,或者相邻帧的画面几乎相同,增强后这些近似重复样本占了训练集的相当比例,模型对它们过拟合,真正分布差异大的样本没学到。
解决:对训练集做一次感知哈希去重。用dhash算法计算每张图的指纹,汉明距离小于 5 的视为重复图,保留一张,其余移到备份目录。我一般会把去重后的数据量变化记录在训练日志里——如果去掉了 15% 以上的数据,说明原始采集环节的“连拍”问题比你预想的严重。
5. 把这份分割数据喂给 YOLOv8-seg:转换脚本与验证闭环
5.1 转成 YOLO 分割格式的注意事项与命令
如果你的目标不是教学演示,而是落地一个能跑的病害分割模型,YOLOv8-seg 是目前性价比最高的选择,因为它同时给出检测框和分割掩码,部署也方便。YOLO-seg 的数据格式要求每个目标有多边形坐标,且坐标归一化到 0~1。这里需要把前面转出的 polygon 再次加工。
import os import cv2 import numpy as np def convert_to_yolo_seg(img_dir, mask_dir, out_dir): os.makedirs(f'{out_dir}/images', exist_ok=True) os.makedirs(f'{out_dir}/labels', exist_ok=True) for mask_name in os.listdir(mask_dir): if not mask_name.endswith('.png'): continue mask = cv2.imread(os.path.join(mask_dir, mask_name), cv2.IMREAD_GRAYSCALE) img = cv2.imread(os.path.join(img_dir, mask_name.replace('.png', '.jpg'))) h, w = mask.shape[:2] lines = [] for cls_idx in np.unique(mask): if cls_idx == 0: continue binary = (mask == cls_idx).astype(np.uint8) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: if len(contour) < 3: continue contour = contour.squeeze().astype(float) contour[:, 0] /= w # x 归一化 contour[:, 1] /= h # y 归一化 points = ' '.join([f'{x:.4f} {y:.4f}' for x, y in contour]) lines.append(f'{int(cls_idx - 1)} {points}') with open(f'{out_dir}/labels/{mask_name.replace(".png", ".txt")}', 'w') as f: f.write('\n'.join(lines)) cv2.imwrite(f'{out_dir}/images/{mask_name.replace(".png", ".jpg")}', img) convert_to_yolo_seg('images/train', 'masks/train', 'yolo_data/train')逻辑说明:cls_idx - 1是 YOLO 格式的类别索引调整,因为 YOLO 的类别是从 0 开始算目标类别,背景不参与训练。这段脚本的坑在于:轮廓点过多时,.txt文件会很臃肿,训练时数据加载也会变慢,所以前面那段approxPolyDP的压缩逻辑在转 YOLO 格式时更得做好。
参数说明:w和h是原图的宽高,归一化时必须是 float 类型,Python 3 里int除法已经是浮点了,但这里显式转一下更安全。归一化坐标保留 4 位小数够用,太多位只会增大文件体积。
5.2 训练配置与类别权重设置
训练配置文件里最重要的两个参数是task=segment和imgsz。叶片分割不像是车辆检测需要大视野,512 或 640 像素就够,过大会让 GPU 显存爆掉而精度提升有限。
yolo segment train \ model=yolov8s-seg.pt \ data=potato.yaml \ imgsz=640 \ batch=8 \ epochs=100 \ class_weights=weights.json逻辑说明:potato.yaml里需要指定train和val的图片路径,以及names列表。class_weights从label_info.csv的统计结果生成,类别占比低的病斑给更大权重。如果类别不平衡没有严重到超过 1:4,class_weights可以不设,因为 YOLOv8 本身有 Focal Loss 兜底。
参数说明:imgsz=640是精度与显存的平衡点。如果 GPU 是 8GB 或更小,建议降为 512 并把batch降到 4。训练时的cos_lr默认开启,这几个参数组合下来一般 100 个 epoch 能在验证集上看到 mAP50-95 收敛到 0.7 左右。
5.3 用 IoU 和病斑面积做最终验证
模型训完不能只盯着 mAP 看。农业病害分割的特殊点在于,最终业务上要用“病斑面积占叶片面积的比例”来决定打药策略,所以这个输出值必须校准过。
from ultralytics import YOLO model = YOLO('runs/segment/train/weights/best.pt') img = cv2.imread('images/test/Potato_Late_Blight_011.jpg') results = model.predict(img, conf=0.25, iou=0.5, verbose=False) mask = results[0].masks.data.cpu().numpy() # (n, H, W) 掩码 area_ratio = mask.sum(axis=(1, 2)) / (mask.shape[1] * mask.shape[2]) print('病斑面积占比:', round(float(area_ratio.sum()), 4))逻辑说明:conf=0.25是置信度阈值,低于这个值的预测会被丢弃,农业场景建议别设太高,因为病斑的视觉特征本身就不强。mask.sum计算每个预测掩码的像素数,除以总像素就是面积占比。这个数值要和你从label_info.csv里查到的标注占比做对比——偏差超过 0.1 说明模型输出的掩码边缘向某一侧偏移了。
参数说明:iou=0.5是 NMS 阈值,对分割任务影响不大,保持默认即可。如果你发现预测的掩码碎成很多小点,通常是conf太低,调到 0.3 以上能滤掉大部分噪声。
自从被掩码尺寸不一致坑过一次后,我每拿到一套分割数据,不管是从哪个渠道下的,都会强制走一遍“列目录 → 查 unique values → 可视化抽检 → 转格式”这个闭环。这套流程跑下来半小时以内,但能省掉后面训练时至少两天的调试时间。希望这篇文章里写的这些细节,能让你拿到 potato 叶片病害数据集后少走几步弯路。
本文还有配套的精品资源,点击获取