Ai创想实验室是专门为中小学AI教育开发的教学平台,包含了值计算、图像分类、音频分类、文本分类、数值回归、图像回归、图像分类+回归、平衡杆、手写数字生成、文本生成等中小学人工智能学习类项目。无需编程基础、无需添加硬件、无需购买算力、无隐私担忧、无需师资培训即可进行教学实践。
本文介绍如何使用AiEduLab.tech中的图像分类项目类型来做病虫害分类——Ai创想实验室是以数据为驱动的,你可以任意选择一个图像分类项目来完成(例如手写数字识别、猫狗分类……)模型构建、数据清洗、模型训练、预测、导出模型并部署(本文模型较大,导出时请耐心等待2-3分钟)。整体流程如下:
1、训练模型达到满意程度
2、导出模型(TF Lite + .H + ONNX)
3、通过人工、高清摄像头、行走机器人等采样
4、模型预测
5、病虫害警告或自动给喷淋加料
本文主要介绍模型训练、导出相关部分。至于在移动端、嵌入硬件、其他框架下使用之后再进行介绍。
一、数据获取和清洗
在百度星河、阿里天池上有很多数据集,在下载和使用时我们应遵守使用协议。本文使用的数据集下载自百度星河,具体地址为农业病虫害_数据集-飞桨AI Studio星河社区,其协议为CC0:

