植物保护专业主要有植物病理学、昆虫学、农药学三个方向,目前在前两个领域中计算机视觉技术已经在病害鉴定、物种识别等方面有了广泛的应用。尽管计算机视觉识别目前仍有较大进步空间,但对于初入该专业的学生和缺乏相关知识的普通民众来说,在遇到自己不认识的动植物时拿出手机扫一扫拍照识别仍是一个方便快捷的方法。有一定经验的学习者也可以借助此技术缩小范围然后再根据专业书籍进行鉴定。
案例分析
以昆虫识别为例,在众多昆虫识别项目中,小程序“晓虫”在爱好者之间有着比较高的知名度,其作者采石工(quarrying)在 GitHub 开放了早期代码和识别模型,让我们得以对其进行深入分析。
代码部分
该项目由 Python 编写,代码主要分为三部分:检测器、识别器和主程序。
检测器
检测器通过一个单独的文件进行调用,首先导入模型,之后再进行预处理,主要流程有:
- 检测输入图像格式和形状是否正确
check_image_dtype_and_shape(image)
- 转化为模型能够使用的图像格式(缩放补齐为固定大小、颜色通道归一化,HWC to CHW,to Tensor……)
image, lb_detail = khandy.letterbox_image(image, self.input_width, self.input_height, 0)
# image channel normalization
image = khandy.normalize_image_channel(image, swap_rb=True)
# image dtype normalization
image = khandy.rescale_image(image, 'auto', np.float32)
# to tensor
image = np.transpose(image, (2,0,1))
image = np.expand_dims(image, axis=0)
- 然后调用模型得到 outputs_list,包含各边界框坐标、置信度和类别概率。最后对其进行后处理(边界框坐标转换、减少重叠、综合置信度和类别概率,根据预设参数进行过滤……)返回 boxes(边界框坐标 [x1, y1, x2, y2])、confs(置信度值 [0-1])和 classes(类别索引)三个数组。
preds = outputs_list[0][0]
preds = preds[preds[:, 4] > conf_thresh]
boxes = khandy.convert_boxes_format(preds[:, :4], 'cxcywh', 'xyxy')
boxes = khandy.unletterbox_2d_points(boxes, lb_detail, False)
confs = np.max(preds[:, 5:] * preds[:, 4:5], axis=-1)
classes = np.argmax(preds[:, 5:] * preds[:, 4:5], axis=-1)
keep = khandy.non_max_suppression(boxes, confs, iou_thresh)
识别器
识别器负责对检测到的昆虫区域进行细粒度分类。其主要流程如下:
首先,加载预训练的识别模型和标签映射文件:
model_path = os.path.join(current_dir, 'models/quarrying_insect_identifier.onnx')
label_map_path = os.path.join(current_dir, 'models/quarrying_insectid_label_map.txt')
标签映射文件包含类别索引、中文名称和拉丁学名的对应关系。预处理阶段包括:
- 图像格式检查
- 尺寸归一化至 224×224 像素
- 颜色通道调整(BGR 转 RGB)
- 像素值归一化(使用 ImageNet 的均值和标准差)
- 张量格式转换(HWC → CHW)
check_image_dtype_and_shape(image)
# image size normalization
image, _ = khandy.letterbox_image(image, 224, 224)
# image channel normalization
image = khandy.normalize_image_channel(image, swap_rb=True)
# image dtype and value range normalization
mean, stddev = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]
image = khandy.normalize_image_value(image, mean, stddev, 'auto')
# to tensor
image = np.transpose(image, (2,0,1))
image = np.expand_dims(image, axis=0)
return image
模型推理后得到各类别的 logits,经过 softmax 转换为概率分布:
def predict(self, image):
inputs = self._preprocess(image)
logits = self.forward(inputs)
probs = khandy.softmax(logits)
return probs
识别结果按概率排序返回前 topk 个,每个结果包含中文名、拉丁学名及置信度:
def identify(self, image, topk=5):
assert isinstance(topk, int)
if topk <= 0 or topk > self.num_classes:
topk = self.num_classes
probs = self.predict(image)
topk_probs, topk_indices = khandy.top_k(probs, topk)
results = []
for ind, prob in zip(topk_indices[0], topk_probs[0]):
one_result = copy.deepcopy(self.label_name_dict[ind])
one_result['probability'] = prob
results.append(one_result)
return results
主程序
主程序整合了检测与识别流程,实现完整的昆虫识别应用:
- 初始化模块:创建检测器和识别器实例
- 图像遍历:递归读取指定目录下的图像文件,按修改时间排序
- 图像预处理:对过大图像进行缩放(长边 ≤1280 像素)
- 检测阶段:调用 InsectDetector 获取昆虫边界框
- 过滤小目标:忽略宽度或高度小于 30 像素的检测框
- 识别阶段:对每个有效区域裁剪后送入 InsectIdentifier
- 结果可视化:绘制边界框并在上方显示识别结果(置信度 ≥0.10 显示中文学名及概率,否则标记为 “Unknown”)
- 交互展示:通过 OpenCV 窗口逐张显示结果,按 ESC 键退出
尽管作者自 2021 年后就不再开放新的模型和代码,但仅从以上代码中就已经能一窥其设计精妙之处:
- 模块化设计:检测与识别分离,便于模型独立优化与替换
- 工程优化:包含图像预处理、结果后处理、可视化等完整环节
- 实用性:支持批量处理、交互式查看,可直接用于野外调查辅助
- 可扩展性:通过替换模型文件即可更新识别能力
这意味着我们可以自己训练新的模型来替换旧有模型,提升识别准确率。可模型是什么,该如何训练?作为“晓虫”小程序最为核心的部分,这一点同样值得我们去研究。
模型训练实践
要训练一个模型需要经历两个步骤:数据搜集整理与模型训练。原作者并没有分享过关于如何训练此识别模型的信息,但他在一个类似的植物识别开源项目中提到了关于模型训练的细节,可以作为参考。
原作者方案
数据搜集与整理阶段
数据的主要来源有:百度图片,必应图片,新浪微博,百度贴吧,新浪博客和一些专业的植物网站等。除了新浪微博,其他都用了爬虫。另外还有一些数据是原作者自己拍摄的。
数据的分类
这些通常在搜集阶段就已通过自动化工具分门别类整理完成。
数据的清洗
分为自动化清洗、半自动化清洗与手动清洗。自动化清洗利用算法剔除掉尺寸过小、宽高比过大/过小、重复的图像以及灰度图。半自动化清洗需要使用预先训练好的植物/非植物识别模型进行筛选。手动清洗则需要人工一一比对,对相关专业知识要求较高。此外作者还提供了数种用于分类集/测试集等的筛选算法。
训练阶段
作者原文:“受算力和显存所限(仅有一块 GTX 1660),骨干网络选用轻量级的网络(如 ResNet18, MobileNetV2_1.0),损失函数为 softmax 交叉熵。优化器为 SGD,使用了 L2 正则化,标签平滑正则化,余弦退火学习率衰减策略和学习率预热。这个方案比较保守,待笔者有了更多的算力,会尝试一些新的方案,如细粒度图像检索(FGIR),度量学习,自监督学习,模型蒸馏等。当前的模型直接输出各类的置信度,也可以将模型改造成特征提取器,用自己的植物图像来构造底库,这样可以用图像检索的方式来进行植物识别,可扩展性更高。”
好吧,这段基本上看不懂。不过没关系,在 AI 如此发达的现在,合理的运用 AI 工具可以帮助我们更加方便地学习新知识。
训练方案
在询问 AI 后,我初步理解了训练模型的步骤并决定使用以下方案:
数据搜集与整理阶段
在现在的网络环境中使用爬虫搜集已经不再是一个方便的选择,一是 AI 兴起后,各网站对爬虫的限制越来越严格,生态也日渐封闭;二是当前的互联网上已经充斥着各种各样真假难辨的 AI 图像,对相关词条污染极大;再者出于先快速跑通整个流程的考虑,最终决定使用已经整理好的训练集。
数据来源
大部分下载自 Kaggle,手动整理出 10 份粗略的分类(Ant,Bee,Beetle,Butterfly,Dragonfly,Fly,Grasshopper,Ladybug,Mosquito,Spider,Wasp),按 6:2:2 的比例分为训练集、验证集和测试集。
数据清洗
尽管训练集整理状况要远好于搜集来的图像,但考虑到本次训练样本较小,轻微的污染也可能对模型准确度造成较大影响,还是需要进行一次数据清洗。自动化清洗初步筛选掉灰度图后发现部分分类中图片重复度较高,于是再对剩余图片再进行一次哈希图像感知去重。这次去重为半自动化,先自动筛掉完全一样的图像,再手动筛选相似度较高的图像(这些图像往往是原图的简单变形版本,尽管在数据集中保留这些有助于提高模型泛化能力,但考虑到本次训练集较少,再加之训练前一般会对数据进行简单增强,某些分类重复过多可能反而会影响其能力)。最终清洗完成后数据集各分类图像数量都在百张以上。
模型训练
结合 AI 给出的示例综合考虑后决定采用 ResNet50 作为骨干网络,交叉熵损失函数,优化器为 Adam。基本步骤为“加载预训练模型 → 冻结底层 → 替换分类头 → 训练 → 验证 → 测试”,这是一种典型的迁移微调训练步骤,事实上要从头训练一个模型完成此类任务并不现实,使用迁移训练已经是最好的办法。其中最关键的点就是冻结底层和替换分类头,作为一个深度卷积神经网络,其拥有 50 个层,冻结底层是为了让分类头前的特征提取层不受影响,只针对新替换的分类头(即全连接层)进行训练。原始的 ResNet50 全连接层输出维度与目前的分类任务(10 类)不匹配,因此替换新的分类头是有必要的。
部分代码示例与训练结果
# 加载预训练 ResNet50
model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)
# 冻结所有特征提取层的参数
for param in model.parameters():
param.requires_grad = False
# 替换最后的全连接层
num_ftrs = model.fc.in_features
num_classes = len(class_names)
model.fc = torch.nn.Linear(num_ftrs, num_classes)
# 定义损失函数和优化器(只优化新添加的 fc 层)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.fc.parameters(), lr=0.001) # 学习率 0.001
# 训练轮数
num_epochs = 10
for epoch in range(num_epochs):
# ---------- 训练阶段 ----------
model.train() # 设置为训练模式
running_loss = 0.0
# 使用 tqdm 包装 train_loader 显示进度条
for inputs, labels in tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs} [Train]'):
inputs = inputs.to(device)
labels = labels.to(device)
# 清零梯度
optimizer.zero_grad()
# 前向传播 + 计算损失
outputs = model(inputs)
loss = criterion(outputs, labels)
# 反向传播 + 优化
loss.backward()
optimizer.step()
# 统计损失
running_loss += loss.item() * inputs.size(0)
epoch_train_loss = running_loss / len(train_dataset)
# ---------- 验证阶段 ----------
model.eval() # 设置为评估模式
val_running_loss = 0.0
correct = 0
total = 0
with torch.no_grad(): # 禁用梯度计算,节省内存和计算
for inputs, labels in val_loader:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
val_running_loss += loss.item() * inputs.size(0)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
epoch_val_loss = val_running_loss / len(val_dataset)
epoch_val_acc = correct / total
# 记录历史
train_loss_history.append(epoch_train_loss)
val_loss_history.append(epoch_val_loss)
val_acc_history.append(epoch_val_acc)
# 打印本轮结果
print(f'Epoch {epoch+1}/{num_epochs} | '
f'Train Loss: {epoch_train_loss:.4f} | '
f'Val Loss: {epoch_val_loss:.4f} | '
f'Val Acc: {epoch_val_acc:.4f}')
# 保存验证集上最好的模型
if epoch_val_acc >= max(val_acc_history, default=0):
best_model_state = model.state_dict()
torch.save(best_model_state, 'insect_resnet50_weights.pth')
print(f'** 保存最佳模型 (准确率: {epoch_val_acc:.4f}) **')
# 训练结束,加载最佳模型状态
model.load_state_dict(best_model_state)
print('训练完成')
经过近一个小时的训练,得到的模型在大部分类别中准确率均在 0.9 以上,但部分类别对涉及复杂背景的昆虫识别效果较差,可能需要对数据集做出改进。
总结
计算机视觉技术在植物保护领域的应用,尤其是在植物病害识别与昆虫物种鉴定方面,展现出广阔的应用前景与实践价值。本文通过系统梳理其技术框架与实践路径,并结合“晓虫”这一典型开源项目进行案例分析,揭示了从理论到落地实现的关键环节。
从应用价值来看,计算机视觉不仅为专业植物保护工作者提供了高效、可扩展的辅助工具,也为普通民众和初学者降低了物种识别的门槛,推动了科学知识的普及与实践能力的提升。在技术实现上,“晓虫”项目体现了模块化、工程化与实用性的良好结合,其检测–识别分离的架构、完整的前后处理流程以及友好的交互设计,为同类应用提供了可借鉴的范本。
在模型训练方面,本文通过对比原作者方案与自主实践,说明了数据搜集、清洗与模型微调的具体步骤与方法。实践表明,基于迁移学习的训练策略能够在小规模数据集上取得较好的识别效果,同时也揭示出数据质量、类别平衡与背景干扰等因素对模型性能的影响,为后续优化指明了方向。
尽管当前技术仍存在一定的局限性——如对复杂环境下的目标识别效果不佳、模型泛化能力有限等,但随着数据集的不断丰富、算法模型的持续演进以及计算资源的日益普及,计算机视觉在植物保护中的应用将更加深入与广泛。未来,可进一步探索细粒度识别、多模态融合、轻量化部署等方向,并结合实际应用场景持续迭代,以技术赋能植保工作,助力生态环境保护与农业可持续发展。
总的来说,计算机视觉为植物保护注入了新的技术活力,其跨学科融合的特性也为我们提供了一个典型的技术应用范例——唯有将扎实的专业知识、清晰的工程思维与不断发展的 AI 技术相结合,才能真正推动科研落地、服务社会需求。