ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PyTorch桃树叶片病害识别:从单张图片到视频与GUI连续识别实战

PyTorch桃树叶片病害识别:从单张图片到视频与GUI连续识别实战

做桃树叶片病害识别的朋友,应该都有这个痛点:模型在训练集上效果不错,但到了实际使用场景,往往只能拿一张张图片去跑脚本,遇到果园里录好的视频,或者想在现场连续识别一堆叶片样本时,就完全抓瞎了。我这个项目要解决的,就是把这个过程从“单张图片分类”升级到“连续识别”和“视频识别”这一层——底层依然用深度卷积网络模型,外圈套一个GUI识别界面,中间用Python和PyTorch把整个流程串起来。更关键的是,模型部分做成了可替换结构,训练好的ResNet也好、MobileNet也好,想换EfficientNet或者VGG,改动量很小,不需要把整个工程推倒重来。

我用这套方案做过一个桃叶病害识别的演示系统,分别跑通了图像连续识别(一个文件夹里几十张叶片图片按顺序自动识别)和视频识别(本地视频文件、摄像头实时画面都能处理),界面可以直接通过按钮操作,对不熟悉命令行的人来说,这比在黑窗口里敲指令友好得多。下面我把整个项目的设计思路、核心实现、踩过的坑一次讲清楚,内容偏工程实践,尽量不绕弯子。

1. 项目整体设计与模型选型思路

1.1 为什么把任务拆成“单帧分类 + 窗口抽帧”两段

视频本质上就是一帧一帧的连续图片,每秒25到30帧而已。所以我在设计这个项目时,没有把视频识别当做一个全新的任务来处理,而是把它拆成两层:底层是图片分类模型,上层是视频读取和抽帧逻辑。这两层职责完全分离——模型只负责“给一张叶片图,告诉我它属于哪类病害”,而上层逻辑负责“从视频流中按一定的规则取出图片,喂给模型,再把结果叠加回画面”。

这个设计带来的直接好处是:同一套模型、同一套预处理代码,既能处理“单张图片”输入,也能处理“一个文件夹里的图片序列”输入,还能处理“视频文件或摄像头流”输入。区别只在外层循环的写法上,核心预测函数只有一份。这样代码维护成本低,也不会出现“视频识别用的模型和图片识别的模型表现不一致”这种诡异问题。

另一个要提的点是:我一开始考虑过用目标检测模型(比如YOLO系列)来做病害识别,但后来放弃了。原因是桃树叶片病害识别这个场景里,叶片通常已经是画面主体,拍摄时基本会填满整个取景框,我们需要判断的是“这片叶子得了什么病”,而不是“画面里哪个位置有病斑”。这种任务用图像分类就足够,而且分类模型的训练数据标注成本比检测模型低很多——每张图只要一个文件夹级别的标签,不需要画框。检测模型不是不能用,但在这个项目里属于大材小用,还增加了训练成本和推理延迟。

1.2 模型封装成“工厂函数”:替换模型的核心技巧

很多开源项目里的模型定义是写死的,想换网络结构就得改前向传播代码、改训练脚本,甚至改数据预处理,非常痛苦。我做这套项目时,从一开始就把模型构造做成一个工厂函数:给它传入模型名称和类别数,它返回一个配置好分类头的模型实例。这样后续训练、验证、GUI推理用的都是同一个入口,换模型时只需要改一个字头,而不是改到处散布的代码。

import torch import torch.nn as nn from torchvision import models from torchvision.models import ResNet18_Weights, MobileNet_V3_Small_Weights def build_model(model_name="resnet18", num_classes=3): if model_name == "resnet18": model = models.resnet18(weights=ResNet18_Weights.DEFAULT) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) elif model_name == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) elif model_name == "mobilenet_v3_small": model = models.mobilenet_v3_small(weights=MobileNet_V3_Small_Weights.DEFAULT) in_features = model.classifier[0].in_features model.classifier = nn.Sequential( nn.Dropout(0.2), nn.Linear(in_features, num_classes), ) elif model_name == "efficientnet_b0": model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT) in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(0.2), nn.Linear(in_features, num_classes), ) elif model_name == "vgg16": model = models.vgg16(weights=models.VGG16_Weights.DEFAULT) in_features = model.classifier[6].in_features model.classifier[6] = nn.Linear(in_features, num_classes) else: raise ValueError(f"Unsupported model: {model_name}") return model

