跳转至

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

几个关键设计点

  1. 纯线程模型:ModuleManager 用 threading.Thread 启动子模块,不用 subprocess。这意味着子模块的 OpenCV 窗口与主进程共享 GIL/资源,也意味着子模块挂了能拖垮主程序
  2. temp_frame.jpg 中介:所有推理都走「OpenCV 抓帧 → 写磁盘 → PIL 读回 → PyTorch」的路径。设计粗糙但工作。
  3. 状态回调队列:子模块状态变化通过 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 里被重命名为 ExpansionClassifierswellcamera.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 开发指南]