03 · 系统架构¶
本篇用 Mermaid 图把 MonitorSystem 的整体结构、数据流、类关系、调用时序一次性看清楚。 阅读前置:02 · 环境与运行(推荐先跑一遍) 下一篇:04 · 主程序与 GUI
3.1 整体架构图¶
整个系统是一个单进程多线程的桌面应用,分四层:
flowchart TB
subgraph GUI ["GUI 层 (Tkinter)"]
APP[CameraDetectionApp<br/>主窗口 + 按钮回调 + 状态显示]
end
subgraph MGR ["调度层"]
MM[ModuleManager<br/>threading.Thread × 2<br/>threading.Event × 2]
QUEUE[(status_queue<br/>queue.Queue)]
end
subgraph DET ["检测层 (子模块)"]
SWELL[swellcamera.main<br/>逐帧采集 + 推理]
WATER[watercamera.main<br/>5 秒一次采集 + 投票]
end
subgraph ML ["模型层 (PyTorch)"]
BACKBONE[EfficientNet-B3<br/>backbone]
M1[AgeClassifier × 3<br/>含水量二分类<br/>model1/2/3]
M4[AgeClassifier<br/>膨胀二分类<br/>model4]
LOADER[utils.model_loader<br/>importlib 动态加载]
end
subgraph IO ["I/O 层"]
CAM[OpenCV<br/>cv2.VideoCapture]
FILE[temp_frame.jpg<br/>临时磁盘文件]
DISPLAY[OpenCV imshow<br/>+ PIL 中文绘制]
end
APP -- "start/stop" --> MM
MM -- "Thread 启动" --> SWELL
MM -- "Thread 启动" --> WATER
SWELL -- "callback(status)" --> QUEUE
WATER -- "callback(status)" --> QUEUE
QUEUE -- "100ms 轮询" --> APP
SWELL --> CAM
WATER --> CAM
CAM --> FILE
FILE --> M1
FILE --> M4
SWELL --> M4
WATER --> LOADER
LOADER --> M1
M1 --> BACKBONE
M4 --> BACKBONE
SWELL --> DISPLAY
WATER --> DISPLAY
几个关键设计点:
- 纯线程模型:ModuleManager 用
threading.Thread启动子模块,不用 subprocess。这意味着子模块的 OpenCV 窗口与主进程共享 GIL/资源,也意味着子模块挂了能拖垮主程序。 - temp_frame.jpg 中介:所有推理都走「OpenCV 抓帧 → 写磁盘 → PIL 读回 → PyTorch」的路径。设计粗糙但工作。
- 状态回调队列:子模块状态变化通过
module_status_callback入队,GUI 100ms 轮询出队 — 经典的 Tkinter 线程间通信模式。
3.2 膨胀检测数据流¶
flowchart LR
A[USB 摄像头<br/>index=1<br/>1280×720] -->|cv2.VideoCapture.read| B[OpenCV BGR frame]
B -->|cv2.imwrite| C[temp_frame.jpg]
C -->|Image.open| D[PIL RGB Image]
D -->|transforms.Resize 300×300<br/>+ Normalize ImageNet] E[Tensor 1×3×300×300]
E -->|EfficientNet-B3<br/>features| F[Tensor 1×1536]
F -->|model4._fc<br/>Dropout-Linear-ReLU-BN-Dropout-Linear| G[logit ∈ ℝ]
G -->|sigmoid| H[probability ∈ 0~1]
H -->|"> 0.5 ? 不膨胀 : 膨胀"| I[原始 class_names]
I -->|label_mapping 对调| J["显示的 class<br/>⚠️ 已对调"]
J -->|PIL ImageDraw<br/>simhei.ttf| K[中文标注]
K -->|cv2.imshow| L[OpenCV 窗口]
B -.每帧重复.- A
特别注意:
- 推理频率 = 摄像头帧率 ≈ 30 fps,每秒约 30 次磁盘写
- 输出标签经过
swellcamera.py:48-53反转,模型说"膨胀"显示"不膨胀"
3.3 含水量检测数据流(含三模型投票)¶
flowchart TB
A[USB 摄像头<br/>index=1<br/>640×480 / 30fps] -->|cv2.VideoCapture.read<br/>每帧| B[OpenCV frame]
B --> T{距上次推理<br/>≥ 5 秒?}
T -->|否| DISP
T -->|是| C[cv2.imwrite<br/>temp_frame.jpg]
C --> M1[model1.predict<br/>35-40 vs 30-35]
C --> M2[model2.predict<br/>40-45 vs 35-40]
C --> M3[model3.predict<br/>40-45 vs 30-35]
M1 --> F1{|p₀ - p₁| ≥ 0.1?}
M2 --> F2{|p₀ - p₁| ≥ 0.1?}
M3 --> F3{|p₀ - p₁| ≥ 0.1?}
F1 -->|是| V1["× 0.91"]
F2 -->|是| V2["× 0.95"]
F3 -->|是| V3["× 0.90"]
F1 -->|否| SKIP1[跳过]
F2 -->|否| SKIP2[跳过]
F3 -->|否| SKIP3[跳过]
V1 --> SUM[加权累加<br/>vote_results<br/>defaultdict by class]
V2 --> SUM
V3 --> SUM
SUM --> NORM[归一化<br/>v / total_weight]
NORM --> ARGMAX[argmax 取最终类别]
ARGMAX --> RES[final_class ∈<br/>30-35 / 35-40 / 40-45]
RES --> DISP[PIL 绘制<br/>含水量 + 置信度]
DISP --> CV[cv2.imshow<br/>显示画面]
CV -.下一帧.- A
算法本质:把"三分类含水量"问题拆成 3 个二分类器(C(3,2)=3 种两两组合),用加权投票合成最终结果。
关键参数(watercamera.py:14-18):
self.model_weights = {
'1': 0.91, # model1 (30-35 vs 35-40)
'2': 0.95, # model2 (35-40 vs 40-45)
'3': 0.90 # model3 (30-35 vs 40-45)
}
权重是写死的常数,来源应该是 gxl 训练时各模型的验证集准确率。
3.4 类关系图¶
classDiagram
class CameraDetectionApp {
+root: tk.Tk
+module_manager: ModuleManager
+status_queue: queue.Queue
+swell_stats: dict
+water_stats: dict
+start_swell_detection()
+stop_swell_detection()
+toggle_water_detection()
+stop_all_detections()
+module_status_callback(type, status)
+start_status_updater()
+start_timer()
+exit_app()
}
class ModuleManager {
+swell_thread: Thread
+water_thread: Thread
+swell_stop_event: Event
+water_stop_event: Event
+swell_running: bool
+water_running: bool
+start_swell_module(callback)
+stop_swell_module()
+start_water_module(callback)
+stop_water_module()
+stop_all_modules()
}
class RealTimeExpansionClassifier {
+classifier: ExpansionClassifier
+label_mapping: dict
+font_path: str
+stats: defaultdict
+process_frame(frame)
+draw_chinese_text(frame, text, pos)
+display_result(frame, result)
}
class WaterContentClassifier {
+model_weights: dict
+class_names: dict
+stats: defaultdict
+predict_single_image(path)
+process_frame(frame)
+display_result(frame, result)
}
class AgeClassifier_model4 {
+device: torch.device
+class_names: ["膨胀","不膨胀"]
+img_size: 300
+model: EfficientNet
+__init__(model_path)
+predict(image_path) dict
}
class AgeClassifier_model123 {
+device: torch.device
+class_names: list[str,str]
+img_size: 300
+model: EfficientNet
+__init__()
+predict(image_path) tuple
}
class model_loader {
<<module>>
+MODEL_MAP: dict
+load_model(model_id) module
}
CameraDetectionApp --> ModuleManager : owns
ModuleManager ..> RealTimeExpansionClassifier : threading.Thread runs swellcamera.main()
ModuleManager ..> WaterContentClassifier : threading.Thread runs watercamera.main()
RealTimeExpansionClassifier --> AgeClassifier_model4 : composes
WaterContentClassifier ..> model_loader : load_model('1'/'2'/'3')
model_loader ..> AgeClassifier_model123 : importlib
特别注意类名混乱:所有 4 个模型类都叫 AgeClassifier(年龄分类器,明显是从其他项目复制粘贴),但通过不同模块路径区分。在 swellcamera 里被重命名为 ExpansionClassifier(swellcamera.py:24)。详见 06 · 模型与推理。
3.5 时序图:用户点「启动膨胀检测」的完整调用栈¶
sequenceDiagram
autonumber
actor 用户
participant Btn as swell_button
participant App as CameraDetectionApp
participant MM as ModuleManager
participant T as swell_thread
participant SW as swellcamera.main
participant M4 as model4.AgeClassifier
participant CV as OpenCV
participant Q as status_queue
用户->>Btn: 点击
Btn->>App: toggle_swell_detection()
App->>App: 检查 is_swell_running()=False
App->>App: start_swell_detection()
App->>MM: start_swell_module(callback)
MM->>MM: swell_stop_event.clear()
MM->>T: Thread(target=run_swell).start()
Note over T: 新线程开始运行
T->>SW: import swellcamera; swellcamera.main()
SW->>M4: AgeClassifier(model_path)
M4->>M4: 加载 best_model4.pth (~3s)
SW->>CV: cv2.VideoCapture(1)
T->>Q: callback("swell","started")
Q-->>App: 100ms 后被 check_status() 出队
App->>Btn: text="停止膨胀检测", bg="#E55982"
loop 每帧(约 33ms)
CV->>SW: ret, frame = cap.read()
SW->>SW: cv2.imwrite("temp_frame.jpg")
SW->>M4: predict("temp_frame.jpg")
M4-->>SW: {class, probabilities, confidence}
SW->>SW: label_mapping 对调
SW->>CV: cv2.imshow(annotated_frame)
CV->>SW: cv2.waitKey(1) & 0xFF
end
用户->>CV: 按 'q' 键
SW->>CV: cap.release(); destroyAllWindows()
SW-->>T: main() 返回
T->>Q: callback("swell","stopped")
Q-->>App: 出队 → 按钮恢复
注意步骤 4(start_swell_detection)与步骤 9(callback("swell","started"))之间,模型加载耗时约 3 秒(CPU 推理时),所以用户从点击按钮到 OpenCV 窗口出现会有可感知的延迟。
3.6 进程/线程边界图¶
flowchart TB
subgraph Proc ["单进程 python main.py"]
direction TB
subgraph MainThread ["主线程 (Tkinter mainloop)"]
T1[GUI 事件循环]
T2[after(100) 状态轮询]
T3[after(1000) 计时器]
end
subgraph SwellThread ["swell_thread (daemon)"]
T4[swellcamera.main]
T5[OpenCV 窗口 1]
end
subgraph WaterThread ["water_thread (daemon)"]
T6[watercamera.main]
T7[OpenCV 窗口 2]
end
QQ[(status_queue<br/>thread-safe)]
STATE[swell_running / water_running<br/>布尔标志]
EV[stop_event × 2<br/>⚠️ 设置但未被检查]
T4 -- "通过 callback" --> QQ
T6 -- "通过 callback" --> QQ
T2 -- "get_nowait" --> QQ
T1 -- "读写" --> STATE
T1 -- "set 但子线程不读" --> EV
end
线程共享的状态:
status_queue(线程安全,正确用法)swell_running / water_running(普通 bool,多线程下应该加锁,但代码没加)swell_stop_event / water_stop_event(线程安全的 Event,但子线程没检查它,是 P0-1 bug 的根源)
3.7 文件依赖图¶
flowchart LR
MAIN[main.py] --> SC[swellcamera/__init__.py]
MAIN --> WC[watercamera/__init__.py]
SC --> SCM[swellcamera.swellcamera]
WC --> WCM[watercamera.watercamera]
SCM --> M4F[models.model4]
WCM --> LOADER[utils.model_loader]
WCM --> SIMHEI[simhei.ttf<br/>当前工作目录]
LOADER -.importlib.-> M1F[models.model1]
LOADER -.importlib.-> M2F[models.model2]
LOADER -.importlib.-> M3F[models.model3]
M1F --> PTH1[best_model1.pth]
M2F --> PTH2[best_model2.pth]
M3F --> PTH3[best_model3.pth]
M4F --> PTH4[best_model4.pth]
SCM --> FONT[fonts/simhei.ttf]
MAIN --> ICO[logo.ico]
暗坑:
utils/model_loader.py只被watercamera使用,main.py自己不直接导入 model_loader 也不直接导入 models(main.py 全文)。models 是被两个子模块导入的。
3.8 各篇文档导航¶
下一步该读哪一篇?
flowchart LR
HERE[本篇<br/>03 架构] --> A04[04 主程序<br/>GUI 与 ModuleManager]
A04 --> A05[05 检测模块<br/>swellcamera + watercamera]
A05 --> A06[06 模型推理<br/>4 个 AgeClassifier]
A06 --> A07[07 打包<br/>PyInstaller]
A07 --> A08[08 问题清单<br/>P0/P1/P2]
A08 --> A09[09 开发指南]