简介:本资源是一套基于Python与百度AI图像识别API开发的植物识别桌面应用,专为本科毕业设计或课程设计项目打造,适合具备基础Python编程能力的学习者实践GUI开发、第三方API集成与AI应用落地。压缩包共16个文件,含2个核心Python源码(app.py、Ui_ocrui.py)、2个PNG界面截图、5个XML配置及IDE相关文件、1个可直接运行的exe程序,以及LICENSE、README.md等工程规范文件,整体37.34MB,结构完整,兼顾开发调试与一键运行需求。已有1678人学习下载,资源附带清晰的项目组织逻辑:PyQt5构建主界面,requests调用百度植物识别接口,支持图片上传、异步请求、结果解析与UI动态展示,并包含预处理提示与基础错误处理机制。读者可直接复现完整AI识别流程,深入理解API鉴权、JSON响应解析、信号槽绑定及可执行程序打包等关键环节。
1. 毕业设计真能跑通?用 Python + 百度植物识别 API + PyQt5 做出可交互、带截图、能本地缓存的识别工具,不是 Demo 是交付物
你交毕业设计答辩 PPT 时,老师点开你的程序——界面清爽,拖张图进去,3 秒弹出「银杏:Ginkgo biloba,国家一级保护植物」,再点「历史记录」,昨天识别的七叶树、紫薇、鸢尾全在列表里,双击还能回看原图和置信度。这不是网页调用演示,是本地打包好的.exe,U 盘一插就能在答辩电脑上运行,不依赖浏览器、不报错、不弹授权窗口。很多同学卡在「调通 API 就算完事」,但真正过审的毕设,必须解决三个硬问题:请求失败怎么兜底、图片路径中文乱码怎么破、PyQt5 界面卡死怎么防阻塞、识别结果怎么结构化存档。这篇笔记不讲百度 AI 开放平台注册流程(那不是技术难点),只聚焦你写代码时真实会撞墙的环节:从pip install那一刻起,到打包成dist/下那个能双击运行的文件为止。适合正在赶毕设 deadline、手头只有 Python 基础、没接触过 API 封装和 GUI 多线程的同学——我当年就是靠这套流程,把答辩现场从「老师皱眉问‘这能稳定运行吗’」变成「老师主动问‘能给我份源码参考下吗’」。
2. 从零搭起识别骨架:百度植物识别 API 接入与 PyQt5 主窗体初始化
2.1 为什么选百度植物识别 API 而不是 TensorFlow 自训练?
毕业设计不是科研项目,核心诉求是「可控、可解释、可复现、可答辩」。自建 CNN 模型要标注上千张植物图、调参耗时、GPU 显存不够、准确率难保证(尤其相似种如玉兰 vs 木兰)、答辩时老师问「为什么选 ResNet50 不选 EfficientNet?」你答不上来就露馅。而百度植物识别 API 是成熟商用服务:
- 接口极简:POST 一张 Base64 图片,返回 JSON 含
result(植物名)、score(置信度)、baike_url(百度百科链接); - 覆盖广:支持 1 万+ 物种,含常见校园/公园植物(香樟、桂花、月季、蒲公英),且对模糊、旋转、局部遮挡图片鲁棒性优于学生自训模型;
- 免费额度够用:新账号送 5 万次/月调用(毕设全程最多用 200 次),超限后自动降级为「识别失败」而非报错崩溃,便于做容错处理;
- 返回字段结构统一:不用自己解析 XML 或多层嵌套 JSON,直接
res['result'][0]['name']取名,res['result'][0]['score']取分。
提示:别用百度 OCR 或通用图像识别 API 替代——它们不返回植物学名,只返回「叶子」「花朵」等泛化词,毕设答辩会被质疑「识别精度不足」。
2.2 安装依赖与创建 PyQt5 主窗体(最小可运行结构)
先确认环境:Python 3.8+(避免 PyQt5 与高版本兼容问题),Windows/macOS 均可。执行以下命令一次性装齐:
pip install requests pyqt5 pillow lxml beautifulsoup4requests:发 HTTP 请求调用百度 API;pyqt5:构建图形界面;pillow:处理图片缩放、格式转换(PyQt5 的 QImage 对中文路径支持差,PIL 更稳);lxml+beautifulsoup4:后续解析百度百科页面获取植物简介(加分项,非必需但让毕设更丰满)。
创建主窗体main.py,仅保留最简结构(后续再加功能):
# main.py import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog, QTextEdit, QTableWidget, QTableWidgetItem from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QPixmap, QImage class PlantRecognitionApp(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("植物识别毕设系统 v1.0") self.setGeometry(100, 100, 900, 600) # 中央部件 central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) # 标题区 title_label = QLabel("🌿 植物识别系统(基于百度AI开放平台)") title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("font-size: 16px; font-weight: bold;") layout.addWidget(title_label) # 图片区(占位) self.image_label = QLabel("点击【选择图片】上传植物照片") self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet("border: 1px dashed #aaa; min-height: 300px;") layout.addWidget(self.image_label) # 按钮区 btn_layout = QHBoxLayout() self.select_btn = QPushButton("📁 选择图片") self.recognize_btn = QPushButton("🔍 识别植物") self.recognize_btn.setEnabled(False) # 初始禁用,选图后才启用 btn_layout.addWidget(self.select_btn) btn_layout.addWidget(self.recognize_btn) layout.addLayout(btn_layout) # 结果区(占位) self.result_text = QTextEdit() self.result_text.setPlaceholderText("识别结果将显示在此处...") self.result_text.setReadOnly(True) layout.addWidget(self.result_text) # 绑定事件 self.select_btn.clicked.connect(self.load_image) self.recognize_btn.clicked.connect(self.start_recognition) def load_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择植物图片", "", "图片文件 (*.png *.jpg *.jpeg)" ) if file_path: # 用 PIL 加载,规避 PyQt5 中文路径问题 from PIL import Image try: pil_img = Image.open(file_path) # 缩放至宽度≤600px,保持比例,避免大图撑爆界面 w, h = pil_img.size if w > 600: pil_img = pil_img.resize((600, int(h * 600 / w)), Image.Resampling.LANCZOS) # 转为 QPixmap 显示 qimg = QImage(pil_img.tobytes(), pil_img.width, pil_img.height, pil_img.width * 3, QImage.Format_RGB888) pixmap = QPixmap.fromImage(qimg) self.image_label.setPixmap(pixmap.scaled(600, 400, Qt.KeepAspectRatio)) self.current_image_path = file_path self.recognize_btn.setEnabled(True) except Exception as e: self.result_text.setText(f"❌ 图片加载失败:{str(e)}") def start_recognition(self): self.result_text.setText("⏳ 正在识别,请稍候...") if __name__ == "__main__": app = QApplication(sys.argv) window = PlantRecognitionApp() window.show() sys.exit(app.exec_())关键逻辑说明:
QFileDialog.getOpenFileName返回的是绝对路径字符串,直接传给QPixmap会因中文路径报错(Windows 常见),所以用PIL.Image.open()先加载,再转QImage;- 图片缩放用
Image.Resampling.LANCZOS(高质量双三次插值),比默认NEAREST清晰得多,答辩时放大看叶片纹理不糊; self.current_image_path存储路径供后续识别用,不存图片对象本身(避免内存泄漏);self.recognize_btn.setEnabled(False)是 UX 基线:没图不能点识别,防止空操作报错。
3. 实现百度 API 调用与结果解析:带重试、超时、错误码映射的健壮封装
3.1 获取百度 API Key 与 Secret Key(仅需一次,不写进代码)
登录 百度 AI 开放平台 → 进入「控制台」→「创建应用」→ 选择「图像识别」→「植物识别」→ 填写应用名称(如「毕设植物识别」)→ 提交。生成后得到:
API Key(如abcd1234efgh5678ijkl9012)Secret Key(如mnop3456qrst7890uvwx1234)
重要安全实践:
- 绝不硬编码在
.py文件里!创建config.py单独存放:
# config.py BAIDU_API_KEY = "your_api_key_here" BAIDU_SECRET_KEY = "your_secret_key_here"- 在
.gitignore中加入config.py,防止上传 GitHub 泄露密钥; - 答辩前删掉
config.py,改用环境变量或手动输入(老师不会检查密钥,但流程规范体现工程素养)。
3.2 封装百度植物识别请求(含重试、超时、错误码翻译)
新建api_client.py,实现核心调用逻辑:
# api_client.py import requests import base64 import time import json from urllib.parse import urlencode from config import BAIDU_API_KEY, BAIDU_SECRET_KEY class BaiduPlantRecognizer: def __init__(self): self.access_token = None self.token_expire_time = 0 def _get_access_token(self): """获取 access_token,缓存 28 天(百度实际有效期 30 天,留 2 天缓冲)""" if time.time() < self.token_expire_time: return self.access_token url = "https://aip.baidubce.com/oauth/2.0/token" params = { "grant_type": "client_credentials", "client_id": BAIDU_API_KEY, "client_secret": BAIDU_SECRET_KEY } try: res = requests.get(url, params=params, timeout=10) res.raise_for_status() data = res.json() self.access_token = data["access_token"] self.token_expire_time = time.time() + 28 * 24 * 3600 # 缓存 28 天 return self.access_token except Exception as e: raise RuntimeError(f"获取 access_token 失败:{e}") def recognize_plant(self, image_path, max_retries=3): """ 识别植物图片 :param image_path: 本地图片路径 :param max_retries: 失败重试次数(网络抖动时有用) :return: dict 包含 'name', 'score', 'baike_url', 'error_msg' """ # 1. 读取图片并转 Base64 try: with open(image_path, "rb") as f: img_base64 = base64.b64encode(f.read()).decode("utf-8") except Exception as e: return {"error_msg": f"图片读取失败:{e}"} # 2. 获取 access_token try: token = self._get_access_token() except Exception as e: return {"error_msg": f"认证失败:{e}"} # 3. 调用植物识别 API url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/plant?access_token={token}" headers = {"Content-Type": "application/x-www-form-urlencoded"} data = {"image": img_base64} for attempt in range(max_retries): try: res = requests.post(url, headers=headers, data=data, timeout=15) res.raise_for_status() result = res.json() # 解析百度返回的错误码(比 HTTP 状态码更细) if "error_code" in result: error_map = { 0: "成功", 110: "access_token 无效", 111: "access_token 过期", 282000: "图片格式错误(非 JPG/PNG)", 282001: "图片大小超限(>4MB)", 282002: "图片分辨率过高(>4096x4096)", 282003: "图片内容为空", 282004: "无植物目标(纯背景/文字图)" } msg = error_map.get(result["error_code"], f"未知错误码 {result['error_code']}") return {"error_msg": f"识别失败:{msg}"} # 成功返回 if "result" in result and len(result["result"]) > 0: top_result = result["result"][0] return { "name": top_result.get("name", "未知植物"), "score": round(top_result.get("score", 0) * 100, 2), # 转百分比 "baike_url": top_result.get("baike_url", ""), "error_msg": "" } else: return {"error_msg": "未识别到植物,请换一张清晰正面图"} except requests.exceptions.Timeout: if attempt == max_retries - 1: return {"error_msg": "请求超时,请检查网络"} time.sleep(1) # 重试前等待 1 秒 except requests.exceptions.ConnectionError: if attempt == max_retries - 1: return {"error_msg": "网络连接失败,请检查代理或防火墙"} time.sleep(1) except Exception as e: return {"error_msg": f"请求异常:{e}"} return {"error_msg": "重试后仍失败"} # 全局实例(避免重复创建) recognizer = BaiduPlantRecognizer()参数与逻辑说明:
max_retries=3:实测百度 API 在校园网偶尔 502,重试 2 次基本 recover;timeout=15:图片 Base64 编码+上传+识别,15 秒足够(比默认 30 秒更早暴露问题);- 错误码映射表
error_map直接抄百度文档,答辩时老师问「如果识别失败怎么提示用户」,你能说出 282004 是「无植物目标」,比说「报错了」专业十倍; score乘 100 并round(..., 2):返回 92.37% 而非 0.9237,符合人眼阅读习惯;baike_url保留:后续可点击跳转百度百科,展示植物简介(加分项)。
3.3 在主窗体中集成识别逻辑(防界面卡死)
回到main.py,修改start_recognition方法,必须用 QThread 防止 GUI 冻结:
# main.py 中追加 from api_client import recognizer class RecognitionWorker(QThread): """后台线程执行识别,避免阻塞 UI""" finished = pyqtSignal(dict) # 发送识别结果字典 error = pyqtSignal(str) # 发送错误信息 def __init__(self, image_path): super().__init__() self.image_path = image_path def run(self): try: result = recognizer.recognize_plant(self.image_path) self.finished.emit(result) except Exception as e: self.error.emit(str(e)) # 修改 PlantRecognitionApp 类中的 start_recognition 方法: def start_recognition(self): self.result_text.setText("⏳ 正在识别,请稍候...") self.recognize_btn.setEnabled(False) # 点击后禁用,防重复提交 # 启动后台线程 self.worker = RecognitionWorker(self.current_image_path) self.worker.finished.connect(self.on_recognition_finished) self.worker.error.connect(self.on_recognition_error) self.worker.start() def on_recognition_finished(self, result): self.recognize_btn.setEnabled(True) if result.get("error_msg"): self.result_text.setText(f"❌ {result['error_msg']}") else: # 格式化显示结果 text = f"✅ 识别成功!\n\n植物名称:{result['name']}\n置信度:{result['score']}%\n百度百科:{result['baike_url']}" self.result_text.setText(text) # 记录到历史(下一章实现) self.save_to_history(result) def on_recognition_error(self, error_msg): self.recognize_btn.setEnabled(True) self.result_text.setText(f"❌ 后台线程异常:{error_msg}")为什么必须用 QThread?
recognizer.recognize_plant()会阻塞主线程 1~3 秒(网络 I/O + Base64 编码);- 若不分离线程,界面会白屏、按钮无法响应、鼠标变沙漏,答辩时老师点「识别」后等 2 秒没反应,直接判定「程序不稳定」;
pyqtSignal是线程安全的通信方式,比全局变量可靠。
4. 构建完整交互体验:历史记录、百科摘要、本地缓存与打包发布
4.1 实现历史记录表格(带时间戳、可双击查看原图)
在main.py主窗体中添加历史记录区域:
# main.py 中,在 __init__ 方法末尾添加: # 历史记录表格 history_label = QLabel("📋 识别历史(双击行查看原图)") layout.addWidget(history_label) self.history_table = QTableWidget() self.history_table.setColumnCount(4) self.history_table.setHorizontalHeaderLabels(["时间", "植物名", "置信度", "操作"]) self.history_table.horizontalHeader().setStretchLastSection(True) self.history_table.setEditTriggers(QTableWidget.NoEditTriggers) # 禁止编辑 self.history_table.cellDoubleClicked.connect(self.on_history_double_click) layout.addWidget(self.history_table) # 初始化历史数据(从本地 JSON 读取) self.history_data = [] # [{time, name, score, image_path, baike_url}, ...] self.load_history_from_file() # 新增方法:保存单次识别到历史 def save_to_history(self, result): import datetime record = { "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "name": result["name"], "score": result["score"], "image_path": self.current_image_path, "baike_url": result["baike_url"] } self.history_data.append(record) self.save_history_to_file() self.update_history_table() def update_history_table(self): self.history_table.setRowCount(len(self.history_data)) for row, record in enumerate(self.history_data): self.history_table.setItem(row, 0, QTableWidgetItem(record["time"])) self.history_table.setItem(row, 1, QTableWidgetItem(record["name"])) self.history_table.setItem(row, 2, QTableWidgetItem(f"{record['score']}%")) # 添加「查看」按钮 view_btn = QPushButton("👁️ 查看") view_btn.clicked.connect(lambda _, r=record: self.show_original_image(r["image_path"])) self.history_table.setCellWidget(row, 3, view_btn) def show_original_image(self, image_path): from PIL import Image try: pil_img = Image.open(image_path) w, h = pil_img.size if w > 800: pil_img = pil_img.resize((800, int(h * 800 / w)), Image.Resampling.LANCZOS) qimg = QImage(pil_img.tobytes(), pil_img.width, pil_img.height, pil_img.width * 3, QImage.Format_RGB888) pixmap = QPixmap.fromImage(qimg) # 弹出新窗口显示 from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel dialog = QDialog(self) dialog.setWindowTitle(f"原图:{image_path.split('/')[-1]}") layout = QVBoxLayout(dialog) label = QLabel() label.setPixmap(pixmap.scaled(800, 600, Qt.KeepAspectRatio)) layout.addWidget(label) dialog.exec_() except Exception as e: self.result_text.setText(f"❌ 查看原图失败:{e}") def load_history_from_file(self): import os, json if os.path.exists("history.json"): try: with open("history.json", "r", encoding="utf-8") as f: self.history_data = json.load(f) except: self.history_data = [] self.update_history_table() def save_history_to_file(self): import json with open("history.json", "w", encoding="utf-8") as f: json.dump(self.history_data, f, ensure_ascii=False, indent=2)关键细节:
QTableWidget表格列宽自适应:horizontalHeader().setStretchLastSection(True)让最后一列撑满剩余空间;- 「查看」按钮用
lambda _, r=record绑定,避免闭包陷阱(否则所有按钮都显示最后一行的图); history.json用 UTF-8 编码 +ensure_ascii=False,中文植物名不乱码;- 双击行触发
cellDoubleClicked,比单击更符合用户直觉(单击常用于选中,双击才是「打开」)。
4.2 解析百度百科摘要(提升毕设深度)
在api_client.py中追加百科解析方法(需安装的lxml和beautifulsoup4已备好):
# api_client.py 中追加 from bs4 import BeautifulSoup import requests def fetch_baike_summary(self, baike_url): """ 抓取百度百科摘要(前 200 字) :param baike_url: 百科链接,如 https://baike.baidu.com/item/银杏 :return: str 摘要文本 """ if not baike_url: return "暂无百科信息" try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } res = requests.get(baike_url, headers=headers, timeout=10) res.raise_for_status() soup = BeautifulSoup(res.content, "lxml") # 百科摘要在 class="lemma-summary" 的 div 中 summary_div = soup.find("div", class_="lemma-summary") if summary_div: text = summary_div.get_text().strip().replace("\n", " ").replace(" ", " ") return text[:200] + "..." if len(text) > 200 else text else: return "百科页面结构变化,暂无法提取摘要" except Exception as e: return f"摘要获取失败:{e}" # 在 recognize_plant 返回结果中追加摘要 # 修改 api_client.py 中 recognize_plant 的成功分支: # ... # 成功返回部分追加: summary = self.fetch_baike_summary(result["baike_url"]) return { "name": top_result.get("name", "未知植物"), "score": round(top_result.get("score", 0) * 100, 2), "baike_url": top_result.get("baike_url", ""), "summary": summary, # 新增字段 "error_msg": "" }为什么值得做?
- 答辩时老师问「这个系统有什么实用价值?」,你可以说:「不仅能识别名称,还能提供植物科属、分布、用途等科普信息,辅助园林教学或野外考察」;
fetch_baike_summary用User-Agent模拟浏览器,避免被百度反爬返回 403;- 摘要截取 200 字 +
...,避免长文本撑爆QTextEdit。
4.3 打包成独立 exe(PyInstaller + 图标 + 无控制台)
确保main.py是入口文件,执行:
pip install pyinstaller pyinstaller --onefile --windowed --icon=icon.ico --name="PlantRecognizer" main.py--onefile:打包成单个 exe(毕设交付友好);--windowed:关键!避免弹出黑 cmd 窗口(答辩时很减分);--icon=icon.ico:准备一个 256x256 的.ico图标文件(可用在线转换网站生成);--name:指定输出 exe 名称。
打包后必做三件事:
- 进入
dist/目录,双击PlantRecognizer.exe测试是否能启动、识别、存历史; - 将
config.py放入dist/目录(否则运行时报ModuleNotFoundError); - 压缩
dist/整个文件夹为.zip提交,不要只交.exe(老师可能需要查配置或日志)。
注意:PyInstaller 默认不打包
PIL的图像解码器,若运行报OSError: cannot identify image file,在main.py开头加:from PIL import Image, ImageFile ImageFile.LOAD_TRUNCATED_IMAGES = True # 允许加载截断图片
5. 毕设答辩避坑指南:5 个血泪经验总结,避开导师高频质疑点
5.1 现象:点击「识别」后界面假死 3 秒,再弹出结果
原因:未用QThread,recognize_plant()在主线程同步执行,GUI 事件循环被阻塞。
解决:严格按第 3 章实现RecognitionWorker类,run()中调用 API,finished信号更新 UI。验证方法:识别时尝试拖动窗口、点击其他按钮,应全部响应。
5.2 现象:中文路径图片加载失败,报OSError: cannot open resource
原因:QPixmap构造函数不支持 Unicode 路径(Windows 特有),直接传QFileDialog返回的路径会崩。
解决:必须用PIL.Image.open()先加载,再转QImage。切记不要写QPixmap("C:/用户/图片/xxx.jpg"),这是毕设最高频翻车点。
5.3 现象:百度 API 返回{'error_code': 110, 'error_msg': 'invalid access_token'}
原因:config.py中BAIDU_API_KEY或BAIDU_SECRET_KEY复制漏字符,或百度控制台应用被停用。
解决:
- 在
api_client.py的_get_access_token方法中print(res.text)查看原始响应; - 登录百度 AI 平台,确认应用状态为「启用」,且「调用量」未超限;
- 密钥复制后用 Notepad++ 查看是否有隐藏空格(尤其开头结尾)。
5.4 现象:打包后的 exe 运行报ModuleNotFoundError: No module named 'requests'
原因:PyInstaller 未自动检测requests依赖(少见,但发生过)。
解决:显式指定隐藏导入:
pyinstaller --hidden-import=requests --hidden-import=pillow --onefile --windowed main.py5.5 现象:历史记录表格双击无反应,或弹出空白窗口
原因:show_original_image中pil_img = Image.open(image_path)读取失败(路径被删除、权限不足)。
解决:
- 在
show_original_image开头加if not os.path.exists(image_path): ...提示; - 使用
QMessageBox.warning而非self.result_text.setText,避免用户忽略; - 答辩前用测试图反复验证历史记录功能,这是导师最爱点的交互点。
6. 让毕设脱颖而出的 3 个进阶技巧:离线缓存、置信度阈值过滤、一键导出报告
6.1 本地缓存识别结果(避免重复调用 API)
百度 API 免费额度虽够,但频繁调用易触发风控(1 分钟内超 10 次可能限流)。在api_client.py中加入 MD5 缓存:
# api_client.py 中追加 import hashlib import os import json def _get_image_md5(self, image_path): """计算图片 MD5,作为缓存 key""" with open(image_path, "rb") as f: return hashlib.md5(f.read()).hexdigest() def recognize_plant_cached(self, image_path): """带缓存的识别方法""" cache_dir = "cache" os.makedirs(cache_dir, exist_ok=True) md5 = self._get_image_md5(image_path) cache_path = os.path.join(cache_dir, f"{md5}.json") if os.path.exists(cache_path): try: with open(cache_path, "r", encoding="utf-8") as f: return json.load(f) except: pass # 缓存损坏,重新识别 # 调用原方法 result = self.recognize_plant(image_path) # 缓存成功结果(失败不缓存) if not result.get("error_msg"): with open(cache_path, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) return result效果:同一张图第二次识别,毫秒级返回,答辩时老师连点 3 次「识别」也不会触发限流。
6.2 置信度阈值过滤(提升结果可信度)
植物识别常返回低置信度结果(如 45% 说「银杏」,实际是梧桐)。在on_recognition_finished中加过滤:
# main.py 中修改 def on_recognition_finished(self, result): self.recognize_btn.setEnabled(True) if result.get("error_msg"): self.result_text.setText(f"❌ {result['error_msg']}") else: # 新增:置信度低于 70% 提示用户重拍 if result["score"] < 70: self.result_text.setText( f"⚠️ 置信度较低({result['score']}%),建议:\n" "• 拍摄更清晰、正面的植物特写\n" "• 避免强光/阴影干扰\n" "• 确保画面中只有一株主要植物" ) else: text = f"✅ 识别成功!\n\n植物名称:{result['name']}\n置信度:{result['score']}%\n百度百科:{result['baike_url']}" if "summary" in result: text += f"\n\n📖 科普摘要:{result['summary']}" self.result_text.setText(text) self.save_to_history(result)答辩话术:「我加入了置信度过滤机制,当识别结果不可靠时,不强行输出,而是引导用户优化拍摄条件,这比盲目返回错误答案更符合实际应用场景。」
6.3 一键导出识别报告(PDF 格式,含图片+文字)
用reportlab库生成专业 PDF:
pip install reportlab在main.py中添加导出按钮和方法:
# main.py __init__ 中,在按钮区后添加: self.export_btn = QPushButton("📄 导出报告") self.export_btn.clicked.connect(self.export_report) btn_layout.addWidget(self.export_btn) def export_report(self): from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 from reportlab.lib.units import mm from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as RLImage from reportlab.lib.styles import getSampleStyleSheet import datetime if not hasattr(self, 'current_result') or not self.current_result: self.result_text.setText("❌ 请先完成一次识别") return # 创建 PDF filename = f"plant_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" doc = SimpleDocTemplate(filename, pagesize=A4) styles = getSampleStyleSheet() story = [] # 标题 title = Paragraph("植物识别报告", styles['Title']) story.append(title) story.append(Spacer(1, 12)) # 识别信息 info = f""" 识别时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}<br/> 植物名称:{self.current_result['name']}<br/> 置信度:{self.current_result['score']}%<br/> 百科链接:<a href="{self.current_result['baike_url']}">{self.current_result['baike_url']}</a> """ story.append(Paragraph(info, styles['Normal'])) story.append(Spacer(1, 12)) # 原图(缩放后嵌入) try: from PIL import Image as PILImage pil_img = PILImage.open(self.current_image_path) pil_img.thumbnail((400, 400), PILImage.Resampling.LANCZOS) pil_img.save("temp_report_img.jpg", "JPEG") story.append(RLImage("temp_report_img.jpg", width=400, height=300)) story.append(Spacer(1, 12)) except: story.append(Paragraph("原图嵌入失败", styles['Italic'])) # 摘要 if "summary" in self.current_result: story.append(Paragraph("📖 科普摘要:", styles['Heading3'])) story.append(Paragraph(self.current_result['summary'], styles['Normal'])) doc.build(story) import os <p> <a href="https://download.csdn.net/download/babyai996/27704110" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>