这里的关键点在于,不同模型最后的分类头结构完全不同:ResNet是model.fc,MobileNet是model.classifier这个Sequential容器,EfficientNet的classifier比MobileNet多一个Dropout,VGG16的classifier又是一个很长的Sequential。所以替换模型时,不能只改models.resnet18()那一行,还要同步修改分类头的替换方式。上面这个封装把所有差异都收拢到一起,替换模型时只需要改传入的字符串。

还有一个常被忽略的问题:不同的预训练模型,默认的输入尺寸和预处理参数可能不一样。ResNet和VGG用的是224×224,EfficientNet有些版本是240或256,MobileNet是224。我的实现里统一把输入缩放到224×224,所以用上面这些模型都没问题。如果你要换一个对输入尺寸有特殊要求的模型,必须同步改预处理里的resize参数,否则模型能跑但效果很迷惑。

1.3 数据集与训练基础:换模型之前先确认这些事

桃树叶片常见的病害包括褐腐病、炭疽病、细菌性穿孔病、缩叶病等,再加上健康叶片,起步至少是4到5个类别。如果还想细分到不同严重程度,类别数会更多。我建议的目录结构是直接采用PyTorch的ImageFolder格式:

data/ train/ healthy/ brown_rot/ anthracnose/ bacterial_shot_hole/ leaf_curl/ val/ healthy/ brown_rot/ anthracnose/ bacterial_shot_hole/ leaf_curl/

每个类别至少准备300到500张图片,太少的话预训练模型微调之后容易过拟合。训练时按7:2:1划分训练集、验证集和测试集,优化器用AdamW,初始学习率1e-4,配合ReduceLROnPlateau调度器,一般在30个epoch左右就能收敛到一个可用的效果。如果你不想自己训练,也可以直接在项目里加载别人训练好的权重文件——只要类别数一致,torch.load读取权重后灌进build_model返回的网络即可。

2. 核心细节解析与实操要点

2.1 推理时数据预处理必须和训练时严格一致

这个坑我踩过不止一次:模型训练时用Resize到224、中心裁剪、ImageNet的mean/std做归一化,到了推理阶段换成另一套预处理,结果准确率突然掉了十几个点。原因很简单——卷积神经网络学到的特征分布与输入的数值范围强相关,训练时喂给模型的是“中心裁剪+归一化”后的图像张量,推理时你给它原图,分布对不上,学到的特征自然就错乱了。

我项目中用到的推理预处理代码如下,和训练时的流程保持一致:

import torchvision.transforms as T inference_transform = T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

这三项归一化参数是ImageNet数据集的统计量,如果你用torchvision自带的预训练模型,就必须沿用这套参数。但如果你是自己从零训练(不加载预训练权重),那归一化参数最好根据你的数据集重新统计——不过实践中99%的情况都会加载预训练权重,直接用ImageNet的统计量没问题。

还有一个细节是ToTensor()会把HWC格式的uint8图像自动变成CHW格式的float32张量,同时把像素值从0到255缩放到0.0到1.0。这个操作必须放在Resize之后、Normalize之前,顺序颠倒会直接报错或者得到全黑的张量。对新手来说这个顺序经常弄反,我在代码里用注释标得非常明显。

2.2 图片连续识别和视频识别:循环逻辑不一样

图片连续识别相对简单:用一个列表收集文件夹下所有图片路径,然后循环调用预测函数。这个场景通常用于批量分析实验样本,对实时性要求不高。视频识别就复杂一些,因为要兼顾帧率和推理速度。

假设视频是30fps,我的模型用GPU推理一张图需要30毫秒,那么理论上每秒能处理30帧出头,刚好能实时跑。但实际使用时,不可能每帧都推理——一是推理本身有耗时,二是相邻帧之间的结果高度重复,连续推理纯属浪费算力。更合理的做法是每隔N帧取一帧做推理,或者每隔固定时间间隔(比如0.3秒)取一帧推理。这样既保证识别结果能跟上视频播放节奏,又大幅降低计算压力。

