forked from masonhuang/KNEO-Academy
570 lines
23 KiB
Markdown
570 lines
23 KiB
Markdown
# Design Doc: KNEO Academy(Innovedus AI Playground)v2.0
|
||
|
||
**作者**:Architect Agent
|
||
**狀態**:Draft(從既有程式碼反向整理)
|
||
**日期**:2026-04-04
|
||
**產品版本**:v2.0
|
||
|
||
---
|
||
|
||
## 1. 背景與目標
|
||
|
||
### 1.1 產品概述
|
||
|
||
KNEO Academy(對外名稱:Innovedus AI Playground)是一套 Windows 桌面應用程式,讓擁有 Kneron NPU USB Dongle 的使用者能夠在本機端執行 Edge AI 推論,無需雲端服務、無需撰寫程式碼。
|
||
|
||
**核心使用情境**:
|
||
- 業務展示:即插即用,開箱展示 AI 推論能力
|
||
- 研發驗證:快速測試 Kneron NPU 在特定任務的推論效果
|
||
- 客戶自訂:上傳自訂 `.nef` 模型進行推論測試
|
||
|
||
### 1.2 設計目標
|
||
|
||
- **即插即用**:連接 Kneron dongle 後,數秒內可開始推論
|
||
- **Plugin 化架構**:透過目錄結構 + `config.json` + `script.py` 新增模型,不需修改主程式
|
||
- **Thread 隔離**:UI 執行緒與推論執行緒完全分離,確保畫面不卡頓
|
||
- **PyInstaller 相容**:可打包為單一可執行檔,方便分發
|
||
|
||
### 1.3 非目標(Out of Scope)
|
||
|
||
- 雲端推論或 API 服務
|
||
- 多裝置同時推論
|
||
- 行動平台支援
|
||
|
||
---
|
||
|
||
## 2. 整體架構概覽
|
||
|
||
### 2.1 設計模式
|
||
|
||
本應用程式採用 **MVC(Model-View-Controller)** 架構,搭配 Qt 的 **Signal/Slot** 機制實現跨執行緒通訊。
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Views(呈現層) │
|
||
│ SelectionScreen LoginScreen MainWindow UtilitiesScreen │
|
||
│ QWidget QWidget QWidget QWidget │
|
||
└────────────────────────────┬────────────────────────────────────┘
|
||
│ Signal/Slot
|
||
┌────────────────────────────▼────────────────────────────────────┐
|
||
│ AppController(main.py) │
|
||
│ QStackedWidget — 頁面路由中樞 │
|
||
└──────┬──────────────────┬───────────────────┬───────────────────┘
|
||
│ │ │
|
||
┌──────▼──────┐ ┌────────▼────────┐ ┌──────▼──────────┐
|
||
│ Device │ │ Inference │ │ Media │
|
||
│ Controller │ │ Controller │ │ Controller │
|
||
│ (裝置管理) │ │ (推論管理) │ │ (相機/媒體) │
|
||
└──────┬──────┘ └───────┬─────────┘ └──────┬──────────┘
|
||
│ │ │
|
||
┌──────▼──────┐ ┌───────▼─────────┐ ┌──────▼──────────┐
|
||
│ device_ │ │ InferenceWorker │ │ VideoThread │
|
||
│ service.py │ │ Thread │ │ (QThread) │
|
||
│ (kp SDK) │ │ (QThread) │ │ (OpenCV 擷取) │
|
||
└─────────────┘ └────────┬────────┘ └─────────────────┘
|
||
│ 動態載入
|
||
┌────────▼────────┐
|
||
│ script.py │
|
||
│ (Plugin 推論) │
|
||
└─────────────────┘
|
||
```
|
||
|
||
### 2.2 頁面導航架構
|
||
|
||
AppController 使用 `QStackedWidget` 作為根容器,所有頁面在啟動時一次性初始化,透過 `setCurrentWidget()` 切換顯示,不需重新建立物件。
|
||
|
||
```
|
||
AppController
|
||
└── QStackedWidget(stack)
|
||
├── [index 0] SelectionScreen ← 預設顯示
|
||
├── [index 1] LoginScreen
|
||
├── [index 2] UtilitiesScreen
|
||
└── [index 3] MainWindow
|
||
```
|
||
|
||
**Signal 連接關係(頁面切換)**:
|
||
|
||
| 發出者 | Signal | 接收者(Slot) | 效果 |
|
||
|--------|--------|--------------|------|
|
||
| SelectionScreen | `open_utilities` | `AppController.show_login_screen` | 跳至登入頁 |
|
||
| SelectionScreen | `open_demo_app` | `AppController.show_demo_app` | 跳至主視窗 |
|
||
| LoginScreen | `login_success` | `AppController.show_utilities_screen` | 登入成功,進入工具頁 |
|
||
| LoginScreen | `back_to_selection` | `AppController.show_selection_screen` | 返回首頁 |
|
||
| UtilitiesScreen | `back_to_selection` | `AppController.show_selection_screen` | 返回首頁 |
|
||
|
||
### 2.3 MainWindow 內部架構
|
||
|
||
MainWindow 是 AI Demo 推論的核心容器,內部持有三個 Controller 組成協作關係:
|
||
|
||
```
|
||
MainWindow(QWidget)
|
||
├── DeviceController — 管理 kp SDK 裝置連接
|
||
├── InferenceController — 管理推論 Worker Thread 與 Queue
|
||
│ └── inference_queue(queue.Queue, maxsize=5)
|
||
└── MediaController — 管理相機擷取與畫面更新
|
||
└── VideoThread(QThread)
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 模組依賴圖
|
||
|
||
```
|
||
main.py(AppController)
|
||
├── views/selection_screen.py(SelectionScreen)
|
||
│ └── config.py
|
||
├── views/login_screen.py(LoginScreen)
|
||
│ └── config.py
|
||
├── views/utilities_screen.py(UtilitiesScreen)
|
||
│ ├── config.py
|
||
│ ├── controllers/device_controller.py(DeviceController)
|
||
│ └── services/device_service.py(check_available_device)
|
||
└── views/mainWindows.py(MainWindow)
|
||
├── config.py
|
||
├── controllers/device_controller.py(DeviceController)
|
||
│ ├── config.py
|
||
│ └── services/device_service.py
|
||
├── controllers/inference_controller.py(InferenceController)
|
||
│ ├── config.py
|
||
│ ├── models/inference_worker.py(InferenceWorkerThread)
|
||
│ │ ├── config.py
|
||
│ │ └── [動態載入] utils/{mode}/{model}/script.py
|
||
│ └── models/custom_inference_worker.py(CustomInferenceWorkerThread)
|
||
├── controllers/media_controller.py(MediaController)
|
||
│ ├── models/video_thread.py(VideoThread)
|
||
│ └── utils/image_utils.py
|
||
├── services/file_service.py(FileService)
|
||
└── utils/config_utils.py(ConfigUtils)
|
||
```
|
||
|
||
**注意**:`UtilitiesScreen` 建立了**自己的** `DeviceController` 實例(與 `MainWindow` 的是不同物件),兩者不共享裝置狀態。
|
||
|
||
---
|
||
|
||
## 4. 資料流程圖
|
||
|
||
### 4.1 Video 即時推論流程
|
||
|
||
```
|
||
相機硬體
|
||
│ 每幀(~30fps)
|
||
▼
|
||
VideoThread.run()
|
||
│ QImage
|
||
│ change_pixmap_signal.emit(qt_image)
|
||
▼
|
||
MediaController.update_image(qt_image)
|
||
├── 1. 繪製 Bounding Box → canvas_label.setPixmap(pixmap)
|
||
└── 2. qimage_to_numpy(qt_image) → frame_np
|
||
│
|
||
▼
|
||
InferenceController.add_frame_to_queue(frame_np)
|
||
│ 若 queue 未滿(maxsize=5)
|
||
▼
|
||
inference_queue.put(frame_np)
|
||
│
|
||
▼
|
||
InferenceWorkerThread.run()
|
||
├── 1. MSE 比較(與前一幀)→ 差異不大時,emit 快取結果
|
||
├── 2. 時間間隔檢查(min_interval=2秒)
|
||
└── 3. script.inference(frame, params) → result
|
||
│
|
||
│ inference_result_signal.emit(result)
|
||
▼
|
||
MainWindow.handle_inference_result(result)
|
||
├── 若有 "bounding box"/"bounding boxes"
|
||
│ → 更新 current_bounding_boxes(下一幀繪製)
|
||
└── 若無 bounding box → QMessageBox 彈出顯示
|
||
```
|
||
|
||
### 4.2 Image 推論流程
|
||
|
||
```
|
||
使用者點擊「Upload」
|
||
│
|
||
▼
|
||
FileService.upload_file()
|
||
├── 1. 暫停相機 Signal(disconnect change_pixmap_signal)
|
||
├── 2. QFileDialog 選檔
|
||
├── 3. shutil.copy2() → %LOCALAPPDATA%/Kneron_Academy/uploads/
|
||
├── 4. 顯示圖片於 canvas_label
|
||
└── 5. InferenceController.process_uploaded_image(file_path)
|
||
│
|
||
▼
|
||
_clear_inference_queue()
|
||
inference_queue.put(img)
|
||
│
|
||
▼
|
||
InferenceWorkerThread(once_mode=True)
|
||
│ 只處理一幀後停止
|
||
▼
|
||
script.inference(frame, params) → result
|
||
│
|
||
│ inference_result_signal.emit(result)
|
||
▼
|
||
MainWindow.handle_inference_result(result)
|
||
```
|
||
|
||
### 4.3 Custom Model 推論流程
|
||
|
||
```
|
||
使用者提供:
|
||
- custom_model_path(.nef 檔)
|
||
- custom_scpu_path(fw_scpu.bin)
|
||
- custom_ncpu_path(fw_ncpu.bin)
|
||
- custom_labels(可選)
|
||
│
|
||
▼
|
||
InferenceController.select_custom_tool(tool_config)
|
||
│
|
||
▼
|
||
CustomInferenceWorkerThread(QThread)
|
||
│
|
||
├── initialize_device()(首次執行時)
|
||
│ ├── kp.core.connect_devices([port_id])
|
||
│ ├── kp.core.load_firmware_from_file(scpu, ncpu)
|
||
│ └── kp.core.load_model_from_file(model_path)
|
||
│
|
||
└── run_single_inference(frame)
|
||
├── preprocess_frame()(resize to 640, BGR → BGR565)
|
||
├── kp.GenericImageInferenceDescriptor
|
||
├── kp.inference.generic_image_inference_send()
|
||
├── kp.inference.generic_image_inference_receive()
|
||
├── kp.inference.generic_inference_retrieve_float_node()
|
||
└── post_process_yolo_v5() → ExampleYoloResult
|
||
│
|
||
│ inference_result_signal.emit(result_dict)
|
||
▼
|
||
MainWindow.handle_inference_result()
|
||
```
|
||
|
||
**注意**:`CustomInferenceWorkerThread` 在 Worker Thread 內部**自行連接/重置裝置**,與 `DeviceController` 管理的 `device_group` 是**不同的連接**。這是一個雙重連接問題(見第 8 節技術問題)。
|
||
|
||
---
|
||
|
||
## 5. Thread 架構
|
||
|
||
### 5.1 執行緒關係圖
|
||
|
||
```
|
||
Qt Main Thread(UI Thread)
|
||
├── AppController(QStackedWidget 管理)
|
||
├── MainWindow(UI 事件處理)
|
||
│ ├── handle_inference_result() ← 由 Signal 呼叫(執行在主執行緒)
|
||
│ └── update_image() via MediaController ← 由 Signal 呼叫(執行在主執行緒)
|
||
│
|
||
├── VideoThread(QThread #1)
|
||
│ └── 職責:相機擷取、QImage 轉換、emit change_pixmap_signal
|
||
│ └── 內部:threading.Thread(用於相機開啟 timeout 機制)
|
||
│
|
||
├── InferenceWorkerThread(QThread #2)
|
||
│ └── 職責:從 queue 取幀、MSE 比較、呼叫 script.inference()、emit 結果
|
||
│
|
||
└── CustomInferenceWorkerThread(QThread #3,替代 InferenceWorkerThread)
|
||
└── 職責:device init、kp 推論、YOLOv5 後處理、emit 結果
|
||
```
|
||
|
||
### 5.2 裝置掃描的執行緒
|
||
|
||
`check_available_device()` 使用 `threading.Thread`(非 QThread)執行 `kp.core.scan_devices()`,並設 5 秒 timeout:
|
||
|
||
```
|
||
check_available_device() in Main Thread
|
||
└── threading.Thread(daemon=True)
|
||
└── kp.core.scan_devices()(阻塞式 SDK 呼叫)
|
||
thread.join(timeout=5.0)
|
||
```
|
||
|
||
同樣地,`VideoThread._open_camera_with_timeout()` 也使用 `threading.Thread` 開啟相機,timeout 為 5 秒。
|
||
|
||
### 5.3 跨執行緒通訊
|
||
|
||
所有跨執行緒通訊均透過 Qt Signal/Slot 機制,Qt 保證跨執行緒的 Signal 會在接收執行緒的 Event Loop 中排隊執行:
|
||
|
||
| Signal | 發出執行緒 | 接收執行緒(Slot) |
|
||
|--------|-----------|----------------|
|
||
| `VideoThread.change_pixmap_signal` | VideoThread | Main Thread(`MediaController.update_image`) |
|
||
| `InferenceWorkerThread.inference_result_signal` | InferenceWorkerThread | Main Thread(`MainWindow.handle_inference_result`) |
|
||
| `CustomInferenceWorkerThread.inference_result_signal` | CustomInferenceWorkerThread | Main Thread(`MainWindow.handle_inference_result`) |
|
||
|
||
---
|
||
|
||
## 6. Plugin 系統設計
|
||
|
||
### 6.1 架構概念
|
||
|
||
Plugin 系統讓 Kneron 或第三方可以透過放置目錄和設定檔來新增 AI 工具,完全不需修改主程式碼。
|
||
|
||
### 6.2 目錄結構
|
||
|
||
```
|
||
%LOCALAPPDATA%/Kneron_Academy/utils/
|
||
├── config.json ← 全域 Plugin 索引(自動產生)
|
||
├── {mode_name}/ ← 推論模式目錄(如 object_detection)
|
||
│ └── {model_name}/ ← 模型目錄(如 yolov5_person)
|
||
│ ├── config.json ← 模型設定
|
||
│ ├── script.py ← 推論腳本(Plugin 核心)
|
||
│ └── {model_name}.nef ← Kneron 模型檔
|
||
```
|
||
|
||
### 6.3 Plugin 載入流程
|
||
|
||
```
|
||
應用程式啟動
|
||
│
|
||
▼
|
||
ConfigUtils.generate_global_config()
|
||
├── 掃描 utils/ 下所有 mode 目錄(跳過 _ 開頭的目錄)
|
||
├── 掃描每個 mode 下所有 model 目錄
|
||
├── 讀取每個 model/config.json
|
||
└── 輸出 utils/config.json(Plugin 索引)
|
||
|
||
使用者選擇工具
|
||
│
|
||
▼
|
||
InferenceController.select_tool(tool_config)
|
||
│
|
||
▼
|
||
InferenceWorkerThread.__init__()
|
||
└── load_inference_module(mode, model_name)
|
||
└── importlib.util.spec_from_file_location()
|
||
→ 動態 import utils/{mode}/{model}/script.py
|
||
```
|
||
|
||
### 6.4 `script.py` 介面規範
|
||
|
||
每個 Plugin 的 `script.py` 必須實作以下介面:
|
||
|
||
```python
|
||
def inference(frame: np.ndarray, params: dict) -> dict | None:
|
||
"""
|
||
Args:
|
||
frame: 影像幀,numpy array,形狀 (H, W, 3),RGB 格式
|
||
params: 推論參數字典(詳見 config.json schema)
|
||
|
||
Returns:
|
||
dict 或 None(None 表示跳過此幀)
|
||
|
||
支援的回傳格式(在 handle_inference_result 中處理):
|
||
|
||
格式 A:單一 Bounding Box
|
||
{
|
||
"bounding box": [x1, y1, x2, y2],
|
||
"result": "class_label"
|
||
}
|
||
|
||
格式 B:多個 Bounding Box(推薦)
|
||
{
|
||
"bounding boxes": [[x1, y1, x2, y2], ...],
|
||
"results": ["label1", "label2", ...]
|
||
}
|
||
|
||
格式 C:任意分類結果(彈出 QMessageBox 顯示)
|
||
{
|
||
"key": "value",
|
||
...
|
||
}
|
||
"""
|
||
```
|
||
|
||
### 6.5 `params` 字典的內容
|
||
|
||
`InferenceController.select_tool()` 在建立 `InferenceWorkerThread` 前,會將以下資訊注入 `input_params`:
|
||
|
||
| Key | 型別 | 來源 |
|
||
|-----|------|------|
|
||
| `device_group` | kp.DeviceGroup | DeviceController |
|
||
| `usb_port_id` | int | 已連接裝置 |
|
||
| `scpu_path` | str | firmware 路徑 |
|
||
| `ncpu_path` | str | firmware 路徑 |
|
||
| `model` | str | model .nef 完整路徑 |
|
||
| `model_descriptor` | kp.ModelDescriptor | 已上傳的模型描述 |
|
||
| `file_path` | str | 圖片/聲音模式的上傳檔案路徑 |
|
||
| (其他) | any | 來自 model config.json 的 `input_parameters` |
|
||
|
||
---
|
||
|
||
## 7. 資料存放設計
|
||
|
||
### 7.1 執行期資料目錄
|
||
|
||
全部存放於 Windows 的 `%LOCALAPPDATA%\Kneron_Academy\`:
|
||
|
||
```
|
||
%LOCALAPPDATA%\Kneron_Academy\
|
||
├── utils\
|
||
│ ├── config.json ← Plugin 全域索引(啟動時自動產生)
|
||
│ ├── {mode}\
|
||
│ │ └── {model}\
|
||
│ │ ├── config.json ← 模型設定
|
||
│ │ ├── script.py ← 推論腳本
|
||
│ │ └── *.nef ← 模型檔
|
||
│ └── ...
|
||
├── uploads\ ← 使用者上傳的圖片(不自動清理)
|
||
│ └── *.jpg / *.png / *.wav / ...
|
||
└── firmware\
|
||
├── KL520\
|
||
│ ├── fw_scpu.bin
|
||
│ └── fw_ncpu.bin
|
||
└── KL720\
|
||
├── fw_scpu.bin
|
||
└── fw_ncpu.bin
|
||
```
|
||
|
||
### 7.2 靜態 UI 資源
|
||
|
||
打包在應用程式內(`uxui/` 目錄),路徑透過 `PROJECT_ROOT` 常數計算:
|
||
|
||
```python
|
||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
|
||
```
|
||
|
||
**PyInstaller 注意**:打包後 `__file__` 的位置會改變,需確認 `UXUI_ASSETS` 路徑在打包後仍正確(詳見第 8.3 節)。
|
||
|
||
---
|
||
|
||
## 8. 打包架構(PyInstaller)
|
||
|
||
### 8.1 打包工具鏈
|
||
|
||
- **PyInstaller 6.12.0**:將 Python 應用打包為 Windows .exe
|
||
- **Inno Setup**(`dist/test.iss`):製作 Windows 安裝包 (.exe installer)
|
||
- **PyArmor**(計畫中):混淆/加密 Python 原始碼
|
||
|
||
### 8.2 打包注意事項
|
||
|
||
| 項目 | 問題 | 解決方向 |
|
||
|------|------|---------|
|
||
| `kp` SDK | kp 是 C Extension,需確認是否能被 PyInstaller 正確打包 | 需設定 `hiddenimports` 或 `binaries` |
|
||
| 動態 import | `importlib.util.spec_from_file_location()` 在打包後需從外部路徑載入 | `script.py` 必須放在 `%LOCALAPPDATA%`,不能打包進 exe |
|
||
| `UXUI_ASSETS` 路徑 | 打包後 `__file__` 指向臨時目錄 | 需在 `.spec` 中設定 `datas`,並使用 `sys._MEIPASS` 處理路徑 |
|
||
| OpenCV | OpenCV 需包含 DLL | 通常 PyInstaller 能自動偵測 |
|
||
|
||
### 8.3 目錄結構(打包後)
|
||
|
||
```
|
||
安裝目錄/
|
||
├── Innovedus AI Playground.exe ← 主執行檔
|
||
├── uxui/ ← 靜態資源(需隨 exe 一起安裝)
|
||
└── ...
|
||
|
||
%LOCALAPPDATA%\Kneron_Academy\ ← 使用者資料(安裝時建立)
|
||
├── utils/
|
||
├── uploads/
|
||
└── firmware/
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 已知技術問題 / 技術債
|
||
|
||
### 9.1 雙重裝置連接(⚠️ 嚴重)
|
||
|
||
**問題**:`CustomInferenceWorkerThread` 在 Worker Thread 內部調用 `kp.core.connect_devices()`,但 `DeviceController` 可能已經對同一個 `usb_port_id` 建立了連接(在 MainWindow 流程中)。
|
||
|
||
**影響**:可能導致 kp SDK 報告「裝置已被連接」的錯誤,或產生未定義行為。
|
||
|
||
**建議**:`CustomInferenceWorkerThread` 應改為接受外部傳入的 `device_group`,而非自行連接。
|
||
|
||
### 9.2 UtilitiesScreen 的 DeviceController 孤立問題(⚠️ 中度)
|
||
|
||
**問題**:`UtilitiesScreen` 建立了自己的 `DeviceController(self)` 實例,與 `MainWindow` 的 `DeviceController` 完全獨立,兩者各自管理自己的 `device_group`。
|
||
|
||
**影響**:使用者在 UtilitiesScreen 連接裝置後,切換到 MainWindow 並不知道裝置已連接;反之亦然。
|
||
|
||
**建議**:將 `DeviceController` 提升到 `AppController` 層級,作為共享的單例。
|
||
|
||
### 9.3 推論 Queue 丟幀而不通知(⚠️ 中度)
|
||
|
||
**問題**:`inference_queue` 的 `maxsize=5`,當 queue 滿時,`add_frame_to_queue()` 靜默丟棄幀(只印 print,不通知 UI)。
|
||
|
||
**影響**:在高推論延遲時,使用者不知道有幀被丟棄,可能誤以為推論仍在即時進行。
|
||
|
||
**建議**:新增 UI 指示推論 queue 壓力(如幀率顯示、lag 指示)。
|
||
|
||
### 9.4 LoginScreen 的驗證邏輯未實作(⚠️ 中度)
|
||
|
||
**問題**:`LoginScreen.attempt_login()` 的實際 Server 驗證邏輯未實作,目前只要輸入任何非空帳密就會成功登入。
|
||
|
||
```python
|
||
# 目前實作(不安全)
|
||
if not username or not password:
|
||
self.show_error("Please enter both username and password")
|
||
return
|
||
self.login_success.emit() # 永遠成功
|
||
```
|
||
|
||
**建議**:需補齊 Server 端驗證 API 呼叫。
|
||
|
||
### 9.5 debug print 語句散落各處(低優先)
|
||
|
||
**問題**:各 Controller 和 Thread 中有大量 `print()` 呼叫作為 debug 輸出,打包後仍會執行(輸出被丟棄,但有效能成本)。
|
||
|
||
**建議**:改用 Python 的 `logging` 模組,並設定適當的 log level。
|
||
|
||
### 9.6 `custom_inference_worker.py` 中的 `kp` 全域引用問題(⚠️ 中度)
|
||
|
||
**問題**:`_boxes_scale()` 和 `post_process_yolo_v5()` 函數的型別標注直接引用 `kp.HwPreProcInfo`、`kp.InferenceFloatNodeOutput`(如 `def _boxes_scale(boxes, hardware_preproc_info: kp.HwPreProcInfo)`),但 `kp` 在模組頂層未被 import。實際 kp import 是在函數內部的 `run_single_inference()` 中延遲進行的。
|
||
|
||
**影響**:型別標注在模組載入時會被解析(在 Python 3.10+ 以下),可能導致 `NameError`。
|
||
|
||
**建議**:在頂層加入 `if TYPE_CHECKING: import kp`,或改用字串型別標注 `"kp.HwPreProcInfo"`。
|
||
|
||
### 9.7 VideoThread 的 `threading.Thread` 記憶體洩漏風險(低優先)
|
||
|
||
**問題**:`_open_camera_with_timeout()` 啟動了 `daemon=True` 的 `threading.Thread` 並等待最多 5 秒,但如果 thread 仍存活(timeout),其仍會繼續嘗試開啟相機,可能導致相機資源被不正確佔用。
|
||
|
||
**建議**:使用 cv2 的 nonblocking 方式或設定相機 timeout 參數,避免 daemon thread 的不確定行為。
|
||
|
||
### 9.8 MSE 計算的效能問題(低優先)
|
||
|
||
**問題**:`InferenceWorkerThread` 和 `CustomInferenceWorkerThread` 的 MSE 計算會把整個 frame 轉成 float32 進行運算:
|
||
```python
|
||
mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2)
|
||
```
|
||
對於 640x480 的 3 通道影像,每次計算需要處理 ~921,600 個浮點數。
|
||
|
||
**建議**:可改為縮小解析度後再計算 MSE,或使用 histogram 比較等更快速的方式。
|
||
|
||
---
|
||
|
||
## 10. 容量與效能估算
|
||
|
||
### 10.1 系統需求(桌面應用)
|
||
|
||
| 資源 | 需求 | 備註 |
|
||
|------|------|------|
|
||
| CPU | 雙核心以上 | 主要用於影像轉換和後處理 |
|
||
| RAM | 2GB 以上 | kp SDK + OpenCV + PyQt5 |
|
||
| USB | USB 3.0 | KL720 需要 USB 3.0 |
|
||
| GPU | 不需要 | 推論在 NPU 執行 |
|
||
| 磁碟 | 500MB 以上 | 安裝包 + 模型檔 |
|
||
|
||
### 10.2 推論速度特性
|
||
|
||
- **Queue maxsize**:5 幀
|
||
- **VideoThread 輸出**:~30fps(640x480)
|
||
- **InferenceWorkerThread min_interval**:2 秒(標準模式)/ 0.5 秒(Custom 模式)
|
||
- **MSE threshold**:500(低於此值視為相似幀,使用快取結果)
|
||
- **相機開啟 timeout**:5 秒 × 最多 3 次嘗試
|
||
|
||
---
|
||
|
||
## 11. 安全性設計
|
||
|
||
### 11.1 目前狀態
|
||
|
||
| 項目 | 狀態 | 說明 |
|
||
|------|------|------|
|
||
| Server 登入驗證 | ❌ 未實作 | `attempt_login()` 永遠成功 |
|
||
| 程式碼保護 | ⚠️ 計畫中 | PyArmor 列在計畫中 |
|
||
| 自定義模型驗證 | ❌ 無 | 任何 .nef 檔都能上傳 |
|
||
| 網路通訊加密 | ❌ 未知 | Server 驗證端點未見 TLS 設定 |
|
||
|
||
### 11.2 Plugin 安全風險
|
||
|
||
`load_inference_module()` 使用 `importlib` 動態執行 `script.py`,等同於執行任意 Python 程式碼。若 `%LOCALAPPDATA%` 中的 `script.py` 被惡意替換,攻擊者可以完整控制推論行為。
|
||
|
||
**建議**:考慮對 `script.py` 進行簽章驗證,或限制其沙盒執行環境。
|