智慧农业农业智能诊断、植物保护 葡萄叶片病害分割数据集 基于 PyTorch + Torchvision 的 DeepLabV3+ 训练葡萄叶片分割数据集
智慧农业农业智能诊断、植物保护 葡萄叶片病害分割数据集
🍇 葡萄叶片病害分割数据集概览表
| 项目指标 | 详细数据/描述 |
|---|---|
| 数据集名称 | 葡萄叶片病害分割数据集 (Grape Leaf Disease Segmentation Dataset) |
| 图像总数 | 15,034张 (含数据增强) |
| 图像分辨率 | 1200 × 1600 像素 |
| 标注格式 | 支持YOLO和VOC(XML) 两种格式 |
| 任务类型 | 实例分割 / 语义分割 |
—
📊 数据集划分统计
| 数据集子集 | 图像数量 (Images) | 占比 (约) | 用途 |
|---|---|---|---|
| 训练集 (Train) | 12,121 | 80.6% | 模型训练与参数学习 |
| 验证集 (Val) | 1,455 | 9.7% | 超参数调整与模型选择 |
| 测试集 (Test) | 1,458 | 9.7% | 最终模型性能评估 |
| 总计 | 15,034 | 100% | - |
🦠 病害类别与标注详情
| 类别 ID | 英文名称 | 中文名称 | 图像数量(含该类的图片数) | 标注数量(实例/掩膜总数) | 平均每图标注数 | 病害特征描述 |
|---|---|---|---|---|---|---|
| 0 | Healthy | 健康叶片 | 7,746 | 54,070 | ~6.98 | 无病斑,叶片状态良好 (通常指健康区域分割) |
| 1 | Birds_Eye_Rot | 鸟眼斑腐烂 | 6,150 | 16,084 | ~2.62 | 圆形斑点,中心灰白边缘深褐,形似鸟眼 |
| 2 | Powdery_Mildew | 白粉病 | 7,947 | 24,593 | ~3.09 | 叶片表面覆盖白色粉状霉层,影响光合作用 |
| - | 总计/平均 | - | 15,034* | 94,747 | ~6.30 | - |
注:图像数量总和超过15,034是因为单张图片可能同时包含多种病害或同时包含健康与病斑区域(多标签/多实例情况)。
在这里插入图片描述
💡 数据集特点分析
- 高分辨率:
- 所有图像均为1200×1600高清分辨率,有利于捕捉细微的病斑纹理(如白粉病的粉末状细节、鸟眼斑的边缘特征)。
- 类别平衡性:
- 健康样本最多,作为背景参照。
- 白粉病图像覆盖面最广 (7,947张),标注实例丰富。
- 鸟眼斑虽然图像数略少,但标注清晰,适合训练小目标检测能力。
- 标注密度:
- 总标注数高达94,747个,平均每张图约有6.3个标注对象,数据密度大,利于模型学习复杂的叶片遮挡和多病斑并发场景。
- 格式兼容性:
- 同时提供YOLO(txt) 和VOC(xml) 格式,可无缝对接 YOLOv8/v9/v10-seg、Mask R-CNN、DeepLabV3+ 等主流分割网络。
训练需要特别注意以下几点:
- 数据格式转换:DeepLabV3+ 通常使用 PNG 掩膜图(Mask)作为标签,而您提供的是 YOLO (txt) 或 VOC (xml) 格式。需要编写脚本将其转换为单通道的索引色 PNG 图片。
- 显存优化:由于原图分辨率高达1200x1600,直接训练会爆显存。必须使用Crop(裁剪)或Resize(缩放)策略。代码中将默认采用随机裁剪。
- 类别映射:需要将您的3个类别(健康、鸟眼斑、白粉病)映射为 0, 1, 2。
数据预处理脚本和基于 PyTorch + Torchvision 的 DeepLabV3+ 训练代码。
📂 第一步:项目目录结构准备
建议将数据处理后的文件整理为以下结构(Cityscapes/Pascal VOC 风格):
Grape_Dataset/ ├── images/ # 存放原始 jpg 图片 (train/val/test 子文件夹) ├── masks/ # 存放转换后的 png 掩膜 (train/val/test 子文件夹) ├── splits/ # 存放训练/验证列表 txt 文件 └── train_deeplab.py # 训练脚本🛠️ 第二步:数据预处理脚本 (VOC/YOLO -> Mask PNG)
DeepLabV3+ 需要标签是单通道图片,像素值代表类别ID(0=背景/健康, 1=鸟眼斑, 2=白粉病)。
注意:如果您的“Healthy”是指整张健康的叶子作为背景,通常在分割任务中,我们只标注病害区域(1和2),健康区域视为背景(0)。如果您的数据中“Healthy”也是被标注出来的多边形,请根据实际需求调整下面的逻辑。以下代码假设:0=背景(含健康叶肉), 1=鸟眼斑, 2=白粉病。
importosimportxml.etree.ElementTreeasETimportcv2importnumpyasnpfromPILimportImage# 配置路径DATASET_ROOT='Grape_Dataset_Raw'# 原始数据根目录OUTPUT_ROOT='Grape_Dataset'# 输出目录IMG_DIR=os.path.join(OUTPUT_ROOT,'images')MASK_DIR=os.path.join(OUTPUT_ROOT,'masks')# 类别映射 (根据实际标注调整)# 假设 VOC XML 中的 name 字段如下,若为 YOLO 需解析 txt 坐标CLASS_MAP={'Birds_Eye_Rot':1,'Powdery_Mildew':2,# Healthy 通常不作为前景标注,或者如果标注了,设为 0 或其他,这里假设只分割病害}defcreate_dirs():forsplitin['train','val','test']:os.makedirs(os.path.join(IMG_DIR,split),exist_ok=True)os.makedirs(os.path.join(MASK_DIR,split),exist_ok=True)defvoc_xml_to_mask(xml_path,img_size):"""将 VOC XML 转换为单通道 Mask"""mask=np.zeros(img_size[:2],dtype=np.uint8)tree=ET.parse(xml_path)root=tree.getroot()forobjinroot.findall('object'):name=obj.find('name').textifnamenotinCLASS_MAP:continuebbox=obj.find('bndbox')xmin=int(float(bbox.find('xmin').text))ymin=int(float(bbox.find('ymin').text))xmax=int(float(bbox.find('xmax').text))ymax=int(float(bbox.find('ymax').text))# 简单处理:将边界框内填充为类别ID# 如果是实例分割(YOLO Segmentation),需要解析多边形点坐标绘制 fillPoly# 此处以 VOC 检测框为例演示,若有分割多边形数据,请使用 cv2.fillPolymask[ymin:ymax,xmin:xmax]=CLASS_MAP[name]returnmaskdefprocess_dataset():create_dirs()# 此处仅为示例逻辑,实际需根据您的原始文件结构遍历# 假设原始数据在 Grape_Dataset_Raw/VOCAnnotations 和 Grape_Dataset_Raw/JPEGImages# 模拟划分 (实际请读取您的 train.txt, val.txt)splits={'train':list(range(12121)),'val':list(range(12121,12121+1455)),'test':list(range(12121+1455,15034))}print("⚠️ 请根据实际文件路径修改此函数的遍历逻辑!")print("核心逻辑:读取原图 -> 读取标注(XML/TXT) -> 生成单通道PNG (0,1,2) -> 保存")if__name__=='__main__':process_dataset()💡 重要提示:如果您拥有的是YOLO 分割格式 (txt containing polygons),需要使用
cv2.fillPoly将多边形点画在 mask 上,而不是画矩形框。
🚀 第三步:DeepLabV3+ 训练代码 (PyTorch)
此代码使用了torchvision内置的 DeepLabV3+ (ResNet101 backbone),并针对高分辨率图像做了随机裁剪处理。
importosimporttorchimporttorch.nnasnnimporttorch.utils.dataasdataimporttorchvisionimporttorchvision.transformsasTimporttorchvision.modelsasmodelsimporttorchvision.models.segmentationasmsfromPILimportImageimportnumpyasnpfromtqdmimporttqdmimportmatplotlib.pyplotasplt# ================= 配置区 =================CONFIG={'data_root':'Grape_Dataset','num_classes':3,# 0:Background/Healthy, 1:Birds_Eye, 2:Powdery_Mildew'batch_size':4,# 根据显存调整,1200x1600裁剪后建议 4-8'epochs':50,'lr':0.001,'img_size':512,# 训练时裁剪大小,解决 1200x1600 显存爆炸问题'device':'cuda'iftorch.cuda.is_available()else'cpu','workers':4,}# ================= 数据集类 =================classGrapeSegmentationDataset(data.Dataset):def__init__(self,root,split,img_size):self.root=root self.split=split self.img_size=img_size self.images=[]self.masks=[]img_dir=os.path.join(root,'images',split)mask_dir=os.path.join(root,'masks',split)# 获取所有图片路径forfnameinsorted(os.listdir(img_dir)):iffname.endswith('.jpg')orfname.endswith('.png'):self.images.append(os.path.join(img_dir,fname))# 假设 mask 文件名与图片一致,只是后缀为 .pngmask_fname=os.path.splitext(fname)[0]+'.png'self.masks.append(os.path.join(mask_dir,mask_fname))print(f"Loaded{len(self.images)}images for{split}")def__getitem__(self,index):# 加载图像img=Image.open(self.images[index]).convert('RGB')mask=Image.open(self.masks[index])# 数据增强与变换# 1. 随机裁剪 (解决高分辨率问题)# 2. 随机水平翻转# 3. 转为 Tensor 并归一化transform_img=T.Compose([T.RandomCrop(self.img_size),T.RandomHorizontalFlip(0.5),T.ToTensor(),T.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225]),])transform_mask=T.Compose([T.RandomCrop(self.img_size),T.RandomHorizontalFlip(0.5),])# 注意:为了保持 img 和 mask 同步增强,需要使用相同的随机种子或自定义变换# 这里简化处理,实际生产建议使用 torchvision.transforms.v2 或自定义类# 简易同步裁剪实现:i,j,h,w=T.RandomCrop.get_params(img,output_size=(self.img_size,self.img_size))img=T.functional.crop(img,i,j,h,w)mask=T.functional.crop(mask,i,j,h,w)ifnp.random.rand()>0.5:img=T.functional.hflip(img)mask=T.functional.hflip(mask)img=T.ToTensor()(img)img=T.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])(img)mask=T.ToTensor()(mask).squeeze(0).long()# Mask 不需要归一化,只需转为 Longreturnimg,maskdef__len__(self):returnlen(self.images)# ================= 模型构建 =================defget_model(num_classes):# 加载预训练的 DeepLabV3+ (ResNet101 backbone)model=ms.deeplabv3_resnet101(pretrained=True,progress=True)# 修改分类器头以适配我们的类别数 (3类)# 原来的辅助分类器也需修改model.classifier[4]=nn.Conv2d(256,num_classes,kernel_size=1)model.aux_classifier[4]=nn.Conv2d(256,num_classes,kernel_size=1)returnmodel# ================= 训练函数 =================deftrain():device=CONFIG['device']print(f"Using device:{device}")# 1. 加载数据train_dataset=GrapeSegmentationDataset(CONFIG['data_root'],'train',CONFIG['img_size'])val_dataset=GrapeSegmentationDataset(CONFIG['data_root'],'val',CONFIG['img_size'])train_loader=data.DataLoader(train_dataset,batch_size=CONFIG['batch_size'],shuffle=True,num_workers=CONFIG['workers'])val_loader=data.DataLoader(val_dataset,batch_size=CONFIG['batch_size'],shuffle=False,num_workers=CONFIG['workers'])# 2. 初始化模型model=get_model(CONFIG['num_classes']).to(device)# 3. 损失函数 (忽略背景0?如果不忽略则不要 ignore_index)# 通常分割任务中,如果背景也是类别之一,则不忽略。如果只想训练病害,可设置 ignore_index=0criterion=nn.CrossEntropyLoss(ignore_index=-1)# 这里假设所有像素都有标签,不设忽略# 4. 优化器optimizer=torch.optim.SGD(model.parameters(),lr=CONFIG['lr'],momentum=0.9,weight_decay=1e-4)scheduler=torch.optim.lr_scheduler.StepLR(optimizer,step_size=10,gamma=0.1)best_miou=0.0# 5. 训练循环forepochinrange(CONFIG['epochs']):model.train()running_loss=0.0pbar=tqdm(train_loader,desc=f"Epoch{epoch+1}/{CONFIG['epochs']}")forimages,masksinpbar:images,masks=images.to(device),masks.to(device)optimizer.zero_grad()outputs=model(images)['out']# DeepLabV3+ 输出字典loss=criterion(outputs,masks)loss.backward()optimizer.step()running_loss+=loss.item()pbar.set_postfix({'loss':f"{loss.item():.4f}"})avg_loss=running_loss/len(train_loader)scheduler.step()# 验证阶段 (简化版,仅计算 Loss)model.eval()val_loss=0.0withtorch.no_grad():forimages,masksinval_loader:images,masks=images.to(device),masks.to(device)outputs=model(images)['out']loss=criterion(outputs,masks)val_loss+=loss.item()print(f"Epoch{epoch+1}: Train Loss={avg_loss:.4f}, Val Loss={val_loss/len(val_loader):.4f}")# 保存最佳模型if(val_loss/len(val_loader))<best_miouorepoch==0:# 这里用 Loss 代替 mIoU 做简单判断best_miou=val_loss/len(val_loader)torch.save(model.state_dict(),f"deeplabv3_grape_epoch{epoch+1}.pth")print(f"✅ Model saved at epoch{epoch+1}")print("🎉 Training Finished!")if__name__=='__main__':train()💡 关键训练技巧与建议
关于高分辨率 (1200x1600):
- 不要直接将图片 Resize 到 512x512 进行训练,这会导致细小的“鸟眼斑”丢失。
- 推荐做法:代码中使用了
RandomCrop(512)。这样既保留了原始高分辨率的细节,又适应了 GPU 显存限制。 - 推理时:可以使用Sliding Window Inference (滑动窗口推理),将大图切块预测后再拼接,或者直接 Resize 后预测(如果精度要求不高)。
类别不平衡:
- 葡萄叶片中,背景(健康部分)通常占据 90% 以上的像素,病害区域很小。
- 解决方案:在
CrossEntropyLoss中添加weight参数。
# 计算各类别频率后,给少数类更高的权重class_weights=torch.tensor([0.1,1.0,1.0]).to(device)# 示例:降低背景权重criterion=nn.CrossEntropyLoss(weight=class_weights)评价指标 (mIoU):
- 上面的代码为了简洁只计算了 Loss。正式训练建议引入
torchmetrics库来计算Mean IoU,这是分割任务最核心的指标。
pipinstalltorchmetrics在代码中加入
MeanIoU(num_classes=3)并在验证集更新。- 上面的代码为了简洁只计算了 Loss。正式训练建议引入
数据增强:
- 除了随机裁剪和翻转,还可以加入
ColorJitter(亮度、对比度变化) 来模拟不同光照下的葡萄园环境,提高模型鲁棒性。
- 除了随机裁剪和翻转,还可以加入
