2026-04-07 14:37:04 +08:00

23 KiB
Raw Blame History

Design Doc: KNEO AcademyInnovedus AI Playgroundv2.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 設計模式

本應用程式採用 MVCModel-View-Controller 架構,搭配 Qt 的 Signal/Slot 機制實現跨執行緒通訊。

┌─────────────────────────────────────────────────────────────────┐
│                         Views呈現層                           │
│  SelectionScreen  LoginScreen  MainWindow  UtilitiesScreen       │
│        QWidget         QWidget      QWidget       QWidget         │
└────────────────────────────┬────────────────────────────────────┘
                             │ Signal/Slot
┌────────────────────────────▼────────────────────────────────────┐
│                      AppControllermain.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
└── QStackedWidgetstack
    ├── [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 組成協作關係:

MainWindowQWidget
├── DeviceController    — 管理 kp SDK 裝置連接
├── InferenceController — 管理推論 Worker Thread 與 Queue
│   └── inference_queuequeue.Queue, maxsize=5
└── MediaController     — 管理相機擷取與畫面更新
    └── VideoThreadQThread

3. 模組依賴圖

main.pyAppController
├── views/selection_screen.pySelectionScreen
│   └── config.py
├── views/login_screen.pyLoginScreen
│   └── config.py
├── views/utilities_screen.pyUtilitiesScreen
│   ├── config.py
│   ├── controllers/device_controller.pyDeviceController
│   └── services/device_service.pycheck_available_device
└── views/mainWindows.pyMainWindow
    ├── config.py
    ├── controllers/device_controller.pyDeviceController
    │   ├── config.py
    │   └── services/device_service.py
    ├── controllers/inference_controller.pyInferenceController
    │   ├── config.py
    │   ├── models/inference_worker.pyInferenceWorkerThread
    │   │   ├── config.py
    │   │   └── [動態載入] utils/{mode}/{model}/script.py
    │   └── models/custom_inference_worker.pyCustomInferenceWorkerThread
    ├── controllers/media_controller.pyMediaController
    │   ├── models/video_thread.pyVideoThread
    │   └── utils/image_utils.py
    ├── services/file_service.pyFileService
    └── utils/config_utils.pyConfigUtils

注意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. 暫停相機 Signaldisconnect 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)
                │
                ▼
            InferenceWorkerThreadonce_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_pathfw_scpu.bin
  - custom_ncpu_pathfw_ncpu.bin
  - custom_labels可選
    │
    ▼
InferenceController.select_custom_tool(tool_config)
    │
    ▼
CustomInferenceWorkerThreadQThread
    │
    ├── 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 ThreadUI Thread
├── AppControllerQStackedWidget 管理)
├── MainWindowUI 事件處理)
│   ├── handle_inference_result()    ← 由 Signal 呼叫(執行在主執行緒)
│   └── update_image() via MediaController  ← 由 Signal 呼叫(執行在主執行緒)
│
├── VideoThreadQThread #1
│   └── 職責相機擷取、QImage 轉換、emit change_pixmap_signal
│   └── 內部threading.Thread用於相機開啟 timeout 機制)
│
├── InferenceWorkerThreadQThread #2
│   └── 職責:從 queue 取幀、MSE 比較、呼叫 script.inference()、emit 結果
│
└── CustomInferenceWorkerThreadQThread #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.Threaddaemon=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 ThreadMediaController.update_image
InferenceWorkerThread.inference_result_signal InferenceWorkerThread Main ThreadMainWindow.handle_inference_result
CustomInferenceWorkerThread.inference_result_signal CustomInferenceWorkerThread Main ThreadMainWindow.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.jsonPlugin 索引)
    
使用者選擇工具
    │
    ▼
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 必須實作以下介面:

def inference(frame: np.ndarray, params: dict) -> dict | None:
    """
    Args:
        frame: 影像幀numpy array形狀 (H, W, 3)RGB 格式
        params: 推論參數字典(詳見 config.json schema
    
    Returns:
        dict 或 NoneNone 表示跳過此幀)
    
    支援的回傳格式(在 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 常數計算:

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 Setupdist/test.iss):製作 Windows 安裝包 (.exe installer)
  • PyArmor(計畫中):混淆/加密 Python 原始碼

8.2 打包注意事項

項目 問題 解決方向
kp SDK kp 是 C Extension需確認是否能被 PyInstaller 正確打包 需設定 hiddenimportsbinaries
動態 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) 實例,與 MainWindowDeviceController 完全獨立,兩者各自管理自己的 device_group

影響:使用者在 UtilitiesScreen 連接裝置後,切換到 MainWindow 並不知道裝置已連接;反之亦然。

建議:將 DeviceController 提升到 AppController 層級,作為共享的單例。

9.3 推論 Queue 丟幀而不通知(⚠️ 中度)

問題inference_queuemaxsize=5,當 queue 滿時,add_frame_to_queue() 靜默丟棄幀(只印 print不通知 UI

影響:在高推論延遲時,使用者不知道有幀被丟棄,可能誤以為推論仍在即時進行。

建議:新增 UI 指示推論 queue 壓力如幀率顯示、lag 指示)。

9.4 LoginScreen 的驗證邏輯未實作(⚠️ 中度)

問題LoginScreen.attempt_login() 的實際 Server 驗證邏輯未實作,目前只要輸入任何非空帳密就會成功登入。

# 目前實作(不安全)
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.HwPreProcInfokp.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=Truethreading.Thread 並等待最多 5 秒,但如果 thread 仍存活timeout其仍會繼續嘗試開啟相機可能導致相機資源被不正確佔用。

建議:使用 cv2 的 nonblocking 方式或設定相機 timeout 參數,避免 daemon thread 的不確定行為。

9.8 MSE 計算的效能問題(低優先)

問題InferenceWorkerThreadCustomInferenceWorkerThread 的 MSE 計算會把整個 frame 轉成 float32 進行運算:

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 maxsize5 幀
  • VideoThread 輸出~30fps640x480
  • InferenceWorkerThread min_interval2 秒(標準模式)/ 0.5 秒Custom 模式)
  • MSE threshold500低於此值視為相似幀使用快取結果
  • 相機開啟 timeout5 秒 × 最多 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 進行簽章驗證,或限制其沙盒執行環境。