这个数据集有3G多,解压缩之后运行以下脚本将json解析并分类存储在organized_dataset文件夹中:
import json
import os
import shutil
from PIL import Image
def process_dataset(json_path, images_dir, output_dir, dataset_name):
"""
处理单个数据集(训练集或验证集)
Args:
json_path: annotations JSON文件路径
images_dir: 原始图片所在文件夹
output_dir: 输出根目录
dataset_name: 数据集名称(用于显示)
"""
print(f"\n{'='*60}")
print(f"正在处理: {dataset_name}")
print(f"{'='*60}")
# 加载JSON文件
print("正在加载JSON文件...")
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 检查JSON结构
if not isinstance(data, list):
print("错误:期望JSON是列表格式")
return
print(f"检测到JSON列表,长度: {len(data)}")
# 从列表中提取标注信息
# 每个元素格式: {'disease_class': xxx, 'image_id': 'filename.jpg'}
img_to_label = {}
for item in data:
if isinstance(item, dict) and 'image_id' in item and 'disease_class' in item:
filename = item['image_id']
img_to_label[filename] = item['disease_class']
print(f"提取到 {len(img_to_label)} 条标注记录")
if not img_to_label:
print("错误:未能提取到有效的标注信息")
return
# 使用预定义的61分类映射表
class_dict = {
0: '苹果_健康', 1: '苹果_黑星病_一般', 2: '苹果_黑星病_严重',
3: '苹果_灰斑病_一般', 4: '苹果_灰斑病_严重', 5: '苹果_锈病_一般',
6: '苹果_锈病_严重', 7: '樱桃_健康', 8: '樱桃_白粉病_一般',
9: '樱桃_白粉病_严重', 10: '玉米_健康', 11: '玉米_灰斑病_一般',
12: '玉米_灰斑病_严重', 13: '玉米_锈病_一般', 14: '玉米_锈病_严重',
15: '玉米_大斑病_一般', 16: '玉米_大斑病_严重', 17: '葡萄_健康',
18: '葡萄_黑腐病_一般', 19: '葡萄_黑腐病_严重', 20: '葡萄_轮斑病_一般',
21: '葡萄_轮斑病_严重', 22: '葡萄_叶枯病_一般', 23: '葡萄_叶枯病_严重',
24: '柑桔_健康', 25: '柑桔_溃疡病_一般', 26: '柑桔_溃疡病_严重',
27: '桃_健康', 28: '桃_流胶病_一般', 29: '桃_流胶病_严重',
30: '葡萄_健康', 31: '葡萄_黑腐病_一般', 32: '葡萄_黑腐病_严重',
33: '柑桔_健康', 34: '柑桔_溃疡病_一般', 35: '柑桔_溃疡病_严重',
36: '桃_健康', 37: '桃_流胶病_一般', 38: '桃_流胶病_严重',
39: '草莓_健康', 40: '草莓_叶枯病_一般', 41: '草莓_叶枯病_严重',
42: '番茄_健康', 43: '番茄_白粉病_一般', 44: '番茄_白粉病_严重',
45: '番茄_早疫病_一般', 46: '番茄_早疫病_严重', 47: '番茄_晚疫病_一般',
48: '番茄_晚疫病_严重', 49: '辣椒_健康', 50: '辣椒_疮痂病_一般',
51: '辣椒_疮痂病_严重', 52: '马铃薯_健康', 53: '马铃薯_早疫病_一般',
54: '马铃薯_早疫病_严重', 55: '马铃薯_晚疫病_一般', 56: '马铃薯_晚疫病_严重',
57: '番茄_黄化曲叶病_一般', 58: '番茄_黄化曲叶病_严重', 59: '番茄_花叶病_一般',
60: '番茄_花叶病_严重'
}
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
# 统计信息
total_images = len(img_to_label)
success_count = 0
error_count = 0
min_width = float('inf')
min_height = float('inf')
max_width = 0
max_height = 0
error_files = []
print(f"需要处理 {total_images} 张图片")
print("开始按类别整理图片并统计尺寸...\n")
processed = 0
for filename, label_id in img_to_label.items():
processed += 1
# 进度输出
if processed % 1000 == 0 or processed == total_images:
print(f"进度: {processed}/{total_images} ({processed*100/total_images:.1f}%)")
# 源文件路径
src_path = os.path.join(images_dir, filename)
# 检查源文件是否存在
if not os.path.exists(src_path):
error_count += 1
error_files.append(f"文件不存在: {filename}")
continue
# 获取图片尺寸
try:
with Image.open(src_path) as img:
width, height = img.size
if width < min_width:
min_width = width
if height < min_height:
min_height = height
if width > max_width:
max_width = width
if height > max_height:
max_height = height
except Exception as e:
error_files.append(f"读取尺寸失败: {filename}, {e}")
# 获取类别名称
class_name = class_dict.get(label_id, f'未知类别_{label_id}')
# 解析植物和病害(去掉程度)
parts = class_name.split('_')
if len(parts) >= 2:
plant = parts[0]
disease = parts[1] # 只取病害名称,忽略程度
else:
plant = '未知'
disease = '未知'
# 构建目标路径:植物/病害/(去掉程度层)
dest_dir = os.path.join(output_dir, plant, disease)
os.makedirs(dest_dir, exist_ok=True)
dest_path = os.path.join(dest_dir, filename)
# 复制文件
try:
shutil.copy2(src_path, dest_path)
success_count += 1
except Exception as e:
error_count += 1
error_files.append(f"复制失败: {filename}, {e}")
# 输出统计结果
print("\n" + "-"*60)
print(f"{dataset_name} 处理完成!")
print(f"总图片数: {total_images}")
print(f"成功复制: {success_count}")
print(f"失败数量: {error_count}")
print("-"*60)
print("图片尺寸统计:")
if min_width != float('inf'):
print(f" 最小宽度: {min_width} 像素")
print(f" 最小高度: {min_height} 像素")
print(f" 最大宽度: {max_width} 像素")
print(f" 最大高度: {max_height} 像素")
else:
print(" 未能获取任何图片的尺寸信息")
if error_files:
print("-"*60)
print(f"错误详情(前10条):")
for err in error_files[:10]:
print(f" {err}")
if len(error_files) > 10:
print(f" ... 还有 {len(error_files)-10} 条错误")
print(f"图片已整理到: {output_dir}")
def main():
# ===== 配置路径 =====
# 训练集
train_json = "AgriculturalDisease_trainingset/AgriculturalDisease_train_annotations.json"
train_images = "AgriculturalDisease_trainingset/images"
train_output = "organized_dataset/train"
# 验证集
val_json = "AgriculturalDisease_validationset/AgriculturalDisease_val_annotations.json"
val_images = "AgriculturalDisease_validationset/images"
val_output = "organized_dataset/val"
# ===== 检查路径 =====
datasets_to_process = []
# 检查训练集
if os.path.exists(train_json) and os.path.exists(train_images):
datasets_to_process.append((train_json, train_images, train_output, "训练集"))
else:
if not os.path.exists(train_json):
print(f"警告:找不到训练集JSON文件 {train_json}")
if not os.path.exists(train_images):
print(f"警告:找不到训练集图片目录 {train_images}")
# 检查验证集
if os.path.exists(val_json) and os.path.exists(val_images):
datasets_to_process.append((val_json, val_images, val_output, "验证集"))
else:
if not os.path.exists(val_json):
print(f"警告:找不到验证集JSON文件 {val_json}")
if not os.path.exists(val_images):
print(f"警告:找不到验证集图片目录 {val_images}")
if not datasets_to_process:
print("\n错误:没有找到任何可用的数据集!")
print("请确认路径配置是否正确")
return
print(f"\n找到 {len(datasets_to_process)} 个数据集待处理")
# ===== 处理所有数据集 =====
for json_path, images_dir, output_dir, dataset_name in datasets_to_process:
process_dataset(json_path, images_dir, output_dir, dataset_name)
print("\n" + "="*60)
print("所有数据集处理完成!")
print("="*60)
if __name__ == "__main__":
main()

