ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从CNN到GUI:垃圾分类识别系统完整实现与工程实践

从CNN到GUI:垃圾分类识别系统完整实现与工程实践 简介这套基于卷积神经网络的垃圾分类系统实现包面向深度学习入门者、计算机视觉学习者以及需要快速搭建分类应用的开发者完整演示了垃圾图片识别系统从数据准备、模型训练到图形界面部署的完整流程可用于课程设计、毕业设计或项目实践参考。压缩包中共有2000个文件其中1978张jpg图片构成主要数据样本另有6个Python脚本负责模型构建与GUI应用5个xml配置、2个txt说明以及项目管理文件辅助运行整体大小约146.47MB目录结构清晰图片与代码分离便于按需检索和调试。实现方案注重模型优化实验从基础的双卷积层出发通过增加卷积层深度、引入Batch Normalization、动态调整学习率并配合Dropout正则化将分类准确率稳定在90%左右展示了一条清晰有效的调参路径适合以此为基础继续改进。配套的PySimpleGUI图形界面支持用户选取图像文件夹、浏览列表并即时获得预测结果交互直观特别适合教学演示或个人项目参考。目前已有202人学习是一份覆盖数据、训练到应用层面的可操作性资源。1. 从“拍张照”到“告诉你扔哪个桶”CNN垃圾分类GUI到底解决什么问题把一袋混着果皮、塑料瓶、废纸的生活垃圾拍张照交给桌面上一个普通窗口程序它半秒内告诉你该扔进哪个桶——这就是基于卷积神经网络的垃圾分类系统实现GUI应用在做的事。它不是一个比赛demo而是一个能跑在你电脑上的完整体前端用GUI接收图片后端用一个训练好的CNN模型推理输出类别和置信度。这类系统常见于小区智能分类终端、环卫教育演示设备也是很多人的毕业设计和工业入门练手项目。适合两类人一是想把图像分类完整落地一遍的开发者学习者二是需要快速做出可演示原型而非论文效果的小团队。它的核心价值不在模型结构多新而在“能稳定跑起来”的那套工程链路。2. 数据先行垃圾分类数据集的类别划分与预处理策略2.1 选6类还是40类类别体系决定模型上限接手这个项目第一件事不是打开PyTorch而是想清楚“分几类”。大部分公开垃圾分类数据集有两套常见口径一种是按回收场景分的大类比如可回收物、厨余垃圾、有害垃圾、其他垃圾这四类另一种是按具体物品分比如玻璃、纸张、纸板、塑料、金属、织物等六类或更多。别小看这个选择它直接决定模型的工作难度和用户的使用体验。如果你做的是社区四分类终端四类就够但四类的内部方差极大——可回收物里塑料瓶和旧报纸长得完全不像这会逼着CNN去学“抽象的共同特征”对数据量和数据多样性要求反而更高。如果你按具体物品分六类每类内部形态相对一致模型容易学但用户拿一个没见过的物品过来大概率会得到一个“哪类都不像”的结果。我一般建议新手先做六类到十类的细分类把准确率堆上去再考虑合并成大类的后处理逻辑。这比直接端上四分类更容易出效果。2.2 数据集目录组织直接复用ImageFolder不管数据集来自公开渠道还是自己拍目录结构统一按分类整理最省事。PyTorch的torchvision.datasets.ImageFolder可以直接按子文件夹名生成标签省去手写标签映射表的麻烦。常见的做法是维护trian/val两个目录每个类一个子文件夹。data/ ├── train/ │ ├── glass/ # 玻璃瓶、玻璃碎片 │ ├── paper/ # 报纸、纸箱、纸杯 │ ├── plastic/ # 塑料瓶、塑料袋 │ ├── metal/ # 易拉罐、铁皮 │ ├── cardboard/ # 瓦楞纸板 │ └── trash/ # 其他不可回收杂物 └── val/ ├── glass/ ├── paper/ ├── plastic/ ├── metal/ ├── cardboard/ └── trash/目录组织好之后加载代码就很简单了关键参数都写在注释里。# dataset.py from torchvision import datasets, transforms from torch.utils.data import DataLoader train_dataset datasets.ImageFolder( rootdata/train, transformtrain_transform ) val_dataset datasets.ImageFolder( rootdata/val, transformval_transform ) # batch_size 先给 32显存不够再降到 16 train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, persistent_workersTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4)ImageFolder会自动把子文件夹名按字母序映射成整数标签dataset.classes里存着名字dataset.class_to_idx字典存着映射关系。后面做GUI推理时要把这个字典保存下来否则预测完不知道整数标签对应的是哪个类。2.3 预处理和数据增强这几组参数别乱改图像分类任务里预处理几乎是“行规”级别的固定值。垃圾图片多数由手机或摄像头拍摄光照、角度、背景都不可控所以训练时的数据增强要比常规分类任务更激进一些。# transforms.py from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])说明几点RandomResizedCrop的scale从0.7起步模拟同一件垃圾在不同距离拍摄的尺度差异比固定Resize更能抵抗比例变化。RandomRotation给15度而不是90度是因为垃圾图片不存在“倒着放”的语义转90度会把瓶子变成横躺反而制造混乱样本。ColorJitter的hue只给0.05颜色微调可以增强鲁棒性但调多了会让塑料和玻璃这种靠颜色区分的类别崩掉。Normalize的mean和std必须用ImageNet预训练模型的统计值因为你后面要加载预训练权重输入分布不一致会导致特征提取阶段就偏了。这里有个反直觉的地方val_transform不要加任何增强只做Resize(256)再CenterCrop(224)。验证集的作用是反映模型在“干净输入”下的真实水平加了增强会让验证集指标变得时高时低早停和调参就失去了参照。2.4 切分验证集别让同一件垃圾“串场”数据切分是这个项目最容易出暗坑的环节。很多人直接从全量数据里按0.8/0.2随机切结果同一张照片或同一个物体的不同角度同时出现在训练集和验证集里验证分数虚高真实场景一测就现原形。垃圾分类数据集经常是“一个物品拍多张”收集来的随机切分等于让模型在考试时偷看了答案。正确做法是先按“物品实例”分组。也就是说同一个瓶子拍出来的十张图必须全部进训练集或全部进验证集不能两头跑。实现思路不复杂文件名统一带上物体ID比如“bottle_001_1.jpg”“bottle_001_2.jpg”切分时按下划线第一段分。# split_dataset.py import os import shutil from collections import defaultdict source_root data/raw train_root data/train val_root data/val # 按类别、物体ID收集文件 for category in os.listdir(source_root): category_path os.path.join(source_root, category) groups defaultdict(list) for fname in os.listdir(category_path): obj_id fname.split(_)[0] # 取物体ID groups[obj_id].append(fname) train_dir os.path.join(train_root, category) val_dir os.path.join(val_root, category) os.makedirs(train_dir, exist_okTrue) os.makedirs(val_dir, exist_okTrue) for obj_id, files in groups.items(): dest_root train_dir if hash(obj_id) % 5 ! 0 else val_dir for fname in files: src os.path.join(category_path, fname) shutil.move(src, os.path.join(dest_root, fname))按物体ID哈希落到训练或验证比每类固定留出20%更稳妥因为不同类的样本量差异大。这个脚本跑完后检查一下每个类的验证集图像数量如果某个类验证集只有一两张说明这个类本身数据太少需要补数据而不是改哈希。3. 模型搭建与训练从ResNet18到轻量CNN的选型与调参3.1 熟手的选型直觉迁移学习为主结构图只看残差连接很多人一上来就想自己搭一个CNN从卷积核尺寸设计到池化层数量逐个手搓。我做了几轮之后的态度是除非你有非常特殊的输入形态否则用现成的卷积神经网络结构图去选一个经过千万级数据考验的主干网络比自己设计靠谱得多。垃圾分类的图片没有医学影像那种领域特殊性ImageNet上预训练出来的通用特征完全能用。ResNet18是这类项目的起步首选它只有1100万参数在CPU上推理一张图不到一秒钟训练也快。看ResNet的结构图时重点看一个东西残差连接。它让梯度能跨层传递所以网络能做到几十层不退化。ResNet34和ResNet18精度接近但参数量翻倍对垃圾分类这种粗粒度任务收益不明显。如果后续要跑到树莓派或老电脑上MobileNetV3或EfficientNet轻量版更合适代价是训练时对学习率更敏感容易震荡。3.2 最小可跑通训练脚本修改分类头是唯一必要改动使用迁移学习时唯一必须改的是最后一层全连接把输出维度从ImageNet的1000改成你实际的类别数。ResNet18的最后一层是model.fc换成带Dropout的两层结构能压一点过拟合。# build_model.py import torch.nn as nn import torchvision.models as models def build_model(num_classes, pretrainedTrue, model_nameresnet18): if model_name resnet18: if pretrained: model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) else: model models.resnet18(weightsNone) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(p0.3), nn.Linear(in_features, 256), nn.ReLU(inplaceTrue), nn.Linear(256, num_classes) ) elif model_name mobilenet_v3_small: if pretrained: model models.mobilenet_v3_small(weightsmodels.MobileNet_V3_Small_Weights.IMAGENET1K_V1) else: model models.mobilenet_v3_small(weightsNone) in_features model.classifier[-1].in_features model.classifier[-1] nn.Linear(in_features, num_classes) return model中间加一个256维的全连接层是想给模型一点“消化特征”的空间而不是直接从2048维特征跳到6类输出。Dropout放在最前面训练时随机砍掉30%的神经元强制模型用冗余特征去判断这对垃圾这种“同类物品形态差异大”的任务有效。训练主循环的关键点在于优化器和学习率策略的选择。# train_loop.py import torch import torch.nn as nn from torch.optim import AdamW model build_model(num_classes6, pretrainedTrue) criterion nn.CrossEntropyLoss() optimizer AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max25, eta_min1e-5 ) best_acc 0.0 patience, trigger 7, 0 for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.cuda(), labels.cuda() outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1}: loss{running_loss/len(train_dataset):.4f}, fval_acc{val_acc:.4f}) # 早停与保存最优权重 if val_acc best_acc: best_acc val_acc trigger 0 torch.save({ model_state: model.state_dict(), class_to_idx: train_dataset.class_to_idx, }, best_model.pth) else: trigger 1 if trigger patience: print(fEarly stop at epoch {epoch1}) break scheduler.step()3.3 三个必调参数学习率、batch size和早停耐心值AdamW用默认的1e-3起步是安全的。真正决定收敛质量的是weight_decay我固定给1e-4太小起不到约束作用太大会把预训练权重冲淡。CosineAnnealingLR把学习率从1e-3余弦退火到1e-5比固定学习率或StepLR更平滑尤其适合迁移学习后期微调。batch size一般先试32。显存不够时优先降batch到16而不是换更小的模型。ResNet18在224分辨率下batch 32大约吃4~5GB显存普通消费级显卡都能带得动。学习率和batch size存在联动batch越大梯度越平滑学习率可以适当调大batch减半学习率最好也减半否则前几个epoch loss容易起飞。早停的patience给7~8个epoch比较合理。这个项目常见的问题是验证集准确率在0.85附近反复横跳patience给3的话几乎必定误杀好模型给10以上又浪费时间。另外注意保存checkpoint时除了model_state_dict我把class_to_idx也一起存了这是GUI推理时必需的映射关系——很多人把这个字典忘在训练脚本里换机器推理时只能对着整数标签干瞪眼。如果还想断点续训把optimizer_state_dict和epoch也存进去不然恢复训练后学习率和优化器动量全部重置。4. GUI应用落地把训练好的权重接进桌面界面4.1 技术选型Tkinter和PyQt5怎么选GUI这一层没有标准答案常见做法是两种Tkinter和PyQt5。Tkinter是Python标准库自带的装完Python就能用打包体积小适合快速交付和毕设演示缺点是控件样式比较老旧做不出“现代感”。PyQt5控件漂亮、支持QSS样式表、文件拖拽有现成事件但打包体积至少大50MB写起来也啰嗦。我的建议是只做单图片选择、显示和识别结果展示用Tkinter够了如果你想做类似“垃圾图片批量检测”“摄像头实时识别”这种带交互流程的演示直接上PyQt5它的事件循环和线程模型比Tkinter成熟。下面我以Tkinter为例因为它能保证任何人都能无依赖跑起来。4.2 推理模块封装模型加载、预处理、后处理一次到位GUI里的推理不能把训练代码直接搬过来。训练代码里的模型是用于反向传播的而推理需要关闭梯度、关闭Dropout、把输入转成四维张量这些必须封装成一个独立类。# classifier.py import torch import torch.nn as nn from torchvision import transforms from PIL import Image class WasteClassifier: def __init__(self, weights_path, class_to_idx): self.device torch.device(cuda if torch.cuda.is_available() else cpu) # 这里的 build_model 必须与训练时完全一致 self.model build_model(num_classeslen(class_to_idx), pretrainedFalse) checkpoint torch.load(weights_path, map_locationself.device) self.model.load_state_dict(checkpoint[model_state]) self.model.to(self.device) self.model.eval() # 翻转映射字典{0: glass, 1: paper, ...} self.idx_to_class {v: k for k, v in class_to_idx.items()} self.transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict(self, pil_image): tensor self.transform(pil_image.convert(RGB)).unsqueeze(0) tensor tensor.to(self.device) with torch.no_grad(): output self.model(tensor) probs torch.softmax(output, dim1)[0] conf, idx torch.max(probs, dim0) return self.idx_to_class[idx.item()], conf.item()两个关键细节。第一build_model里pretrained必须传False因为这里不需要重新下载权重而是从你自己训练好的checkpoint里加载。第二class_to_idx来自训练时保存的文件如果你训练脚本没有存这里只能从训练时打印的dataset.classes硬编码一旦类别顺序对不上预测结果会错乱。torch.max返回的是最大概率和对应索引idx.item()再查字典就能得到类名。如果希望看到每个类别的概率改成probs.tolist()返回一个列表即可。4.3 主界面代码选图、显示、识别一条龙GUI主程序就是建窗口、放按钮、把选图函数绑定到按钮上。为了不让窗口卡死识别必须在单独线程里跑这个后面避坑章节会专门说但代码结构上现在就要预留线程入口。# app.py import tkinter as tk from tkinter import filedialog from PIL import Image, ImageTk import threading from classifier import WasteClassifier class WasteApp: def __init__(self, root, checkpoint_path, class_to_idx): self.root root self.root.title(垃圾分类识别系统) self.root.geometry(560x520) self.classifier WasteClassifier(checkpoint_path, class_to_idx) self.canvas tk.Label(root, text点击下方按钮选择图片, bgwhite, width60, height15) self.canvas.pack(pady10) self.btn_select tk.Button(root, text选择图片并识别, commandself.select_and_predict) self.btn_select.pack(pady5) self.result_label tk.Label(root, text, font(微软雅黑, 16)) self.result_label.pack(pady10) def select_and_predict(self): file_path filedialog.askopenfilename( filetypes[(图片文件, *.jpg *.jpeg *.png *.bmp)] ) if not file_path: return self.canvas.config(text正在识别...) threading.Thread(targetself._run_prediction, args(file_path,), daemonTrue).start() def _run_prediction(self, file_path): image Image.open(file_path) # 预览图缩放显示 preview image.copy() preview.thumbnail((320, 240)) photo ImageTk.PhotoImage(preview) class_name, confidence self.classifier.predict(image) text f{class_name}置信度 {confidence:.2%} # Tkinter 线程安全要求更新界面必须在主线程 self.root.after(0, self._update_ui, photo, text) def _update_ui(self, photo, text): self.canvas.config(imagephoto) self.canvas.image photo self.result_label.config(texttext) if __name__ __main__: root tk.Tk() app WasteApp(root, best_model.pth, class_to_idx) root.mainloop()这里有个容易忽略的坑线程里不能直接改Tkinter控件Tkinter不是线程安全的直接用config会偶发崩溃。正确做法是用root.after(0, callback)把更新动作丢回主线程。4.4 摄像头实时识别循环读帧加阈值控制如果想把系统从“传图片识别”升级成“摄像头对准垃圾实时出结果”套路是在主线程里开一个定时器每100毫秒从OpenCV读一帧送入推理。这里压力最大的不是模型而是图像解码和界面刷新。常见做法是降分辨率到320x240再送识别一来减轻预处理负担二来对垃圾这种大目标物体320分辨率足够。# camera.py 片段 import cv2 import tkinter as tk from PIL import Image, ImageTk def capture_loop(root, classifier, label_widget): cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) framecount 0 def update_frame(): nonlocal framecount ok, frame cap.read() if ok: # 每 3 帧推理一次留下缓存时间给界面刷新 if framecount % 3 0: rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(rgb) class_name, conf classifier.predict(pil_img) text f{class_name} ({conf:.2%}) label_widget.config(texttext) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img Image.fromarray(rgb).resize((480, 360)) photo ImageTk.PhotoImage(img) label_widget.config(imagephoto) label_widget.image photo framecount 1 root.after(100, update_frame) # 启动时先调用一次 update_frame() return cap这里的“每3帧推理一次”是血泪经验如果每帧都送模型CPU或低端GPU根本追不上摄像头帧率画面会越积越卡。垃圾又不会动牺牲点实时性换流畅度完全值得。5. 避坑垃圾分类系统从训练到打包的5个典型翻车现场5.1 类别不均衡模型学会“偷懒”只分大类现象训练loss一直在降验证准确率看起来不错但打开混淆矩阵发现厨余垃圾几乎全对塑料瓶大量误判成厨余。原因厨余样本占比过高CNN学到“只要不确定就猜厨余”这个捷径。解决先统计每个类别的样本数对样本多的类做随机欠采样或者对样本少的类做重复采样torch的WeightedRandomSampler可以直接用训练结束后再检查每一类的召回率不能只看总体准确率。5.2 训练验证都好真实照片一测就翻车现象验证集准确率0.92但把自己手机拍的垃圾照片丢进去识别结果乱七八糟。原因公开数据集里的图片背景干净、光照均匀模型把背景纹理和物体特征一起学了。这个叫背景过拟合是垃圾分类最普遍的泛化陷阱。解决训练时把RandomResizedCrop的scale下限调到0.5强迫模型看到更大的物体局部另外每类补充30~50张带真实室内背景的照片混进训练集对泛化能力的提升比加任何数据增强都明显。5.3 GUI点击识别时窗口卡死现象点击“选择图片并识别”后窗口白屏拖动窗口时整个界面跟死了一样等模型推理完才恢复。原因推理是在主线程执行的PyTorch的forward在CPU上跑几百毫秒到一两秒这期间Tkinter事件循环被阻塞。解决把推理放进threading.Thread推理完成后用root.after回主线程更新界面。记住Tkinter和PyQt都禁止在工作线程里直接操作界面控件。5.4 继续训练时loss震荡不收敛现象加载之前的checkpoint接着训练前几个epoch验证准确率剧烈抖动甚至比保存时的值还低。原因恢复训练时只恢复了model_state_dict优化器的Adam动量参数是重新初始化的学习率scheduler也从头开始等于用旧模型配新优化器步子大了自然震荡。解决保存checkpoint时把optimizer_state_dict、epoch、scheduler_state_dict一起存如果实在没存优化器把学习率降到原来十分之一再继续训练。5.5 打包exe后找不到模型文件现象在开发环境python app.py跑得好好的用PyInstaller打包后双击运行点击选图直接报错“模型文件不存在”。原因PyInstaller可执行文件运行时的工作目录和脚本目录可能不一致相对路径model.pth指向了exe所在目录资源没被带到那个位置。解决用sys._MEIPASS判断打包环境把模型文件放到资源目录代码里用绝对路径拼接。# paths.py import sys import os def resource_path(relative_path): if hasattr(sys, _MEIPASS): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath(.), relative_path) # 使用方式 model_path resource_path(best_model.pth)这个路径处理是所有GUI打包都会踩到的坑早点写进工具函数比每次手动改要省心得多。6. 别急着交付用混淆矩阵和真实照片验证模型边界模型训练完、GUI能跑通事情只完成了一半。在交付之前我习惯做三件验证。第一件是打印分类报告和混淆矩阵逐类看召回率。总体准确率会骗人但每类的precision和recall不会。垃圾分类最怕的不是总体掉两个点而是某一种垃圾永远识别不出来——比如金属类召回率只有0.5说明这类样本特征还没学好。用一行代码就能看清楚问题在哪。# evaluate.py import torch from sklearn.metrics import classification_report, confusion_matrix from classifier import WasteClassifier with torch.no_grad(): all_preds, all_labels [], [] for images, labels in val_loader: images images.cuda() outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().tolist()) all_labels.extend(labels.tolist()) print(classification_report(all_labels, all_preds, target_nameslist(class_to_idx.keys()))) print(confusion_matrix(all_labels, all_preds))第二件是拿20~30张“训练分布之外”的真实照片做冒烟测试。这里的照片要满足两个条件没在这个数据集里出现过、拍摄环境尽量接近实际使用场景室内灯光、有阴影、垃圾放在桌上。如果这批照片的准确率比验证集低超过10个百分点说明泛化边界比你想象的窄得回去补数据而不是继续调超参数。模型是个黑匣子但它到底被你训成了“认垃圾”还是“认背景”只有真实照片能照出来。第三件是模型瘦身。如果目标机器没有GPU而你想让识别耗时压到500毫秒以内别只想着换大显卡。把模型转成ONNX格式再用ONNX Runtime推理CPU上的速度通常能提高2~3倍而精度几乎无损。导出也很简单。# export_onnx.py import torch model build_model(num_classes6, pretrainedFalse) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)[model_state]) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, waste_classifier.onnx, input_names[input], output_names[output], opset_version12)导出之后推理脚本里把torch.load换成onnxruntime.InferenceSession输入张量要用numpy而不是torch.Tensor其余预处理和后处理逻辑完全一致。如果还想再压缩体积可以尝试torch.quantization做动态量化把模型权重从FP32压到INT8精度损失通常在1~2个点以内但文件和内存占用几乎减半。我的习惯是把这三件事写进项目的README里每换一次数据都跑一遍。分类模型最容易出现的问题不是“做得不够好”而是“不知道自己哪里不行”。混淆矩阵给你透视眼真实照片给你清醒剂模型瘦身给你交付的底气。这一套走下来再交给别人用的时候心里就有谱了。希望帮到你。本文还有配套的精品资源点击获取
返回列表