def calc_frame_interval(fps: float, predict_every_seconds: float = 0.3): interval = int(round(fps * predict_every_seconds)) return max(1, interval) frame_interval = calc_frame_interval(30.0, 0.3) frame_count = 0 while True: ret, frame = cap.read() if not ret: break if frame_count % frame_interval == 0: result = predict_frame(frame) # 把识别结果绘制到画面或推送到UI draw_and_emit(frame.copy(), result) frame_count += 1

这里的predict_every_seconds我通常设成0.3秒,意味着1秒内大概推理3次。因为叶片病害在视频里短时间内不会突变,3次每秒的采样率足够稳定,也不会出现界面卡顿。如果你处理的视频里叶片移动很快,可以把这个值调小,但不要小于0.1秒——低于这个值后,模型推理耗时本身可能就会拖累整个循环,画面上会出现明显的延迟感。

2.3 GUI识别界面的线程设计:别把推理放进主线程

GUI开发里最容易踩的坑,是在按钮点击事件里直接调用模型推理。看起来没问题,但推理是一个阻塞操作——GPU算得快还好,CPU推理一张图可能要几百毫秒,视频识别更是无限循环。只要推理在主线程里,窗口就会假死、拖不动、无法最小化,给人的感觉就是程序崩了。

我的做法是把视频循环和图片预测全部丢到单独的QThread里,通过信号和槽把结果传回主线程,再由主线程更新界面。这样界面线程永远只负责响应UI事件,耗时操作都在后台线程完成,界面始终流畅。

信号传回的结果我封装成一个字典,包含:预测类别标签、置信度、当前帧(用于在画面上画框和标注),以及这是哪一帧产生的。用字典而不是元组,是为了后续扩展方便——之后想加上识别耗时、多类别TopK结果,直接往字典里加键就行,不用改函数签名。

下面是我常用的VideoThread线程类骨架,基于PySide6/PyQt6,改成PyQt5或者PySide2只是import语句的差别:

from PySide6.QtCore import QThread, Signal import cv2 class VideoThread(QThread): result_ready = Signal(dict) def __init__(self, video_source: int | str, model, device, transform, parent=None): super().__init__(parent) self.video_source = video_source self.model = model self.device = device self.transform = transform self._running = False def run(self): self._running = True cap = cv2.VideoCapture(self.video_source) if not cap.isOpened(): self.result_ready.emit({"error": "无法打开视频源"}) return fps = cap.get(cv2.CAP_PROP_FPS) or 25.0 interval = max(1, int(round(fps * 0.3))) frame_idx = 0 while self._running: ret, frame = cap.read() if not ret: break if frame_idx % interval == 0: result = self._predict_frame(frame) result["frame"] = frame result["frame_idx"] = frame_idx self.result_ready.emit(result) frame_idx += 1 cap.release() def _predict_frame(self, frame_bgr): rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(rgb) tensor = self.transform(pil_image).unsqueeze(0).to(self.device) with torch.no_grad(): outputs = self.model(tensor) probs = torch.softmax(outputs, dim=1) conf, pred = torch.topk(probs, k=1) label = self.class_names[pred.item()] return {"label": label, "confidence": round(conf.item(), 4)} def stop(self): self._running = False self.wait()

这里有几处细节容易出错。首先是cv2读出来的帧是BGR格式,而模型训练时用的是RGB格式,如果不转颜色空间,类别的输出模式会被严重歪曲,尤其对于桃叶的绿色和病斑的褐色这种颜色特征明显的任务,效果会非常差。其次是torch.no_grad()必须加上,否则Pytorch会为了反传保留整个计算图,视频推理跑到几十帧后显存就爆了。

GUI界面里,我用了非常经典的控件组合:一个按钮负责打开图片/视频文件,一个按钮负责启动/停止识别,一个QLabel显示当前画面,一个QComboBox选择模型类型,一个QLabel显示识别结果。用QImage(frame.data, frame.shape[1], frame.shape[0], QImage.Format.Format_RGB888)把OpenCV的帧转换成Qt能显示的图像时,必须记得先转成RGB再转换,否则画面颜色会变成蓝绿颠倒的鬼样子。