本文仅使用训练集中的番茄部分的数据训练一个能够辨别番茄病虫害的模型。为了得到归一化的图像,使用通用图像处理工具对图像进行缩放和归一化:

注意:不点击“清空”将会在当前包中添加数据。新类别数据应先清空之前的数据。
为了导入数据时能够比较均衡和知道我们训练时用了哪些数据——预测时避开它们,给文件重命名一下(据对不是写第一个脚本时忘了- -!!!):
import os
from pathlib import Path
def rename_png_in_subfolders(root_dir, dry_run=False):
"""
将每个子文件夹内的PNG图片从1开始重新编号
Args:
root_dir: 根目录路径
dry_run: True时只预览不实际重命名
"""
root_path = Path(root_dir)
if not root_path.exists():
print(f"错误:目录不存在 - {root_dir}")
return
# 统计信息
total_folders = 0
total_files = 0
renamed_files = 0
skipped_files = 0
# 遍历所有子文件夹(包括根目录本身)
for folder in sorted([root_path] + [d for d in root_path.iterdir() if d.is_dir()]):
# 获取该文件夹下所有PNG文件(按文件名排序)
png_files = sorted([f for f in folder.glob('*.png') if f.is_file()])
if not png_files:
continue
total_folders += 1
total_files += len(png_files)
print(f"\n📁 处理文件夹: {folder.relative_to(root_path)}")
print(f" 找到 {len(png_files)} 张PNG图片")
# 计算编号位数(自动补零)
digits = len(str(len(png_files)))
if digits < 2:
digits = 2
for idx, file_path in enumerate(png_files, start=1):
# 生成新文件名(如:01.png, 02.png, ...)
new_name = f"{idx:0{digits}d}.png"
new_path = folder / new_name
# 检查是否冲突(如果新文件名已存在且不是当前文件)
if new_path.exists() and new_path != file_path:
print(f" ⚠️ 冲突: {file_path.name} -> {new_name} (目标文件已存在,跳过)")
skipped_files += 1
continue
if not dry_run:
# 执行重命名
try:
file_path.rename(new_path)
print(f" ✅ {file_path.name} -> {new_name}")
renamed_files += 1
except Exception as e:
print(f" ❌ 重命名失败: {file_path.name} -> {new_name}, 错误: {e}")
skipped_files += 1
else:
# 预览模式
print(f" 🔍 {file_path.name} -> {new_name}")
renamed_files += 1
# 输出统计信息
print("\n" + "="*50)
print("📊 统计信息:")
print(f" - 处理的文件夹数量: {total_folders}")
print(f" - 总PNG文件数量: {total_files}")
print(f" - 成功重命名: {renamed_files}")
print(f" - 跳过/失败: {skipped_files}")
if dry_run:
print("\n⚠️ 这是预览模式,未实际执行重命名操作")
print(" 如需执行,请将 dry_run=False")
else:
print(f"\n✅ 重命名完成!共处理 {total_folders} 个文件夹,{renamed_files} 张图片")
print("="*50)
def main():
# ===== 配置 =====
# 要处理的根目录
root_dir = "." # 当前目录,可以修改为你的具体路径
# 是否预览(True=只预览不执行,False=实际执行)
dry_run = False # 改为 True 先预览,确认无误后改为 False 执行
# ===== 执行 =====
print("="*50)
print("🖼️ PNG图片批量重命名工具")
print("="*50)
print(f"📂 目标目录: {os.path.abspath(root_dir)}")
print(f"📋 模式: {'预览模式' if dry_run else '执行模式'}")
print("="*50)
if not dry_run:
confirm = input("\n⚠️ 即将重命名所有PNG文件,是否继续?(y/n): ")
if confirm.lower() != 'y':
print("已取消操作")
return
rename_png_in_subfolders(root_dir, dry_run=dry_run)
if __name__ == "__main__":
main()
此时,我们就可以构建训练数据。对于手写数字来说,32*32单通道足够;对于猫狗分类64*64三通道即可;本文我们做一个稍大的模型:使用128*128三通道:

