08 · 问题与改进清单¶
本篇是
调研报告.md的升级版: - 沿用原有 P0/P1/P2 分级 - 每个问题补充代码定位 + 最小修复 patch + 副作用分析 - 加入 Windows 验证回合后发现的新问题 - 末尾给出修复执行顺序与风险矩阵用途:改 bug 时的工作清单。 前置:建议先读 04 · 主程序与 GUI、05 · 检测模块、06 · 模型与推理。 下一篇:09 · 开发指南
8.0 问题清单速览¶
| 编号 | 严重度 | 标题 | 影响 | 工作量 |
|---|---|---|---|---|
| P0-1 | 🔴 | 模块停止机制失效 | 用户点停止后摄像头仍占用,GUI 状态不同步 | 1h |
| P0-2 | 🔴 | 每帧写入临时文件 | 性能瓶颈 + 双模块文件冲突 | 0.5h |
| P0-3 | 🔴 | 摄像头索引硬编码 | 无外接 USB 摄像头的电脑直接崩 | 0.5h |
| P0-4 | 🔴 NEW | swellcamera 标签对调 | 推理结果可能与训练含义反 | 需向 gxl 确认 |
| P1-1 | 🟡 | 模型类名错误(都叫 AgeClassifier) | 可读性差 | 0.5h |
| P1-2 | 🟡 | 模型接口不统一 | model4 vs model1/2/3 调用方式不一样 | 1h |
| P1-3 | 🟡 | watercamera 字体无 fallback | Linux/macOS 中文显示崩 | 0.5h |
| P1-4 | 🟡 | torch.load 无 weights_only | PyTorch 2.6+ 加载失败 + 安全风险 | 0.5h |
| P1-5 | 🟡 NEW | watercamera 注释与 model 类别顺序不一致 | 阅读时误导 | 5min |
| P2-1 | 🟢 | 裸 except: pass |
调试困难 | 0.5h |
| P2-2 | 🟢 | 无日志系统 | 全用 print() | 1h |
| P2-3 | 🟢 | 模型无延迟加载 | 启动慢 | 1h |
| P2-4 | 🟢 | model*.py 测试代码硬编码 Windows 路径 | 泄漏开发者信息 + 测试不可复用 | 0.2h |
| P2-5 | 🟢 | requirements.txt 大部分被注释 | 新用户不知道装啥 | 0.2h |
| P2-6 | 🟢 | .idea/ 目录被打包 | exe 体积增加 + 泄漏 IDE 信息 | 0.1h |
| P2-7 | 🟢 | tempCodeRunnerFile.py 残留 | 临时文件污染 | 0.1h |
| P2-8 | 🟢 | 无训练代码 | 无法复现训练 | 不可修复(需 gxl 提供) |
| P2-9 | 🟢 NEW | spec 引用不存在的 build.bat | 打包警告 | 0.1h |
| P2-10 | 🟢 NEW | add_hidden_imports.py 命名不符合 hook 规范 | 文件根本没被加载 | 0.1h |
| P2-11 | 🟢 NEW | ModuleManager 的 swell_process/water_process 字段死代码 | 误导阅读 | 0.1h |
| P2-12 | 🟢 NEW | WaterContentClassifier.class_names 字段未被使用 | 死数据 | 0.1h |
| P2-13 | 🟢 NEW | efficientnet_b3_pytorch.pth 文件未被加载 | 多打包 48MB | 0.1h |
| P2-14 | 🟢 NEW | model1/2/3 的 ReLU 被替换为 Identity | 推理与训练不一致 | 需向 gxl 确认 |
预估总工作量:约 7-9 小时(不含 P0-4、P2-14 需要 gxl 确认的部分)
8.1 P0 严重问题¶
🔴 P0-1:模块停止机制失效¶
位置:
- main.py:112-120(stop_swell_module)
- main.py:165-173(stop_water_module)
- swellcamera.py:179-196(主循环不查 event)
- watercamera.py:183-208(主循环不查 event)
Bug 链条:
sequenceDiagram
participant 用户
participant App as CameraDetectionApp
participant MM as ModuleManager
participant SW as swellcamera.main
用户->>App: 点"停止膨胀检测"
App->>MM: stop_swell_module()
MM->>MM: swell_stop_event.set()
MM->>MM: swell_running = False
MM-->>App: return True
App->>App: 按钮文字变"启动膨胀检测"
App->>用户: 弹窗"请按 q 键"
Note over SW: ⚠️ swellcamera 主循环根本不查 swell_stop_event
SW->>SW: while True 继续跑
SW->>SW: 摄像头继续占用
SW->>SW: OpenCV 窗口继续显示
根本原因:threading.Event 是双向协议——设置方设了,读取方必须主动 .is_set() 检查才有用。子模块完全没参与这个协议。
最小修复 patch:
# 1. 改 swellcamera.py:160-196 让 main() 接受 stop_event 参数
def main(stop_event=None):
classifier = RealTimeExpansionClassifier()
camera_index = 1
cap = cv2.VideoCapture(camera_index)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
if not cap.isOpened():
print(f"无法打开摄像头,请检查索引{camera_index}是否正确")
return
print("实时膨胀分类系统已启动 (按Q退出)...")
while True:
# ★ 新增:检查停止事件
if stop_event is not None and stop_event.is_set():
print("收到停止信号,退出膨胀检测")
break
ret, frame = cap.read()
if not ret:
break
result = classifier.process_frame(frame)
frame = classifier.display_result(frame, result)
cv2.imshow('Real-time Expansion Detection', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
对 watercamera 做同样修改:watercamera.py:151 的 def main(): → def main(stop_event=None):,循环里加同样的 break 检查。
副作用:无。已有的 q 键退出仍然工作。
🔴 P0-2:每帧写入临时文件¶
位置:
- swellcamera.py:78-80
- watercamera.py:82-83
temp_path = "temp_frame.jpg"
cv2.imwrite(temp_path, frame)
# 然后 classifier.predict(temp_path) 又会 Image.open(temp_path)
两个问题:
1. 性能:30 fps 摄像头 = 每秒 30 次磁盘写 + 30 次磁盘读 + 30 次 delete。固态硬盘还好,机械盘明显卡
2. 文件冲突:两个模块都用 "temp_frame.jpg",同时运行会互相覆盖
最小修复 patch:
# swellcamera.py:76-111
def process_frame(self, frame):
"""处理单帧图像并返回分类结果(标签已对调)"""
try:
# ★ 内存转换替代磁盘 I/O
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(frame_rgb)
# 但 classifier.predict 接受路径,需要改成接受 PIL Image
result = self.classifier.predict_pil(pil_image)
# ... 后续逻辑不变
同时改 model4.py:65-83 加一个 predict_pil 方法:
def predict(self, image_path):
"""原有接口保留"""
img = Image.open(image_path).convert('RGB')
return self.predict_pil(img)
def predict_pil(self, pil_image):
"""新增:直接接受 PIL Image"""
if pil_image.mode != 'RGB':
pil_image = pil_image.convert('RGB')
img_tensor = self.transform(pil_image).unsqueeze(0)
return self._predict(img_tensor)
对 model1/2/3 + watercamera 做对称修改。
副作用:
- API 增量变化(新增方法),向后兼容
- 如果要彻底改造,可以让 predict() 直接接受 Union[str, Image]
🔴 P0-3:摄像头索引硬编码¶
位置:
- swellcamera.py:165 camera_index = 1
- watercamera.py:156 camera_index = 1
影响:
- 笔记本自带摄像头通常是 0,外接 USB 是 1。设 1 是 gxl 的开发环境
- 只有一台摄像头(笔记本默认)的电脑会 cap.isOpened() == False
- 没有外接摄像头的台式机直接崩
最小修复 patch:
# swellcamera.py:160-175
def main(stop_event=None, camera_index=None):
classifier = RealTimeExpansionClassifier()
# ★ 智能 fallback:先试参数,再试 0,再试 1
candidate_indices = [camera_index] if camera_index is not None else [0, 1, 2]
cap = None
for idx in candidate_indices:
if idx is None:
continue
cap_try = cv2.VideoCapture(idx)
if cap_try.isOpened():
cap = cap_try
actual_index = idx
print(f"已打开摄像头索引 {idx}")
break
cap_try.release()
if cap is None or not cap.isOpened():
print(f"无法打开任何摄像头(试过 {candidate_indices})")
return
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
# ... 后续不变
副作用:增加启动时间(最坏情况扫 3 个索引),但仅一次。
更完善的方案:在 GUI 加摄像头选择下拉框,让用户主动选——但这就是 P2 优化了。
🔴 P0-4 (NEW):swellcamera 标签对调¶
位置:swellcamera.py:48-53
问题:model4 实际输出"膨胀"时,会被强制改成"不膨胀"再显示。
可能的成因(推测): 1. 训练时数据标签错位("膨胀"目录被打成 label=0 但应该是 label=1) 2. gxl 推理验证时发现结果"看起来反了",懒得重训,加 mapping 修正
风险: - 如果 gxl 当时真的搞反了标签,这个 mapping 是必要的 hack,删了会让结果变反 - 如果 gxl 误以为搞反了(其实没反),那 mapping 才是 bug,删了反而对
必须做的事:向 gxl 当面确认。具体问法:
「水气模型 model4 的训练标签里,'膨胀'这个类对应的是 label=0 还是 label=1?数据集里 '膨胀结束/' 这个目录下的图片,标签是 0 还是 1?」
「swellcamera.py 第 48-53 行的 label_mapping,是因为模型输出反了所以加了 hack,还是别的原因?」
临时建议:在修复之前保留 label_mapping——它至少让 gxl 验证时的指标对得上。
8.2 P1 中等问题¶
🟡 P1-1:模型类名都叫 AgeClassifier¶
位置:model1.py:18 / model2.py:19 / model3.py:18 / model4.py:18
问题:明显是从某个年龄分类项目复制改来的,没改类名。
修复:
# model1.py / model2.py / model3.py
class WaterContentBinaryClassifier: # 或 WaterModel{N}
...
# model4.py
class ExpansionClassifier:
...
副作用:
- 需要同步改 swellcamera.py:24 的 import alias
- watercamera.py:40 的 model_module.AgeClassifier() 也要改
- utils/model_loader.py 可能需要返回类而不是模块
工作量:0.5h,但需小心改 import 链。
🟡 P1-2:predict() 接口不统一¶
位置:见 06 · 模型与推理 § 6.5。
目标:统一所有 predict() 都返回相同结构:
@dataclass
class PredictionResult:
pred_label: str # 预测类别名
confidence: float # 主类置信度
probabilities: dict[str, float] # 各类别概率
或返回 dict:
修复:把 model1/2/3 改成与 model4 一样的 dict 返回。watercamera 调用方相应改造。
副作用:所有调用方都要改。这是与 P1-1 一起改最划算(反正都要动)。
🟡 P1-3:watercamera 字体无 fallback¶
位置:watercamera.py:113
问题:相对路径,依赖工作目录有 simhei.ttf。
修复:模仿 swellcamera 的字体搜索:
def _get_font_path():
candidates = [
get_resource_path(os.path.join("fonts", "simhei.ttf")),
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/msyh.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/System/Library/Fonts/PingFang.ttc", # macOS
]
for c in candidates:
if c and os.path.exists(c):
return c
return None
# 在 display_result 里:
font_path = _get_font_path()
if font_path:
font = ImageFont.truetype(font_path, 32)
else:
font = ImageFont.load_default()
🟡 P1-4:torch.load 无 weights_only¶
位置:所有 4 个 model*.py 的 torch.load 调用:
- model1.py:40
- model2.py:41
- model3.py:40
- model4.py:52
问题:
1. PyTorch ≥2.6 默认 weights_only=True,但旧 .pth 文件可能包含非张量数据(如 Optimizer state),会报错
2. weights_only=False 模式下,torch.load 走 pickle 反序列化,理论上可以执行任意代码——如果 .pth 来自不可信源就有 RCE 风险
修复:
# 改成显式声明(旧 PyTorch 也兼容)
model.load_state_dict(torch.load(
model_weight_path,
map_location=self.device,
weights_only=True # 强制只加载权重
))
如果 weights_only=True 加载失败(说明 .pth 里有非权重数据),就 fallback:
try:
state = torch.load(path, map_location=device, weights_only=True)
except (RuntimeError, AttributeError):
# 旧 .pth 可能含 Optimizer state,回退
state = torch.load(path, map_location=device, weights_only=False)
model.load_state_dict(state)
🟡 P1-5 (NEW):watercamera 注释与 model 类别顺序不一致¶
位置:watercamera.py:14-18 与 model{1,2,3}.py:23
| 注释(watercamera) | 实际(model.class_names) |
|---|---|
| Model1 (30-35 vs 35-40) | ["35-40", "30-35"] |
| Model2 (35-40 vs 40-45) | ["40-45", "35-40"] |
| Model3 (30-35 vs 40-45) | ["40-45", "30-35"] |
影响:不影响运行(投票走 dict 键名),但阅读时让人困惑。
修复:要么改注释,要么改 model.class_names 的顺序(前者更省事)。
# watercamera.py:14-18
self.model_weights = {
'1': 0.91, # Model1 (35-40 vs 30-35)
'2': 0.95, # Model2 (40-45 vs 35-40)
'3': 0.90 # Model3 (40-45 vs 30-35)
}
8.3 P2 轻微问题¶
🟢 P2-1:裸 except: pass¶
位置:main.py:800 (exit_app 末尾)
修复:改成具体异常:
🟢 P2-2:无日志系统¶
位置:全局,所有 print() 调用。
修复:引入 logging:
# 在 main.py 顶部
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(name)s] %(levelname)s: %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('monitor.log', encoding='utf-8')
]
)
logger = logging.getLogger(__name__)
# 然后把所有 print() 改成 logger.info() / .error()
🟢 P2-3:模型无延迟加载¶
问题:主程序启动时不会加载模型,只在用户点"启动检测"按钮时才加载。首次点击会卡 3 秒(CPU)。
轻度优化:在主程序启动后异步预热模型(loading 一次):
# main.py:CameraDetectionApp.__init__ 末尾
threading.Thread(target=self._preload_models, daemon=True).start()
def _preload_models(self):
"""后台预加载模型,避免首次点击的延迟"""
try:
from models.model4 import AgeClassifier
_ = AgeClassifier('models/best_model4.pth')
# 加载 model1/2/3 同理
except Exception as e:
logger.warning(f"模型预热失败: {e}")
🟢 P2-4:测试代码硬编码 Windows 路径¶
位置:model1.py:90 / model2.py:99 / model3.py:90 / model4.py:109
# model4.py:108-117
classifier = AgeClassifier(
model_path=r"C:\Users\lenovo\Desktop\ovo策略尝试\models\best_model4.pth"
)
问题:
- 路径不存在,无法运行
- 泄漏开发者本地路径(C:\Users\lenovo 暗示 gxl 用的联想电脑、E:\圆盘干燥机智能化运行图片 透露原始数据集命名)
修复:改用 argparse 或环境变量:
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--image', required=True, help='输入图片路径')
parser.add_argument('--model', default='models/best_model4.pth')
args = parser.parse_args()
classifier = AgeClassifier(args.model)
result = classifier.predict(args.image)
print(result)
🟢 P2-5:requirements.txt 大部分被注释¶
位置:requirements.txt:14-21
问题:让新用户以为这些是可选的,实际是必需的。
修复:取消注释 + 补全所有真实依赖(详见 02 · 环境与运行 § 2.2)。
🟢 P2-6:.idea/ 被打包¶
修复:
1. 加 .gitignore:
- 删除现有
.idea/:
- 在
CameraMonitorSystem.spec的 glob 部分加过滤:
for f in glob.glob('models/**/*', recursive=True):
if os.path.isfile(f) and '.idea' not in f and '__pycache__' not in f:
datas.append((f, ...))
🟢 P2-7:tempCodeRunnerFile.py 残留¶
位置:models/tempCodeRunnerFile.py(64 字节)
修复:直接删除并加进 .gitignore。这是 VS Code 用"Run Selection"时生成的临时文件。
🟢 P2-8:无训练代码¶
问题:仓库只有推理,无法复现训练。
修复:不可修复,只能找 gxl 要训练脚本(09 · 开发指南 § 9.2 提供了不重训也能换模型的方案)。
🟢 P2-9 (NEW):spec 引用不存在的 build.bat¶
位置:CameraMonitorSystem.spec:26
影响:打包时 PyInstaller 警告找不到该文件,但不会致命。
修复:要么删掉这一行,要么补建 build.bat:
@echo off
echo Building CameraMonitorSystem...
pyinstaller --clean CameraMonitorSystem.spec
echo Done. Output in dist/CameraMonitorSystem/
pause
🟢 P2-10 (NEW):add_hidden_imports.py 命名不符合 hook 规范¶
位置:add_hidden_imports.py
问题:PyInstaller 只自动加载命名为 hook-*.py 的文件,本文件名为 add_hidden_imports.py,实际上没被加载。
修复:要么重命名为 hook-pywin32.py,要么把内容并入 spec:
# CameraMonitorSystem.spec 顶部
from PyInstaller.utils.hooks import collect_submodules
hiddenimports = (
collect_submodules('pywintypes')
+ collect_submodules('win32api')
+ collect_submodules('win32ctypes')
+ [...] # 现有 hiddenimports
)
🟢 P2-11 (NEW):ModuleManager 死字段¶
位置:main.py:60-61
问题:通篇没有再用到这两个字段。
修复:删除即可,或加注释说明"曾打算用 subprocess,后改 threading"。
🟢 P2-12 (NEW):WaterContentClassifier.class_names 死字段¶
位置:watercamera.py:20-24
问题:只赋值不读取,L52-57 唯一一处使用 self.class_names[model_id] 是写进 all_probs 用作调试输出。可以保留也可以删。
🟢 P2-13 (NEW):efficientnet_b3_pytorch.pth 文件未被加载¶
位置:models/efficientnet_b3_pytorch.pth(48 MB)
问题:代码用 EfficientNet.from_name(...) 而非 from_pretrained,这个备份权重从未被加载,但被打包进 exe 多占 48 MB。
修复:直接删除文件,或在 spec 里过滤:
for f in glob.glob('models/**/*', recursive=True):
if os.path.isfile(f) and 'efficientnet_b3_pytorch' not in f:
datas.append((f, ...))
🟢 P2-14 (NEW):model1/2/3 的 ReLU 被替换为 Identity¶
位置:model1.py:31-38 等
问题:分类头中本应是 ReLU 的位置(_fc.2)被替换为 Identity():
nn.Identity(), # _fc.0 占位(原为 Dropout)
nn.Linear(num_features, 256), # _fc.1
nn.Identity(), # _fc.2 占位(原为 ReLU?) ← 这里
nn.BatchNorm1d(256), # _fc.3
nn.Identity(), # _fc.4 占位(原为 Dropout)
nn.Linear(256, 1) # _fc.5
影响: - 推理时少了一次非线性激活 - 训练时如果用了 ReLU,那现在的推理结果不与训练时严格一致 - 但 BatchNorm 之后激活幅度不大,差异可能很小
修复:需向 gxl 确认训练时 _fc.2 是什么层。如果确实是 ReLU,则改为:
nn.Identity(),
nn.Linear(num_features, 256),
nn.ReLU(), # 恢复
nn.BatchNorm1d(256),
nn.Identity(),
nn.Linear(256, 1)
8.4 风险优先级矩阵¶
quadrantChart
title 风险矩阵(影响 × 工作量)
x-axis "工作量小" --> "工作量大"
y-axis "影响小" --> "影响大"
quadrant-1 重要紧急(先做)
quadrant-2 长期投入
quadrant-3 可忽略
quadrant-4 顺手做
"P0-1 停止机制": [0.4, 0.95]
"P0-2 临时文件": [0.2, 0.85]
"P0-3 摄像头索引": [0.2, 0.9]
"P0-4 标签对调": [0.1, 0.7]
"P1-1 类名混乱": [0.3, 0.5]
"P1-2 接口不统一": [0.5, 0.5]
"P1-3 字体fallback": [0.2, 0.4]
"P1-4 weights_only": [0.2, 0.45]
"P1-5 注释错位": [0.05, 0.15]
"P2-1 except pass": [0.2, 0.2]
"P2-2 日志系统": [0.5, 0.3]
"P2-3 延迟加载": [0.4, 0.25]
"P2-4 测试路径": [0.1, 0.2]
"P2-5 requirements": [0.1, 0.2]
"P2-6 idea清理": [0.05, 0.1]
"P2-7 临时文件": [0.05, 0.05]
"P2-13 48MB备份": [0.05, 0.15]
8.5 推荐修复执行顺序¶
第一阶段:让结题时能演示(必做,~3h)¶
- P0-3 摄像头索引可配置(30 min)← 演示时拿到任何电脑都能跑
- P2-5 修 requirements.txt(10 min)← 让老师能
pip install -r复现环境 - P0-1 修停止机制(1h)← 演示时点停止能真的停
- P0-2 消除临时文件(30 min)← 解决双模块冲突
- P2-6/P2-7 清理 .idea/ 和 tempCodeRunnerFile.py(10 min)← 仓库面貌
- P1-3 watercamera 字体 fallback(30 min)← 跨平台兼容
第二阶段:让代码可读(可选,~2h)¶
- P1-1 + P1-2 类名 + 接口统一(一起改,1.5h)
- P1-5 修注释(5 min)
- P2-11/P2-12 删死代码(10 min)
第三阶段:长期改进(不影响结题)¶
- P0-4 + P2-14:向 gxl 确认标签对调与 ReLU 替换的真实原因
- P1-4 weights_only:升级 PyTorch 时再改
- P2-2 logging 系统:从 print 迁移
- P2-3 延迟加载:优化启动速度
不要做的事¶
- 不要在结题前重训模型(来不及,也无必要——gxl 的模型够用)
- 不要大重构(如把 model1/2/3 合并、把 ModuleManager 改成 subprocess)——会引入新 bug
- 不要改动模型权重文件(.pth 是黑盒)
8.6 修复前/后对比示例:P0-1¶
当前行为:
修复后行为:
用户点"停止膨胀检测"
├─ GUI 按钮变"启动膨胀检测"
├─ swell_stop_event.set()
├─ swellcamera.main() 主循环检测到 → break
├─ cap.release() + destroyAllWindows()
└─ OpenCV 窗口关闭 ← 真的停了
弹窗"请按 q 键" 这句话也可以从 main.py:649 删掉(或改成"已停止")。