3. 核心代码实现与完整流程

3.1 项目文件结构与运行流程

整个项目的文件组织如下:

peach_leaf_recognizer/ train.py # 训练脚本,产出 best_model.pth predict.py # 命令行预测脚本 gui_app.py # GUI入口 models/ __init__.py build_model.py # 模型工厂函数 utils/ __init__.py transforms.py # 预处理定义 video_thread.py # 视频识别线程 datasets/ train/ healthy/ brown_rot/ ...

运行流程分两段。第一段是训练:把数据放进datasets/train和datasets/val,跑train.py得到权重文件best_model.pth。第二段是推理:启动gui_app.py,在界面上选择模型文件、选择模型结构、打开图片或视频,即可开始识别。如果不想走训练流程,这个项目也支持直接加载外来的.pth权重——只要你的网络结构和build_model里定义的某一种一致就行。

3.2 视频识别的线程完整实现

video_thread.py里的核心线程类前面已经给出骨架。实际项目里,还应该补充一个“识别结果平滑”的小模块。因为单帧预测可能存在偶发误报,比如某一帧把健康叶片误判成了炭疽病,下一帧又恢复正常。这种闪烁感在视频里非常明显,用户会以为程序不稳定。

我用的平滑策略是最近N帧投票:

from collections import deque class ResultSmoother: def __init__(self, window_size=5): self.window = deque(maxlen=window_size) def update(self, label, confidence): self.window.append((label, confidence)) counter = {} for item in self.window: counter[item[0]] = counter.get(item[0], 0) + 1 best_label = max(counter, key=counter.get) conf = sum(c for lb, c in self.window if lb == best_label) / max(1, counter[best_label]) return best_label, round(conf, 4)

窗口大小设成5,对应约1.5秒内的识别结果。投票机制比单纯的均值滤波更稳,因为它对类别标签做的是“少数服从多数”,不会把两个不同类的置信度揉在一起。这个平滑器在视频识别线程里调用,图片连续识别则不需要——单张图片没有时间连续性,平滑没有意义。

3.3 GUI主界面实现:从启动到显示结果的完整链路

GUI界面用PySide6实现,整体逻辑并不复杂,但有几个细节我特意放进了代码里。一个是QComboBox选择模型时,对应的build_model参数要能正确映射;另一个是打开视频后,要让“打开图片”和“打开视频”这两个按钮进入互斥状态,防止用户同时跑两路推理导致资源竞争。

class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("桃树叶片病害识别系统") self.setMinimumSize(960, 640) self.model_combo = QComboBox() self.model_combo.addItems(["resnet18", "resnet50", "mobilenet_v3_small", "efficientnet_b0", "vgg16"]) self.open_image_btn = QPushButton("打开图片") self.open_video_btn = QPushButton("打开视频") self.start_stop_btn = QPushButton("开始识别") self.image_label = QLabel("画面显示区域") self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.result_label = QLabel("识别结果: 未检测") self.open_image_btn.clicked.connect(self.open_image) self.open_video_btn.clicked.connect(self.open_video) self.start_stop_btn.clicked.connect(self.toggle_video) layout = QVBoxLayout() control_row = QHBoxLayout() control_row.addWidget(self.model_combo) control_row.addWidget(self.open_image_btn) control_row.addWidget(self.open_video_btn) control_row.addWidget(self.start_stop_btn) layout.addLayout(control_row) layout.addWidget(self.image_label) layout.addWidget(self.result_label) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) self.model = None self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.video_thread = None

界面布局用的是最简单的QVBoxLayout套QHBoxLayout,没用QGraphicsView,因为纯展示场景QLabel加setPixmap就足够。如果你要叠加目标检测框,需要更复杂的绘制逻辑,那时考虑QGraphicsView也不迟。

识别结果展示我做得比较朴素:一个标签显示“类别:炭疽病,置信度:0.932”。如果想要更直观的效果,可以在画面上直接叠加半透明色块和文字——在result_ready信号回调里,用cv2.rectangle和cv2.putText在帧上画框和写文字,再转成QImage显示。颜色我建议病斑相关的类别用红色,健康叶片用绿色,这样看一眼画面就能快速判断。