二、模型构建
只需修改手写数字识别的模型的输入层为none,128,128,3,输出层为6类别即可。其他我们不进行修改。


三、训练
打开全部数据增强、使用默认参数进行训练:

这个模型有点大,但在核显上训练依然没问题,由于模型和数据都比较大,所以训练稍慢:约3分钟一轮。从日志可以看出,模型很快就学会了这些分类。
四、预测
把模型训练到参数表现比较好的时候停止训练,如果不满意可以点击继续训练来再次训练。以下是训练10轮之后的模型的表现:


测试更多图片或使用原数据集中validationset的数据进行测试实际准确率。使用更多一些的数据训练更多轮次会有效提升置信度和准确率。当模型满意之后,在训练器中“导出移动&嵌入&通用模型”(如果训练器中没有模型了,可以导入模型)。
五、部署
通过“导出移动&嵌入&通用模型”会得到3个模型:

其中.h模型和tflite模型都是经过量化的。onnx模型可以用于多种框架,多个国内模型平台都可以使用。包括华为(Ascend)、华为(HarmonyOS、阿里巴巴、百度、国芯科技、龙芯中科等等,它们要么具有原生支持、要么有框架或工具链支持,可以在这些平台上进行使用。.h模型的使用在之前的文章中有详细的全流程讲解,后续会有文章介绍其他模型的部署——会自己写一个Demo。
在AI创想实验室中,我们无需编程基础,不用学习框架,不用配置环境,无需购买费用高昂的显卡,更不用为云端算力付费,使用当前已有的各种硬件:仅有核显的个人、办公、机房电脑,希沃白板等都能达到理想的教学效果。操作简单但AI核心知识样样俱全,无需师资培训就可以进行教学且能取得理想的教学效果。如果加入试点或合作方那么只需要一台局域网服务器(无需显卡、服务器不用供算力)即可一次投入永久使用全部项目和功能,通过后台管理一分钟即可创建一个本地化、校本化的项目实例。
演示版本地址:www.AiEduLab.tech
有任何问题欢迎留言或发送邮件至:hello@AiEduLab.tech
142

被折叠的 条评论
为什么被折叠?



