Refactor: Clean up codebase and improve documentation

- Remove unused files (model_controller.py, model_service.py, device_connection_popup.py)
- Clean up commented code in device_service.py, device_popup.py, config.py
- Update docstrings and comments across all modules
- Improve code organization and readability
This commit is contained in:
HuangMason320 2025-12-30 16:47:31 +08:00
parent 09156cce94
commit 17deba3bdb
18 changed files with 1388 additions and 867 deletions

68
main.py
View File

@ -1,3 +1,11 @@
"""
main.py - Kneron Academy Application Entry Point
This module serves as the main entry point for the application.
It initializes the PyQt5 application and uses AppController to manage
navigation and screen transitions.
"""
import sys import sys
from PyQt5.QtWidgets import QApplication, QStackedWidget from PyQt5.QtWidgets import QApplication, QStackedWidget
from src.views.mainWindows import MainWindow from src.views.mainWindows import MainWindow
@ -6,7 +14,25 @@ from src.views.login_screen import LoginScreen
from src.views.utilities_screen import UtilitiesScreen from src.views.utilities_screen import UtilitiesScreen
from src.config import APP_NAME, WINDOW_SIZE from src.config import APP_NAME, WINDOW_SIZE
class AppController: class AppController:
"""
Application Controller Class
Manages the entire application lifecycle, including:
- Initializing all screens
- Setting up signal connections between screens
- Controlling screen navigation logic
Attributes:
app (QApplication): The PyQt5 application instance
stack (QStackedWidget): Stacked widget container for managing multiple screens
selection_screen (SelectionScreen): The selection/home screen
login_screen (LoginScreen): The login screen
utilities_screen (UtilitiesScreen): The utilities screen
main_window (MainWindow): The main demo application window
"""
def __init__(self): def __init__(self):
self.app = QApplication(sys.argv) self.app = QApplication(sys.argv)
self.stack = QStackedWidget() self.stack = QStackedWidget()
@ -23,53 +49,81 @@ class AppController:
self.show_selection_screen() self.show_selection_screen()
def init_screens(self): def init_screens(self):
# Selection screen """
Initialize all application screens.
Creates instances of each screen and adds them to the stacked widget.
The order of addition determines the default index of each screen.
"""
self.selection_screen = SelectionScreen() self.selection_screen = SelectionScreen()
self.stack.addWidget(self.selection_screen) self.stack.addWidget(self.selection_screen)
# Login screen
self.login_screen = LoginScreen() self.login_screen = LoginScreen()
self.stack.addWidget(self.login_screen) self.stack.addWidget(self.login_screen)
# Utilities screen
self.utilities_screen = UtilitiesScreen() self.utilities_screen = UtilitiesScreen()
self.stack.addWidget(self.utilities_screen) self.stack.addWidget(self.utilities_screen)
# Demo app (main window)
self.main_window = MainWindow() self.main_window = MainWindow()
self.stack.addWidget(self.main_window) self.stack.addWidget(self.main_window)
def connect_signals(self): def connect_signals(self):
# Selection screen signals """
Connect signals between screens for navigation.
Sets up Qt signal-slot connections to enable screen transitions:
- Selection screen -> Login screen (for utilities access)
- Selection screen -> Demo app
- Login screen -> Utilities screen (on successful login)
- Login screen -> Selection screen (back navigation)
- Utilities screen -> Selection screen (back navigation)
"""
self.selection_screen.open_utilities.connect(self.show_login_screen) self.selection_screen.open_utilities.connect(self.show_login_screen)
self.selection_screen.open_demo_app.connect(self.show_demo_app) self.selection_screen.open_demo_app.connect(self.show_demo_app)
# Login screen signals
self.login_screen.login_success.connect(self.show_utilities_screen) self.login_screen.login_success.connect(self.show_utilities_screen)
self.login_screen.back_to_selection.connect(self.show_selection_screen) self.login_screen.back_to_selection.connect(self.show_selection_screen)
# Utilities screen signals
self.utilities_screen.back_to_selection.connect(self.show_selection_screen) self.utilities_screen.back_to_selection.connect(self.show_selection_screen)
def show_selection_screen(self): def show_selection_screen(self):
"""Switch to the selection/home screen."""
self.stack.setCurrentWidget(self.selection_screen) self.stack.setCurrentWidget(self.selection_screen)
def show_login_screen(self): def show_login_screen(self):
"""Switch to the login screen."""
self.stack.setCurrentWidget(self.login_screen) self.stack.setCurrentWidget(self.login_screen)
def show_utilities_screen(self): def show_utilities_screen(self):
"""Switch to the utilities screen."""
self.stack.setCurrentWidget(self.utilities_screen) self.stack.setCurrentWidget(self.utilities_screen)
def show_demo_app(self): def show_demo_app(self):
"""Switch to the main demo application window."""
self.stack.setCurrentWidget(self.main_window) self.stack.setCurrentWidget(self.main_window)
def run(self): def run(self):
"""
Start the application event loop.
Returns:
int: The exit code from the Qt application event loop.
"""
self.stack.show() self.stack.show()
return self.app.exec_() return self.app.exec_()
def main(): def main():
"""
Application entry point function.
Creates the AppController and starts the application event loop.
Returns:
int: The exit code from the application.
"""
controller = AppController() controller = AppController()
return controller.run() return controller.run()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,15 +1,26 @@
"""
config.py - Application Configuration
This module contains all global configuration constants, paths, and styles
for the Kneron Academy AI Playground application.
"""
from enum import Enum from enum import Enum
import os import os
# Application data path (platform-specific)
APPDATA_PATH = os.environ.get("LOCALAPPDATA") APPDATA_PATH = os.environ.get("LOCALAPPDATA")
# APPDATA_PATH = "/Users/mason/Developer/Kneron-Academy/test_images"
# 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑 # Get project root directory and set UXUI_ASSETS as absolute path
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "") UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
# 新版路徑結構 (不需要獨立的 models 和 scripts 資料夾)
# Directory structure for utilities, scripts, uploads, and firmware
UTILS_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils") UTILS_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils")
SCRIPT_CONFIG = os.path.join(UTILS_DIR, "config.json") SCRIPT_CONFIG = os.path.join(UTILS_DIR, "config.json")
UPLOAD_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "uploads") UPLOAD_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "uploads")
FW_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "firmware") FW_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "firmware")
# Global Constants # Global Constants
APP_NAME = "Innovedus AI Playground" APP_NAME = "Innovedus AI Playground"
WINDOW_SIZE = (1200, 900) WINDOW_SIZE = (1200, 900)
@ -45,22 +56,13 @@ FIRMWARE_PATHS = {
"ncpu": "../../res/firmware/fw_ncpu.bin", "ncpu": "../../res/firmware/fw_ncpu.bin",
} }
# MODEL_BUTTON = [
# ('Face Detection', self.run_face_detection),
# ('Gender/Age Detection', self.run_gender_age_detection),
# ('Object Detection', self.run_object_detection),
# ('Mask Detection', self.run_mask_detection),
# ('Image Project', self.start_image_project),
# ('Upload Model', self.upload_model)
# ]
# Model Inference Parameter # Model Inference Parameter
MODEL_TIMEOUT = 5000 MODEL_TIMEOUT = 5000
# TODO: Mapping of the values # Device type enumeration for Kneron hardware
class DeviceType(Enum): class DeviceType(Enum):
"""Enumeration of supported Kneron device types with their product IDs."""
KL520 = 256 KL520 = 256
# KL720 = 720
KL720 = 1824 KL720 = 1824
KL720_L = 512 KL720_L = 512
KL530 = 530 KL530 = 530
@ -69,11 +71,14 @@ class DeviceType(Enum):
KL630 = 630 KL630 = 630
KL540 = 540 KL540 = 540
# Mapping from product_id hex string to device model name
DongleModelMap = { DongleModelMap = {
"0x100": "KL520", # product_id "0x100" 對應到 520 系列 "0x100": "KL520", # product_id "0x100" maps to KL520 series
"0x720": "KL720", # product_id "0x720" 對應到 720 系列 "0x720": "KL720", # product_id "0x720" maps to KL720 series
} }
# Mapping from product_id hex string to device icon filename
DongleIconMap = { DongleIconMap = {
"0x100": "ic_dongle_520.png", "0x100": "ic_dongle_520.png",
"0x720": "ic_dongle_720.png" "0x720": "ic_dongle_720.png"

View File

@ -1,45 +1,75 @@
# src/controllers/device_controller.py """
device_controller.py - Device Controller
This module handles Kneron device connection, selection, and management.
It provides functionality for scanning, connecting, and disconnecting devices.
"""
from PyQt5.QtWidgets import QWidget, QListWidgetItem from PyQt5.QtWidgets import QWidget, QListWidgetItem
from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
import os import os
import kp # 新增 kp 模組的引入 import kp
from src.services.device_service import check_available_device from src.services.device_service import check_available_device
from src.config import UXUI_ASSETS, DongleModelMap, DongleIconMap, FW_DIR from src.config import UXUI_ASSETS, DongleModelMap, DongleIconMap, FW_DIR
class DeviceController: class DeviceController:
"""
Controller class for managing Kneron device connections.
Attributes:
main_window: Reference to the main application window
selected_device: Currently selected device
connected_devices: List of connected devices
device_group: Kneron Plus device group for connected devices
"""
def __init__(self, main_window): def __init__(self, main_window):
"""
Initialize the DeviceController.
Args:
main_window: Reference to the main application window.
"""
self.main_window = main_window self.main_window = main_window
self.selected_device = None self.selected_device = None
self.connected_devices = [] self.connected_devices = []
self.device_group = None # 新增儲存連接的 device_group self.device_group = None # Stores connected device group
def refresh_devices(self): def refresh_devices(self):
"""Refresh the list of connected devices""" """
Refresh the list of connected devices.
Scans for available Kneron devices and updates the UI.
Returns:
bool: True if devices were found, False otherwise.
"""
try: try:
print("[CTRL] Refreshing devices...") print("[CTRL] Refreshing devices...")
device_descriptors = check_available_device() device_descriptors = check_available_device()
print("[CTRL] check_available_device 已返回") print("[CTRL] check_available_device returned")
print(f"[CTRL] device_descriptors 類型: {type(device_descriptors)}") print(f"[CTRL] device_descriptors type: {type(device_descriptors)}")
# 分開訪問屬性以便調試 # Access attributes separately for debugging
print("[CTRL] 嘗試訪問 device_descriptor_number...") print("[CTRL] Accessing device_descriptor_number...")
desc_num = device_descriptors.device_descriptor_number desc_num = device_descriptors.device_descriptor_number
print(f"[CTRL] device_descriptor_number: {desc_num}") print(f"[CTRL] device_descriptor_number: {desc_num}")
self.connected_devices = [] self.connected_devices = []
if device_descriptors.device_descriptor_number > 0: if device_descriptors.device_descriptor_number > 0:
print("[DEBUG] 開始 parse_and_store_devices...") print("[DEBUG] Starting parse_and_store_devices...")
self.parse_and_store_devices(device_descriptors.device_descriptor_list) self.parse_and_store_devices(device_descriptors.device_descriptor_list)
print("[DEBUG] parse_and_store_devices 完成") print("[DEBUG] parse_and_store_devices completed")
print("[DEBUG] 開始 display_devices...") print("[DEBUG] Starting display_devices...")
self.display_devices(device_descriptors.device_descriptor_list) self.display_devices(device_descriptors.device_descriptor_list)
print("[DEBUG] display_devices 完成") print("[DEBUG] display_devices completed")
return True return True
else: else:
print("[DEBUG] 沒有檢測到設備") print("[DEBUG] No devices detected")
self.main_window.show_no_device_gif() self.main_window.show_no_device_gif()
return False return False
except Exception as e: except Exception as e:
@ -49,7 +79,12 @@ class DeviceController:
return False return False
def parse_and_store_devices(self, devices): def parse_and_store_devices(self, devices):
"""Parse device information and store it""" """
Parse device information and store it in connected_devices list.
Args:
devices: List of device descriptors from the scanner.
"""
for device in devices: for device in devices:
try: try:
product_id = hex(device.product_id).strip().lower() product_id = hex(device.product_id).strip().lower()
@ -77,7 +112,12 @@ class DeviceController:
print(f"Error processing device: {e}") print(f"Error processing device: {e}")
def display_devices(self, devices): def display_devices(self, devices):
"""Display the connected devices in the UI""" """
Display the connected devices in the UI list widget.
Args:
devices: List of device descriptors to display.
"""
try: try:
if not hasattr(self.main_window, 'device_list_widget'): if not hasattr(self.main_window, 'device_list_widget'):
print("Warning: main_window does not have device_list_widget attribute") print("Warning: main_window does not have device_list_widget attribute")
@ -113,7 +153,12 @@ class DeviceController:
print(f"Error in display_devices: {e}") print(f"Error in display_devices: {e}")
def get_devices(self): def get_devices(self):
"""Get the list of connected devices""" """
Get the list of connected devices.
Returns:
list: List of device descriptors, or empty list if no devices found.
"""
try: try:
device_descriptors = check_available_device() device_descriptors = check_available_device()
if device_descriptors.device_descriptor_number > 0: if device_descriptors.device_descriptor_number > 0:
@ -126,33 +171,50 @@ class DeviceController:
return [] return []
def get_selected_device(self): def get_selected_device(self):
"""Get the currently selected device""" """
Get the currently selected device.
Returns:
The selected device object, or None if no device is selected.
"""
return self.selected_device return self.selected_device
def select_device(self, device, list_item, list_widget): def select_device(self, device, list_item, list_widget):
"""選擇設備(不自動連接和載入 firmware""" """
self.selected_device = device Select a device (does not automatically connect or load firmware).
print("選擇 dongle:", device)
# 更新列表項目的視覺選擇 Args:
device: The device object to select.
list_item: The QListWidgetItem representing the device.
list_widget: The QListWidget containing the devices.
"""
self.selected_device = device
print("Selected dongle:", device)
# Update visual selection for list items
for index in range(list_widget.count()): for index in range(list_widget.count()):
item = list_widget.item(index) item = list_widget.item(index)
widget = list_widget.itemWidget(item) widget = list_widget.itemWidget(item)
if widget: # 檢查 widget 是否存在再設定樣式 if widget: # Check if widget exists before setting style
widget.setStyleSheet("background: none;") widget.setStyleSheet("background: none;")
list_item_widget = list_widget.itemWidget(list_item) list_item_widget = list_widget.itemWidget(list_item)
if list_item_widget: # 檢查 widget 是否存在再設定樣式 if list_item_widget: # Check if widget exists before setting style
list_item_widget.setStyleSheet("background-color: lightblue;") list_item_widget.setStyleSheet("background-color: lightblue;")
def connect_device(self): def connect_device(self):
"""連接選定的設備並上傳固件""" """
Connect to the selected device and upload firmware.
Returns:
bool: True if connection successful, False otherwise.
"""
if not self.selected_device: if not self.selected_device:
print("未選擇設備,無法連接") print("No device selected, cannot connect")
return False return False
try: try:
# 取得 USB port ID # Get USB port ID
if isinstance(self.selected_device, dict): if isinstance(self.selected_device, dict):
usb_port_id = self.selected_device.get("usb_port_id", 0) usb_port_id = self.selected_device.get("usb_port_id", 0)
product_id = self.selected_device.get("product_id", 0) product_id = self.selected_device.get("product_id", 0)
@ -160,7 +222,7 @@ class DeviceController:
usb_port_id = getattr(self.selected_device, "usb_port_id", 0) usb_port_id = getattr(self.selected_device, "usb_port_id", 0)
product_id = getattr(self.selected_device, "product_id", 0) product_id = getattr(self.selected_device, "product_id", 0)
# 將 product_id 轉換為小寫十六進制字串 # Convert product_id to lowercase hex string
if isinstance(product_id, int): if isinstance(product_id, int):
product_id = hex(product_id).lower() product_id = hex(product_id).lower()
elif isinstance(product_id, str) and not product_id.startswith('0x'): elif isinstance(product_id, str) and not product_id.startswith('0x'):
@ -169,42 +231,37 @@ class DeviceController:
except ValueError: except ValueError:
pass pass
# 對應 product_id 到 dongle 類型 # Map product_id to dongle type
dongle = DongleModelMap.get(product_id, "unknown") dongle = DongleModelMap.get(product_id, "unknown")
print(f"連接設備: product_id={product_id}, mapped to={dongle}") print(f"Connecting device: product_id={product_id}, mapped to={dongle}")
# 設置固件路徑 # Set firmware paths
scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin") scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin")
ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin")
# 確認固件文件是否存在 # Check if firmware files exist
if not os.path.exists(scpu_path) or not os.path.exists(ncpu_path): if not os.path.exists(scpu_path) or not os.path.exists(ncpu_path):
print(f"固件文件不存在: {scpu_path} {ncpu_path}") print(f"Firmware files not found: {scpu_path} or {ncpu_path}")
return False return False
# 連接設備 # Connect to device
print('[連接設備]') print('[Connecting device]')
self.device_group = kp.core.connect_devices(usb_port_ids=[usb_port_id]) self.device_group = kp.core.connect_devices(usb_port_ids=[usb_port_id])
print(' - 連接成功') print(' - Connection successful')
# # 設置超時 # Upload firmware
# print('[設置超時]') print('[Uploading firmware]')
# kp.core.set_timeout(device_group=self.device_group, milliseconds=10000)
# print(' - 設置成功')
# 上傳固件
print('[上傳固件]')
kp.core.load_firmware_from_file( kp.core.load_firmware_from_file(
device_group=self.device_group, device_group=self.device_group,
scpu_fw_path=scpu_path, scpu_fw_path=scpu_path,
ncpu_fw_path=ncpu_path ncpu_fw_path=ncpu_path
) )
print(' - 上傳成功') print(' - Upload successful')
return True return True
except Exception as e: except Exception as e:
print(f"連接設備時發生錯誤: {e}") print(f"Error connecting to device: {e}")
# 發生錯誤時嘗試清理 # Try to clean up on error
if self.device_group: if self.device_group:
try: try:
kp.core.disconnect_devices(device_group=self.device_group) kp.core.disconnect_devices(device_group=self.device_group)
@ -214,20 +271,30 @@ class DeviceController:
return False return False
def disconnect_device(self): def disconnect_device(self):
"""中斷與設備的連接""" """
Disconnect from the currently connected device.
Returns:
bool: True if disconnection successful, False otherwise.
"""
if self.device_group: if self.device_group:
try: try:
print('[中斷設備連接]') print('[Disconnecting device]')
kp.core.disconnect_devices(device_group=self.device_group) kp.core.disconnect_devices(device_group=self.device_group)
print(' - 已中斷連接') print(' - Disconnected')
self.device_group = None self.device_group = None
return True return True
except Exception as e: except Exception as e:
print(f"中斷設備連接時發生錯誤: {e}") print(f"Error disconnecting device: {e}")
self.device_group = None self.device_group = None
return False return False
return True # 如果沒有連接的設備,視為成功 return True # If no connected device, treat as success
def get_device_group(self): def get_device_group(self):
"""獲取已連接的設備群組""" """
Get the connected device group.
Returns:
The Kneron Plus device group object, or None if not connected.
"""
return self.device_group return self.device_group

View File

@ -1,15 +1,44 @@
# src/controllers/inference_controller.py """
import os, queue, cv2, json inference_controller.py - Inference Controller
This module handles AI model inference operations including tool selection,
model loading, and frame processing for the Kneron AI Playground.
"""
import os
import queue
import cv2
import json
from PyQt5.QtWidgets import QMessageBox, QApplication from PyQt5.QtWidgets import QMessageBox, QApplication
from PyQt5.QtCore import QTimer, Qt from PyQt5.QtCore import QTimer, Qt
import kp # 新增 kp 模組的引入 import kp
from src.models.inference_worker import InferenceWorkerThread from src.models.inference_worker import InferenceWorkerThread
from src.models.custom_inference_worker import CustomInferenceWorkerThread from src.models.custom_inference_worker import CustomInferenceWorkerThread
from src.config import UTILS_DIR, FW_DIR, DongleModelMap from src.config import UTILS_DIR, FW_DIR, DongleModelMap
class InferenceController: class InferenceController:
"""
Controller class for managing AI model inference operations.
Attributes:
main_window: Reference to the main application window
device_controller: Reference to the device controller
inference_worker: Thread worker for running inference
inference_queue: Queue for frames to be processed
current_tool_config: Current AI tool configuration
model_descriptor: Loaded model descriptor
"""
def __init__(self, main_window, device_controller): def __init__(self, main_window, device_controller):
"""
Initialize the InferenceController.
Args:
main_window: Reference to the main application window.
device_controller: Reference to the device controller.
"""
self.main_window = main_window self.main_window = main_window
self.device_controller = device_controller self.device_controller = device_controller
self.inference_worker = None self.inference_worker = None
@ -17,22 +46,30 @@ class InferenceController:
self.current_tool_config = None self.current_tool_config = None
self.previous_tool_config = None self.previous_tool_config = None
self._camera_was_active = False self._camera_was_active = False
# 儲存原始影格尺寸,用於邊界框縮放計算 # Store original frame dimensions for bounding box scaling
self.original_frame_width = 640 # 預設值 self.original_frame_width = 640 # Default value
self.original_frame_height = 480 # 預設值 self.original_frame_height = 480 # Default value
self.model_descriptor = None self.model_descriptor = None
def select_tool(self, tool_config): def select_tool(self, tool_config):
"""選擇AI工具並配置推論""" """
Select an AI tool and configure inference.
Args:
tool_config (dict): Configuration dictionary for the AI tool.
Returns:
bool: True if tool selection successful, False otherwise.
"""
try: try:
print("選擇工具:", tool_config.get("display_name")) print("Selecting tool:", tool_config.get("display_name"))
self.current_tool_config = tool_config self.current_tool_config = tool_config
# 獲取模式和模型名稱 # Get mode and model name
mode = tool_config.get("mode", "") mode = tool_config.get("mode", "")
model_name = tool_config.get("model_name", "") model_name = tool_config.get("model_name", "")
# 載入詳細模型配置 # Load detailed model configuration
model_path = os.path.join(UTILS_DIR, mode, model_name) model_path = os.path.join(UTILS_DIR, mode, model_name)
model_config_path = os.path.join(model_path, "config.json") model_config_path = os.path.join(model_path, "config.json")
@ -42,31 +79,31 @@ class InferenceController:
detailed_config = json.load(f) detailed_config = json.load(f)
tool_config = {**tool_config, **detailed_config} tool_config = {**tool_config, **detailed_config}
except Exception as e: except Exception as e:
print(f"讀取模型配置時發生錯誤: {e}") print(f"Error reading model config: {e}")
# 獲取工具輸入類型 # Get tool input type
input_info = tool_config.get("input_info", {}) input_info = tool_config.get("input_info", {})
tool_type = input_info.get("type", "video") tool_type = input_info.get("type", "video")
once_mode = True if tool_type == "image" else False once_mode = True if tool_type == "image" else False
# 檢查是否從視訊模式切換到圖片模式,或從圖片模式切換到視訊模式 # Check if switching from video mode to image mode or vice versa
previous_tool_type = "video" previous_tool_type = "video"
if hasattr(self, 'previous_tool_config') and self.previous_tool_config: if hasattr(self, 'previous_tool_config') and self.previous_tool_config:
previous_input_info = self.previous_tool_config.get("input_info", {}) previous_input_info = self.previous_tool_config.get("input_info", {})
previous_tool_type = previous_input_info.get("type", "video") previous_tool_type = previous_input_info.get("type", "video")
# 清空推論佇列,確保在模式切換時不會使用舊數據 # Clear inference queue to avoid using old data when switching modes
self._clear_inference_queue() self._clear_inference_queue()
# 儲存當前工具類型以供下次比較 # Store current tool type for next comparison
self.previous_tool_config = tool_config self.previous_tool_config = tool_config
# 準備輸入參數 # Prepare input parameters
input_params = tool_config.get("input_parameters", {}).copy() input_params = tool_config.get("input_parameters", {}).copy()
# 取得連接的設備群組 # Get connected device group
device_group = self.device_controller.get_device_group() device_group = self.device_controller.get_device_group()
# 新增設備群組到輸入參數 # Add device group to input parameters
input_params["device_group"] = device_group input_params["device_group"] = device_group
# Configure device-related settings # Configure device-related settings
@ -108,7 +145,7 @@ class InferenceController:
msgBox.exec_() msgBox.exec_()
return False return False
# 僅添加固件路徑作為參考,實際不再使用 (因為裝置已經在選擇時連接) # Add firmware paths as reference only (device already connected during selection)
scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin") scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin")
ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin")
input_params["scpu_path"] = scpu_path input_params["scpu_path"] = scpu_path
@ -147,20 +184,20 @@ class InferenceController:
model_file_path = os.path.join(model_path, model_file) model_file_path = os.path.join(model_path, model_file)
input_params["model"] = model_file_path input_params["model"] = model_file_path
# 上傳模型 (新增) # Upload model to device
if device_group: if device_group:
try: try:
print('[上傳模型]') print('[Uploading model]')
self.model_descriptor = kp.core.load_model_from_file( self.model_descriptor = kp.core.load_model_from_file(
device_group=device_group, device_group=device_group,
file_path=model_file_path file_path=model_file_path
) )
print(' - 上傳成功') print(' - Upload successful')
# 將模型描述符添加到輸入參數 # Add model descriptor to input parameters
input_params["model_descriptor"] = self.model_descriptor input_params["model_descriptor"] = self.model_descriptor
except Exception as e: except Exception as e:
print(f"上傳模型時發生錯誤: {e}") print(f"Error uploading model: {e}")
self.model_descriptor = None self.model_descriptor = None
print("Input parameters:", input_params) print("Input parameters:", input_params)
@ -210,110 +247,124 @@ class InferenceController:
return True return True
except Exception as e: except Exception as e:
print(f"選擇工具時發生錯誤: {e}") print(f"Error selecting tool: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
return False return False
def _clear_inference_queue(self): def _clear_inference_queue(self):
"""清空推論佇列中的所有數據""" """Clear all data from the inference queue."""
try: try:
# 清空現有佇列 # Clear existing queue
while not self.inference_queue.empty(): while not self.inference_queue.empty():
try: try:
self.inference_queue.get_nowait() self.inference_queue.get_nowait()
except queue.Empty: except queue.Empty:
break break
print("推論佇列已清空") print("Inference queue cleared")
except Exception as e: except Exception as e:
print(f"清空推論佇列時發生錯誤: {e}") print(f"Error clearing inference queue: {e}")
def add_frame_to_queue(self, frame): def add_frame_to_queue(self, frame):
"""將影格添加到推論佇列""" """
Add a frame to the inference queue.
Args:
frame: The image frame to add (numpy array).
"""
try: try:
# 更新原始影格尺寸 # Update original frame dimensions
if frame is not None and hasattr(frame, 'shape'): if frame is not None and hasattr(frame, 'shape'):
height, width = frame.shape[:2] height, width = frame.shape[:2]
self.original_frame_width = width self.original_frame_width = width
self.original_frame_height = height self.original_frame_height = height
# 添加到佇列 # Add to queue
if not self.inference_queue.full(): if not self.inference_queue.full():
self.inference_queue.put(frame) self.inference_queue.put(frame)
except Exception as e: except Exception as e:
print(f"添加影格到佇列時發生錯誤: {e}") print(f"Error adding frame to queue: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
def stop_inference(self): def stop_inference(self):
"""Stop the inference worker""" """Stop the inference worker thread."""
if self.inference_worker: if self.inference_worker:
self.inference_worker.stop() self.inference_worker.stop()
self.inference_worker = None self.inference_worker = None
def process_uploaded_image(self, file_path): def process_uploaded_image(self, file_path):
"""處理上傳的圖片並進行推論""" """
Process an uploaded image and run inference.
Args:
file_path (str): Path to the uploaded image file.
"""
try: try:
if not os.path.exists(file_path): if not os.path.exists(file_path):
print(f"錯誤: 檔案不存在 {file_path}") print(f"Error: File does not exist {file_path}")
return return
# 清空推論佇列,確保只處理最新的圖片 # Clear inference queue to ensure only the latest image is processed
self._clear_inference_queue() self._clear_inference_queue()
# 讀取圖片 # Read image
img = cv2.imread(file_path) img = cv2.imread(file_path)
if img is None: if img is None:
print(f"錯誤: 無法讀取圖片 {file_path}") print(f"Error: Unable to read image {file_path}")
return return
# 更新推論工作器參數 # Update inference worker parameters
if self.inference_worker: if self.inference_worker:
self.inference_worker.input_params["file_path"] = file_path self.inference_worker.input_params["file_path"] = file_path
# 將圖片添加到推論佇列 # Add image to inference queue
if not self.inference_queue.full(): if not self.inference_queue.full():
self.inference_queue.put(img) self.inference_queue.put(img)
print(f"已將圖片 {file_path} 添加到推論佇列") print(f"Added image {file_path} to inference queue")
else: else:
print("警告: 推論佇列已滿") print("Warning: Inference queue is full")
else: else:
print("錯誤: 推論工作器未初始化") print("Error: Inference worker not initialized")
except Exception as e: except Exception as e:
print(f"處理上傳圖片時發生錯誤: {e}") print(f"Error processing uploaded image: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
def select_custom_tool(self, tool_config): def select_custom_tool(self, tool_config):
""" """
選擇自訂模型工具並配置推論 Select a custom model tool and configure inference.
Args: Args:
tool_config: 包含自訂模型配置的字典必須包含: tool_config (dict): Configuration dictionary containing:
- custom_model_path: .nef 模型檔案路徑 - custom_model_path: Path to .nef model file
- custom_scpu_path: SCPU firmware 路徑 - custom_scpu_path: Path to SCPU firmware
- custom_ncpu_path: NCPU firmware 路徑 - custom_ncpu_path: Path to NCPU firmware
- custom_labels: Optional list of class labels
Returns:
bool: True if custom tool selection successful, False otherwise.
""" """
try: try:
print("選擇自訂模型:", tool_config.get("display_name")) print("Selecting custom model:", tool_config.get("display_name"))
self.current_tool_config = tool_config self.current_tool_config = tool_config
# 清空推論佇列 # Clear inference queue
self._clear_inference_queue() self._clear_inference_queue()
# 儲存當前工具類型以供下次比較 # Store current tool type for next comparison
self.previous_tool_config = tool_config self.previous_tool_config = tool_config
# 準備輸入參數 # Prepare input parameters
input_params = tool_config.get("input_parameters", {}).copy() input_params = tool_config.get("input_parameters", {}).copy()
# 添加自訂模型路徑 # Add custom model paths
input_params["custom_model_path"] = tool_config.get("custom_model_path") input_params["custom_model_path"] = tool_config.get("custom_model_path")
input_params["custom_scpu_path"] = tool_config.get("custom_scpu_path") input_params["custom_scpu_path"] = tool_config.get("custom_scpu_path")
input_params["custom_ncpu_path"] = tool_config.get("custom_ncpu_path") input_params["custom_ncpu_path"] = tool_config.get("custom_ncpu_path")
input_params["custom_labels"] = tool_config.get("custom_labels") input_params["custom_labels"] = tool_config.get("custom_labels")
# 取得設備相關設定 # Get device-related settings
selected_device = self.device_controller.get_selected_device() selected_device = self.device_controller.get_selected_device()
if selected_device: if selected_device:
if isinstance(selected_device, dict): if isinstance(selected_device, dict):
@ -324,19 +375,19 @@ class InferenceController:
devices = self.device_controller.connected_devices devices = self.device_controller.connected_devices
if devices and len(devices) > 0: if devices and len(devices) > 0:
input_params["usb_port_id"] = devices[0].get("usb_port_id", 0) input_params["usb_port_id"] = devices[0].get("usb_port_id", 0)
print("警告: 未選擇特定設備,使用第一個可用設備") print("Warning: No device selected, using first available device")
else: else:
input_params["usb_port_id"] = 0 input_params["usb_port_id"] = 0
print("警告: 沒有已連接的設備,使用預設 usb_port_id 0") print("Warning: No connected devices, using default usb_port_id 0")
print("自訂模型輸入參數:", input_params) print("Custom model input parameters:", input_params)
# 停止現有的推論工作器 # Stop existing inference worker
if self.inference_worker: if self.inference_worker:
self.inference_worker.stop() self.inference_worker.stop()
self.inference_worker = None self.inference_worker = None
# 創建新的自訂推論工作器 # Create new custom inference worker
self.inference_worker = CustomInferenceWorkerThread( self.inference_worker = CustomInferenceWorkerThread(
self.inference_queue, self.inference_queue,
min_interval=0.5, min_interval=0.5,
@ -347,23 +398,23 @@ class InferenceController:
self.main_window.handle_inference_result self.main_window.handle_inference_result
) )
self.inference_worker.start() self.inference_worker.start()
print("自訂模型推論工作器已啟動") print("Custom model inference worker started")
# 啟動相機 (自訂模型預設使用視訊模式) # Start camera (custom model defaults to video mode)
tool_type = tool_config.get("input_info", {}).get("type", "video") tool_type = tool_config.get("input_info", {}).get("type", "video")
if tool_type == "video": if tool_type == "video":
if self._camera_was_active and self.main_window.media_controller.video_thread is not None: if self._camera_was_active and self.main_window.media_controller.video_thread is not None:
self.main_window.media_controller.video_thread.change_pixmap_signal.connect( self.main_window.media_controller.video_thread.change_pixmap_signal.connect(
self.main_window.media_controller.update_image self.main_window.media_controller.update_image
) )
print("相機已重新連接用於視訊處理") print("Camera reconnected for video processing")
else: else:
self.main_window.media_controller.start_camera() self.main_window.media_controller.start_camera()
return True return True
except Exception as e: except Exception as e:
print(f"選擇自訂模型時發生錯誤: {e}") print(f"Error selecting custom model: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
return False return False

View File

@ -1,3 +1,10 @@
"""
media_controller.py - Media Controller
This module handles media operations including camera capture, video/audio
recording, screenshot capture, and image display with bounding box overlay.
"""
import cv2 import cv2
import os import os
from PyQt5.QtWidgets import QFileDialog from PyQt5.QtWidgets import QFileDialog
@ -7,8 +14,28 @@ from PyQt5.QtCore import Qt, QRect
from src.models.video_thread import VideoThread from src.models.video_thread import VideoThread
from src.utils.image_utils import qimage_to_numpy from src.utils.image_utils import qimage_to_numpy
class MediaController: class MediaController:
"""
Controller class for managing media operations.
Attributes:
main_window: Reference to the main application window
inference_controller: Reference to the inference controller
video_thread: Thread for camera video capture
recording: Flag indicating if video recording is active
recording_audio: Flag indicating if audio recording is active
recorded_frames: List of recorded video frames
"""
def __init__(self, main_window, inference_controller): def __init__(self, main_window, inference_controller):
"""
Initialize the MediaController.
Args:
main_window: Reference to the main application window.
inference_controller: Reference to the inference controller.
"""
self.main_window = main_window self.main_window = main_window
self.inference_controller = inference_controller self.inference_controller = inference_controller
self.video_thread = None self.video_thread = None
@ -16,14 +43,18 @@ class MediaController:
self.recording_audio = False self.recording_audio = False
self.recorded_frames = [] self.recorded_frames = []
self._signal_was_connected = False # Track if signal was previously connected self._signal_was_connected = False # Track if signal was previously connected
self._inference_paused = False # 追蹤推論是否暫停 self._inference_paused = False # Track if inference is paused
def start_camera(self): def start_camera(self):
"""啟動相機進行視訊擷取""" """
Start the camera for video capture.
Initializes the video thread and connects the signal for frame updates.
"""
try: try:
if self.video_thread is None: if self.video_thread is None:
print("初始化相機執行緒...") print("Initializing camera thread...")
# 先清除畫布上的任何文字或圖像 # Clear any text or image on the canvas
if hasattr(self.main_window, 'canvas_label'): if hasattr(self.main_window, 'canvas_label'):
self.main_window.canvas_label.clear() self.main_window.canvas_label.clear()
@ -32,94 +63,103 @@ class MediaController:
try: try:
self.video_thread.change_pixmap_signal.connect(self.update_image) self.video_thread.change_pixmap_signal.connect(self.update_image)
self._signal_was_connected = True self._signal_was_connected = True
print("相機信號連接成功") print("Camera signal connected successfully")
except Exception as e: except Exception as e:
print(f"連接相機信號時發生錯誤: {e}") print(f"Error connecting camera signal: {e}")
# 啟動相機執行緒 # Start camera thread
self.video_thread.start() self.video_thread.start()
print("相機執行緒啟動成功") print("Camera thread started successfully")
else: else:
print("相機已經在運行中") print("Camera is already running")
except Exception as e: except Exception as e:
print(f"啟動相機時發生錯誤: {e}") print(f"Error starting camera: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
def stop_camera(self): def stop_camera(self):
"""停止相機""" """
Stop the camera and release resources.
Disconnects signal connections and stops the video thread.
"""
try: try:
if self.video_thread is not None: if self.video_thread is not None:
print("停止相機執行緒") print("Stopping camera thread")
# 確保先斷開信號連接 # Disconnect signal connection first
if self._signal_was_connected: if self._signal_was_connected:
try: try:
self.video_thread.change_pixmap_signal.disconnect() self.video_thread.change_pixmap_signal.disconnect()
self._signal_was_connected = False self._signal_was_connected = False
print("已斷開相機信號連接") print("Camera signal disconnected")
except Exception as e: except Exception as e:
print(f"斷開信號連接時發生錯誤: {e}") print(f"Error disconnecting signal: {e}")
# 停止執行緒 # Stop thread
self.video_thread.stop() self.video_thread.stop()
self.video_thread = None self.video_thread = None
print("相機已完全停止") print("Camera completely stopped")
except Exception as e: except Exception as e:
print(f"停止相機時發生錯誤: {e}") print(f"Error stopping camera: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
def update_image(self, qt_image): def update_image(self, qt_image):
"""更新圖像顯示並處理推論""" """
Update the image display and process inference.
Args:
qt_image (QImage): The image frame from the camera.
"""
try: try:
# 更新畫布上的圖像 # Update image on canvas
if hasattr(self.main_window, 'canvas_label'): if hasattr(self.main_window, 'canvas_label'):
pixmap = QPixmap.fromImage(qt_image) pixmap = QPixmap.fromImage(qt_image)
# 如果有邊界框,繪製它們 # If there are bounding boxes, draw them
if hasattr(self.main_window, 'current_bounding_boxes') and self.main_window.current_bounding_boxes is not None: if hasattr(self.main_window, 'current_bounding_boxes') and self.main_window.current_bounding_boxes is not None:
painter = QPainter(pixmap) painter = QPainter(pixmap)
pen = QPen(Qt.red) pen = QPen(Qt.red)
pen.setWidth(2) pen.setWidth(2)
painter.setPen(pen) painter.setPen(pen)
# 遍歷並繪製所有邊界框 # Iterate and draw all bounding boxes
for bbox_info in self.main_window.current_bounding_boxes: for bbox_info in self.main_window.current_bounding_boxes:
# 確保邊界框資訊是合法的 # Ensure bounding box info is valid
if isinstance(bbox_info, list) and len(bbox_info) >= 4: if isinstance(bbox_info, list) and len(bbox_info) >= 4:
# 繪製矩形 # Draw rectangle
x1, y1, x2, y2 = bbox_info[0], bbox_info[1], bbox_info[2], bbox_info[3] x1, y1, x2, y2 = bbox_info[0], bbox_info[1], bbox_info[2], bbox_info[3]
painter.drawRect(QRect(x1, y1, x2 - x1, y2 - y1)) painter.drawRect(QRect(x1, y1, x2 - x1, y2 - y1))
# 如果有標籤邊界框的第5個元素繪製它 # If there's a label (5th element of bounding box), draw it
if len(bbox_info) > 4 and bbox_info[4]: if len(bbox_info) > 4 and bbox_info[4]:
font = QFont() font = QFont()
font.setPointSize(10) font.setPointSize(10)
painter.setFont(font) painter.setFont(font)
painter.setPen(QColor(255, 0, 0)) # 紅色 painter.setPen(QColor(255, 0, 0)) # Red color
# 計算標籤位置(邊界框上方) # Calculate label position (above bounding box)
label_x = x1 label_x = x1
label_y = y1 - 10 label_y = y1 - 10
# 確保標籤在畫布範圍內 # Ensure label is within canvas bounds
if label_y < 10: if label_y < 10:
label_y = y2 + 15 # 如果上方空間不足,放在底部 label_y = y2 + 15 # If not enough space above, place below
painter.drawText(label_x, label_y, str(bbox_info[4])) painter.drawText(label_x, label_y, str(bbox_info[4]))
painter.end() painter.end()
# 顯示圖像 # Display image
self.main_window.canvas_label.setPixmap(pixmap) self.main_window.canvas_label.setPixmap(pixmap)
# 只有在推論未暫停時才將影格添加到推論佇列 # Only add frame to inference queue if inference is not paused
if not self._inference_paused: if not self._inference_paused:
frame_np = qimage_to_numpy(qt_image) frame_np = qimage_to_numpy(qt_image)
self.inference_controller.add_frame_to_queue(frame_np) self.inference_controller.add_frame_to_queue(frame_np)
except Exception as e: except Exception as e:
print(f"更新圖像時發生錯誤: {e}") print(f"Error updating image: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
@ -256,23 +296,33 @@ class MediaController:
print(f"Error taking screenshot: {e}") print(f"Error taking screenshot: {e}")
def toggle_inference_pause(self): def toggle_inference_pause(self):
"""切換推論暫停狀態""" """
Toggle the inference pause state.
Returns:
bool: The new pause state (True if paused, False if running).
"""
try: try:
self._inference_paused = not self._inference_paused self._inference_paused = not self._inference_paused
if self._inference_paused: if self._inference_paused:
# 暫停時清除邊界框 # Clear bounding boxes when paused
self.main_window.current_bounding_boxes = None self.main_window.current_bounding_boxes = None
else: else:
# 恢復推論時,確保相機仍在運行 # When resuming inference, ensure camera is still running
if self.video_thread is None or not self.video_thread.isRunning(): if self.video_thread is None or not self.video_thread.isRunning():
self.start_camera() self.start_camera()
return self._inference_paused return self._inference_paused
except Exception as e: except Exception as e:
print(f"切換推論暫停狀態時發生錯誤: {e}") print(f"Error toggling inference pause state: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
return self._inference_paused return self._inference_paused
def is_inference_paused(self): def is_inference_paused(self):
"""檢查推論是否暫停""" """
Check if inference is paused.
Returns:
bool: True if inference is paused, False otherwise.
"""
return self._inference_paused return self._inference_paused

View File

@ -1,7 +1,8 @@
""" """
Custom Inference Worker custom_inference_worker.py - Custom Inference Worker
使用使用者上傳的自訂模型進行推論
前後處理使用 script.py 中定義的 YOLO V5 處理邏輯 This module provides a worker thread for running inference using user-uploaded
custom models. It uses YOLO V5 pre/post-processing logic for object detection.
""" """
import os import os
import time import time
@ -15,7 +16,7 @@ import kp
from kp.KPBaseClass.ValueBase import ValueRepresentBase from kp.KPBaseClass.ValueBase import ValueRepresentBase
# COCO 數據集的類別名稱 # COCO dataset class names (80 classes)
COCO_CLASSES = [ COCO_CLASSES = [
'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat',
'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat',
@ -80,7 +81,7 @@ class ExampleYoloResult(ValueRepresentBase):
return member_variable_dict return member_variable_dict
# YOLO 常數 # YOLO constants
YOLO_V3_CELL_BOX_NUM = 3 YOLO_V3_CELL_BOX_NUM = 3
NMS_THRESH_YOLOV5 = 0.5 NMS_THRESH_YOLOV5 = 0.5
YOLO_MAX_DETECTION_PER_CLASS = 100 YOLO_MAX_DETECTION_PER_CLASS = 100
@ -246,26 +247,27 @@ def post_process_yolo_v5(inference_float_node_output_list: List[kp.InferenceFloa
def preprocess_frame(frame, target_size=640): def preprocess_frame(frame, target_size=640):
""" """
預處理影像 Preprocess image frame for YOLO inference.
Args: Args:
frame: 原始 BGR 影像 frame: Original BGR image (numpy array).
target_size: 目標大小 (default 640 for YOLO) target_size (int): Target size for resizing (default 640 for YOLO).
Returns: Returns:
processed_frame: 處理後的影像 (BGR565 格式) tuple: (processed_frame in BGR565 format, original_width, original_height)
original_width: 原始寬度
original_height: 原始高度 Raises:
Exception: If input frame is None.
""" """
if frame is None: if frame is None:
raise Exception("輸入的 frame 為 None") raise Exception("Input frame is None")
original_height, original_width = frame.shape[:2] original_height, original_width = frame.shape[:2]
# 調整大小 # Resize to target size
resized_frame = cv2.resize(frame, (target_size, target_size)) resized_frame = cv2.resize(frame, (target_size, target_size))
# 轉換為 BGR565 格式 # Convert to BGR565 format
frame_bgr565 = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2BGR565) frame_bgr565 = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2BGR565)
return frame_bgr565, original_width, original_height return frame_bgr565, original_width, original_height
@ -273,18 +275,18 @@ def preprocess_frame(frame, target_size=640):
def postprocess(output_list, hw_preproc_info, original_width, original_height, target_size=640, thresh=0.2): def postprocess(output_list, hw_preproc_info, original_width, original_height, target_size=640, thresh=0.2):
""" """
後處理 YOLO 輸出 Post-process YOLO model output.
Args: Args:
output_list: 模型輸出節點列表 output_list: List of model output nodes.
hw_preproc_info: 硬體預處理資訊 hw_preproc_info: Hardware preprocessing info from Kneron device.
original_width: 原始影像寬度 original_width (int): Original image width.
original_height: 原始影像高度 original_height (int): Original image height.
target_size: 縮放目標大小 target_size (int): Resize target size used during preprocessing.
thresh: 閾值 thresh (float): Detection confidence threshold.
Returns: Returns:
yolo_result: YOLO 偵測結果 ExampleYoloResult: YOLO detection results with bounding boxes.
""" """
yolo_result = post_process_yolo_v5( yolo_result = post_process_yolo_v5(
inference_float_node_output_list=output_list, inference_float_node_output_list=output_list,
@ -292,7 +294,7 @@ def postprocess(output_list, hw_preproc_info, original_width, original_height, t
thresh_value=thresh thresh_value=thresh
) )
# 調整邊界框座標以符合原始尺寸 # Adjust bounding box coordinates to match original dimensions
width_ratio = original_width / target_size width_ratio = original_width / target_size
height_ratio = original_height / target_size height_ratio = original_height / target_size
@ -307,12 +309,29 @@ def postprocess(output_list, hw_preproc_info, original_width, original_height, t
class CustomInferenceWorkerThread(QThread): class CustomInferenceWorkerThread(QThread):
""" """
自訂模型推論工作線程 Custom model inference worker thread.
使用使用者上傳的模型和韌體進行推論
Uses user-uploaded model and firmware to perform inference on video frames.
Attributes:
inference_result_signal: Signal emitted with inference results
frame_queue: Queue containing frames to process
device_group: Connected Kneron device group
model_descriptor: Loaded model descriptor
custom_labels: Custom class labels (optional)
""" """
inference_result_signal = pyqtSignal(object) inference_result_signal = pyqtSignal(object)
def __init__(self, frame_queue, min_interval=0.5, mse_threshold=500): def __init__(self, frame_queue, min_interval=0.5, mse_threshold=500):
"""
Initialize the CustomInferenceWorkerThread.
Args:
frame_queue: Queue containing frames to process.
min_interval (float): Minimum seconds between inferences.
mse_threshold (float): MSE threshold for detecting frame changes.
"""
super().__init__() super().__init__()
self.frame_queue = frame_queue self.frame_queue = frame_queue
self.min_interval = min_interval self.min_interval = min_interval
@ -323,63 +342,76 @@ class CustomInferenceWorkerThread(QThread):
self.cached_result = None self.cached_result = None
self.input_params = {} self.input_params = {}
# 設備和模型相關 # Device and model related
self.device_group = None self.device_group = None
self.model_descriptor = None self.model_descriptor = None
self.is_initialized = False self.is_initialized = False
# 自訂標籤 # Custom labels
self.custom_labels = None self.custom_labels = None
def initialize_device(self): def initialize_device(self):
"""初始化設備、上傳韌體和模型""" """
Initialize device, upload firmware and model.
Returns:
bool: True if initialization successful, False otherwise.
"""
try: try:
model_path = self.input_params.get("custom_model_path") model_path = self.input_params.get("custom_model_path")
scpu_path = self.input_params.get("custom_scpu_path") scpu_path = self.input_params.get("custom_scpu_path")
ncpu_path = self.input_params.get("custom_ncpu_path") ncpu_path = self.input_params.get("custom_ncpu_path")
port_id = self.input_params.get("usb_port_id", 0) port_id = self.input_params.get("usb_port_id", 0)
# 載入自訂標籤 # Load custom labels
self.custom_labels = self.input_params.get("custom_labels") self.custom_labels = self.input_params.get("custom_labels")
if self.custom_labels: if self.custom_labels:
print(f'[自訂標籤] 已載入 {len(self.custom_labels)} 個類別') print(f'[Custom Labels] Loaded {len(self.custom_labels)} classes')
else: else:
print('[自訂標籤] 未提供,使用預設 COCO 類別') print('[Custom Labels] Not provided, using default COCO classes')
if not all([model_path, scpu_path, ncpu_path]): if not all([model_path, scpu_path, ncpu_path]):
print("缺少必要的檔案路徑") print("Missing required file paths")
return False return False
# 連接設備 # Connect to device
print('[連接裝置]') print('[Connecting device]')
self.device_group = kp.core.connect_devices(usb_port_ids=[port_id]) self.device_group = kp.core.connect_devices(usb_port_ids=[port_id])
kp.core.set_timeout(device_group=self.device_group, milliseconds=5000) kp.core.set_timeout(device_group=self.device_group, milliseconds=5000)
print(' - 連接成功') print(' - Connection successful')
# 上傳韌體 # Upload firmware
print('[上傳韌體]') print('[Uploading firmware]')
kp.core.load_firmware_from_file(self.device_group, scpu_path, ncpu_path) kp.core.load_firmware_from_file(self.device_group, scpu_path, ncpu_path)
print(' - 韌體上傳成功') print(' - Firmware upload successful')
# 上傳模型 # Upload model
print('[上傳模型]') print('[Uploading model]')
self.model_descriptor = kp.core.load_model_from_file( self.model_descriptor = kp.core.load_model_from_file(
self.device_group, self.device_group,
file_path=model_path file_path=model_path
) )
print(' - 模型上傳成功') print(' - Model upload successful')
self.is_initialized = True self.is_initialized = True
return True return True
except Exception as e: except Exception as e:
print(f"初始化設備時發生錯誤: {e}") print(f"Error initializing device: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
return False return False
def run_single_inference(self, frame): def run_single_inference(self, frame):
"""執行單次推論""" """
Execute a single inference on the given frame.
Args:
frame: Input image frame (numpy array in BGR format).
Returns:
dict: Inference results with bounding boxes and labels, or None on error.
"""
try: try:
if not self.is_initialized: if not self.is_initialized:
if not self.initialize_device(): if not self.initialize_device():
@ -425,12 +457,12 @@ class CustomInferenceWorkerThread(QThread):
original_height original_height
) )
# 轉換為標準格式 # Convert to standard format
bounding_boxes = [ bounding_boxes = [
[box.x1, box.y1, box.x2, box.y2] for box in yolo_result.box_list [box.x1, box.y1, box.x2, box.y2] for box in yolo_result.box_list
] ]
# 使用自訂標籤或預設 COCO 類別 # Use custom labels or default COCO classes
labels_to_use = self.custom_labels if self.custom_labels else COCO_CLASSES labels_to_use = self.custom_labels if self.custom_labels else COCO_CLASSES
results = [] results = []
@ -447,13 +479,18 @@ class CustomInferenceWorkerThread(QThread):
} }
except Exception as e: except Exception as e:
print(f"推論時發生錯誤: {e}") print(f"Error during inference: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
return None return None
def run(self): def run(self):
"""主執行循環""" """
Main execution loop.
Continuously processes frames from the queue, runs inference,
and emits results. Uses MSE-based frame change detection.
"""
while self._running: while self._running:
try: try:
frame = self.frame_queue.get(timeout=0.1) frame = self.frame_queue.get(timeout=0.1)
@ -464,7 +501,7 @@ class CustomInferenceWorkerThread(QThread):
if current_time - self.last_inference_time < self.min_interval: if current_time - self.last_inference_time < self.min_interval:
continue continue
# MSE 檢測以優化效能 # MSE detection to optimize performance
if self.last_frame is not None: if self.last_frame is not None:
if frame.shape != self.last_frame.shape: if frame.shape != self.last_frame.shape:
self.last_frame = None self.last_frame = None
@ -476,11 +513,11 @@ class CustomInferenceWorkerThread(QThread):
self.inference_result_signal.emit(self.cached_result) self.inference_result_signal.emit(self.cached_result)
continue continue
except Exception as e: except Exception as e:
print(f"計算 MSE 時發生錯誤: {e}") print(f"Error calculating MSE: {e}")
self.last_frame = None self.last_frame = None
self.cached_result = None self.cached_result = None
# 執行推論 # Execute inference
result = self.run_single_inference(frame) result = self.run_single_inference(frame)
self.last_inference_time = current_time self.last_inference_time = current_time
@ -490,22 +527,22 @@ class CustomInferenceWorkerThread(QThread):
if result is not None: if result is not None:
self.inference_result_signal.emit(result) self.inference_result_signal.emit(result)
# 斷開設備連接 # Disconnect device
self.cleanup() self.cleanup()
self.quit() self.quit()
def cleanup(self): def cleanup(self):
"""清理資源""" """Clean up resources and disconnect device."""
try: try:
if self.device_group is not None: if self.device_group is not None:
kp.core.disconnect_devices(self.device_group) kp.core.disconnect_devices(self.device_group)
print('[已斷開裝置]') print('[Device disconnected]')
self.device_group = None self.device_group = None
except Exception as e: except Exception as e:
print(f"清理資源時發生錯誤: {e}") print(f"Error cleaning up resources: {e}")
def stop(self): def stop(self):
"""停止工作線程""" """Stop the worker thread and clean up resources."""
self._running = False self._running = False
self.wait() self.wait()
self.cleanup() self.cleanup()

View File

@ -1,9 +1,30 @@
import os, time, queue, numpy as np, importlib.util """
inference_worker.py - Inference Worker Thread
This module provides a QThread-based worker for running AI model inference
on video frames. It supports dynamic module loading and frame caching.
"""
import os
import time
import queue
import numpy as np
import importlib.util
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
from src.config import UTILS_DIR from src.config import UTILS_DIR
def load_inference_module(mode, model_name): def load_inference_module(mode, model_name):
"""Dynamically load an inference module""" """
Dynamically load an inference module from the utils directory.
Args:
mode (str): The inference mode/category (e.g., 'object_detection').
model_name (str): The name of the model.
Returns:
module: The loaded Python module containing the inference function.
"""
script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py")
module_name = f"{mode}_{model_name}" module_name = f"{mode}_{model_name}"
spec = importlib.util.spec_from_file_location(module_name, script_path) spec = importlib.util.spec_from_file_location(module_name, script_path)
@ -11,10 +32,38 @@ def load_inference_module(mode, model_name):
spec.loader.exec_module(module) spec.loader.exec_module(module)
return module return module
class InferenceWorkerThread(QThread): class InferenceWorkerThread(QThread):
"""
Worker thread for running AI model inference on frames.
This thread processes frames from a queue, runs inference using a
dynamically loaded module, and emits results via Qt signals.
Attributes:
inference_result_signal: Signal emitted with inference results
frame_queue: Queue containing frames to process
mode: Inference mode/category
model_name: Name of the model to use
min_interval: Minimum time between inferences (seconds)
mse_threshold: MSE threshold for frame change detection
once_mode: If True, stop after one inference
"""
inference_result_signal = pyqtSignal(object) inference_result_signal = pyqtSignal(object)
def __init__(self, frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False): def __init__(self, frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False):
"""
Initialize the InferenceWorkerThread.
Args:
frame_queue: Queue containing frames to process.
mode (str): Inference mode/category.
model_name (str): Name of the model.
min_interval (float): Minimum seconds between inferences.
mse_threshold (float): MSE threshold for detecting frame changes.
once_mode (bool): If True, stop after processing one frame.
"""
super().__init__() super().__init__()
self.frame_queue = frame_queue self.frame_queue = frame_queue
self.mode = mode self.mode = mode
@ -32,6 +81,13 @@ class InferenceWorkerThread(QThread):
self.inference_module = load_inference_module(mode, model_name) self.inference_module = load_inference_module(mode, model_name)
def run(self): def run(self):
"""
Main thread execution loop.
Continuously processes frames from the queue, runs inference,
and emits results. Uses MSE-based frame change detection to
optimize performance by skipping similar frames.
"""
while self._running: while self._running:
try: try:
frame = self.frame_queue.get(timeout=0.1) frame = self.frame_queue.get(timeout=0.1)
@ -43,18 +99,18 @@ class InferenceWorkerThread(QThread):
continue continue
if self.last_frame is not None: if self.last_frame is not None:
# 檢查當前幀與上一幀的尺寸是否相同 # Check if current frame and previous frame have same dimensions
if frame.shape != self.last_frame.shape: if frame.shape != self.last_frame.shape:
print(f"幀尺寸變更: 從 {self.last_frame.shape} 變更為 {frame.shape}") print(f"Frame size changed: from {self.last_frame.shape} to {frame.shape}")
# 尺寸不同時,重置上一幀和緩存結果 # Reset last frame and cached result when dimensions differ
self.last_frame = None self.last_frame = None
self.cached_result = None self.cached_result = None
else: else:
# 只有在尺寸相同時才進行 MSE 計算 # Only calculate MSE when dimensions are the same
try: try:
mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2) mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2)
if mse < self.mse_threshold and self.cached_result is not None: if mse < self.mse_threshold and self.cached_result is not None:
# 只有在結果不為 None 時才發送信號 # Only emit signal if result is not None
if self.cached_result is not None: if self.cached_result is not None:
self.inference_result_signal.emit(self.cached_result) self.inference_result_signal.emit(self.cached_result)
if self.once_mode: if self.once_mode:
@ -62,8 +118,8 @@ class InferenceWorkerThread(QThread):
break break
continue continue
except Exception as e: except Exception as e:
print(f"計算 MSE 時發生錯誤: {e}") print(f"Error calculating MSE: {e}")
# 發生錯誤時重置上一幀和緩存結果 # Reset last frame and cached result on error
self.last_frame = None self.last_frame = None
self.cached_result = None self.cached_result = None
@ -77,7 +133,7 @@ class InferenceWorkerThread(QThread):
self.last_frame = frame.copy() self.last_frame = frame.copy()
self.cached_result = result self.cached_result = result
# 只有在結果不為 None 時才發送信號 # Only emit signal if result is not None
if result is not None: if result is not None:
self.inference_result_signal.emit(result) self.inference_result_signal.emit(result)
@ -88,5 +144,6 @@ class InferenceWorkerThread(QThread):
self.quit() self.quit()
def stop(self): def stop(self):
"""Stop the inference worker thread and wait for it to finish."""
self._running = False self._running = False
self.wait() self.wait()

View File

@ -1,22 +1,51 @@
"""
video_thread.py - Video Capture Thread
This module provides a QThread-based video capture worker for webcam streaming.
It handles camera connection, frame capture, and automatic reconnection.
"""
import cv2 import cv2
import time import time
import threading import threading
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtGui import QImage from PyQt5.QtGui import QImage
class VideoThread(QThread): class VideoThread(QThread):
"""
Thread for capturing video frames from a webcam.
Emits frames as QImage objects via Qt signals for display in the UI.
Handles camera connection with timeout and automatic reconnection.
Signals:
change_pixmap_signal: Emitted with each new frame (QImage)
camera_error_signal: Emitted when camera errors occur (str)
"""
change_pixmap_signal = pyqtSignal(QImage) change_pixmap_signal = pyqtSignal(QImage)
camera_error_signal = pyqtSignal(str) # 新增:相機錯誤信號 camera_error_signal = pyqtSignal(str) # Camera error signal
def __init__(self): def __init__(self):
"""Initialize the VideoThread with default settings."""
super().__init__() super().__init__()
self._run_flag = True self._run_flag = True
self._camera_open_attempts = 0 self._camera_open_attempts = 0
self._max_attempts = 3 self._max_attempts = 3
self._camera_timeout = 5 # 相機開啟超時秒數 self._camera_timeout = 5 # Camera open timeout in seconds
def _open_camera_with_timeout(self, camera_index, backend=None): def _open_camera_with_timeout(self, camera_index, backend=None):
"""使用超時機制開啟相機""" """
Open camera with timeout mechanism.
Args:
camera_index (int): Index of the camera to open.
backend: OpenCV video capture backend (e.g., cv2.CAP_DSHOW).
Returns:
cv2.VideoCapture: Opened camera object, or None if failed.
"""
cap = None cap = None
open_success = False open_success = False
@ -29,17 +58,17 @@ class VideoThread(QThread):
cap = cv2.VideoCapture(camera_index) cap = cv2.VideoCapture(camera_index)
open_success = cap is not None and cap.isOpened() open_success = cap is not None and cap.isOpened()
except Exception as e: except Exception as e:
print(f"開啟相機時發生異常: {e}") print(f"Exception while opening camera: {e}")
open_success = False open_success = False
# 在單獨的線程中嘗試開啟相機 # Try to open camera in a separate thread
thread = threading.Thread(target=try_open) thread = threading.Thread(target=try_open)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
thread.join(timeout=self._camera_timeout) thread.join(timeout=self._camera_timeout)
if thread.is_alive(): if thread.is_alive():
print(f"相機開啟超時 ({self._camera_timeout})") print(f"Camera open timeout ({self._camera_timeout} seconds)")
return None return None
if open_success: if open_success:
@ -50,73 +79,79 @@ class VideoThread(QThread):
return None return None
def run(self): def run(self):
# 嘗試多次開啟相機 """
Main thread execution loop.
Attempts to open the camera with multiple backends and retries.
Captures frames and emits them as QImage signals.
"""
# Try multiple times to open camera
while self._camera_open_attempts < self._max_attempts and self._run_flag: while self._camera_open_attempts < self._max_attempts and self._run_flag:
self._camera_open_attempts += 1 self._camera_open_attempts += 1
print(f"嘗試開啟相機 (嘗試 {self._camera_open_attempts}/{self._max_attempts})...") print(f"Attempting to open camera (attempt {self._camera_open_attempts}/{self._max_attempts})...")
# 嘗試使用DirectShow後端通常在Windows上更快 # Try DirectShow backend first (usually faster on Windows)
cap = self._open_camera_with_timeout(0, cv2.CAP_DSHOW) cap = self._open_camera_with_timeout(0, cv2.CAP_DSHOW)
if cap is None: if cap is None:
print("無法使用DirectShow開啟相機嘗試預設後端") print("Unable to open camera with DirectShow, trying default backend")
cap = self._open_camera_with_timeout(0) cap = self._open_camera_with_timeout(0)
if cap is None: if cap is None:
print(f"無法使用任何後端開啟相機等待1秒後重試...") print("Unable to open camera with any backend, retrying in 1 second...")
time.sleep(1) time.sleep(1)
continue continue
# 設置相機屬性以獲得更好的性能 # Set camera properties for better performance
# 降低解析度以提高啟動速度和幀率 # Lower resolution for faster startup and higher frame rate
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30) cap.set(cv2.CAP_PROP_FPS, 30)
# 設置緩衝區大小為1減少延遲 # Set buffer size to 1 to reduce latency
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
# 預熱相機,丟棄前幾幀以加快穩定速度 # Warm up camera by discarding first few frames
for _ in range(5): for _ in range(5):
cap.read() cap.read()
# 相機開啟成功,重置嘗試計數 # Camera opened successfully, reset attempt count
self._camera_open_attempts = 0 self._camera_open_attempts = 0
# 主循環 # Main capture loop
while self._run_flag: while self._run_flag:
ret, frame = cap.read() ret, frame = cap.read()
if ret: if ret:
# 轉換為RGB格式 # Convert to RGB format
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
height, width, channel = frame.shape height, width, channel = frame.shape
bytes_per_line = channel * width bytes_per_line = channel * width
qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888) qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
self.change_pixmap_signal.emit(qt_image) self.change_pixmap_signal.emit(qt_image)
else: else:
print("無法讀取相機幀,相機可能已斷開連接") print("Unable to read camera frame, camera may be disconnected")
break break
# 釋放相機資源 # Release camera resources
cap.release() cap.release()
# 如果是因為停止信號而退出循環,則不再重試 # If stopped by stop signal, don't retry
if not self._run_flag: if not self._run_flag:
break break
print("相機連接中斷,嘗試重新連接...") print("Camera connection lost, attempting to reconnect...")
if self._camera_open_attempts >= self._max_attempts: if self._camera_open_attempts >= self._max_attempts:
print("達到最大嘗試次數,無法開啟相機") print("Maximum attempts reached, unable to open camera")
def stop(self): def stop(self):
"""停止執行緒""" """Stop the video capture thread."""
try: try:
print("正在停止相機執行緒...") print("Stopping camera thread...")
self._run_flag = False self._run_flag = False
# 等待執行緒完成 # Wait for thread to finish
if self.isRunning(): if self.isRunning():
self.wait() self.wait()
print("相機執行緒已停止") print("Camera thread stopped")
except Exception as e: except Exception as e:
print(f"停止相機執行緒時發生錯誤: {e}") print(f"Error stopping camera thread: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())

View File

@ -1,20 +1,37 @@
"""
device_service.py - Device Service
This module provides device scanning functionality with timeout mechanism
for detecting connected Kneron devices.
"""
import kp import kp
import threading import threading
class EmptyDescriptor: class EmptyDescriptor:
"""
Empty device descriptor placeholder.
Used when no devices are found or when device scanning fails.
"""
def __init__(self): def __init__(self):
"""Initialize with empty device list."""
self.device_descriptor_number = 0 self.device_descriptor_number = 0
self.device_descriptor_list = [] self.device_descriptor_list = []
def check_available_device(timeout=0.5): def check_available_device(timeout=0.5):
""" """
掃描可用設備帶有超時機制 Scan for available Kneron devices with timeout mechanism.
Args: Args:
timeout: 超時秒數預設 5 timeout (float): Timeout in seconds (default 0.5).
Returns: Returns:
設備描述符 Device descriptor object containing found devices,
or EmptyDescriptor if no devices found or scan failed.
""" """
result = [None] result = [None]
error = [None] error = [None]
@ -26,32 +43,32 @@ def check_available_device(timeout=0.5):
error[0] = e error[0] = e
try: try:
print("[SCAN] 開始掃描設備...") print("[SCAN] Starting device scan...")
# 在單獨的線程中執行掃描 # Execute scan in a separate thread
thread = threading.Thread(target=scan_devices) thread = threading.Thread(target=scan_devices)
thread.daemon = True thread.daemon = True
print("[SCAN] 啟動掃描線程...") print("[SCAN] Starting scan thread...")
thread.start() thread.start()
print(f"[SCAN] 等待掃描完成 (超時: {timeout})...") print(f"[SCAN] Waiting for scan to complete (timeout: {timeout}s)...")
thread.join(timeout=timeout) thread.join(timeout=timeout)
if thread.is_alive(): if thread.is_alive():
print(f"[SCAN] 設備掃描超時 ({timeout})") print(f"[SCAN] Device scan timeout ({timeout}s)")
return EmptyDescriptor() return EmptyDescriptor()
print("[SCAN] 掃描線程已完成") print("[SCAN] Scan thread completed")
if error[0]: if error[0]:
print(f"[SCAN] Error scanning devices: {error[0]}") print(f"[SCAN] Error scanning devices: {error[0]}")
return EmptyDescriptor() return EmptyDescriptor()
if result[0] is None: if result[0] is None:
print("[SCAN] 結果為 None") print("[SCAN] Result is None")
return EmptyDescriptor() return EmptyDescriptor()
print("[SCAN] device_descriptors:", result[0]) print("[SCAN] device_descriptors:", result[0])
print("[SCAN] 準備返回結果...") print("[SCAN] Preparing to return results...")
import sys import sys
sys.stdout.flush() sys.stdout.flush()
return result[0] return result[0]
@ -59,60 +76,3 @@ def check_available_device(timeout=0.5):
except Exception as e: except Exception as e:
print(f"[SCAN] Error scanning devices: {e}") print(f"[SCAN] Error scanning devices: {e}")
return EmptyDescriptor() return EmptyDescriptor()
# def check_available_device():
# # 模擬設備描述符
# print("checking available devices")
# class EmptyDescriptor:
# def __init__(self):
# self.device_descriptor_number = 0
# self.device_descriptor_list = [{
# "usb_port_id": 4,
# "vendor_id": "0x3231",
# "product_id": "0x720",
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
# "kn_number": "0xB306224C",
# "is_connectable": True,
# "usb_port_path": "4-1",
# "firmware": "KDP2 Comp/F"
# },
# {
# "usb_port_id": 5,
# "vendor_id": "0x3231",
# "product_id": "0x520",
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
# "kn_number": "0xB306224C",
# "is_connectable": True,
# "usb_port_path": "4-1",
# "firmware": "KDP2 Comp/F"
# }]
# return EmptyDescriptor()
# device_descriptors = [
# {
# "usb_port_id": 4,
# "vendor_id": "0x3231",
# "product_id": "0x720",
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
# "kn_number": "0xB306224C",
# "is_connectable": True,
# "usb_port_path": "4-1",
# "firmware": "KDP2 Comp/F"
# },
# {
# "usb_port_id": 5,
# "vendor_id": "0x3231",
# "product_id": "0x520",
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
# "kn_number": "0xB306224C",
# "is_connectable": True,
# "usb_port_path": "4-1",
# "firmware": "KDP2 Comp/F"
# }
# ]
# return device_descriptors
# def get_dongle_type(self, product_id):
# for dongle_type in self.K_:
# if dongle_type.value == product_id:
# return dongle_type
# return None

View File

@ -1,3 +1,10 @@
"""
file_service.py - File Service
This module provides file upload functionality including image file handling
and integration with the inference system.
"""
import os import os
import shutil import shutil
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication
@ -5,111 +12,139 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtGui import QImage, QPixmap
import cv2 import cv2
class FileService: class FileService:
"""
Service class for handling file uploads.
Manages file selection, copying, and integration with the camera
and inference systems.
Attributes:
main_window: Reference to the main application window
upload_dir: Directory path for storing uploaded files
destination: Path to the most recently uploaded file
"""
def __init__(self, main_window, upload_dir): def __init__(self, main_window, upload_dir):
"""
Initialize the FileService.
Args:
main_window: Reference to the main application window.
upload_dir (str): Directory path for storing uploaded files.
"""
self.main_window = main_window self.main_window = main_window
self.upload_dir = upload_dir self.upload_dir = upload_dir
self.destination = None self.destination = None
self._camera_was_active = False # Track if camera was active before upload self._camera_was_active = False # Track if camera was active before upload
def upload_file(self): def upload_file(self):
"""處理檔案上傳流程""" """
Handle the file upload process.
Opens a file dialog, copies the selected file to the upload directory,
and triggers inference processing if a tool is configured.
Returns:
str: Path to the uploaded file, or None if upload failed/cancelled.
"""
try: try:
# 1. 先完全停止相機(如果正在運行) # 1. Pause camera if running
if hasattr(self.main_window, 'media_controller') and self.main_window.media_controller.video_thread is not None: if hasattr(self.main_window, 'media_controller') and self.main_window.media_controller.video_thread is not None:
print("上傳前停止相機") print("Stopping camera before upload")
try: try:
# 儲存狀態以指示相機正在運行 # Save state to indicate camera was running
self._camera_was_active = True self._camera_was_active = True
# 顯示上傳中提示 # Show upload preparation message
if hasattr(self.main_window, 'canvas_label'): if hasattr(self.main_window, 'canvas_label'):
self.main_window.canvas_label.setText("準備上傳檔案...") self.main_window.canvas_label.setText("Preparing to upload file...")
self.main_window.canvas_label.setAlignment(Qt.AlignCenter) self.main_window.canvas_label.setAlignment(Qt.AlignCenter)
self.main_window.canvas_label.setStyleSheet("color: white; font-size: 24px;") self.main_window.canvas_label.setStyleSheet("color: white; font-size: 24px;")
# 確保 UI 更新 # Ensure UI updates
QApplication.processEvents() QApplication.processEvents()
# 只暫停推論,不完全停止相機 # Only pause inference, don't completely stop camera
if not self.main_window.media_controller._inference_paused: if not self.main_window.media_controller._inference_paused:
self.main_window.media_controller.toggle_inference_pause() self.main_window.media_controller.toggle_inference_pause()
# 斷開信號連接但不停止相機執行緒 # Disconnect signal but don't stop camera thread
if hasattr(self.main_window.media_controller, '_signal_was_connected') and self.main_window.media_controller._signal_was_connected: if hasattr(self.main_window.media_controller, '_signal_was_connected') and self.main_window.media_controller._signal_was_connected:
try: try:
self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect() self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect()
self.main_window.media_controller._signal_was_connected = False self.main_window.media_controller._signal_was_connected = False
print("已暫時斷開相機信號連接") print("Temporarily disconnected camera signal")
except Exception as e: except Exception as e:
print(f"斷開信號連接時發生錯誤: {e}") print(f"Error disconnecting signal: {e}")
except Exception as e: except Exception as e:
print(f"準備上傳時發生錯誤: {e}") print(f"Error preparing for upload: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
# 即使發生錯誤,也繼續嘗試上傳 # Continue trying to upload even if error occurs
else: else:
self._camera_was_active = False self._camera_was_active = False
print("呼叫 QFileDialog.getOpenFileName") print("Calling QFileDialog.getOpenFileName")
options = QFileDialog.Options() options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName( file_path, _ = QFileDialog.getOpenFileName(
self.main_window, self.main_window,
"上傳檔案", "Upload File",
"", "",
"所有檔案 (*)", "All Files (*)",
options=options options=options
) )
print("檔案路徑取得:", file_path) print("File path obtained:", file_path)
if file_path: if file_path:
print("檢查上傳目錄是否存在") print("Checking if upload directory exists")
if not os.path.exists(self.upload_dir): if not os.path.exists(self.upload_dir):
os.makedirs(self.upload_dir) os.makedirs(self.upload_dir)
print(f"建立上傳目錄: {self.upload_dir}") print(f"Created upload directory: {self.upload_dir}")
print("檢查原始檔案是否存在:", file_path) print("Checking if source file exists:", file_path)
if not os.path.exists(file_path): if not os.path.exists(file_path):
self.show_message(QMessageBox.Critical, "錯誤", "選擇的檔案不存在") self.show_message(QMessageBox.Critical, "Error", "Selected file does not exist")
return None return None
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
self.destination = os.path.join(self.upload_dir, file_name) self.destination = os.path.join(self.upload_dir, file_name)
print("目標路徑:", self.destination) print("Destination path:", self.destination)
# 檢查目標路徑是否可寫入 # Check if destination is writable
try: try:
print("測試檔案寫入權限") print("Testing file write permissions")
with open(self.destination, 'wb') as test_file: with open(self.destination, 'wb') as test_file:
pass pass
os.remove(self.destination) os.remove(self.destination)
print("測試檔案建立和刪除成功") print("Test file creation and deletion successful")
except PermissionError: except PermissionError:
self.show_message(QMessageBox.Critical, "錯誤", "無法寫入目標目錄") self.show_message(QMessageBox.Critical, "Error", "Cannot write to destination directory")
return None return None
print("開始檔案複製") print("Starting file copy")
try: try:
shutil.copy2(file_path, self.destination) shutil.copy2(file_path, self.destination)
print("檔案複製成功") print("File copy successful")
# 更新主視窗目的地 # Update main window destination
self.main_window.destination = self.destination self.main_window.destination = self.destination
print(f"更新主視窗目的地: {self.main_window.destination}") print(f"Updated main window destination: {self.main_window.destination}")
# 處理上傳的影像 # Process uploaded image
if self.main_window.inference_controller.current_tool_config: if self.main_window.inference_controller.current_tool_config:
print("使用推論控制器處理上傳的影像") print("Processing uploaded image with inference controller")
# 先在畫布上顯示影像 # First display image on canvas
try: try:
# 載入和顯示影像 # Load and display image
image = cv2.imread(self.destination) image = cv2.imread(self.destination)
if image is not None: if image is not None:
# 轉換為 RGB 顯示 # Convert to RGB for display
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
height, width, channel = image_rgb.shape height, width, channel = image_rgb.shape
bytes_per_line = channel * width bytes_per_line = channel * width
qt_image = QImage(image_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888) qt_image = QImage(image_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888)
# 將影像縮放以適應畫布 # Scale image to fit canvas
canvas_size = self.main_window.canvas_label.size() canvas_size = self.main_window.canvas_label.size()
scaled_image = qt_image.scaled( scaled_image = qt_image.scaled(
int(canvas_size.width() * 0.95), int(canvas_size.width() * 0.95),
@ -118,28 +153,28 @@ class FileService:
Qt.SmoothTransformation Qt.SmoothTransformation
) )
self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image)) self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image))
print("影像顯示在畫布上") print("Image displayed on canvas")
except Exception as e: except Exception as e:
print(f"顯示影像時發生錯誤: {e}") print(f"Error displaying image: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
# 然後使用推論處理它 # Then process it with inference
self.main_window.inference_controller.process_uploaded_image(self.destination) self.main_window.inference_controller.process_uploaded_image(self.destination)
return self.destination return self.destination
except Exception as e: except Exception as e:
import traceback import traceback
print("檔案複製過程中發生錯誤:\n", traceback.format_exc()) print("Error during file copy:\n", traceback.format_exc())
self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}") self.show_message(QMessageBox.Critical, "Error", f"Upload error: {str(e)}")
return None return None
return None return None
except Exception as e: except Exception as e:
import traceback import traceback
print("上傳過程中發生錯誤:\n", traceback.format_exc()) print("Error during upload:\n", traceback.format_exc())
self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}") self.show_message(QMessageBox.Critical, "Error", f"Upload error: {str(e)}")
return None return None
finally: finally:
# 如果相機之前是活動的,嘗試恢復相機連接 # 如果相機之前是活動的,嘗試恢復相機連接

View File

@ -1,14 +0,0 @@
import kp
import cv2, os, shutil, sys
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton,
QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget,
QScrollArea, QFrame, QListWidgetItem, QTextEdit)
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtMultimedia import QCamera, QCameraImageCapture, QCameraInfo, QMediaRecorder, QAudioRecorder
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtGui import QPixmap, QMovie
from PyQt5.QtCore import Qt, QTimer, QUrl
from ..config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR
def show_error_popup(self, message):
error_dialog = QMessageBox.critical(self, "Error", message)

View File

@ -87,69 +87,6 @@ def create_device_popup(parent, device_controller):
# Store reference to this list widget for later use # Store reference to this list widget for later use
parent.device_list_widget_popup = device_list_widget_popup parent.device_list_widget_popup = device_list_widget_popup
# Comment out the device details section
"""
# Device details section (initially hidden)
device_details = QFrame(popup)
device_details.setObjectName("device-details")
device_details.setProperty("class", "device-info")
device_details.setVisible(False) # Initially hidden
details_layout = QGridLayout(device_details)
details_layout.setColumnStretch(1, 1)
# 增加行間距
details_layout.setVerticalSpacing(2)
# Device Type
device_type_label_title = QLabel("Device Type:")
device_type_label_title.setStyleSheet("font-size: 14px;")
details_layout.addWidget(device_type_label_title, 0, 0)
device_type_label = QLabel("-")
device_type_label.setObjectName("device-type")
device_type_label.setStyleSheet("font-size: 14px;")
details_layout.addWidget(device_type_label, 0, 1)
# Port ID
port_id_label_title = QLabel("Port ID:")
port_id_label_title.setStyleSheet("font-size: 14px;")
details_layout.addWidget(port_id_label_title, 1, 0)
port_id_label = QLabel("-")
port_id_label.setObjectName("port-id")
port_id_label.setStyleSheet("font-size: 14px;")
details_layout.addWidget(port_id_label, 1, 1)
# KN Number
kn_number_label_title = QLabel("KN Number:")
kn_number_label_title.setStyleSheet("font-size: 14px;")
details_layout.addWidget(kn_number_label_title, 2, 0)
kn_number_label = QLabel("-")
kn_number_label.setObjectName("kn-number")
kn_number_label.setStyleSheet("font-size: 14px;")
details_layout.addWidget(kn_number_label, 2, 1)
# Status
status_label_title = QLabel("Status:")
status_label_title.setStyleSheet("font-size: 14px;")
details_layout.addWidget(status_label_title, 3, 0)
status_label = QLabel("-")
status_label.setObjectName("status")
status_label.setStyleSheet("font-size: 14px;")
details_layout.addWidget(status_label, 3, 1)
# Store references to labels
parent.device_detail_labels = {
"device-type": device_type_label,
"port-id": port_id_label,
"kn-number": kn_number_label,
"status": status_label,
"frame": device_details
}
# Connect item selection to show details
device_list_widget_popup.itemClicked.connect(lambda item: show_device_details(parent, item))
list_layout.addWidget(device_details)
"""
popup_layout.addWidget(list_section) popup_layout.addWidget(list_section)
# Button area # Button area
@ -178,31 +115,6 @@ def create_device_popup(parent, device_controller):
print(f"Error in create_device_popup: {e}") print(f"Error in create_device_popup: {e}")
return QWidget(parent) return QWidget(parent)
# Also need to comment out the show_device_details function since it's no longer used
# def show_device_details(parent, item):
# """Show details for the selected device"""
# try:
# # Get the device data from the item
# device_data = item.data(Qt.UserRole)
# print("device_data", device_data)
# if not device_data:
# return
# # Update the detail labels
# labels = parent.device_detail_labels
# # Set values
# labels["device-type"].setText(device_data.get("dongle", "-"))
# labels["port-id"].setText(str(device_data.get("usb_port_id", "-")))
# labels["kn-number"].setText(str(device_data.get("kn_number", "-")))
# labels["status"].setText("Connected" if device_data.get("is_connectable", True) else "Not Available")
# # Show the details frame
# labels["frame"].setVisible(True)
# except Exception as e:
# print(f"Error showing device details: {e}")
def refresh_devices(parent, device_controller): def refresh_devices(parent, device_controller):
"""Refresh the device list and update the UI""" """Refresh the device list and update the UI"""
@ -215,13 +127,6 @@ def refresh_devices(parent, device_controller):
# Clear the list # Clear the list
parent.device_list_widget_popup.clear() parent.device_list_widget_popup.clear()
# Comment out the details frame visibility setting
"""
# Hide details frame
if hasattr(parent, "device_detail_labels") and "frame" in parent.device_detail_labels:
parent.device_detail_labels["frame"].setVisible(False)
"""
# Track unique device models to avoid duplicates # Track unique device models to avoid duplicates
seen_models = set() seen_models = set()

View File

@ -1,70 +0,0 @@
# srcs/views/device_connection_popup.py
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtCore import Qt
from ..config import UXUI_ASSETS, SECONDARY_COLOR, BUTTON_STYLE
import os
class DeviceConnectionPopup(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.init_ui()
def init_ui(self):
# 設定彈出視窗的大小和樣式
self.setFixedSize(int(self.parent.width() * 0.67), int(self.parent.height() * 0.67))
self.setStyleSheet(f"""
QWidget {{
background-color: {SECONDARY_COLOR};
border-radius: 20px;
padding: 20px;
}}
""")
layout = QVBoxLayout(self)
# 標題列
title_layout = QHBoxLayout()
title_layout.setAlignment(Qt.AlignCenter)
# 容器來放置圖示和標籤
title_container = QWidget()
container_layout = QHBoxLayout(title_container)
container_layout.setSpacing(10)
# 添加圖示
device_icon = QSvgWidget(os.path.join(UXUI_ASSETS, "Assets_svg/ic_window_device.svg"))
device_icon.setFixedSize(35, 35)
container_layout.addWidget(device_icon)
# 添加標籤
popup_label = QLabel("Device Connection")
popup_label.setStyleSheet("color: white; font-size: 25px;")
container_layout.addWidget(popup_label)
# 設置容器的對齊方式
container_layout.setAlignment(Qt.AlignCenter)
# 將容器添加到標題布局中
title_layout.addWidget(title_container)
layout.addLayout(title_layout)
# 設備列表
self.device_list_widget = QListWidget(self)
layout.addWidget(self.device_list_widget)
# 按鈕區域
button_layout = QHBoxLayout()
refresh_button = QPushButton("Refresh")
refresh_button.clicked.connect(self.parent.refresh_devices)
refresh_button.setStyleSheet(BUTTON_STYLE)
button_layout.addWidget(refresh_button)
done_button = QPushButton("Done")
done_button.setStyleSheet(BUTTON_STYLE)
done_button.clicked.connect(self.parent.hide_device_popup)
button_layout.addWidget(done_button)
layout.addLayout(button_layout)

View File

@ -1,23 +1,68 @@
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, """
QLineEdit, QComboBox, QFrame, QMessageBox) login_screen.py - User Login Screen
This module contains the LoginScreen class which provides the authentication
interface for accessing the utilities section of the application.
"""
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QFrame, QMessageBox
)
from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QFont from PyQt5.QtGui import QPixmap, QFont
import os import os
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR
class LoginScreen(QWidget): class LoginScreen(QWidget):
"""
Login Screen Class
Provides user authentication interface with:
- Server authentication type selection
- Username and password input fields
- Login and back navigation buttons
Signals:
login_success: Emitted when login is successful
back_to_selection: Emitted when user clicks back button
Attributes:
server_combo (QComboBox): Authentication type dropdown
username_input (QLineEdit): Username input field
password_input (QLineEdit): Password input field (masked)
error_label (QLabel): Error message display label
"""
# Signals for navigation # Signals for navigation
login_success = pyqtSignal() login_success = pyqtSignal()
back_to_selection = pyqtSignal() back_to_selection = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
"""
Initialize the LoginScreen.
Args:
parent: Optional parent widget.
"""
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
def init_ui(self): def init_ui(self):
"""
Initialize the user interface.
Creates the login form layout with:
- Header with Kneron logo
- Authentication type selection dropdown
- Username and password input fields
- Login and back buttons
- Footer with copyright information
"""
# Basic window setup # Basic window setup
self.setGeometry(100, 100, *WINDOW_SIZE) self.setGeometry(100, 100, *WINDOW_SIZE)
self.setStyleSheet(f"background-color: #F5F7FA;") # Light gray background self.setStyleSheet("background-color: #F5F7FA;")
# Main layout # Main layout
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@ -211,17 +256,30 @@ class LoginScreen(QWidget):
layout.addWidget(footer_label) layout.addWidget(footer_label)
def attempt_login(self): def attempt_login(self):
"""
Attempt to log in with the provided credentials.
Validates that both username and password are provided.
In production, this would authenticate against a server.
Emits login_success signal on successful validation.
"""
username = self.username_input.text() username = self.username_input.text()
password = self.password_input.text() password = self.password_input.text()
# For demo purposes, use a simple validation # Basic validation - ensure both fields are filled
if not username or not password: if not username or not password:
self.show_error("Please enter both username and password") self.show_error("Please enter both username and password")
return return
# Simulate login success (in a real app, you would validate with your server) # Simulate login success (in production, validate with server)
self.login_success.emit() self.login_success.emit()
def show_error(self, message): def show_error(self, message):
"""
Display an error message to the user.
Args:
message (str): The error message to display.
"""
self.error_label.setText(message) self.error_label.setText(message)
self.error_label.show() self.error_label.show()

View File

@ -1,5 +1,19 @@
import os, sys, json, queue, numpy as np """
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QMessageBox, QApplication, QListWidgetItem mainWindows.py - Main Application Window
This module contains the MainWindow class which serves as the primary UI
container for the Kneron Academy AI Playground application.
"""
import os
import sys
import json
import queue
import numpy as np
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout,
QFrame, QMessageBox, QApplication, QListWidgetItem
)
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QPixmap, QMovie from PyQt5.QtGui import QPixmap, QMovie
@ -16,7 +30,27 @@ from src.views.components.media_panel import create_media_panel
from src.views.components.device_popup import create_device_popup, refresh_devices from src.views.components.device_popup import create_device_popup, refresh_devices
from src.views.components.custom_model_block import create_custom_model_block from src.views.components.custom_model_block import create_custom_model_block
class MainWindow(QWidget): class MainWindow(QWidget):
"""
Main Application Window Class
The primary UI container for the Kneron Academy AI Playground.
Manages the overall layout, device connections, media display, and AI inference.
Attributes:
device_controller (DeviceController): Handles Kneron device connections
inference_controller (InferenceController): Manages AI model inference
media_controller (MediaController): Controls camera and media playback
file_service (FileService): Handles file upload operations
config_utils (ConfigUtils): Manages configuration settings
destination: Current output destination
current_bounding_boxes: Stores current bounding box detection results
canvas_label (QLabel): Display area for camera/media content
device_popup: Device connection popup dialog
overlay: Semi-transparent overlay for popup dialogs
"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -31,13 +65,18 @@ class MainWindow(QWidget):
# Set up UI and configuration # Set up UI and configuration
self.destination = None self.destination = None
self.current_bounding_boxes = None # 儲存當前的邊界框資訊(改為複數形式) self.current_bounding_boxes = None # Stores current bounding box info (plural form)
self.generate_global_config() self.generate_global_config()
self.init_ui() self.init_ui()
def init_ui(self): def init_ui(self):
"""
Initialize the user interface.
Sets up the main window geometry, title, and background color.
Shows a welcome screen first, then transitions to the main page.
"""
try: try:
# Basic window setup
self.setGeometry(100, 100, *WINDOW_SIZE) self.setGeometry(100, 100, *WINDOW_SIZE)
self.setWindowTitle('Kneron Academy AI Playground') self.setWindowTitle('Kneron Academy AI Playground')
self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};") self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};")
@ -56,6 +95,7 @@ class MainWindow(QWidget):
print(f"Error in init_ui: {e}") print(f"Error in init_ui: {e}")
def show_welcome_label(self): def show_welcome_label(self):
"""Display the welcome screen with Kneron logo."""
try: try:
welcome_label = QLabel(self) welcome_label = QLabel(self)
logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png") logo_path = os.path.join(UXUI_ASSETS, "Assets_png/kneron_logo.png")
@ -77,8 +117,13 @@ class MainWindow(QWidget):
print(f"Error in show_welcome_label: {e}") print(f"Error in show_welcome_label: {e}")
def show_device_popup_and_main_page(self): def show_device_popup_and_main_page(self):
"""
Transition from welcome screen to main page.
Clears the welcome screen, creates the main page layout,
sets up the device popup overlay, and auto-starts the camera.
"""
try: try:
# Clear welcome screen
self.clear_layout() self.clear_layout()
# 1. Initialize main page # 1. Initialize main page
@ -93,25 +138,32 @@ class MainWindow(QWidget):
# 3. Refresh devices - do this after UI is fully set up # 3. Refresh devices - do this after UI is fully set up
QTimer.singleShot(100, self.device_controller.refresh_devices) QTimer.singleShot(100, self.device_controller.refresh_devices)
# # 4. Show popup # 4. Show popup
self.show_device_popup() self.show_device_popup()
# 5. 延遲啟動相機,讓 UI 先完全顯示 # 5. Delay camera start to let UI fully display first
QTimer.singleShot(500, self.auto_start_camera) QTimer.singleShot(500, self.auto_start_camera)
print("Popup window setup complete") print("Popup window setup complete")
# # 5. Start camera automatically after a short delay
# QTimer.singleShot(100, self.auto_start_camera)
except Exception as e: except Exception as e:
print(f"Error in show_device_popup_and_main_page: {e}") print(f"Error in show_device_popup_and_main_page: {e}")
def create_main_page(self): def create_main_page(self):
"""
Create the main page layout.
Builds a two-column layout:
- Left column: Logo, device list, and custom model block
- Right column: Canvas area for media display and media panel controls
Returns:
QWidget: The main page widget containing all UI components.
"""
try: try:
main_page = QWidget(self) main_page = QWidget(self)
main_layout = QHBoxLayout(main_page) main_layout = QHBoxLayout(main_page)
main_page.setLayout(main_layout) main_page.setLayout(main_layout)
# Left layout - 使用固定寬度的容器,避免滾輪 # Left layout - fixed width container to avoid scrolling
left_container = QWidget() left_container = QWidget()
left_container.setFixedWidth(260) left_container.setFixedWidth(260)
left_layout = QVBoxLayout(left_container) left_layout = QVBoxLayout(left_container)
@ -131,11 +183,11 @@ class MainWindow(QWidget):
self.device_frame, self.device_list_widget = create_device_layout(self, self.device_controller) self.device_frame, self.device_list_widget = create_device_layout(self, self.device_controller)
left_layout.addWidget(self.device_frame) left_layout.addWidget(self.device_frame)
# 使用 Custom Model Block 取代原本的 AI Toolbox # Use Custom Model Block instead of original AI Toolbox
self.custom_model_frame = create_custom_model_block(self, self.inference_controller) self.custom_model_frame = create_custom_model_block(self, self.inference_controller)
left_layout.addWidget(self.custom_model_frame) left_layout.addWidget(self.custom_model_frame)
# 添加彈性空間,確保元件不會被拉伸 # Add stretch to prevent components from being stretched
left_layout.addStretch() left_layout.addStretch()
# Right layout # Right layout
@ -161,6 +213,12 @@ class MainWindow(QWidget):
return QWidget(self) return QWidget(self)
def device_popup_mask_setup(self): def device_popup_mask_setup(self):
"""
Set up the device popup overlay and mask.
Creates a semi-transparent dark overlay with a centered device
connection popup dialog.
"""
try: try:
print("Setting up popup mask") print("Setting up popup mask")
# Add transparent overlay # Add transparent overlay
@ -184,25 +242,35 @@ class MainWindow(QWidget):
print(f"Error in device_popup_mask_setup: {e}") print(f"Error in device_popup_mask_setup: {e}")
def show_device_popup(self): def show_device_popup(self):
"""
Show the device connection popup.
Displays the overlay and triggers a delayed device list refresh
to allow the UI to render first.
"""
try: try:
# 先顯示彈窗
self.overlay.show() self.overlay.show()
# 延遲刷新設備列表,讓 UI 先顯示 # Delay device list refresh to let UI display first
from src.views.components.device_popup import refresh_devices from src.views.components.device_popup import refresh_devices
QTimer.singleShot(100, lambda: refresh_devices(self, self.device_controller)) QTimer.singleShot(100, lambda: refresh_devices(self, self.device_controller))
except Exception as e: except Exception as e:
print(f"Error in show_device_popup: {e}") print(f"Error in show_device_popup: {e}")
def hide_device_popup(self): def hide_device_popup(self):
"""Hide the device connection popup overlay."""
try: try:
self.overlay.hide() self.overlay.hide()
except Exception as e: except Exception as e:
print(f"Error in hide_device_popup: {e}") print(f"Error in hide_device_popup: {e}")
def clear_layout(self): def clear_layout(self):
"""
Clear all widgets from the main layout.
Removes and deletes all child widgets from the layout.
"""
try: try:
# Clear all widgets
while self.layout.count(): while self.layout.count():
child = self.layout.takeAt(0) child = self.layout.takeAt(0)
if child.widget(): if child.widget():
@ -211,72 +279,82 @@ class MainWindow(QWidget):
print(f"Error in clear_layout: {e}") print(f"Error in clear_layout: {e}")
def handle_inference_result(self, result): def handle_inference_result(self, result):
"""處理來自推論工作器的結果""" """
Handle inference results from the inference worker.
Processes the inference result and either:
- Updates bounding boxes for drawing on canvas (for detection results)
- Shows a popup dialog (for classification/other results)
Args:
result: The inference result, can be dict with 'bounding box',
'bounding boxes', or other classification results.
"""
try: try:
# 如果結果為 None直接返回不處理 # If result is None, skip processing
if result is None: if result is None:
print("收到空的推論結果 (None),略過處理") print("Received empty inference result (None), skipping")
return return
# 輸出結果到控制台 print("Received inference result:", result)
print("收到推論結果:", result)
# 檢查結果是否包含邊界框資訊(支持新舊兩種格式) # Check if result contains bounding box info (supports both old and new formats)
if isinstance(result, dict) and "bounding box" in result: if isinstance(result, dict) and "bounding box" in result:
# 舊格式兼容: 單一邊界框 # Legacy format compatibility: single bounding box
self.current_bounding_boxes = [result["bounding box"]] self.current_bounding_boxes = [result["bounding box"]]
# 如果結果中有標籤,將其添加到邊界框中 # If result contains a label, add it to the bounding box
if "result" in result: if "result" in result:
# 確保邊界框列表至少有5個元素 # Ensure bounding box list has at least 5 elements
while len(self.current_bounding_boxes[0]) < 5: while len(self.current_bounding_boxes[0]) < 5:
self.current_bounding_boxes[0].append(None) self.current_bounding_boxes[0].append(None)
# 設置第5個元素為結果標籤 # Set the 5th element as the result label
self.current_bounding_boxes[0][4] = result["result"] self.current_bounding_boxes[0][4] = result["result"]
# 不需要顯示彈窗,因為邊界框會直接繪製在畫面上 # No popup needed as bounding boxes are drawn directly on canvas
return return
# 新格式: 多邊界框
# New format: multiple bounding boxes
elif isinstance(result, dict) and "bounding boxes" in result: elif isinstance(result, dict) and "bounding boxes" in result:
bboxes = result["bounding boxes"] bboxes = result["bounding boxes"]
results = result.get("results", []) results = result.get("results", [])
# 確保邊界框是列表格式 # Ensure bounding boxes is in list format
if not isinstance(bboxes, list): if not isinstance(bboxes, list):
print("錯誤: 'bounding boxes' 必須是列表格式") print("Error: 'bounding boxes' must be a list")
return return
# 如果只有邊界框座標沒有標籤 # If only bounding box coordinates without labels
if not results: if not results:
self.current_bounding_boxes = bboxes self.current_bounding_boxes = bboxes
else: else:
# 結合邊界框和標籤 # Combine bounding boxes with labels
self.current_bounding_boxes = [] self.current_bounding_boxes = []
for i, bbox in enumerate(bboxes): for i, bbox in enumerate(bboxes):
# 創建包含座標的新列表 # Create new list containing coordinates
new_bbox = list(bbox) new_bbox = list(bbox)
# 添加標籤(如果有) # Add label if available
if i < len(results): if i < len(results):
new_bbox.append(results[i]) new_bbox.append(results[i])
else: else:
new_bbox.append(None) new_bbox.append(None)
self.current_bounding_boxes.append(new_bbox) self.current_bounding_boxes.append(new_bbox)
# 不需要顯示彈窗,因為邊界框會直接繪製在畫面上 # No popup needed as bounding boxes are drawn directly on canvas
return return
# 對於非邊界框結果,使用彈窗顯示 (限制每次只顯示一個彈窗) # For non-bounding-box results, show popup (limit to one popup at a time)
# 檢查是否有其他消息框正在顯示 # Check if another message box is already showing
for widget in QApplication.topLevelWidgets(): for widget in QApplication.topLevelWidgets():
if isinstance(widget, QMessageBox) and widget.isVisible(): if isinstance(widget, QMessageBox) and widget.isVisible():
print("已有彈窗顯示中,跳過此次結果顯示") print("Popup already showing, skipping this result")
return return
# 創建QMessageBox # Create QMessageBox
msgBox = QMessageBox(self) msgBox = QMessageBox(self)
msgBox.setWindowTitle("推論結果") msgBox.setWindowTitle("Inference Result")
# 根據結果類型格式化文字 # Format text based on result type
if isinstance(result, dict): if isinstance(result, dict):
result_str = "\n".join(f"{key}: {value}" for key, value in result.items()) result_str = "\n".join(f"{key}: {value}" for key, value in result.items())
else: else:
@ -285,17 +363,15 @@ class MainWindow(QWidget):
msgBox.setText(result_str) msgBox.setText(result_str)
msgBox.setStandardButtons(QMessageBox.Ok) msgBox.setStandardButtons(QMessageBox.Ok)
# 設置樣式
msgBox.setStyleSheet(""" msgBox.setStyleSheet("""
QLabel { QLabel {
color: white; color: white;
} }
""") """)
# 顯示彈窗
msgBox.exec_() msgBox.exec_()
except Exception as e: except Exception as e:
print(f"處理推論結果時發生錯誤: {e}") print(f"Error handling inference result: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
@ -337,20 +413,25 @@ class MainWindow(QWidget):
return self.config_utils.generate_global_config() return self.config_utils.generate_global_config()
def auto_start_camera(self): def auto_start_camera(self):
"""自動啟動相機並顯示預覽""" """
print("開始啟動相機...") Automatically start the camera and display preview.
Shows a loading message on the canvas while the camera initializes,
then starts the camera in a separate thread to avoid blocking the UI.
"""
print("Starting camera...")
try: try:
# 先清除畫布,顯示加載中的信息 # Clear canvas and show loading message
if hasattr(self, 'canvas_label'): if hasattr(self, 'canvas_label'):
self.canvas_label.setText("相機啟動中...") self.canvas_label.setText("Starting camera...")
self.canvas_label.setAlignment(Qt.AlignCenter) self.canvas_label.setAlignment(Qt.AlignCenter)
self.canvas_label.setStyleSheet("color: white; font-size: 24px;") self.canvas_label.setStyleSheet("color: white; font-size: 24px;")
# 確保UI更新 # Ensure UI updates
QApplication.processEvents() QApplication.processEvents()
# 在單獨的線程中啟動相機,避免阻塞UI # Start camera in separate thread to avoid blocking UI
self.media_controller.start_camera() self.media_controller.start_camera()
except Exception as e: except Exception as e:
print(f"啟動相機時發生錯誤: {e}") print(f"Error starting camera: {e}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())

View File

@ -1,22 +1,56 @@
"""
selection_screen.py - Application Selection Screen
This module contains the SelectionScreen class which provides the initial
screen where users can choose between the Utilities or Demo App functions.
"""
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame
from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QFont, QIcon from PyQt5.QtGui import QPixmap, QFont, QIcon
import os import os
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, APP_NAME from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, APP_NAME
class SelectionScreen(QWidget): class SelectionScreen(QWidget):
"""
Selection Screen Class
The initial screen displayed to users, allowing them to choose between:
- Device Utilities: For managing and configuring Kneron devices
- Demo App: For testing AI inference capabilities
Signals:
open_utilities: Emitted when user selects the utilities option
open_demo_app: Emitted when user selects the demo app option
"""
# Signals for navigation # Signals for navigation
open_utilities = pyqtSignal() open_utilities = pyqtSignal()
open_demo_app = pyqtSignal() open_demo_app = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
"""
Initialize the SelectionScreen.
Args:
parent: Optional parent widget.
"""
super().__init__(parent) super().__init__(parent)
self.init_ui() self.init_ui()
def init_ui(self): def init_ui(self):
"""
Initialize the user interface.
Creates a card-based layout with:
- Header containing the Kneron logo
- Content area with title and two selection cards
- Footer with copyright information
"""
# Basic window setup # Basic window setup
self.setGeometry(100, 100, *WINDOW_SIZE) self.setGeometry(100, 100, *WINDOW_SIZE)
self.setStyleSheet(f"background-color: #F8F9FA;") # Slightly lighter background self.setStyleSheet("background-color: #F8F9FA;")
# Main layout # Main layout
layout = QVBoxLayout(self) layout = QVBoxLayout(self)

View File

@ -1,6 +1,16 @@
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, """
QFrame, QMessageBox, QScrollArea, QTableWidget, QTableWidgetItem, utilities_screen.py - Device Utilities Screen
QHeaderView, QProgressBar, QLineEdit, QAbstractItemView)
This module contains the UtilitiesScreen class which provides device management
functionality including device scanning, firmware updates, driver installation,
and purchased items management.
"""
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QFrame, QMessageBox, QScrollArea, QTableWidget, QTableWidgetItem,
QHeaderView, QProgressBar, QLineEdit, QAbstractItemView
)
from PyQt5.QtCore import Qt, pyqtSignal, QTimer from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QPixmap, QFont, QIcon, QColor from PyQt5.QtGui import QPixmap, QFont, QIcon, QColor
import os import os
@ -10,20 +20,53 @@ from src.controllers.device_controller import DeviceController
from src.services.device_service import check_available_device from src.services.device_service import check_available_device
from ..config import FW_DIR from ..config import FW_DIR
class UtilitiesScreen(QWidget): class UtilitiesScreen(QWidget):
"""
Utilities Screen Class
Provides device management functionality with two main pages:
1. Utilities Page: Device connection, firmware updates, driver installation
2. Purchased Items Page: Download and manage purchased AI models
Signals:
back_to_selection: Emitted when user clicks back button
Attributes:
device_controller (DeviceController): Controller for device operations
current_page (str): Current page being displayed ("utilities" or "purchased_items")
device_table (QTableWidget): Table displaying connected devices
purchased_table (QTableWidget): Table displaying purchased items
progress_bar (QProgressBar): Progress bar for operations
status_label (QLabel): Status message display
"""
# Signals for navigation # Signals for navigation
back_to_selection = pyqtSignal() back_to_selection = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
"""
Initialize the UtilitiesScreen.
Args:
parent: Optional parent widget.
"""
super().__init__(parent) super().__init__(parent)
self.device_controller = DeviceController(self) self.device_controller = DeviceController(self)
self.current_page = "utilities" # 追蹤當前頁面: "utilities" 或 "purchased_items" self.current_page = "utilities" # Track current page: "utilities" or "purchased_items"
self.init_ui() self.init_ui()
def init_ui(self): def init_ui(self):
"""
Initialize the user interface.
Creates the main layout with:
- Header with navigation buttons and logo
- Content container with switchable pages (utilities and purchased items)
"""
# Basic window setup # Basic window setup
self.setGeometry(100, 100, *WINDOW_SIZE) self.setGeometry(100, 100, *WINDOW_SIZE)
self.setStyleSheet(f"background-color: #F5F7FA;") # Light gray background self.setStyleSheet("background-color: #F5F7FA;")
# Main layout # Main layout
self.main_layout = QVBoxLayout(self) self.main_layout = QVBoxLayout(self)
@ -34,7 +77,7 @@ class UtilitiesScreen(QWidget):
header_frame = self.create_header() header_frame = self.create_header()
self.main_layout.addWidget(header_frame) self.main_layout.addWidget(header_frame)
# 創建主要內容容器 # Create main content container
self.content_container = QFrame(self) self.content_container = QFrame(self)
self.content_container.setStyleSheet(""" self.content_container.setStyleSheet("""
QFrame { QFrame {
@ -47,32 +90,43 @@ class UtilitiesScreen(QWidget):
content_layout.setContentsMargins(20, 20, 20, 20) content_layout.setContentsMargins(20, 20, 20, 20)
content_layout.setSpacing(20) content_layout.setSpacing(20)
# 創建兩個頁面的容器 # Create containers for both pages
self.utilities_page = QWidget() self.utilities_page = QWidget()
self.purchased_items_page = QWidget() self.purchased_items_page = QWidget()
# 設置 utilities 頁面 # Set up utilities page
self.setup_utilities_page() self.setup_utilities_page()
# 設置 purchased items 頁面 # Set up purchased items page
self.setup_purchased_items_page() self.setup_purchased_items_page()
# 添加頁面到內容容器 # Add pages to content container
content_layout.addWidget(self.utilities_page) content_layout.addWidget(self.utilities_page)
content_layout.addWidget(self.purchased_items_page) content_layout.addWidget(self.purchased_items_page)
# 初始顯示 utilities 頁面 # Initially show utilities page
self.utilities_page.show() self.utilities_page.show()
self.purchased_items_page.hide() self.purchased_items_page.hide()
# 添加內容容器到主佈局 # Add content container to main layout
self.main_layout.addWidget(self.content_container, 1) self.main_layout.addWidget(self.content_container, 1)
# Initialize with device refresh (暫時禁用自動刷新,避免阻塞) # Note: Auto-refresh disabled to prevent blocking
# QTimer.singleShot(500, self.refresh_devices) # QTimer.singleShot(500, self.refresh_devices)
def create_header(self): def create_header(self):
"""Create the header with back button and logo""" """
Create the header section with navigation elements.
Builds a header frame containing:
- Back button for returning to selection screen
- Title label showing current page
- Navigation buttons for switching between Utilities and Purchased Items pages
- Kneron logo on the right side
Returns:
QFrame: The header frame widget.
"""
header_frame = QFrame(self) header_frame = QFrame(self)
header_frame.setStyleSheet("background-color: #2C3E50; border-radius: 0px;") header_frame.setStyleSheet("background-color: #2C3E50; border-radius: 0px;")
header_frame.setFixedHeight(60) header_frame.setFixedHeight(60)
@ -167,7 +221,13 @@ class UtilitiesScreen(QWidget):
return header_frame return header_frame
def setup_utilities_page(self): def setup_utilities_page(self):
"""Create the utilities page""" """
Set up the utilities page layout.
Creates the utilities page with:
- Device connection section for managing connected devices
- Status section showing operation progress and device status
"""
utilities_layout = QVBoxLayout(self.utilities_page) utilities_layout = QVBoxLayout(self.utilities_page)
utilities_layout.setContentsMargins(0, 0, 0, 0) utilities_layout.setContentsMargins(0, 0, 0, 0)
utilities_layout.setSpacing(20) utilities_layout.setSpacing(20)
@ -181,17 +241,27 @@ class UtilitiesScreen(QWidget):
utilities_layout.addWidget(status_section) utilities_layout.addWidget(status_section)
def setup_purchased_items_page(self): def setup_purchased_items_page(self):
"""Create the purchased items page""" """
Set up the purchased items page layout.
Creates the page for displaying and downloading purchased AI models
and packages from the Kneron store.
"""
purchased_items_layout = QVBoxLayout(self.purchased_items_page) purchased_items_layout = QVBoxLayout(self.purchased_items_page)
purchased_items_layout.setContentsMargins(0, 0, 0, 0) purchased_items_layout.setContentsMargins(0, 0, 0, 0)
purchased_items_layout.setSpacing(20) purchased_items_layout.setSpacing(20)
# 已購買項目區域 # Purchased items section
purchased_items_section = self.create_purchased_items_section() purchased_items_section = self.create_purchased_items_section()
purchased_items_layout.addWidget(purchased_items_section) purchased_items_layout.addWidget(purchased_items_section)
def create_purchased_items_section(self): def create_purchased_items_section(self):
"""創建已購買項目區域""" """
Create the purchased items section.
Returns:
QFrame: A frame containing the purchased items table and action buttons.
"""
purchased_section = QFrame() purchased_section = QFrame()
purchased_section.setStyleSheet(""" purchased_section.setStyleSheet("""
QFrame { QFrame {
@ -205,36 +275,35 @@ class UtilitiesScreen(QWidget):
purchased_layout.setContentsMargins(15, 15, 15, 15) purchased_layout.setContentsMargins(15, 15, 15, 15)
purchased_layout.setSpacing(15) purchased_layout.setSpacing(15)
# 標題 # Title
title_label = QLabel("Your Purchased Items", purchased_section) title_label = QLabel("Your Purchased Items", purchased_section)
title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;") title_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2C3E50;")
purchased_layout.addWidget(title_label) purchased_layout.addWidget(title_label)
# 描述 # Description
desc_label = QLabel("Select items to download to your device", purchased_section) desc_label = QLabel("Select items to download to your device", purchased_section)
desc_label.setStyleSheet("font-size: 14px; color: #7F8C8D;") desc_label.setStyleSheet("font-size: 14px; color: #7F8C8D;")
purchased_layout.addWidget(desc_label) purchased_layout.addWidget(desc_label)
# 項目表格 # Items table (5 columns, "Action" column removed)
self.purchased_table = QTableWidget() self.purchased_table = QTableWidget()
# 修改為只有5列移除 "Action" 列
self.purchased_table.setColumnCount(5) self.purchased_table.setColumnCount(5)
self.purchased_table.setHorizontalHeaderLabels([ self.purchased_table.setHorizontalHeaderLabels([
"Select", "Product", "Model", "Current Version", "Compatible Dongles" "Select", "Product", "Model", "Current Version", "Compatible Dongles"
]) ])
# 設置行寬 # Set column widths
header = self.purchased_table.horizontalHeader() header = self.purchased_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # 勾選框列 header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Checkbox column
header.setSectionResizeMode(1, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Stretch)
header.setSectionResizeMode(2, QHeaderView.Stretch) header.setSectionResizeMode(2, QHeaderView.Stretch)
header.setSectionResizeMode(3, QHeaderView.ResizeToContents) header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
header.setSectionResizeMode(4, QHeaderView.Stretch) header.setSectionResizeMode(4, QHeaderView.Stretch)
# 設置表格高度 # Set table height
self.purchased_table.setMinimumHeight(300) self.purchased_table.setMinimumHeight(300)
# 啟用整行選擇 # Enable row selection
self.purchased_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.purchased_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.purchased_table.setStyleSheet(""" self.purchased_table.setStyleSheet("""
@ -263,10 +332,10 @@ class UtilitiesScreen(QWidget):
""") """)
purchased_layout.addWidget(self.purchased_table) purchased_layout.addWidget(self.purchased_table)
# 添加一些模擬數據 # Add mock data for demonstration
self.populate_mock_purchased_items() self.populate_mock_purchased_items()
# 下載按鈕 # Download buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
button_layout.setSpacing(10) button_layout.setSpacing(10)
@ -319,11 +388,17 @@ class UtilitiesScreen(QWidget):
return purchased_section return purchased_section
def populate_mock_purchased_items(self): def populate_mock_purchased_items(self):
"""填充模擬的已購買項目數據""" """
# 清空表格 Populate the purchased items table with mock data.
Clears the existing table and fills it with sample purchased items
for demonstration purposes. In production, this would fetch real
data from the server.
"""
# Clear the table
self.purchased_table.setRowCount(0) self.purchased_table.setRowCount(0)
# 模擬數據 # Mock data for demonstration
mock_items = [ mock_items = [
{ {
"product": "KL720 AI Package", "product": "KL720 AI Package",
@ -357,11 +432,11 @@ class UtilitiesScreen(QWidget):
} }
] ]
# 添加數據到表格 # Add data to table
for i, item in enumerate(mock_items): for i, item in enumerate(mock_items):
self.purchased_table.insertRow(i) self.purchased_table.insertRow(i)
# 創建勾選框 # Create checkbox item
checkbox_item = QTableWidgetItem() checkbox_item = QTableWidgetItem()
checkbox_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsSelectable) checkbox_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsSelectable)
checkbox_item.setCheckState(Qt.Unchecked) checkbox_item.setCheckState(Qt.Unchecked)
@ -373,31 +448,47 @@ class UtilitiesScreen(QWidget):
self.purchased_table.setItem(i, 4, QTableWidgetItem(item["dongles"])) self.purchased_table.setItem(i, 4, QTableWidgetItem(item["dongles"]))
def download_item(self, row): def download_item(self, row):
"""下載特定項目""" """
Download a specific item from the purchased items list.
Args:
row (int): The row index of the item to download.
"""
product = self.purchased_table.item(row, 1).text() product = self.purchased_table.item(row, 1).text()
model = self.purchased_table.item(row, 2).text() model = self.purchased_table.item(row, 2).text()
# 顯示進度條 # Show progress bar
self.show_progress(f"Downloading {product} - {model}...", 0) self.show_progress(f"Downloading {product} - {model}...", 0)
# 模擬下載過程 # Simulate download process
for i in range(1, 11): for i in range(1, 11):
progress = i * 10 progress = i * 10
QTimer.singleShot(i * 300, lambda p=progress: self.update_progress(p)) QTimer.singleShot(i * 300, lambda p=progress: self.update_progress(p))
# 完成下載 # Complete download
QTimer.singleShot(3000, lambda: self.handle_download_complete(product, model)) QTimer.singleShot(3000, lambda: self.handle_download_complete(product, model))
def handle_download_complete(self, product, model): def handle_download_complete(self, product, model):
"""處理下載完成""" """
Handle download completion for a single item.
Args:
product (str): The product name that was downloaded.
model (str): The model name that was downloaded.
"""
self.hide_progress() self.hide_progress()
QMessageBox.information(self, "Download Complete", f"{product} - {model} has been downloaded successfully!") QMessageBox.information(self, "Download Complete", f"{product} - {model} has been downloaded successfully!")
def download_selected_items(self): def download_selected_items(self):
"""下載所有勾選的項目""" """
Download all selected/checked items from the purchased items list.
Iterates through the table to find checked items and initiates
download for each. Shows progress bar during the download process.
"""
selected_rows = set() selected_rows = set()
# 檢查勾選的項目 # Check for selected items
for row in range(self.purchased_table.rowCount()): for row in range(self.purchased_table.rowCount()):
if self.purchased_table.item(row, 0).checkState() == Qt.Checked: if self.purchased_table.item(row, 0).checkState() == Qt.Checked:
selected_rows.add(row) selected_rows.add(row)
@ -406,38 +497,57 @@ class UtilitiesScreen(QWidget):
QMessageBox.warning(self, "No Selection", "Please select at least one item to download") QMessageBox.warning(self, "No Selection", "Please select at least one item to download")
return return
# 顯示進度條 # Show progress bar
self.show_progress(f"Downloading {len(selected_rows)} items...", 0) self.show_progress(f"Downloading {len(selected_rows)} items...", 0)
# 模擬下載過程 # Simulate download process
total_items = len(selected_rows) total_items = len(selected_rows)
for i, row in enumerate(selected_rows): for i, row in enumerate(selected_rows):
product = self.purchased_table.item(row, 1).text() product = self.purchased_table.item(row, 1).text()
model = self.purchased_table.item(row, 2).text() model = self.purchased_table.item(row, 2).text()
progress = int((i / total_items) * 100) progress = int((i / total_items) * 100)
# 更新進度條 # Update progress bar
self.update_progress(progress) self.update_progress(progress)
self.progress_title.setText(f"Downloading {product} - {model}... ({i+1}/{total_items})") self.progress_title.setText(f"Downloading {product} - {model}... ({i+1}/{total_items})")
# 模擬下載延遲 # Simulate download delay
QTimer.singleShot((i+1) * 1000, lambda p=product, m=model: self.status_label.setText(f"Downloaded {p} - {m}")) QTimer.singleShot((i+1) * 1000, lambda p=product, m=model: self.status_label.setText(f"Downloaded {p} - {m}"))
# 完成所有下載 # Complete all downloads
QTimer.singleShot((total_items+1) * 1000, self.handle_all_downloads_complete) QTimer.singleShot((total_items+1) * 1000, self.handle_all_downloads_complete)
def update_download_progress(self, progress, message): def update_download_progress(self, progress, message):
"""更新下載進度""" """
Update the download progress display.
Args:
progress (int): The progress percentage (0-100).
message (str): The status message to display.
"""
self.update_progress(progress) self.update_progress(progress)
self.progress_title.setText(message) self.progress_title.setText(message)
def handle_all_downloads_complete(self): def handle_all_downloads_complete(self):
"""處理所有下載完成""" """
Handle completion of all selected downloads.
Hides the progress bar and shows a success message to the user.
"""
self.hide_progress() self.hide_progress()
QMessageBox.information(self, "Downloads Complete", "All selected items have been downloaded successfully!") QMessageBox.information(self, "Downloads Complete", "All selected items have been downloaded successfully!")
def create_device_section(self): def create_device_section(self):
"""Create the device connection section""" """
Create the device connection section.
Builds a section containing:
- Device table showing connected Kneron devices with their details
- Action buttons for refresh, register, update firmware, and install drivers
Returns:
QFrame: The device section frame widget.
"""
device_section = QFrame(self) device_section = QFrame(self)
device_section.setStyleSheet(""" device_section.setStyleSheet("""
QFrame { QFrame {
@ -618,7 +728,16 @@ class UtilitiesScreen(QWidget):
return device_section return device_section
def create_status_section(self): def create_status_section(self):
"""Create the status section""" """
Create the status section for displaying operation status.
Builds a section containing:
- Status label showing current device status or operation result
- Progress bar for long-running operations (hidden by default)
Returns:
QFrame: The status section frame widget.
"""
status_section = QFrame(self) status_section = QFrame(self)
status_section.setStyleSheet(""" status_section.setStyleSheet("""
QFrame { QFrame {
@ -686,7 +805,16 @@ class UtilitiesScreen(QWidget):
return status_section return status_section
def refresh_devices(self): def refresh_devices(self):
"""Refresh the list of devices""" """
Refresh and scan for connected Kneron devices.
Clears the device table and scans for available devices using the
Kneron Plus SDK. Populates the table with device information including
model, port ID, firmware version, KN number, link speed, and status.
Returns:
bool: True if devices were found, False otherwise.
"""
try: try:
# Clear the table # Clear the table
self.device_table.setRowCount(0) self.device_table.setRowCount(0)
@ -719,32 +847,32 @@ class UtilitiesScreen(QWidget):
usb_id = QTableWidgetItem(port_id) usb_id = QTableWidgetItem(port_id)
self.device_table.setItem(i, 1, usb_id) self.device_table.setItem(i, 1, usb_id)
# 嘗試獲取 system_info 中的 firmware_version # Try to get firmware_version from system_info
firmware_version = "-" firmware_version = "-"
try: try:
if device.is_connectable: if device.is_connectable:
# 連接設備並獲取系統信息 # Connect to device and get system info
device_group = kp.core.connect_devices(usb_port_ids=[port_id]) device_group = kp.core.connect_devices(usb_port_ids=[port_id])
system_info = kp.core.get_system_info( system_info = kp.core.get_system_info(
device_group=device_group, device_group=device_group,
usb_port_id=device.usb_port_id usb_port_id=device.usb_port_id
) )
# 從 system_info 對象獲取固件版本 # Get firmware version from system_info object
if system_info and hasattr(system_info, 'firmware_version'): if system_info and hasattr(system_info, 'firmware_version'):
# firmware_version 是一個對象,獲取其字符串表示 # firmware_version is an object, get its string representation
fw_version = system_info.firmware_version fw_version = system_info.firmware_version
if hasattr(fw_version, 'firmware_version'): if hasattr(fw_version, 'firmware_version'):
# 提取版本號,移除字典格式 # Extract version number, remove dict format
version_str = fw_version.firmware_version version_str = fw_version.firmware_version
# 如果版本是字典格式,提取其中的值 # If version is in dict format, extract the value
if isinstance(version_str, dict) and 'firmware_version' in version_str: if isinstance(version_str, dict) and 'firmware_version' in version_str:
firmware_version = version_str['firmware_version'] firmware_version = version_str['firmware_version']
else: else:
firmware_version = version_str firmware_version = version_str
else: else:
# 將對象轉換為字符串並清理格式 # Convert object to string and clean up format
version_str = str(fw_version) version_str = str(fw_version)
# 嘗試從字符串中提取版本號 # Try to extract version number from string
import re import re
match = re.search(r'"firmware_version":\s*"([^"]+)"', version_str) match = re.search(r'"firmware_version":\s*"([^"]+)"', version_str)
if match: if match:
@ -765,7 +893,7 @@ class UtilitiesScreen(QWidget):
# Link Speed # Link Speed
link_speed_str = "Unknown" link_speed_str = "Unknown"
if hasattr(device, 'link_speed'): if hasattr(device, 'link_speed'):
# 從完整的 link_speed 字符串中提取 SPEED_XXX 部分 # Extract SPEED_XXX part from full link_speed string
full_speed = str(device.link_speed) full_speed = str(device.link_speed)
if "SUPER" in full_speed: if "SUPER" in full_speed:
link_speed_str = "SUPER" link_speed_str = "SUPER"
@ -774,7 +902,7 @@ class UtilitiesScreen(QWidget):
elif "FULL" in full_speed: elif "FULL" in full_speed:
link_speed_str = "FULL" link_speed_str = "FULL"
else: else:
# 嘗試提取 KP_USB_SPEED_XXX 部分 # Try to extract KP_USB_SPEED_XXX part
import re import re
match = re.search(r'KP_USB_SPEED_(\w+)', full_speed) match = re.search(r'KP_USB_SPEED_(\w+)', full_speed)
if match: if match:
@ -810,7 +938,13 @@ class UtilitiesScreen(QWidget):
return False return False
def register_device(self): def register_device(self):
"""Register the selected device""" """
Register the currently selected device.
Checks if a device is selected and initiates the registration process.
Currently shows a placeholder message as this feature is planned for
a future update.
"""
selected_rows = self.device_table.selectedItems() selected_rows = self.device_table.selectedItems()
if not selected_rows: if not selected_rows:
QMessageBox.warning(self, "Warning", "Please select a device to register") QMessageBox.warning(self, "Warning", "Please select a device to register")
@ -820,64 +954,78 @@ class UtilitiesScreen(QWidget):
QMessageBox.information(self, "Info", "Device registration functionality will be implemented in a future update") QMessageBox.information(self, "Info", "Device registration functionality will be implemented in a future update")
def update_firmware(self): def update_firmware(self):
"""Update firmware for the selected device""" """
Update firmware for the currently selected device.
Loads and flashes the SCPU and NCPU firmware files to the selected
Kneron device. Shows progress during the update process.
Raises:
Displays error message if no device is selected, firmware files
are not found, or update fails.
"""
try: try:
# 檢查是否有選擇設備 # Check if a device is selected
selected_rows = self.device_table.selectionModel().selectedRows() selected_rows = self.device_table.selectionModel().selectedRows()
if not selected_rows: if not selected_rows:
QMessageBox.warning(self, "警告", "請選擇要更新韌體的設備") QMessageBox.warning(self, "Warning", "Please select a device to update firmware")
return return
# 獲取選擇的設備資訊 # Get selected device information
row_index = selected_rows[0].row() row_index = selected_rows[0].row()
device_model = self.device_table.item(row_index, 0).text() # 設備型號 device_model = self.device_table.item(row_index, 0).text() # Device model
port_id = self.device_table.item(row_index, 1).text() # Port ID port_id = self.device_table.item(row_index, 1).text() # Port ID
# 顯示進度條 # Show progress bar
self.show_progress(f"正在更新 {device_model} 的韌體...", 0) self.show_progress(f"Updating firmware for {device_model}...", 0)
# 連接設備 # Connect to device
device_group = kp.core.connect_devices(usb_port_ids=[int(port_id)]) device_group = kp.core.connect_devices(usb_port_ids=[int(port_id)])
# 構建韌體檔案路徑 # Build firmware file paths
scpu_fw_path = os.path.join(FW_DIR, device_model, "fw_scpu.bin") scpu_fw_path = os.path.join(FW_DIR, device_model, "fw_scpu.bin")
ncpu_fw_path = os.path.join(FW_DIR, device_model, "fw_ncpu.bin") ncpu_fw_path = os.path.join(FW_DIR, device_model, "fw_ncpu.bin")
# 檢查韌體檔案是否存在 # Check if firmware files exist
if not os.path.exists(scpu_fw_path) or not os.path.exists(ncpu_fw_path): if not os.path.exists(scpu_fw_path) or not os.path.exists(ncpu_fw_path):
self.hide_progress() self.hide_progress()
QMessageBox.critical(self, "錯誤", f"找不到 {device_model} 的韌體檔案") QMessageBox.critical(self, "Error", f"Firmware files not found for {device_model}")
return return
# 更新進度 # Update progress
self.update_progress(30) self.update_progress(30)
# 載入韌體 # Load firmware
kp.core.load_firmware_from_file( kp.core.load_firmware_from_file(
device_group=device_group, device_group=device_group,
scpu_fw_path=scpu_fw_path, scpu_fw_path=scpu_fw_path,
ncpu_fw_path=ncpu_fw_path ncpu_fw_path=ncpu_fw_path
) )
# 更新進度 # Update progress
self.update_progress(100) self.update_progress(100)
# 顯示成功訊息 # Show success message
QMessageBox.information(self, "成功", f"{device_model} 的韌體已成功更新") QMessageBox.information(self, "Success", f"Firmware for {device_model} has been updated successfully")
except Exception as e: except Exception as e:
self.hide_progress() self.hide_progress()
QMessageBox.critical(self, "錯誤", f"更新韌體時發生錯誤: {str(e)}") QMessageBox.critical(self, "Error", f"Error updating firmware: {str(e)}")
finally: finally:
self.hide_progress() self.hide_progress()
def install_drivers(self): def install_drivers(self):
"""Install drivers for all supported Kneron devices""" """
Install drivers for all supported Kneron devices.
Iterates through all supported Kneron product IDs and installs
the corresponding Windows drivers. Shows progress during installation.
"""
try: try:
# 顯示進度條 # Show progress bar
self.show_progress("Installing Kneron Device Drivers...", 0) self.show_progress("Installing Kneron Device Drivers...", 0)
# 列出所有產品 ID # List all product IDs
product_ids = [ product_ids = [
kp.ProductId.KP_DEVICE_KL520, kp.ProductId.KP_DEVICE_KL520,
kp.ProductId.KP_DEVICE_KL720_LEGACY, kp.ProductId.KP_DEVICE_KL720_LEGACY,
@ -890,30 +1038,30 @@ class UtilitiesScreen(QWidget):
success_count = 0 success_count = 0
total_count = len(product_ids) total_count = len(product_ids)
# 安裝每個驅動程式 # Install each driver
for i, product_id in enumerate(product_ids): for i, product_id in enumerate(product_ids):
try: try:
# 更新進度條 # Update progress bar
progress = int((i / total_count) * 100) progress = int((i / total_count) * 100)
self.update_progress(progress) self.update_progress(progress)
self.progress_title.setText(f"Installing [{product_id.name}] driver...") self.progress_title.setText(f"Installing [{product_id.name}] driver...")
# 安裝驅動程式 # Install driver
kp.core.install_driver_for_windows(product_id=product_id) kp.core.install_driver_for_windows(product_id=product_id)
success_count += 1 success_count += 1
# 更新狀態訊息 # Update status message
self.status_label.setText(f"Successfully installed {product_id.name} driver") self.status_label.setText(f"Successfully installed {product_id.name} driver")
except kp.ApiKPException as exception: except kp.ApiKPException as exception:
error_msg = f"Error: install {product_id.name} driver failed, error msg: [{str(exception)}]" error_msg = f"Error: install {product_id.name} driver failed, error msg: [{str(exception)}]"
self.status_label.setText(error_msg) self.status_label.setText(error_msg)
QMessageBox.warning(self, "Driver Installation Error", error_msg) QMessageBox.warning(self, "Driver Installation Error", error_msg)
# 完成安裝 # Complete installation
self.update_progress(100) self.update_progress(100)
self.hide_progress() self.hide_progress()
# 顯示結果訊息 # Show result message
if success_count == total_count: if success_count == total_count:
QMessageBox.information(self, "Success", "All Kneron device drivers installed successfully!") QMessageBox.information(self, "Success", "All Kneron device drivers installed successfully!")
else: else:
@ -927,21 +1075,37 @@ class UtilitiesScreen(QWidget):
QMessageBox.critical(self, "Error", error_msg) QMessageBox.critical(self, "Error", error_msg)
def show_progress(self, title, value): def show_progress(self, title, value):
"""Show the progress bar with the given title and value""" """
Show the progress section with the specified title and initial value.
Args:
title (str): The title/message to display above the progress bar.
value (int): The initial progress value (0-100).
"""
self.progress_title.setText(title) self.progress_title.setText(title)
self.progress_bar.setValue(value) self.progress_bar.setValue(value)
self.progress_section.setVisible(True) self.progress_section.setVisible(True)
def update_progress(self, value): def update_progress(self, value):
"""Update the progress bar value""" """
Update the progress bar to the specified value.
Args:
value (int): The progress value (0-100).
"""
self.progress_bar.setValue(value) self.progress_bar.setValue(value)
def hide_progress(self): def hide_progress(self):
"""Hide the progress section""" """Hide the progress section and reset it to default state."""
self.progress_section.setVisible(False) self.progress_section.setVisible(False)
def on_device_selection_changed(self): def on_device_selection_changed(self):
"""Handle device selection changes""" """
Handle device selection changes in the device table.
Updates the status label to show the currently selected device
information and ensures the entire row is highlighted.
"""
selected_rows = self.device_table.selectionModel().selectedRows() selected_rows = self.device_table.selectionModel().selectedRows()
if selected_rows: if selected_rows:
# Get the selected row index # Get the selected row index
@ -956,6 +1120,12 @@ class UtilitiesScreen(QWidget):
self.device_table.selectRow(row_index) self.device_table.selectRow(row_index)
def show_utilities_page(self): def show_utilities_page(self):
"""
Switch to the utilities page view.
Updates button styles to show utilities as active and purchased items
as inactive. Shows the utilities page and hides the purchased items page.
"""
self.utilities_button.setStyleSheet(""" self.utilities_button.setStyleSheet("""
QPushButton { QPushButton {
background-color: #3498DB; background-color: #3498DB;
@ -996,6 +1166,12 @@ class UtilitiesScreen(QWidget):
self.current_page = "utilities" self.current_page = "utilities"
def show_purchased_items_page(self): def show_purchased_items_page(self):
"""
Switch to the purchased items page view.
Updates button styles to show purchased items as active and utilities
as inactive. Shows the purchased items page and hides the utilities page.
"""
self.purchased_items_button.setStyleSheet(""" self.purchased_items_button.setStyleSheet("""
QPushButton { QPushButton {
background-color: #3498DB; background-color: #3498DB;