3.4 单张图片与文件夹连续识别的实现

视频线程解决的是动态识别场景。对于静态图片,我单独实现了一个“批处理”路径:用户点击按钮后弹窗选择文件夹,程序会扫描所有jpg/png/bmp格式图片,逐张送入预测函数,并把结果写入一个result.csv。这样实验人员可以批量跑完一整组叶片照片,回头再查表。

def batch_predict_folder(self, folder_path): image_exts = {".jpg", ".jpeg", ".png", ".bmp"} image_paths = [p for p in Path(folder_path).iterdir() if p.suffix.lower() in image_exts] rows = [] for idx, img_path in enumerate(image_paths, 1): img = cv2.imread(str(img_path)) rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(rgb) tensor = self.transform(pil_img).unsqueeze(0).to(self.device) with torch.no_grad(): outputs = self.model(tensor) probs = torch.softmax(outputs, dim=1) conf, pred = torch.topk(probs, k=1) label = self.class_names[pred.item()] rows.append([str(img_path), label, f"{conf.item():.4f}"]) self.result_label.setText(f"进度: {idx}/{len(image_paths)} -> {label}") QApplication.processEvents() with open("recognition_result.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["image_path", "label", "confidence"]) writer.writerows(rows)

这里隐藏了两个重要细节。一个是循环里调用了QApplication.processEvents(),它的作用是强制界面线程处理事件队列中的重绘请求,否则进度文字不会刷新,用户会以为程序卡死了。另一个是写CSV时用了utf-8-sig编码,直接用utf-8写出的CSV在Windows上用Excel打开会乱码,加上BOM头就能避免这个问题。这两个细节都是实际跑项目时踩出来的,文档里一般不会写。

4. 常见问题与排查技巧实录

4.1 模型替换后报错维度不匹配

这是替换模型时最常碰到的错误。报错内容类似“size mismatch for fc.0.weight: copying a param with shape torch.Size([xxx]) from checkpoint, the shape in current interface is torch.Size([yyy])”。根本原因有两个:一是类别数变了,原模型是1000类ImageNet分类,你的任务只有5类,输出层维度当然对不上;二是你换了模型结构,比方说用ResNet18训练出的权重,却拿去填装了MobileNet的模型,这几乎不可能加载成功。

解决办法是:在加载权重文件时加上strict=False,先把模型结构创建出来,在真正运行前打印model结构确认输出层形状。更稳妥的方式是训练和推理永远使用同一个build_model入口,并且把模型名记录下来存到一个meta.json里,推理时直接读取这个配置文件,而不是靠手动选择——手动选择容易记错,出错时排查还麻烦。

checkpoint = torch.load("best_model.pth", map_location=self.device) model = build_model(model_name="resnet18", num_classes=5) model.load_state_dict(checkpoint["model_state_dict"], strict=False)

另外提醒一下:如果你只改了num_classes而没有同步修改build_model里的类别数,那strict=False虽然不会报错,但最后一层的权重会随机初始化,推理结果会非常差。最好的做法是训练结束后,额外保存一份模型结构元信息,推理时根据元信息重建模型。

4.2 置信度一直很低或结果闪烁

置信度低通常有四个原因。第一,预处理不一致,排查方式是在推理脚本里故意保存一张预处理后的图片,看看它跟训练集里的图片风格是否一致。第二,类别划分有问题,某些病害之间的特征差异本身就小,炭疽病和褐腐病早期病斑确实容易混淆,这种情况下单纯换模型帮助不大,应该考虑增加训练数据或者划分更细的前置类别。第三,背景干扰严重,叶片没有充满画面,模型的大部分注意力被土壤、枝条、杂草占据,识别结果自然不自信。遇到这种情况,先手动裁剪叶片区域再识别。第四,测试集分布和训练集差异过大,比如训练集都是室内台灯下拍摄的照片,测试时拿果园里的强光视频去跑,这种光照泛化问题只能通过数据增强来缓解。

结果闪烁是视频识别特有的问题,我前面已经提了使用平滑窗口的方案。这里再补充一个细节:不要同时使用“置信度阈值+平滑投票”的严格组合。比如某帧置信度低于0.5直接丢弃,剩下几帧又做投票,最后可能10秒都出不来结果,用户会怀疑系统坏了。我通常的做法是:置信度阈值设置成0.4,低于这个值就显示“待确认”,高于0.4的帧正常参与平滑投票。

4.3 视频识别卡顿、显存占用持续上涨

卡顿的第一嫌疑是推理占住了主线程。如果你在视频循环里直接self.model(tensor)而没有任何线程封装,CPU推理时UI连拖动窗口都费劲,这就是线程设计的问题,回到第2.3节改代码。显存持续上涨通常是忘了加torch.no_grad(),或者是推理时把整张视频帧(而不是裁剪后的张量)留在了内存里准备绘制。实际上,绘制用的帧可以放在一个固定大小的队列里,每帧都只保留最新的一份,别把历史帧全部accumulate起来。

C++ 还有一个常见问题:cap.read()本身是阻塞操作,对于高分辨率视频,单次读取就可能耗时二三十毫秒。如果你的视频是4K分辨率,又每帧都resize到224,CPU开销叠加起来会很可观。建议在读取阶段先做好裁剪或缩放:cap.set(cv2.CAP_PROP_FRAME_WIDTH, 960)减少解码压力,或者直接在循环里对frame做cv2.resize(frame, (448, 448))后再传给预处理流水线,降低后续Resize的CPU开销。

4.4 常见问题速查表

问题现象可能原因解决方式
模型加载报维度不匹配类别数不一致或结构不一致使用相同build_model入口,保存meta信息
推理结果全为同一个类预处理顺序错误、颜色通道颠倒检查Resize→Tensor→Normalize顺序,BGR转RGB
视频界面假死推理在主线程执行将推理逻辑移入QThread,用信号返回结果
显存持续增长缺少torch.no_grad()在推理过程外层包裹with torch.no_grad():
识别结果频繁跳动单帧误报使用N帧平滑投票窗口
GPU利用率很低数据读取和预处理慢于GPU推理使用队列预取下一帧,或跳帧+批量推理
CSV结果Excel打开乱码编码不是带BOM的UTF-8写成utf-8-sig
摄像头画面卡顿解码分辨率过高用CAP_PROP_FRAME_WIDTH/HEIGHT降低分辨率
CPU推理极慢模型过大或未量化换MobileNet/EfficientNet,或用torch.quantization量化

5. 把模型换成自己训练的网络时,我建议按这个顺序操作

整个项目其实就一条主线:数据组织好了,模型训练好了,然后塞进这套推理管线里,通过GUI对外提供服务。模型替换我是按这个顺序做的,你也可以参考。

先把需要替换的新模型写进build_model里,确认分类头的属性名和输出层结构。然后在命令行脚本里单独测试这个模型能不能正常推理一张图片,确认预测结果合理。接着再把模型接入GUI,先测图片连续识别模式,确认置信度和类别映射正确,最后再测视频模式,观察帧率和显存变化。一步一验证,不要一次性改完所有代码再启动GUI,那样出了错也不容易定位。我在项目的初期阶段曾经直接改完视频模块就去测摄像头实时识别,结果预处理忘了转RGB,画面上所有颜色都是反的,排查了大半天才发现问题出在最基础的色彩转换上。

我个人在实际操作中还有一个体会:不要追求视频识别里“每一帧都给结果”。视频识别本质上是按一定频率采样,然后对采样帧做识别。你把采样间隔设成0.2秒和0.5秒,最终的视觉体验差别并不大,但算力开销差了一倍多。对产线上流转速度不快的桃叶,0.3秒间隔足够;对手机拍的果园视频,叶片停留时间短,0.1秒间隔也够用了。这个参数最好做成GUI里的可选项,而不是写死在代码里。

最后分享一个小技巧:如果你的GUI在无GPU的电脑上运行,记得在设备选择上做一个回退逻辑。torch.device("cuda" if torch.cuda.is_available() else "cpu")这行代码大家都会写,但CPU推理MobileNet和VGG的耗时差距很大。在CPU环境下,默认选MobileNet,同时给用户一个“硬件设备”下拉框——服务器有自己的GPU,同事的笔记本没有,这一项能省掉不少远程演示时临时改代码的尴尬。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进