優化 Inference process, 增加Utilities頁面和功能, 修改UI介面 ( 20250324 in update diary)
This commit is contained in:
parent
d58cec837b
commit
527b183576
@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
import os
|
import os
|
||||||
# APPDATA_PATH = os.environ.get("LOCALAPPDATA")
|
APPDATA_PATH = os.environ.get("LOCALAPPDATA")
|
||||||
APPDATA_PATH = "/Users/mason/Developer/Kneron-Academy/test_images"
|
# APPDATA_PATH = "/Users/mason/Developer/Kneron-Academy/test_images"
|
||||||
# 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑
|
# 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑
|
||||||
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", "")
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# src/controllers/device_controller.py
|
# src/controllers/device_controller.py
|
||||||
from PyQt5.QtWidgets import QWidget, QListWidgetItem
|
from PyQt5.QtWidgets import QWidget, QListWidgetItem
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap, QIcon
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ class DeviceController:
|
|||||||
print("Refreshing devices...")
|
print("Refreshing devices...")
|
||||||
device_descriptors = check_available_device()
|
device_descriptors = check_available_device()
|
||||||
self.connected_devices = []
|
self.connected_devices = []
|
||||||
|
# print(self.connected_devices)
|
||||||
|
|
||||||
if device_descriptors.device_descriptor_number > 0:
|
if device_descriptors.device_descriptor_number > 0:
|
||||||
self.parse_and_store_devices(device_descriptors.device_descriptor_list)
|
self.parse_and_store_devices(device_descriptors.device_descriptor_list)
|
||||||
@ -34,27 +35,79 @@ class DeviceController:
|
|||||||
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"""
|
||||||
for device in devices:
|
for device in devices:
|
||||||
product_id = hex(device.product_id).strip().lower()
|
try:
|
||||||
dongle = DongleModelMap.get(product_id, "unknown")
|
product_id = hex(device.product_id).strip().lower()
|
||||||
device.dongle = dongle
|
dongle = DongleModelMap.get(product_id, "unknown")
|
||||||
|
device.dongle = dongle
|
||||||
|
|
||||||
new_device = {
|
new_device = {
|
||||||
'usb_port_id': device.usb_port_id,
|
'usb_port_id': device.usb_port_id,
|
||||||
'product_id': device.product_id,
|
'product_id': device.product_id,
|
||||||
'kn_number': device.kn_number,
|
'kn_number': device.kn_number,
|
||||||
'dongle': dongle
|
'dongle': dongle
|
||||||
}
|
}
|
||||||
|
|
||||||
existing_device_index = next(
|
existing_device_index = next(
|
||||||
(index for (index, d) in enumerate(self.connected_devices)
|
(index for (index, d) in enumerate(self.connected_devices)
|
||||||
if d['usb_port_id'] == new_device['usb_port_id']),
|
if d['usb_port_id'] == new_device['usb_port_id']),
|
||||||
None
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_device_index is not None:
|
if existing_device_index is not None:
|
||||||
self.connected_devices[existing_device_index] = new_device
|
self.connected_devices[existing_device_index] = new_device
|
||||||
else:
|
else:
|
||||||
self.connected_devices.append(new_device)
|
self.connected_devices.append(new_device)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing device: {e}")
|
||||||
|
|
||||||
|
def display_devices(self, devices):
|
||||||
|
"""Display the connected devices in the UI"""
|
||||||
|
try:
|
||||||
|
if not hasattr(self.main_window, 'device_list_widget'):
|
||||||
|
print("Warning: main_window does not have device_list_widget attribute")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.main_window.device_list_widget.clear()
|
||||||
|
|
||||||
|
if not devices:
|
||||||
|
print("No devices to display")
|
||||||
|
return
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
try:
|
||||||
|
product_id = hex(device.product_id).strip().lower()
|
||||||
|
icon_path = os.path.join(UXUI_ASSETS, DongleIconMap.get(product_id, "unknown_dongle.png"))
|
||||||
|
|
||||||
|
item = QListWidgetItem()
|
||||||
|
item.setData(Qt.UserRole, device)
|
||||||
|
|
||||||
|
pixmap = QPixmap(icon_path)
|
||||||
|
icon = QIcon(pixmap) # Convert QPixmap to QIcon
|
||||||
|
item.setIcon(icon)
|
||||||
|
|
||||||
|
# Set device name as the display text
|
||||||
|
dongle_name = DongleModelMap.get(product_id, "Unknown Device")
|
||||||
|
item.setText(f"{dongle_name} (KN: {device.kn_number})")
|
||||||
|
|
||||||
|
self.main_window.device_list_widget.addItem(item)
|
||||||
|
print(f"Added device to list: {dongle_name} (KN: {device.kn_number})")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding device to list: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in display_devices: {e}")
|
||||||
|
|
||||||
|
def get_devices(self):
|
||||||
|
"""Get the list of connected devices"""
|
||||||
|
try:
|
||||||
|
device_descriptors = check_available_device()
|
||||||
|
if device_descriptors.device_descriptor_number > 0:
|
||||||
|
# Parse and store devices to ensure connected_devices is updated
|
||||||
|
self.parse_and_store_devices(device_descriptors.device_descriptor_list)
|
||||||
|
return device_descriptors.device_descriptor_list
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in get_devices: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def get_selected_device(self):
|
def get_selected_device(self):
|
||||||
"""Get the currently selected device"""
|
"""Get the currently selected device"""
|
||||||
@ -69,7 +122,9 @@ class DeviceController:
|
|||||||
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)
|
||||||
widget.setStyleSheet("background: none;")
|
if widget: # Check if widget exists before setting style
|
||||||
|
widget.setStyleSheet("background: none;")
|
||||||
|
|
||||||
list_item_widget = list_widget.itemWidget(list_item)
|
list_item_widget = list_widget.itemWidget(list_item)
|
||||||
list_item_widget.setStyleSheet("background-color: lightblue;")
|
if list_item_widget: # Check if widget exists before setting style
|
||||||
|
list_item_widget.setStyleSheet("background-color: lightblue;")
|
||||||
@ -1,9 +1,10 @@
|
|||||||
# src/controllers/inference_controller.py
|
# src/controllers/inference_controller.py
|
||||||
import os, queue, cv2, json
|
import os, queue, cv2, json
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox, QApplication
|
||||||
|
from PyQt5.QtCore import QTimer, Qt
|
||||||
|
|
||||||
from src.models.inference_worker import InferenceWorkerThread
|
from src.models.inference_worker import InferenceWorkerThread
|
||||||
from src.config import UTILS_DIR, FW_DIR
|
from src.config import UTILS_DIR, FW_DIR, DongleModelMap
|
||||||
|
|
||||||
class InferenceController:
|
class InferenceController:
|
||||||
def __init__(self, main_window, device_controller):
|
def __init__(self, main_window, device_controller):
|
||||||
@ -12,128 +13,248 @@ class InferenceController:
|
|||||||
self.inference_worker = None
|
self.inference_worker = None
|
||||||
self.inference_queue = queue.Queue(maxsize=10)
|
self.inference_queue = queue.Queue(maxsize=10)
|
||||||
self.current_tool_config = None
|
self.current_tool_config = None
|
||||||
|
self.previous_tool_config = None
|
||||||
|
self._camera_was_active = False
|
||||||
|
# 儲存原始影格尺寸,用於邊界框縮放計算
|
||||||
|
self.original_frame_width = 640 # 預設值
|
||||||
|
self.original_frame_height = 480 # 預設值
|
||||||
|
|
||||||
def select_tool(self, tool_config):
|
def select_tool(self, tool_config):
|
||||||
"""Select an AI tool and configure inference"""
|
"""選擇AI工具並配置推論"""
|
||||||
print("Selected tool:", tool_config.get("display_name"))
|
try:
|
||||||
self.current_tool_config = tool_config
|
print("選擇工具:", tool_config.get("display_name"))
|
||||||
|
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")
|
||||||
|
|
||||||
if os.path.exists(model_config_path):
|
if os.path.exists(model_config_path):
|
||||||
try:
|
try:
|
||||||
with open(model_config_path, "r", encoding="utf-8") as f:
|
with open(model_config_path, "r", encoding="utf-8") as f:
|
||||||
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"Error reading model config: {e}")
|
print(f"讀取模型配置時發生錯誤: {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
|
||||||
|
|
||||||
# Prepare input parameters
|
# 檢查是否從視訊模式切換到圖片模式,或從圖片模式切換到視訊模式
|
||||||
input_params = tool_config.get("input_parameters", {}).copy()
|
previous_tool_type = "video"
|
||||||
|
if hasattr(self, 'previous_tool_config') and self.previous_tool_config:
|
||||||
|
previous_input_info = self.previous_tool_config.get("input_info", {})
|
||||||
|
previous_tool_type = previous_input_info.get("type", "video")
|
||||||
|
|
||||||
# Configure device-related settings
|
# 清空推論佇列,確保在模式切換時不會使用舊數據
|
||||||
selected_device = self.device_controller.get_selected_device()
|
self._clear_inference_queue()
|
||||||
if selected_device:
|
|
||||||
input_params["usb_port_id"] = selected_device.get("usb_port_id", 0)
|
|
||||||
dongle = selected_device.get("dongle", "unknown")
|
|
||||||
|
|
||||||
# Verify device compatibility
|
# 儲存當前工具類型以供下次比較
|
||||||
compatible_devices = tool_config.get("compatible_devices", [])
|
self.previous_tool_config = tool_config
|
||||||
if compatible_devices and dongle not in compatible_devices:
|
|
||||||
msgBox = QMessageBox(self.main_window)
|
|
||||||
msgBox.setIcon(QMessageBox.Warning)
|
|
||||||
msgBox.setWindowTitle("Device Incompatible")
|
|
||||||
msgBox.setText(f"The selected model does not support {dongle} device.\nSupported devices: {', '.join(compatible_devices)}")
|
|
||||||
msgBox.setStyleSheet("QLabel { color: white; } QMessageBox { background-color: #2b2b2b; }")
|
|
||||||
msgBox.exec_()
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Configure firmware paths
|
# 準備輸入參數
|
||||||
scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin")
|
input_params = tool_config.get("input_parameters", {}).copy()
|
||||||
ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin")
|
|
||||||
input_params["scpu_path"] = scpu_path
|
# Configure device-related settings
|
||||||
input_params["ncpu_path"] = ncpu_path
|
selected_device = self.device_controller.get_selected_device()
|
||||||
else:
|
if selected_device:
|
||||||
# Default device handling
|
# Get usb_port_id (check if it's a dictionary or object)
|
||||||
devices = self.device_controller.connected_devices
|
if isinstance(selected_device, dict):
|
||||||
if devices and len(devices) > 0:
|
input_params["usb_port_id"] = selected_device.get("usb_port_id", 0)
|
||||||
input_params["usb_port_id"] = devices[0].get("usb_port_id", 0)
|
product_id = selected_device.get("product_id", "unknown")
|
||||||
print("Warning: No device specifically selected, using first available device")
|
else:
|
||||||
|
input_params["usb_port_id"] = getattr(selected_device, "usb_port_id", 0)
|
||||||
|
product_id = getattr(selected_device, "product_id", "unknown")
|
||||||
|
|
||||||
|
# Ensure product_id is in the right format for lookup
|
||||||
|
# Convert to lowercase hex string if it's a number
|
||||||
|
if isinstance(product_id, int):
|
||||||
|
product_id = hex(product_id).lower()
|
||||||
|
# If it's a string but doesn't start with '0x', add it
|
||||||
|
elif isinstance(product_id, str) and not product_id.startswith('0x'):
|
||||||
|
try:
|
||||||
|
# Try to convert to int first, then to hex format
|
||||||
|
product_id = hex(int(product_id, 0)).lower()
|
||||||
|
except ValueError:
|
||||||
|
# If conversion fails, keep as is
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Map product_id to dongle type/series
|
||||||
|
dongle = DongleModelMap.get(product_id, "unknown")
|
||||||
|
print(f"Selected device: product_id={product_id}, mapped to={dongle}")
|
||||||
|
|
||||||
|
# Verify device compatibility
|
||||||
|
compatible_devices = tool_config.get("compatible_devices", [])
|
||||||
|
if compatible_devices and dongle not in compatible_devices:
|
||||||
|
msgBox = QMessageBox(self.main_window)
|
||||||
|
msgBox.setIcon(QMessageBox.Warning)
|
||||||
|
msgBox.setWindowTitle("Device Incompatible")
|
||||||
|
msgBox.setText(f"The selected model does not support {dongle} device.\nSupported devices: {', '.join(compatible_devices)}")
|
||||||
|
msgBox.setStyleSheet("QLabel { color: white; } QMessageBox { background-color: #2b2b2b; }")
|
||||||
|
msgBox.exec_()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Configure firmware paths
|
||||||
|
scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin")
|
||||||
|
ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin")
|
||||||
|
input_params["scpu_path"] = scpu_path
|
||||||
|
input_params["ncpu_path"] = ncpu_path
|
||||||
else:
|
else:
|
||||||
input_params["usb_port_id"] = 0
|
# Default device handling
|
||||||
print("Warning: No connected devices, using default usb_port_id 0")
|
devices = self.device_controller.connected_devices
|
||||||
|
if devices and len(devices) > 0:
|
||||||
|
input_params["usb_port_id"] = devices[0].get("usb_port_id", 0)
|
||||||
|
print("Warning: No device specifically selected, using first available device")
|
||||||
|
else:
|
||||||
|
input_params["usb_port_id"] = 0
|
||||||
|
print("Warning: No connected devices, using default usb_port_id 0")
|
||||||
|
|
||||||
# Handle file inputs for image/voice modes
|
# Handle file inputs for image/voice modes
|
||||||
if tool_type in ["image", "voice"]:
|
if tool_type in ["image", "voice"]:
|
||||||
if hasattr(self.main_window, "destination") and self.main_window.destination:
|
if hasattr(self.main_window, "destination") and self.main_window.destination:
|
||||||
input_params["file_path"] = self.main_window.destination
|
input_params["file_path"] = self.main_window.destination
|
||||||
if tool_type == "image":
|
if tool_type == "image":
|
||||||
uploaded_img = cv2.imread(self.main_window.destination)
|
uploaded_img = cv2.imread(self.main_window.destination)
|
||||||
if uploaded_img is not None:
|
if uploaded_img is not None:
|
||||||
if not self.inference_queue.full():
|
if not self.inference_queue.full():
|
||||||
self.inference_queue.put(uploaded_img)
|
self.inference_queue.put(uploaded_img)
|
||||||
print("Uploaded image added to inference queue")
|
print("Uploaded image added to inference queue")
|
||||||
|
else:
|
||||||
|
print("Warning: inference queue is full")
|
||||||
else:
|
else:
|
||||||
print("Warning: inference queue is full")
|
print("Warning: Unable to read uploaded image")
|
||||||
else:
|
else:
|
||||||
print("Warning: Unable to read uploaded image")
|
input_params["file_path"] = ""
|
||||||
|
print(f"Warning: {tool_type} mode requires a file input, but no file has been uploaded.")
|
||||||
|
|
||||||
|
# Add model file path
|
||||||
|
if "model_file" in tool_config:
|
||||||
|
model_file = tool_config["model_file"]
|
||||||
|
model_file_path = os.path.join(model_path, model_file)
|
||||||
|
input_params["model"] = model_file_path
|
||||||
|
|
||||||
|
print("Input parameters:", input_params)
|
||||||
|
|
||||||
|
# Stop existing inference worker if running
|
||||||
|
if self.inference_worker:
|
||||||
|
self.inference_worker.stop()
|
||||||
|
self.inference_worker = None
|
||||||
|
|
||||||
|
# Create new inference worker
|
||||||
|
self.inference_worker = InferenceWorkerThread(
|
||||||
|
self.inference_queue,
|
||||||
|
mode,
|
||||||
|
model_name,
|
||||||
|
min_interval=0.5,
|
||||||
|
mse_threshold=500,
|
||||||
|
once_mode=once_mode
|
||||||
|
)
|
||||||
|
self.inference_worker.input_params = input_params
|
||||||
|
self.inference_worker.inference_result_signal.connect(self.main_window.handle_inference_result)
|
||||||
|
self.inference_worker.start()
|
||||||
|
print(f"Inference worker started for module: {mode}/{model_name}")
|
||||||
|
|
||||||
|
# Start camera if needed
|
||||||
|
if tool_type == "video":
|
||||||
|
# If camera was previously active but disconnected for image processing
|
||||||
|
if self._camera_was_active and self.main_window.media_controller.video_thread is not None:
|
||||||
|
# Reconnect the signal
|
||||||
|
self.main_window.media_controller.video_thread.change_pixmap_signal.connect(
|
||||||
|
self.main_window.media_controller.update_image
|
||||||
|
)
|
||||||
|
print("Camera reconnected for video processing")
|
||||||
|
else:
|
||||||
|
# Start camera normally
|
||||||
|
self.main_window.media_controller.start_camera()
|
||||||
else:
|
else:
|
||||||
input_params["file_path"] = ""
|
# For image tools, temporarily pause the camera but don't stop it completely
|
||||||
print(f"Warning: {tool_type} mode requires a file input, but no file has been uploaded.")
|
# This allows switching back to video tools without restarting the camera
|
||||||
|
if self.main_window.media_controller.video_thread is not None:
|
||||||
|
# Save current state to indicate camera was running
|
||||||
|
self._camera_was_active = True
|
||||||
|
# Disconnect signal to prevent processing frames during image inference
|
||||||
|
self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect()
|
||||||
|
print("Camera paused for image processing")
|
||||||
|
else:
|
||||||
|
self._camera_was_active = False
|
||||||
|
|
||||||
# Add model file path
|
return True
|
||||||
if "model_file" in tool_config:
|
except Exception as e:
|
||||||
model_file = tool_config["model_file"]
|
print(f"選擇工具時發生錯誤: {e}")
|
||||||
model_file_path = os.path.join(model_path, model_file)
|
import traceback
|
||||||
input_params["model"] = model_file_path
|
print(traceback.format_exc())
|
||||||
|
|
||||||
print("Input parameters:", input_params)
|
def _clear_inference_queue(self):
|
||||||
|
"""清空推論佇列中的所有數據"""
|
||||||
# Stop existing inference worker if running
|
try:
|
||||||
if self.inference_worker:
|
# 清空現有佇列
|
||||||
self.inference_worker.stop()
|
while not self.inference_queue.empty():
|
||||||
self.inference_worker = None
|
try:
|
||||||
|
self.inference_queue.get_nowait()
|
||||||
# Create new inference worker
|
except queue.Empty:
|
||||||
self.inference_worker = InferenceWorkerThread(
|
break
|
||||||
self.inference_queue,
|
print("推論佇列已清空")
|
||||||
mode,
|
except Exception as e:
|
||||||
model_name,
|
print(f"清空推論佇列時發生錯誤: {e}")
|
||||||
min_interval=0.5,
|
|
||||||
mse_threshold=500,
|
|
||||||
once_mode=once_mode
|
|
||||||
)
|
|
||||||
self.inference_worker.input_params = input_params
|
|
||||||
self.inference_worker.inference_result_signal.connect(self.main_window.handle_inference_result)
|
|
||||||
self.inference_worker.start()
|
|
||||||
print(f"Inference worker started for module: {mode}/{model_name}")
|
|
||||||
|
|
||||||
# Start camera if needed
|
|
||||||
if tool_type == "video":
|
|
||||||
self.main_window.media_controller.start_camera()
|
|
||||||
else:
|
|
||||||
print("Tool mode is not video, camera not started")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def add_frame_to_queue(self, frame):
|
def add_frame_to_queue(self, frame):
|
||||||
"""Add a frame to the inference queue"""
|
"""將影格添加到推論佇列"""
|
||||||
if not self.inference_queue.full():
|
try:
|
||||||
self.inference_queue.put(frame)
|
# 更新原始影格尺寸
|
||||||
|
if frame is not None and hasattr(frame, 'shape'):
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
self.original_frame_width = width
|
||||||
|
self.original_frame_height = height
|
||||||
|
|
||||||
|
# 添加到佇列
|
||||||
|
if not self.inference_queue.full():
|
||||||
|
self.inference_queue.put(frame)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"添加影格到佇列時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
def stop_inference(self):
|
def stop_inference(self):
|
||||||
"""Stop the inference worker"""
|
"""Stop the 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
|
||||||
|
|
||||||
|
def process_uploaded_image(self, file_path):
|
||||||
|
"""處理上傳的圖片並進行推論"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print(f"錯誤: 檔案不存在 {file_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 清空推論佇列,確保只處理最新的圖片
|
||||||
|
self._clear_inference_queue()
|
||||||
|
|
||||||
|
# 讀取圖片
|
||||||
|
img = cv2.imread(file_path)
|
||||||
|
if img is None:
|
||||||
|
print(f"錯誤: 無法讀取圖片 {file_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 更新推論工作器參數
|
||||||
|
if self.inference_worker:
|
||||||
|
self.inference_worker.input_params["file_path"] = file_path
|
||||||
|
|
||||||
|
# 將圖片添加到推論佇列
|
||||||
|
if not self.inference_queue.full():
|
||||||
|
self.inference_queue.put(img)
|
||||||
|
print(f"已將圖片 {file_path} 添加到推論佇列")
|
||||||
|
else:
|
||||||
|
print("警告: 推論佇列已滿")
|
||||||
|
else:
|
||||||
|
print("錯誤: 推論工作器未初始化")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"處理上傳圖片時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import cv2
|
import cv2
|
||||||
import os
|
import os
|
||||||
from PyQt5.QtWidgets import QFileDialog
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap, QPainter, QPen, QFont, QColor
|
||||||
from PyQt5.QtCore import Qt
|
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
|
||||||
@ -15,43 +15,158 @@ class MediaController:
|
|||||||
self.recording = False
|
self.recording = False
|
||||||
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._inference_paused = False # 追蹤推論是否暫停
|
||||||
|
|
||||||
def start_camera(self):
|
def start_camera(self):
|
||||||
"""Start the camera for video capture"""
|
"""啟動相機進行視訊擷取"""
|
||||||
if self.video_thread is None:
|
try:
|
||||||
self.video_thread = VideoThread()
|
if self.video_thread is None:
|
||||||
self.video_thread.change_pixmap_signal.connect(self.update_image)
|
print("初始化相機執行緒...")
|
||||||
self.video_thread.start()
|
# 先清除畫布上的任何文字或圖像
|
||||||
print("Camera started")
|
if hasattr(self.main_window, 'canvas_label'):
|
||||||
else:
|
self.main_window.canvas_label.clear()
|
||||||
print("Camera already running")
|
|
||||||
|
self.video_thread = VideoThread()
|
||||||
|
if not self._signal_was_connected:
|
||||||
|
try:
|
||||||
|
self.video_thread.change_pixmap_signal.connect(self.update_image)
|
||||||
|
self._signal_was_connected = True
|
||||||
|
print("相機信號連接成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"連接相機信號時發生錯誤: {e}")
|
||||||
|
|
||||||
|
# 啟動相機執行緒
|
||||||
|
self.video_thread.start()
|
||||||
|
print("相機執行緒啟動成功")
|
||||||
|
else:
|
||||||
|
print("相機已經在運行中")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"啟動相機時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
def stop_camera(self):
|
def stop_camera(self):
|
||||||
"""Stop the camera"""
|
"""停止相機"""
|
||||||
if self.video_thread is not None:
|
try:
|
||||||
self.video_thread.stop()
|
if self.video_thread is not None:
|
||||||
self.video_thread = None
|
print("停止相機執行緒")
|
||||||
print("Camera stopped")
|
# 確保先斷開信號連接
|
||||||
|
if self._signal_was_connected:
|
||||||
|
try:
|
||||||
|
self.video_thread.change_pixmap_signal.disconnect()
|
||||||
|
self._signal_was_connected = False
|
||||||
|
print("已斷開相機信號連接")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"斷開信號連接時發生錯誤: {e}")
|
||||||
|
|
||||||
|
# 停止執行緒
|
||||||
|
self.video_thread.stop()
|
||||||
|
self.video_thread = None
|
||||||
|
print("相機已完全停止")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"停止相機時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
def update_image(self, qt_image):
|
def update_image(self, qt_image):
|
||||||
"""Update the image display and pass to inference queue"""
|
"""更新圖像顯示並處理推論"""
|
||||||
try:
|
try:
|
||||||
# Update canvas display
|
# 更新畫布上的圖像
|
||||||
canvas_size = self.main_window.canvas_label.size()
|
if hasattr(self.main_window, 'canvas_label'):
|
||||||
scaled_image = qt_image.scaled(
|
pixmap = QPixmap.fromImage(qt_image)
|
||||||
canvas_size.width() - 20,
|
|
||||||
canvas_size.height() - 20,
|
|
||||||
Qt.KeepAspectRatio,
|
|
||||||
Qt.SmoothTransformation
|
|
||||||
)
|
|
||||||
self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image))
|
|
||||||
|
|
||||||
# Convert QImage to numpy array and add to inference queue
|
# 如果有邊界框,繪製它
|
||||||
frame_np = qimage_to_numpy(qt_image)
|
if hasattr(self.main_window, 'current_bounding_box') and self.main_window.current_bounding_box is not None:
|
||||||
self.inference_controller.add_frame_to_queue(frame_np)
|
painter = QPainter(pixmap)
|
||||||
|
pen = QPen(Qt.red)
|
||||||
|
pen.setWidth(2)
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
# 獲取邊界框
|
||||||
|
bbox_info = self.main_window.current_bounding_box
|
||||||
|
|
||||||
|
# 檢查邊界框格式
|
||||||
|
if isinstance(bbox_info, dict) and "bounding box" in bbox_info:
|
||||||
|
# 從字典中獲取邊界框座標
|
||||||
|
bbox = bbox_info["bounding box"]
|
||||||
|
if len(bbox) >= 4:
|
||||||
|
# 繪製矩形
|
||||||
|
x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3]
|
||||||
|
painter.drawRect(QRect(x1, y1, x2 - x1, y2 - y1))
|
||||||
|
|
||||||
|
# 如果有結果標籤,繪製它
|
||||||
|
if "result" in bbox_info:
|
||||||
|
font = QFont()
|
||||||
|
font.setPointSize(10)
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.setPen(QColor(255, 0, 0)) # 紅色
|
||||||
|
|
||||||
|
# 計算標籤位置(邊界框上方)
|
||||||
|
label_x = x1
|
||||||
|
label_y = y1 - 10
|
||||||
|
|
||||||
|
# 確保標籤在畫布範圍內
|
||||||
|
if label_y < 10:
|
||||||
|
label_y = y2 + 15 # 如果上方空間不足,放在底部
|
||||||
|
|
||||||
|
painter.drawText(label_x, label_y, bbox_info["result"])
|
||||||
|
elif isinstance(bbox_info, list) and len(bbox_info) >= 4:
|
||||||
|
# 直接使用列表作為邊界框座標
|
||||||
|
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))
|
||||||
|
|
||||||
|
# 如果有標籤,繪製它
|
||||||
|
if len(bbox_info) > 4 and bbox_info[4]:
|
||||||
|
font = QFont()
|
||||||
|
font.setPointSize(10)
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.setPen(QColor(255, 0, 0)) # 紅色
|
||||||
|
|
||||||
|
# 計算標籤位置(邊界框上方)
|
||||||
|
label_x = x1
|
||||||
|
label_y = y1 - 10
|
||||||
|
|
||||||
|
# 確保標籤在畫布範圍內
|
||||||
|
if label_y < 10:
|
||||||
|
label_y = y2 + 15 # 如果上方空間不足,放在底部
|
||||||
|
|
||||||
|
painter.drawText(label_x, label_y, bbox_info[4])
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
# 顯示圖像
|
||||||
|
self.main_window.canvas_label.setPixmap(pixmap)
|
||||||
|
|
||||||
|
# 只有在推論未暫停時才將影格添加到推論佇列
|
||||||
|
if not self._inference_paused:
|
||||||
|
frame_np = qimage_to_numpy(qt_image)
|
||||||
|
self.inference_controller.add_frame_to_queue(frame_np)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in update_image: {e}")
|
print(f"更新圖像時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
def reconnect_camera_signal(self):
|
||||||
|
"""Reconnect the camera signal if it was previously disconnected"""
|
||||||
|
if self.video_thread is not None and self._signal_was_connected:
|
||||||
|
try:
|
||||||
|
# Check if the signal is already connected to avoid duplicate connections
|
||||||
|
# PyQt doesn't provide a direct way to check if a signal is connected,
|
||||||
|
# so we use a try-except block
|
||||||
|
try:
|
||||||
|
# Attempt to disconnect first to avoid multiple connections
|
||||||
|
self.video_thread.change_pixmap_signal.disconnect(self.update_image)
|
||||||
|
except TypeError:
|
||||||
|
# Signal was not connected, which is fine
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reconnect the signal
|
||||||
|
self.video_thread.change_pixmap_signal.connect(self.update_image)
|
||||||
|
print("Camera signal reconnected")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reconnecting camera signal: {e}")
|
||||||
|
|
||||||
def record_video(self, button=None):
|
def record_video(self, button=None):
|
||||||
"""Start or stop video recording"""
|
"""Start or stop video recording"""
|
||||||
@ -164,3 +279,25 @@ class MediaController:
|
|||||||
print(f"Screenshot saved to {filename}")
|
print(f"Screenshot saved to {filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error taking screenshot: {e}")
|
print(f"Error taking screenshot: {e}")
|
||||||
|
|
||||||
|
def toggle_inference_pause(self):
|
||||||
|
"""切換推論暫停狀態"""
|
||||||
|
try:
|
||||||
|
self._inference_paused = not self._inference_paused
|
||||||
|
if self._inference_paused:
|
||||||
|
# 暫停時清除邊界框
|
||||||
|
self.main_window.current_bounding_box = None
|
||||||
|
else:
|
||||||
|
# 恢復推論時,確保相機仍在運行
|
||||||
|
if self.video_thread is None or not self.video_thread.isRunning():
|
||||||
|
self.start_camera()
|
||||||
|
return self._inference_paused
|
||||||
|
except Exception as e:
|
||||||
|
print(f"切換推論暫停狀態時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return self._inference_paused
|
||||||
|
|
||||||
|
def is_inference_paused(self):
|
||||||
|
"""檢查推論是否暫停"""
|
||||||
|
return self._inference_paused
|
||||||
@ -43,13 +43,27 @@ class InferenceWorkerThread(QThread):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if self.last_frame is not None:
|
if self.last_frame is not None:
|
||||||
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 frame.shape != self.last_frame.shape:
|
||||||
self.inference_result_signal.emit(self.cached_result)
|
print(f"幀尺寸變更: 從 {self.last_frame.shape} 變更為 {frame.shape}")
|
||||||
if self.once_mode:
|
# 尺寸不同時,重置上一幀和緩存結果
|
||||||
self._running = False
|
self.last_frame = None
|
||||||
break
|
self.cached_result = None
|
||||||
continue
|
else:
|
||||||
|
# 只有在尺寸相同時才進行 MSE 計算
|
||||||
|
try:
|
||||||
|
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:
|
||||||
|
self.inference_result_signal.emit(self.cached_result)
|
||||||
|
if self.once_mode:
|
||||||
|
self._running = False
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"計算 MSE 時發生錯誤: {e}")
|
||||||
|
# 發生錯誤時重置上一幀和緩存結果
|
||||||
|
self.last_frame = None
|
||||||
|
self.cached_result = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.inference_module.inference(frame, params=self.input_params)
|
result = self.inference_module.inference(frame, params=self.input_params)
|
||||||
|
|||||||
@ -8,23 +8,78 @@ class VideoThread(QThread):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._run_flag = True
|
self._run_flag = True
|
||||||
|
self._camera_open_attempts = 0
|
||||||
|
self._max_attempts = 3
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
cap = cv2.VideoCapture(0)
|
# 嘗試多次開啟相機
|
||||||
if not cap.isOpened():
|
while self._camera_open_attempts < self._max_attempts and self._run_flag:
|
||||||
print("Cannot open camera")
|
self._camera_open_attempts += 1
|
||||||
self._run_flag = False
|
print(f"嘗試開啟相機 (嘗試 {self._camera_open_attempts}/{self._max_attempts})...")
|
||||||
while self._run_flag:
|
|
||||||
ret, frame = cap.read()
|
# 嘗試使用DirectShow後端,通常在Windows上更快
|
||||||
if ret:
|
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
|
||||||
# Convert to RGB format
|
if not cap.isOpened():
|
||||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
print("無法使用DirectShow開啟相機,嘗試預設後端")
|
||||||
height, width, channel = frame.shape
|
cap = cv2.VideoCapture(0)
|
||||||
bytes_per_line = channel * width
|
if not cap.isOpened():
|
||||||
qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
|
print(f"無法使用任何後端開啟相機,等待1秒後重試...")
|
||||||
self.change_pixmap_signal.emit(qt_image)
|
import time
|
||||||
cap.release()
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 設置相機屬性以獲得更好的性能
|
||||||
|
# 降低解析度以提高啟動速度和幀率
|
||||||
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
||||||
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||||||
|
cap.set(cv2.CAP_PROP_FPS, 30)
|
||||||
|
|
||||||
|
# 設置緩衝區大小為1,減少延遲
|
||||||
|
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||||
|
|
||||||
|
# 預熱相機,丟棄前幾幀以加快穩定速度
|
||||||
|
for _ in range(5):
|
||||||
|
cap.read()
|
||||||
|
|
||||||
|
# 相機開啟成功,重置嘗試計數
|
||||||
|
self._camera_open_attempts = 0
|
||||||
|
|
||||||
|
# 主循環
|
||||||
|
while self._run_flag:
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if ret:
|
||||||
|
# 轉換為RGB格式
|
||||||
|
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
height, width, channel = frame.shape
|
||||||
|
bytes_per_line = channel * width
|
||||||
|
qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
|
||||||
|
self.change_pixmap_signal.emit(qt_image)
|
||||||
|
else:
|
||||||
|
print("無法讀取相機幀,相機可能已斷開連接")
|
||||||
|
break
|
||||||
|
|
||||||
|
# 釋放相機資源
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
# 如果是因為停止信號而退出循環,則不再重試
|
||||||
|
if not self._run_flag:
|
||||||
|
break
|
||||||
|
|
||||||
|
print("相機連接中斷,嘗試重新連接...")
|
||||||
|
|
||||||
|
if self._camera_open_attempts >= self._max_attempts:
|
||||||
|
print("達到最大嘗試次數,無法開啟相機")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._run_flag = False
|
"""停止執行緒"""
|
||||||
self.wait()
|
try:
|
||||||
|
print("正在停止相機執行緒...")
|
||||||
|
self._run_flag = False
|
||||||
|
# 等待執行緒完成
|
||||||
|
if self.isRunning():
|
||||||
|
self.wait()
|
||||||
|
print("相機執行緒已停止")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"停止相機執行緒時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
@ -1,46 +1,47 @@
|
|||||||
# import kp
|
import kp
|
||||||
|
|
||||||
# def check_available_device():
|
|
||||||
# try:
|
|
||||||
# print("checking available devices")
|
|
||||||
# device_descriptors = kp.core.scan_devices()
|
|
||||||
# return device_descriptors
|
|
||||||
# except Exception as e:
|
|
||||||
# print(f"Error scanning devices: {e}")
|
|
||||||
# # 返回一個空的設備描述符或模擬數據
|
|
||||||
# class EmptyDescriptor:
|
|
||||||
# def __init__(self):
|
|
||||||
# self.device_descriptor_number = 0
|
|
||||||
# self.device_descriptor_list = []
|
|
||||||
# return EmptyDescriptor()
|
|
||||||
|
|
||||||
def check_available_device():
|
def check_available_device():
|
||||||
# 模擬設備描述符
|
try:
|
||||||
print("checking available devices")
|
print("checking available devices")
|
||||||
class EmptyDescriptor:
|
device_descriptors = kp.core.scan_devices()
|
||||||
def __init__(self):
|
print("device_descriptors", device_descriptors)
|
||||||
self.device_descriptor_number = 0
|
return device_descriptors
|
||||||
self.device_descriptor_list = [{
|
except Exception as e:
|
||||||
"usb_port_id": 4,
|
print(f"Error scanning devices: {e}")
|
||||||
"vendor_id": "0x3231",
|
# 返回一個空的設備描述符或模擬數據
|
||||||
"product_id": "0x720",
|
class EmptyDescriptor:
|
||||||
"link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
def __init__(self):
|
||||||
"kn_number": "0xB306224C",
|
self.device_descriptor_number = 0
|
||||||
"is_connectable": True,
|
self.device_descriptor_list = []
|
||||||
"usb_port_path": "4-1",
|
return EmptyDescriptor()
|
||||||
"firmware": "KDP2 Comp/F"
|
|
||||||
},
|
# def check_available_device():
|
||||||
{
|
# # 模擬設備描述符
|
||||||
"usb_port_id": 5,
|
# print("checking available devices")
|
||||||
"vendor_id": "0x3231",
|
# class EmptyDescriptor:
|
||||||
"product_id": "0x520",
|
# def __init__(self):
|
||||||
"link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
# self.device_descriptor_number = 0
|
||||||
"kn_number": "0xB306224C",
|
# self.device_descriptor_list = [{
|
||||||
"is_connectable": True,
|
# "usb_port_id": 4,
|
||||||
"usb_port_path": "4-1",
|
# "vendor_id": "0x3231",
|
||||||
"firmware": "KDP2 Comp/F"
|
# "product_id": "0x720",
|
||||||
}]
|
# "link_speed": "UsbSpeed.KP_USB_SPEED_SUPER",
|
||||||
return EmptyDescriptor()
|
# "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 = [
|
# device_descriptors = [
|
||||||
# {
|
# {
|
||||||
# "usb_port_id": 4,
|
# "usb_port_id": 4,
|
||||||
|
|||||||
@ -1,70 +1,177 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox
|
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QImage, QPixmap
|
||||||
|
import cv2
|
||||||
|
|
||||||
class FileService:
|
class FileService:
|
||||||
def __init__(self, main_window, upload_dir):
|
def __init__(self, main_window, upload_dir):
|
||||||
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
|
||||||
|
|
||||||
def upload_file(self):
|
def upload_file(self):
|
||||||
"""Handle file upload process"""
|
"""處理檔案上傳流程"""
|
||||||
try:
|
try:
|
||||||
print("Calling QFileDialog.getOpenFileName")
|
# 1. 先完全停止相機(如果正在運行)
|
||||||
|
if hasattr(self.main_window, 'media_controller') and self.main_window.media_controller.video_thread is not None:
|
||||||
|
print("上傳前停止相機")
|
||||||
|
try:
|
||||||
|
# 儲存狀態以指示相機正在運行
|
||||||
|
self._camera_was_active = True
|
||||||
|
# 顯示上傳中提示
|
||||||
|
if hasattr(self.main_window, 'canvas_label'):
|
||||||
|
self.main_window.canvas_label.setText("準備上傳檔案...")
|
||||||
|
self.main_window.canvas_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.main_window.canvas_label.setStyleSheet("color: white; font-size: 24px;")
|
||||||
|
# 確保 UI 更新
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
# 只暫停推論,不完全停止相機
|
||||||
|
if not self.main_window.media_controller._inference_paused:
|
||||||
|
self.main_window.media_controller.toggle_inference_pause()
|
||||||
|
|
||||||
|
# 斷開信號連接但不停止相機執行緒
|
||||||
|
if hasattr(self.main_window.media_controller, '_signal_was_connected') and self.main_window.media_controller._signal_was_connected:
|
||||||
|
try:
|
||||||
|
self.main_window.media_controller.video_thread.change_pixmap_signal.disconnect()
|
||||||
|
self.main_window.media_controller._signal_was_connected = False
|
||||||
|
print("已暫時斷開相機信號連接")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"斷開信號連接時發生錯誤: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"準備上傳時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
# 即使發生錯誤,也繼續嘗試上傳
|
||||||
|
else:
|
||||||
|
self._camera_was_active = False
|
||||||
|
|
||||||
|
print("呼叫 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 obtained:", file_path)
|
print("檔案路徑取得:", file_path)
|
||||||
|
|
||||||
if file_path:
|
if file_path:
|
||||||
print("Checking if upload directory exists")
|
print("檢查上傳目錄是否存在")
|
||||||
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"Created UPLOAD_DIR: {self.upload_dir}")
|
print(f"建立上傳目錄: {self.upload_dir}")
|
||||||
|
|
||||||
print("Checking if original file exists:", file_path)
|
print("檢查原始檔案是否存在:", file_path)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
self.show_message(QMessageBox.Critical, "Error", "Selected file not found")
|
self.show_message(QMessageBox.Critical, "錯誤", "選擇的檔案不存在")
|
||||||
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("Target path:", self.destination)
|
print("目標路徑:", self.destination)
|
||||||
|
|
||||||
# Check if target path is writable
|
# 檢查目標路徑是否可寫入
|
||||||
try:
|
try:
|
||||||
print("Testing file write permission")
|
print("測試檔案寫入權限")
|
||||||
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("Test file creation and deletion successful")
|
print("測試檔案建立和刪除成功")
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
self.show_message(QMessageBox.Critical, "Error", "Cannot write to target directory")
|
self.show_message(QMessageBox.Critical, "錯誤", "無法寫入目標目錄")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
print("Starting file copy")
|
print("開始檔案複製")
|
||||||
shutil.copy2(file_path, self.destination)
|
try:
|
||||||
print("File copy complete")
|
shutil.copy2(file_path, self.destination)
|
||||||
self.show_message(QMessageBox.Information, "Success", f"File uploaded to: {self.destination}")
|
print("檔案複製成功")
|
||||||
|
|
||||||
return self.destination
|
# 更新主視窗目的地
|
||||||
|
self.main_window.destination = self.destination
|
||||||
|
print(f"更新主視窗目的地: {self.main_window.destination}")
|
||||||
|
|
||||||
|
# 處理上傳的影像
|
||||||
|
if self.main_window.inference_controller.current_tool_config:
|
||||||
|
print("使用推論控制器處理上傳的影像")
|
||||||
|
# 先在畫布上顯示影像
|
||||||
|
try:
|
||||||
|
# 載入和顯示影像
|
||||||
|
image = cv2.imread(self.destination)
|
||||||
|
if image is not None:
|
||||||
|
# 轉換為 RGB 顯示
|
||||||
|
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
|
height, width, channel = image_rgb.shape
|
||||||
|
bytes_per_line = channel * width
|
||||||
|
qt_image = QImage(image_rgb.data, width, height, bytes_per_line, QImage.Format_RGB888)
|
||||||
|
|
||||||
|
# 將影像縮放以適應畫布
|
||||||
|
canvas_size = self.main_window.canvas_label.size()
|
||||||
|
scaled_image = qt_image.scaled(
|
||||||
|
int(canvas_size.width() * 0.95),
|
||||||
|
int(canvas_size.height() * 0.95),
|
||||||
|
Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation
|
||||||
|
)
|
||||||
|
self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image))
|
||||||
|
print("影像顯示在畫布上")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"顯示影像時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
# 然後使用推論處理它
|
||||||
|
self.main_window.inference_controller.process_uploaded_image(self.destination)
|
||||||
|
|
||||||
|
return self.destination
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print("檔案複製過程中發生錯誤:\n", traceback.format_exc())
|
||||||
|
self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
print("Exception during upload process:\n", traceback.format_exc())
|
print("上傳過程中發生錯誤:\n", traceback.format_exc())
|
||||||
self.show_message(QMessageBox.Critical, "Error", f"Upload error: {str(e)}")
|
self.show_message(QMessageBox.Critical, "錯誤", f"上傳錯誤: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
finally:
|
||||||
|
# 如果相機之前是活動的,嘗試恢復相機連接
|
||||||
|
if self._camera_was_active and hasattr(self.main_window, 'media_controller'):
|
||||||
|
try:
|
||||||
|
# 延遲一點時間以確保處理完成
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
# 如果相機執行緒仍然存在但信號已斷開,重新連接信號
|
||||||
|
if (self.main_window.media_controller.video_thread is not None and
|
||||||
|
not self.main_window.media_controller._signal_was_connected):
|
||||||
|
try:
|
||||||
|
self.main_window.media_controller.video_thread.change_pixmap_signal.connect(
|
||||||
|
self.main_window.media_controller.update_image
|
||||||
|
)
|
||||||
|
self.main_window.media_controller._signal_was_connected = True
|
||||||
|
print("已重新連接相機信號")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"重新連接相機信號時發生錯誤: {e}")
|
||||||
|
|
||||||
|
# 如果推論被暫停,恢復推論
|
||||||
|
if self.main_window.media_controller._inference_paused:
|
||||||
|
self.main_window.media_controller.toggle_inference_pause()
|
||||||
|
print("已恢復推論")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"恢復相機時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
def show_message(self, icon, title, message):
|
def show_message(self, icon, title, message):
|
||||||
"""Display a message box with custom styling"""
|
"""顯示自訂樣式的訊息盒"""
|
||||||
msgBox = QMessageBox(self.main_window)
|
msgBox = QMessageBox(self.main_window)
|
||||||
msgBox.setIcon(icon)
|
msgBox.setIcon(icon)
|
||||||
msgBox.setWindowTitle(title)
|
msgBox.setWindowTitle(title)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# import kp
|
import kp
|
||||||
import cv2, os, shutil, sys
|
import cv2, os, shutil, sys
|
||||||
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton,
|
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton,
|
||||||
QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget,
|
QComboBox, QFileDialog, QMessageBox, QHBoxLayout, QDialog, QListWidget,
|
||||||
|
|||||||
@ -15,6 +15,7 @@ def create_canvas_area(parent):
|
|||||||
canvas_label = QLabel()
|
canvas_label = QLabel()
|
||||||
canvas_label.setAlignment(Qt.AlignCenter)
|
canvas_label.setAlignment(Qt.AlignCenter)
|
||||||
canvas_label.setStyleSheet("border: none; background: transparent;")
|
canvas_label.setStyleSheet("border: none; background: transparent;")
|
||||||
|
canvas_label.setMinimumSize(880, 730) # Set minimum size to ensure proper display
|
||||||
canvas_layout.addWidget(canvas_label)
|
canvas_layout.addWidget(canvas_label)
|
||||||
|
|
||||||
return canvas_frame, canvas_label
|
return canvas_frame, canvas_label
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, QWidget, QPushButton
|
from PyQt5.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, QWidget, QPushButton, QListWidgetItem
|
||||||
from PyQt5.QtSvg import QSvgWidget
|
from PyQt5.QtSvg import QSvgWidget
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap, QIcon
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, DongleIconMap
|
from src.config import SECONDARY_COLOR, UXUI_ASSETS, BUTTON_STYLE, DongleIconMap, DongleModelMap
|
||||||
|
|
||||||
def create_device_layout(parent, device_controller):
|
def create_device_layout(parent, device_controller):
|
||||||
"""Create the device list layout"""
|
"""Create the device list layout"""
|
||||||
@ -40,6 +40,26 @@ def create_device_layout(parent, device_controller):
|
|||||||
|
|
||||||
# Device list
|
# Device list
|
||||||
device_list_widget = QListWidget(parent)
|
device_list_widget = QListWidget(parent)
|
||||||
|
device_list_widget.setStyleSheet("""
|
||||||
|
QListWidget {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
QListWidget::item {
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
QListWidget::item:selected {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Connect item selection signal
|
||||||
|
device_list_widget.itemClicked.connect(lambda item: device_controller.select_device(
|
||||||
|
item.data(Qt.UserRole), item, device_list_widget
|
||||||
|
))
|
||||||
|
|
||||||
devices_layout.addWidget(device_list_widget)
|
devices_layout.addWidget(device_list_widget)
|
||||||
|
|
||||||
# Detail button
|
# Detail button
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QListWidgetItem, QFrame, QGridLayout, QSizePolicy
|
||||||
from PyQt5.QtSvg import QSvgWidget
|
from PyQt5.QtSvg import QSvgWidget
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QSize, QMargins
|
||||||
|
from PyQt5.QtGui import QPixmap, QIcon, QColor
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from src.config import SECONDARY_COLOR, BUTTON_STYLE, UXUI_ASSETS
|
from src.config import SECONDARY_COLOR, BUTTON_STYLE, UXUI_ASSETS, DongleIconMap
|
||||||
|
|
||||||
def create_device_popup(parent, device_controller):
|
def create_device_popup(parent, device_controller):
|
||||||
"""Create a popup window for device connection management"""
|
"""Create a popup window for device connection management"""
|
||||||
@ -19,10 +20,34 @@ def create_device_popup(parent, device_controller):
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}}
|
}}
|
||||||
|
QLabel {{
|
||||||
|
color: white;
|
||||||
|
}}
|
||||||
|
QListWidget {{
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 5px;
|
||||||
|
color: white;
|
||||||
|
}}
|
||||||
|
QListWidget::item {{
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}}
|
||||||
|
QListWidget::item:selected {{
|
||||||
|
background-color: rgba(52, 152, 219, 0.5);
|
||||||
|
}}
|
||||||
|
QFrame.device-info {{
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
popup_layout = QVBoxLayout(popup)
|
popup_layout = QVBoxLayout(popup)
|
||||||
popup_layout.setContentsMargins(0, 0, 0, 0)
|
popup_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
popup_layout.setSpacing(15)
|
||||||
|
|
||||||
# Title row
|
# Title row
|
||||||
title_layout = QHBoxLayout()
|
title_layout = QHBoxLayout()
|
||||||
@ -37,40 +62,253 @@ def create_device_popup(parent, device_controller):
|
|||||||
container_layout.addWidget(device_icon)
|
container_layout.addWidget(device_icon)
|
||||||
|
|
||||||
popup_label = QLabel("Device Connection")
|
popup_label = QLabel("Device Connection")
|
||||||
popup_label.setStyleSheet("color: white; font-size: 32px;")
|
popup_label.setStyleSheet("color: white; font-size: 32px; font-weight: bold;")
|
||||||
container_layout.addWidget(popup_label)
|
container_layout.addWidget(popup_label)
|
||||||
|
|
||||||
container_layout.setAlignment(Qt.AlignCenter)
|
container_layout.setAlignment(Qt.AlignCenter)
|
||||||
title_layout.addWidget(title_container)
|
title_layout.addWidget(title_container)
|
||||||
popup_layout.addLayout(title_layout)
|
popup_layout.addLayout(title_layout)
|
||||||
|
|
||||||
# Device list
|
# Device list section - 設置為可捲動
|
||||||
|
list_section = QFrame(popup)
|
||||||
|
list_section.setStyleSheet("border: none; background: transparent;")
|
||||||
|
list_layout = QVBoxLayout(list_section)
|
||||||
|
list_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Device list - 設置為可捲動的列表
|
||||||
device_list_widget_popup = QListWidget(popup)
|
device_list_widget_popup = QListWidget(popup)
|
||||||
popup_layout.addWidget(device_list_widget_popup)
|
device_list_widget_popup.setMinimumHeight(250) # 增加整個列表的高度
|
||||||
|
# 啟用垂直捲動條
|
||||||
|
device_list_widget_popup.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
|
# 設置列表項高度和圖示大小
|
||||||
|
device_list_widget_popup.setIconSize(QSize(15, 15)) # 增大圖示
|
||||||
|
list_layout.addWidget(device_list_widget_popup)
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
# Button area
|
# Button area
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
refresh_button = QPushButton("Refresh")
|
refresh_button = QPushButton("Refresh")
|
||||||
refresh_button.clicked.connect(device_controller.refresh_devices)
|
refresh_button.clicked.connect(lambda: refresh_devices(parent, device_controller))
|
||||||
refresh_button.setFixedSize(110, 45)
|
refresh_button.setFixedSize(150, 45)
|
||||||
refresh_button.setStyleSheet(BUTTON_STYLE)
|
refresh_button.setStyleSheet(BUTTON_STYLE)
|
||||||
button_layout.addWidget(refresh_button)
|
button_layout.addWidget(refresh_button)
|
||||||
|
|
||||||
done_button = QPushButton("Done")
|
done_button = QPushButton("Done")
|
||||||
done_button.setStyleSheet(BUTTON_STYLE)
|
done_button.setStyleSheet(BUTTON_STYLE)
|
||||||
done_button.setFixedSize(110, 45)
|
done_button.setFixedSize(150, 45)
|
||||||
done_button.clicked.connect(parent.hide_device_popup)
|
done_button.clicked.connect(parent.hide_device_popup)
|
||||||
button_layout.addWidget(done_button)
|
button_layout.addWidget(done_button)
|
||||||
|
|
||||||
button_layout.setSpacing(10)
|
button_layout.setSpacing(20)
|
||||||
popup_layout.addSpacing(20)
|
# 減少底部間距,讓按鈕更靠近底部
|
||||||
|
popup_layout.addSpacing(5)
|
||||||
popup_layout.addLayout(button_layout)
|
popup_layout.addLayout(button_layout)
|
||||||
|
|
||||||
return popup
|
return popup
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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):
|
||||||
|
"""Refresh the device list and update the UI"""
|
||||||
|
try:
|
||||||
|
# Call the refresh method from device controller
|
||||||
|
device_controller.refresh_devices()
|
||||||
|
|
||||||
|
# Clear the list
|
||||||
|
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
|
||||||
|
seen_models = set()
|
||||||
|
|
||||||
|
# Add devices to the list
|
||||||
|
for device in device_controller.connected_devices:
|
||||||
|
try:
|
||||||
|
item = QListWidgetItem()
|
||||||
|
|
||||||
|
# Store the device data
|
||||||
|
item.setData(Qt.UserRole, device)
|
||||||
|
|
||||||
|
# 使用自定義 widget 來顯示設備信息
|
||||||
|
widget = QWidget()
|
||||||
|
widget.setFixedHeight(60) # 固定高度
|
||||||
|
|
||||||
|
# 使用水平佈局
|
||||||
|
main_layout = QHBoxLayout(widget)
|
||||||
|
# 減少內邊距保持足夠空間
|
||||||
|
main_layout.setContentsMargins(8, 4, 8, 4)
|
||||||
|
main_layout.setSpacing(5)
|
||||||
|
|
||||||
|
# 左側圖示容器(帶有背景色)
|
||||||
|
icon_container = QFrame()
|
||||||
|
icon_container.setFixedSize(30, 30)
|
||||||
|
icon_container.setStyleSheet("background-color: #182D4B; border-radius: 5px;")
|
||||||
|
|
||||||
|
icon_layout = QVBoxLayout(icon_container)
|
||||||
|
icon_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
icon_layout.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
|
# 獲取設備圖示
|
||||||
|
product_id = hex(device.get("product_id", 0)).strip().lower()
|
||||||
|
icon_path = os.path.join(UXUI_ASSETS, "Assets_png", DongleIconMap.get(product_id, "unknown_dongle.png"))
|
||||||
|
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
icon_label = QLabel()
|
||||||
|
pixmap = QPixmap(icon_path)
|
||||||
|
# 確保圖標大小適中
|
||||||
|
scaled_pixmap = pixmap.scaled(30, 30, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
icon_label.setPixmap(scaled_pixmap)
|
||||||
|
icon_layout.addWidget(icon_label)
|
||||||
|
|
||||||
|
main_layout.addWidget(icon_container)
|
||||||
|
|
||||||
|
# 右側信息區域
|
||||||
|
info_container = QWidget()
|
||||||
|
info_layout = QVBoxLayout(info_container)
|
||||||
|
info_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
# info_layout.setSpacing(2)
|
||||||
|
|
||||||
|
# 設備名稱和型號
|
||||||
|
dongle_name = device.get("dongle", "Unknown Device")
|
||||||
|
kn_number = device.get("kn_number", "Unknown")
|
||||||
|
status = "Connected" if device.get("is_connectable", True) else "Not Available"
|
||||||
|
|
||||||
|
# 只顯示型號名稱,避免重複
|
||||||
|
device_model_key = f"{dongle_name}_{product_id}"
|
||||||
|
if device_model_key in seen_models:
|
||||||
|
# 只顯示狀態
|
||||||
|
device_label_text = ""
|
||||||
|
else:
|
||||||
|
seen_models.add(device_model_key)
|
||||||
|
device_label_text = f"{dongle_name}"
|
||||||
|
|
||||||
|
device_label = QLabel(device_label_text)
|
||||||
|
device_label.setStyleSheet("color: white; font-weight: bold; font-size: 16px;")
|
||||||
|
info_layout.addWidget(device_label)
|
||||||
|
|
||||||
|
# KN 號碼
|
||||||
|
kn_label = QLabel(f"KN: {kn_number}")
|
||||||
|
kn_label.setStyleSheet("color: white; font-size: 14px;")
|
||||||
|
info_layout.addWidget(kn_label)
|
||||||
|
|
||||||
|
# 將信息容器添加到主佈局
|
||||||
|
main_layout.addWidget(info_container, 1) # 設置伸展因子為1
|
||||||
|
|
||||||
|
# 狀態標籤(右側)
|
||||||
|
status_label = QLabel(status)
|
||||||
|
status_label.setStyleSheet("color: white; font-size: 12px;")
|
||||||
|
main_layout.addWidget(status_label, 0, Qt.AlignRight | Qt.AlignVCenter) # 右對齊
|
||||||
|
|
||||||
|
# 設置自定義 widget 為列表項的 widget
|
||||||
|
parent.device_list_widget_popup.addItem(item)
|
||||||
|
parent.device_list_widget_popup.setItemWidget(item, widget)
|
||||||
|
|
||||||
|
# 設置項目大小
|
||||||
|
item.setSizeHint(widget.sizeHint())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding device to list: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error refreshing devices: {e}")
|
||||||
@ -14,15 +14,22 @@ def create_media_panel(parent, media_controller, file_service):
|
|||||||
media_layout = QVBoxLayout(media_panel)
|
media_layout = QVBoxLayout(media_panel)
|
||||||
media_layout.setAlignment(Qt.AlignCenter)
|
media_layout.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
|
# 確保使用正確的路徑分隔符
|
||||||
|
assets_path = UXUI_ASSETS.replace('\\', '/')
|
||||||
|
if not assets_path.endswith('/'):
|
||||||
|
assets_path += '/'
|
||||||
|
|
||||||
# Media button information
|
# Media button information
|
||||||
media_buttons_info = [
|
media_buttons_info = [
|
||||||
('screenshot', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_screencapture_normal.svg"),
|
('screenshot', os.path.join(assets_path, "Assets_svg/bt_function_screencapture_normal.svg").replace('\\', '/'),
|
||||||
media_controller.take_screenshot),
|
media_controller.take_screenshot),
|
||||||
('upload file', os.path.join(UXUI_ASSETS, "Assets_svg/bt_function_upload_normal.svg"),
|
('upload file', os.path.join(assets_path, "Assets_svg/bt_function_upload_normal.svg").replace('\\', '/'),
|
||||||
file_service.upload_file),
|
file_service.upload_file),
|
||||||
('voice', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_voice.svg"),
|
('pause/resume', os.path.join(assets_path, "Assets_svg/btn_result_image_delete_hover.svg").replace('\\', '/'),
|
||||||
|
lambda: toggle_pause_button(parent, media_controller)),
|
||||||
|
('voice', os.path.join(assets_path, "Assets_svg/ic_recording_voice.svg").replace('\\', '/'),
|
||||||
lambda: media_controller.record_audio(None)),
|
lambda: media_controller.record_audio(None)),
|
||||||
('video', os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg"),
|
('video', os.path.join(assets_path, "Assets_svg/ic_recording_camera.svg").replace('\\', '/'),
|
||||||
lambda: media_controller.record_video(None)),
|
lambda: media_controller.record_video(None)),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -52,13 +59,43 @@ def create_media_panel(parent, media_controller, file_service):
|
|||||||
icon.setFixedSize(40, 40)
|
icon.setFixedSize(40, 40)
|
||||||
button_layout.addWidget(icon)
|
button_layout.addWidget(icon)
|
||||||
|
|
||||||
|
# 為暫停按鈕儲存參考
|
||||||
|
if button_name == 'pause/resume':
|
||||||
|
parent.pause_button = button
|
||||||
|
parent.pause_icon = icon
|
||||||
|
parent.pause_icon_path = icon_path
|
||||||
|
parent.play_icon_path = os.path.join(assets_path, "Assets_svg/bt_function_video_hover.svg").replace('\\', '/')
|
||||||
|
|
||||||
button.clicked.connect(callback)
|
button.clicked.connect(callback)
|
||||||
media_layout.addWidget(button, alignment=Qt.AlignCenter)
|
media_layout.addWidget(button, alignment=Qt.AlignCenter)
|
||||||
|
|
||||||
media_panel.setLayout(media_layout)
|
media_panel.setLayout(media_layout)
|
||||||
media_panel.setFixedSize(90, 240)
|
media_panel.setFixedSize(90, 290) # 增加高度以容納新按鈕
|
||||||
|
|
||||||
return media_panel
|
return media_panel
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in create_media_panel: {e}")
|
print(f"Error in create_media_panel: {e}")
|
||||||
return QFrame(parent)
|
return QFrame(parent)
|
||||||
|
|
||||||
|
def toggle_pause_button(parent, media_controller):
|
||||||
|
"""切換暫停/恢復按鈕圖示並觸發相應功能"""
|
||||||
|
try:
|
||||||
|
is_paused = media_controller.toggle_inference_pause()
|
||||||
|
|
||||||
|
# 切換圖示
|
||||||
|
if is_paused:
|
||||||
|
# 檢查文件是否存在
|
||||||
|
if os.path.exists(parent.play_icon_path):
|
||||||
|
parent.pause_icon.load(parent.play_icon_path)
|
||||||
|
else:
|
||||||
|
print(f"警告: 播放圖標文件不存在: {parent.play_icon_path}")
|
||||||
|
else:
|
||||||
|
# 檢查文件是否存在
|
||||||
|
if os.path.exists(parent.pause_icon_path):
|
||||||
|
parent.pause_icon.load(parent.pause_icon_path)
|
||||||
|
else:
|
||||||
|
print(f"警告: 暫停圖標文件不存在: {parent.pause_icon_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"切換暫停按鈕時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
@ -17,116 +17,181 @@ class LoginScreen(QWidget):
|
|||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
# Basic window setup
|
# Basic window setup
|
||||||
self.setGeometry(100, 100, *WINDOW_SIZE)
|
self.setGeometry(100, 100, *WINDOW_SIZE)
|
||||||
self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};")
|
self.setStyleSheet(f"background-color: #F5F7FA;") # Light gray background
|
||||||
|
|
||||||
# Main layout
|
# Main layout
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(40, 40, 40, 40)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
|
||||||
|
# Header with logo
|
||||||
|
header_frame = QFrame(self)
|
||||||
|
header_frame.setStyleSheet("background-color: #2C3E50; border-radius: 10px;")
|
||||||
|
header_frame.setFixedHeight(100)
|
||||||
|
|
||||||
|
header_layout = QHBoxLayout(header_frame)
|
||||||
|
header_layout.setContentsMargins(20, 0, 20, 0)
|
||||||
|
|
||||||
# Logo
|
# Logo
|
||||||
logo_label = QLabel(self)
|
logo_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")
|
||||||
if os.path.exists(logo_path):
|
if os.path.exists(logo_path):
|
||||||
logo_pixmap = QPixmap(logo_path)
|
logo_pixmap = QPixmap(logo_path)
|
||||||
logo_label.setPixmap(logo_pixmap)
|
scaled_logo = logo_pixmap.scaled(150, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
logo_label.setPixmap(scaled_logo)
|
||||||
logo_label.setAlignment(Qt.AlignCenter)
|
logo_label.setAlignment(Qt.AlignCenter)
|
||||||
layout.addWidget(logo_label)
|
|
||||||
|
|
||||||
# Title
|
header_layout.addWidget(logo_label, alignment=Qt.AlignCenter)
|
||||||
title_label = QLabel("Login", self)
|
layout.addWidget(header_frame)
|
||||||
title_label.setAlignment(Qt.AlignCenter)
|
|
||||||
title_label.setFont(QFont("Arial", 24, QFont.Bold))
|
|
||||||
layout.addWidget(title_label)
|
|
||||||
|
|
||||||
# Login form container
|
# Login form container
|
||||||
form_container = QFrame(self)
|
form_container = QFrame(self)
|
||||||
form_container.setFrameShape(QFrame.StyledPanel)
|
|
||||||
form_container.setStyleSheet("""
|
form_container.setStyleSheet("""
|
||||||
QFrame {
|
QFrame {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 20px;
|
border: 1px solid #E0E0E0;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
form_layout = QVBoxLayout(form_container)
|
form_layout = QVBoxLayout(form_container)
|
||||||
|
form_layout.setContentsMargins(30, 30, 30, 30)
|
||||||
|
form_layout.setSpacing(20)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_label = QLabel("Login", self)
|
||||||
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
|
title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #2C3E50; margin-bottom: 10px;")
|
||||||
|
form_layout.addWidget(title_label)
|
||||||
|
|
||||||
# Server type
|
# Server type
|
||||||
server_label = QLabel("Server Authentication Type", self)
|
server_label = QLabel("Server Authentication Type", self)
|
||||||
server_label.setFont(QFont("Arial", 12))
|
server_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;")
|
||||||
form_layout.addWidget(server_label)
|
form_layout.addWidget(server_label)
|
||||||
|
|
||||||
self.server_combo = QComboBox(self)
|
self.server_combo = QComboBox(self)
|
||||||
self.server_combo.addItems(["Standard Password Authentication", "Other Authentication Method"])
|
self.server_combo.addItems(["Standard Password Authentication", "Other Authentication Method"])
|
||||||
self.server_combo.setFont(QFont("Arial", 10))
|
|
||||||
self.server_combo.setMinimumHeight(40)
|
self.server_combo.setMinimumHeight(40)
|
||||||
|
self.server_combo.setStyleSheet("""
|
||||||
|
QComboBox {
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2C3E50;
|
||||||
|
}
|
||||||
|
QComboBox::drop-down {
|
||||||
|
border: none;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
QComboBox::down-arrow {
|
||||||
|
image: url(""" + os.path.join(UXUI_ASSETS, "Assets_svg/ic_result_folder_normal.svg").replace("\\", "/") + """);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
selection-background-color: #ECF0F1;
|
||||||
|
selection-color: #2C3E50;
|
||||||
|
background-color: white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
""")
|
||||||
form_layout.addWidget(self.server_combo)
|
form_layout.addWidget(self.server_combo)
|
||||||
|
|
||||||
form_layout.addSpacing(10)
|
|
||||||
|
|
||||||
# Username
|
# Username
|
||||||
username_label = QLabel("Username", self)
|
username_label = QLabel("Username", self)
|
||||||
username_label.setFont(QFont("Arial", 12))
|
username_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;")
|
||||||
form_layout.addWidget(username_label)
|
form_layout.addWidget(username_label)
|
||||||
|
|
||||||
self.username_input = QLineEdit(self)
|
self.username_input = QLineEdit(self)
|
||||||
self.username_input.setPlaceholderText("Enter your username")
|
self.username_input.setPlaceholderText("Enter your username")
|
||||||
self.username_input.setMinimumHeight(40)
|
self.username_input.setMinimumHeight(40)
|
||||||
self.username_input.setFont(QFont("Arial", 10))
|
self.username_input.setStyleSheet("""
|
||||||
|
QLineEdit {
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2C3E50;
|
||||||
|
}
|
||||||
|
QLineEdit:focus {
|
||||||
|
border: 1px solid #3498DB;
|
||||||
|
}
|
||||||
|
""")
|
||||||
form_layout.addWidget(self.username_input)
|
form_layout.addWidget(self.username_input)
|
||||||
|
|
||||||
form_layout.addSpacing(10)
|
|
||||||
|
|
||||||
# Password
|
# Password
|
||||||
password_label = QLabel("Password", self)
|
password_label = QLabel("Password", self)
|
||||||
password_label.setFont(QFont("Arial", 12))
|
password_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2C3E50;")
|
||||||
form_layout.addWidget(password_label)
|
form_layout.addWidget(password_label)
|
||||||
|
|
||||||
self.password_input = QLineEdit(self)
|
self.password_input = QLineEdit(self)
|
||||||
self.password_input.setPlaceholderText("Enter your password")
|
self.password_input.setPlaceholderText("Enter your password")
|
||||||
self.password_input.setEchoMode(QLineEdit.Password)
|
self.password_input.setEchoMode(QLineEdit.Password)
|
||||||
self.password_input.setMinimumHeight(40)
|
self.password_input.setMinimumHeight(40)
|
||||||
self.password_input.setFont(QFont("Arial", 10))
|
self.password_input.setStyleSheet("""
|
||||||
|
QLineEdit {
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2C3E50;
|
||||||
|
}
|
||||||
|
QLineEdit:focus {
|
||||||
|
border: 1px solid #3498DB;
|
||||||
|
}
|
||||||
|
""")
|
||||||
form_layout.addWidget(self.password_input)
|
form_layout.addWidget(self.password_input)
|
||||||
|
|
||||||
form_layout.addSpacing(20)
|
|
||||||
|
|
||||||
# Error message (hidden by default)
|
# Error message (hidden by default)
|
||||||
self.error_label = QLabel("", self)
|
self.error_label = QLabel("", self)
|
||||||
self.error_label.setStyleSheet("color: red;")
|
self.error_label.setStyleSheet("color: #E74C3C; font-size: 14px;")
|
||||||
self.error_label.setFont(QFont("Arial", 10))
|
|
||||||
self.error_label.hide()
|
self.error_label.hide()
|
||||||
form_layout.addWidget(self.error_label)
|
form_layout.addWidget(self.error_label)
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.setSpacing(15)
|
||||||
|
|
||||||
back_button = QPushButton("Back", self)
|
back_button = QPushButton("Back", self)
|
||||||
back_button.setMinimumHeight(40)
|
back_button.setMinimumHeight(45)
|
||||||
back_button.setFont(QFont("Arial", 12))
|
|
||||||
back_button.setStyleSheet("""
|
back_button.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #757575;
|
background-color: #95A5A6;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: #616161;
|
background-color: #7F8C8D;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #616A6B;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
back_button.clicked.connect(self.back_to_selection.emit)
|
back_button.clicked.connect(self.back_to_selection.emit)
|
||||||
|
|
||||||
login_button = QPushButton("Login", self)
|
login_button = QPushButton("Login", self)
|
||||||
login_button.setMinimumHeight(40)
|
login_button.setMinimumHeight(45)
|
||||||
login_button.setFont(QFont("Arial", 12))
|
|
||||||
login_button.setStyleSheet("""
|
login_button.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #1E88E5;
|
background-color: #3498DB;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: #1976D2;
|
background-color: #2980B9;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #1F618D;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
login_button.clicked.connect(self.attempt_login)
|
login_button.clicked.connect(self.attempt_login)
|
||||||
@ -139,6 +204,12 @@ class LoginScreen(QWidget):
|
|||||||
# Add form to main layout
|
# Add form to main layout
|
||||||
layout.addWidget(form_container, 1)
|
layout.addWidget(form_container, 1)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
footer_label = QLabel(" 2025 Innovedus Inc. All rights reserved.", self)
|
||||||
|
footer_label.setAlignment(Qt.AlignCenter)
|
||||||
|
footer_label.setStyleSheet("font-size: 12px; color: #95A5A6;")
|
||||||
|
layout.addWidget(footer_label)
|
||||||
|
|
||||||
def attempt_login(self):
|
def attempt_login(self):
|
||||||
username = self.username_input.text()
|
username = self.username_input.text()
|
||||||
password = self.password_input.text()
|
password = self.password_input.text()
|
||||||
@ -149,7 +220,6 @@ class LoginScreen(QWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Simulate login success (in a real app, you would validate with your server)
|
# Simulate login success (in a real app, you would validate with your server)
|
||||||
# For demo, accept any non-empty username/password
|
|
||||||
self.login_success.emit()
|
self.login_success.emit()
|
||||||
|
|
||||||
def show_error(self, message):
|
def show_error(self, message):
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import os, sys, json, queue, numpy as np
|
import os, sys, json, queue, numpy as np
|
||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QMessageBox
|
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
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ from src.views.components.canvas_area import create_canvas_area
|
|||||||
from src.views.components.device_list import create_device_layout
|
from src.views.components.device_list import create_device_layout
|
||||||
from src.views.components.toolbox import create_ai_toolbox
|
from src.views.components.toolbox import create_ai_toolbox
|
||||||
from src.views.components.media_panel import create_media_panel
|
from src.views.components.media_panel import create_media_panel
|
||||||
from src.views.components.device_popup import create_device_popup
|
from src.views.components.device_popup import create_device_popup, refresh_devices
|
||||||
|
|
||||||
class MainWindow(QWidget):
|
class MainWindow(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -30,6 +30,7 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
# Set up UI and configuration
|
# Set up UI and configuration
|
||||||
self.destination = None
|
self.destination = None
|
||||||
|
self.current_bounding_box = None # 儲存當前的邊界框資訊
|
||||||
self.generate_global_config()
|
self.generate_global_config()
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
@ -86,12 +87,19 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
# 2. Set up popup and mask
|
# 2. Set up popup and mask
|
||||||
self.device_popup_mask_setup()
|
self.device_popup_mask_setup()
|
||||||
|
print("Setting up popup mask")
|
||||||
|
|
||||||
# 3. Refresh devices
|
# 3. Refresh devices - do this after UI is fully set up
|
||||||
self.device_controller.refresh_devices()
|
QTimer.singleShot(100, self.device_controller.refresh_devices)
|
||||||
|
|
||||||
# 4. Show popup
|
self.auto_start_camera()
|
||||||
|
|
||||||
|
# # 4. Show popup
|
||||||
self.show_device_popup()
|
self.show_device_popup()
|
||||||
|
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}")
|
||||||
|
|
||||||
@ -167,6 +175,12 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
def show_device_popup(self):
|
def show_device_popup(self):
|
||||||
try:
|
try:
|
||||||
|
# 顯示彈窗前刷新設備列表
|
||||||
|
# 使用新的 refresh_devices 函數,而不是直接調用 device_controller
|
||||||
|
from src.views.components.device_popup import refresh_devices
|
||||||
|
refresh_devices(self, self.device_controller)
|
||||||
|
|
||||||
|
# 顯示彈窗
|
||||||
self.overlay.show()
|
self.overlay.show()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in show_device_popup: {e}")
|
print(f"Error in show_device_popup: {e}")
|
||||||
@ -188,31 +202,56 @@ 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"""
|
"""處理來自推論工作器的結果"""
|
||||||
# Print result to console
|
try:
|
||||||
print("Inference result received:", result)
|
# 輸出結果到控制台
|
||||||
|
print("收到推論結果:", result)
|
||||||
|
|
||||||
# Create QMessageBox
|
# 檢查結果是否包含邊界框資訊
|
||||||
msgBox = QMessageBox(self)
|
if isinstance(result, dict) and "bounding box" in result:
|
||||||
msgBox.setWindowTitle("Inference Result")
|
# 更新當前邊界框資訊
|
||||||
|
self.current_bounding_box = result["bounding box"]
|
||||||
|
|
||||||
# Format text based on result type
|
# 如果結果中有標籤,將其添加到邊界框中
|
||||||
if isinstance(result, dict):
|
if "result" in result:
|
||||||
result_str = "\n".join(f"{key}: {value}" for key, value in result.items())
|
# 將標籤添加為邊界框列表的第5個元素
|
||||||
else:
|
if isinstance(self.current_bounding_box, list) and len(self.current_bounding_box) >= 4:
|
||||||
result_str = str(result)
|
# 確保邊界框列表至少有5個元素
|
||||||
|
while len(self.current_bounding_box) < 5:
|
||||||
|
self.current_bounding_box.append(None)
|
||||||
|
# 設置第5個元素為結果標籤
|
||||||
|
self.current_bounding_box[4] = result["result"]
|
||||||
|
|
||||||
msgBox.setText(result_str)
|
# 不需要顯示彈窗,因為邊界框會直接繪製在畫面上
|
||||||
msgBox.setStandardButtons(QMessageBox.Ok)
|
return
|
||||||
|
|
||||||
# Set style
|
# 對於非邊界框結果,使用彈窗顯示
|
||||||
msgBox.setStyleSheet("""
|
# 創建QMessageBox
|
||||||
QLabel {
|
msgBox = QMessageBox(self)
|
||||||
color: white;
|
msgBox.setWindowTitle("推論結果")
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
msgBox.exec_()
|
# 根據結果類型格式化文字
|
||||||
|
if isinstance(result, dict):
|
||||||
|
result_str = "\n".join(f"{key}: {value}" for key, value in result.items())
|
||||||
|
else:
|
||||||
|
result_str = str(result)
|
||||||
|
|
||||||
|
msgBox.setText(result_str)
|
||||||
|
msgBox.setStandardButtons(QMessageBox.Ok)
|
||||||
|
|
||||||
|
# 設置樣式
|
||||||
|
msgBox.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 顯示彈窗
|
||||||
|
msgBox.exec_()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"處理推論結果時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
|
|
||||||
def show_no_device_gif(self):
|
def show_no_device_gif(self):
|
||||||
"""Show a GIF indicating no devices are connected"""
|
"""Show a GIF indicating no devices are connected"""
|
||||||
@ -250,3 +289,22 @@ class MainWindow(QWidget):
|
|||||||
def generate_global_config(self):
|
def generate_global_config(self):
|
||||||
"""Generate global configuration if needed"""
|
"""Generate global configuration if needed"""
|
||||||
return self.config_utils.generate_global_config()
|
return self.config_utils.generate_global_config()
|
||||||
|
|
||||||
|
def auto_start_camera(self):
|
||||||
|
"""自動啟動相機並顯示預覽"""
|
||||||
|
print("開始啟動相機...")
|
||||||
|
try:
|
||||||
|
# 先清除畫布,顯示加載中的信息
|
||||||
|
if hasattr(self, 'canvas_label'):
|
||||||
|
self.canvas_label.setText("相機啟動中...")
|
||||||
|
self.canvas_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.canvas_label.setStyleSheet("color: white; font-size: 24px;")
|
||||||
|
# 確保UI更新
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
# 在單獨的線程中啟動相機,避免阻塞UI
|
||||||
|
self.media_controller.start_camera()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"啟動相機時發生錯誤: {e}")
|
||||||
|
import traceback
|
||||||
|
print(traceback.format_exc())
|
||||||
@ -1,8 +1,8 @@
|
|||||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
|
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
|
from PyQt5.QtGui import QPixmap, QFont, QIcon
|
||||||
import os
|
import os
|
||||||
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR
|
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, APP_NAME
|
||||||
|
|
||||||
class SelectionScreen(QWidget):
|
class SelectionScreen(QWidget):
|
||||||
# Signals for navigation
|
# Signals for navigation
|
||||||
@ -16,77 +16,174 @@ class SelectionScreen(QWidget):
|
|||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
# Basic window setup
|
# Basic window setup
|
||||||
self.setGeometry(100, 100, *WINDOW_SIZE)
|
self.setGeometry(100, 100, *WINDOW_SIZE)
|
||||||
self.setStyleSheet(f"background-color: {BACKGROUND_COLOR};")
|
self.setStyleSheet(f"background-color: #F8F9FA;") # Slightly lighter background
|
||||||
|
|
||||||
# Main layout
|
# Main layout
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(40, 40, 40, 40)
|
||||||
|
layout.setSpacing(20)
|
||||||
|
|
||||||
|
# Header with logo
|
||||||
|
header_frame = QFrame(self)
|
||||||
|
header_frame.setStyleSheet("background-color: #34495E; border-radius: 10px;") # Softer dark blue
|
||||||
|
header_frame.setFixedHeight(100)
|
||||||
|
|
||||||
|
header_layout = QHBoxLayout(header_frame)
|
||||||
|
header_layout.setContentsMargins(20, 0, 20, 0)
|
||||||
|
|
||||||
# Logo
|
# Logo
|
||||||
logo_label = QLabel(self)
|
logo_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")
|
||||||
if os.path.exists(logo_path):
|
if os.path.exists(logo_path):
|
||||||
logo_pixmap = QPixmap(logo_path)
|
logo_pixmap = QPixmap(logo_path)
|
||||||
logo_label.setPixmap(logo_pixmap)
|
scaled_logo = logo_pixmap.scaled(150, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
logo_label.setPixmap(scaled_logo)
|
||||||
logo_label.setAlignment(Qt.AlignCenter)
|
logo_label.setAlignment(Qt.AlignCenter)
|
||||||
layout.addWidget(logo_label)
|
|
||||||
|
header_layout.addWidget(logo_label, alignment=Qt.AlignCenter)
|
||||||
|
layout.addWidget(header_frame)
|
||||||
|
|
||||||
|
# Content container
|
||||||
|
content_container = QFrame(self)
|
||||||
|
content_container.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
content_layout = QVBoxLayout(content_container)
|
||||||
|
content_layout.setContentsMargins(30, 30, 30, 30)
|
||||||
|
content_layout.setSpacing(20)
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title_label = QLabel("Kneron Academy", self)
|
title_label = QLabel(APP_NAME, self)
|
||||||
title_label.setAlignment(Qt.AlignCenter)
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
title_label.setFont(QFont("Arial", 24, QFont.Bold))
|
title_label.setStyleSheet("font-size: 28px; font-weight: bold; color: #34495E;")
|
||||||
layout.addWidget(title_label)
|
content_layout.addWidget(title_label)
|
||||||
|
|
||||||
# Subtitle
|
# Subtitle
|
||||||
subtitle_label = QLabel("Please select an option to continue", self)
|
subtitle_label = QLabel("請選擇您要使用的功能", self)
|
||||||
subtitle_label.setAlignment(Qt.AlignCenter)
|
subtitle_label.setAlignment(Qt.AlignCenter)
|
||||||
subtitle_label.setFont(QFont("Arial", 14))
|
subtitle_label.setStyleSheet("font-size: 16px; color: #7F8C8D;")
|
||||||
layout.addWidget(subtitle_label)
|
content_layout.addWidget(subtitle_label)
|
||||||
|
|
||||||
# Add some space
|
# Add some space
|
||||||
layout.addSpacing(40)
|
content_layout.addSpacing(20)
|
||||||
|
|
||||||
# Button container
|
# Button container
|
||||||
button_container = QWidget(self)
|
button_container = QWidget(self)
|
||||||
button_layout = QHBoxLayout(button_container)
|
button_layout = QHBoxLayout(button_container)
|
||||||
button_layout.setContentsMargins(50, 0, 50, 0)
|
button_layout.setContentsMargins(20, 0, 20, 0)
|
||||||
|
button_layout.setSpacing(40) # Increased spacing between buttons
|
||||||
|
|
||||||
# Utilities button
|
# Create card-style buttons with icons and descriptions
|
||||||
utilities_button = QPushButton("Utilities", self)
|
|
||||||
utilities_button.setMinimumHeight(80)
|
# Utilities card button
|
||||||
utilities_button.setFont(QFont("Arial", 14))
|
utilities_card = QFrame(self)
|
||||||
utilities_button.setStyleSheet("""
|
utilities_card.setMinimumSize(300, 250)
|
||||||
QPushButton {
|
utilities_card.setCursor(Qt.PointingHandCursor)
|
||||||
background-color: #1E88E5;
|
utilities_card.setStyleSheet("""
|
||||||
color: white;
|
QFrame {
|
||||||
border-radius: 8px;
|
background-color: white;
|
||||||
padding: 10px;
|
border-radius: 15px;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QFrame:hover {
|
||||||
background-color: #1976D2;
|
background-color: #F5F9FF;
|
||||||
|
border: 1px solid #5DADE2;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
utilities_button.clicked.connect(self.open_utilities.emit)
|
|
||||||
|
|
||||||
# Demo App button
|
utilities_layout = QVBoxLayout(utilities_card)
|
||||||
demo_button = QPushButton("Demo App", self)
|
utilities_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
demo_button.setMinimumHeight(80)
|
utilities_layout.setSpacing(15)
|
||||||
demo_button.setFont(QFont("Arial", 14))
|
utilities_layout.setAlignment(Qt.AlignCenter)
|
||||||
demo_button.setStyleSheet("""
|
|
||||||
QPushButton {
|
# Icon for utilities
|
||||||
background-color: #43A047;
|
utilities_icon_label = QLabel()
|
||||||
color: white;
|
utilities_icon_path = os.path.join(UXUI_ASSETS, "Assets_svg/ic_dialog_device.svg")
|
||||||
border-radius: 8px;
|
if os.path.exists(utilities_icon_path):
|
||||||
padding: 10px;
|
utilities_icon = QPixmap(utilities_icon_path)
|
||||||
|
scaled_icon = utilities_icon.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
utilities_icon_label.setPixmap(scaled_icon)
|
||||||
|
utilities_icon_label.setAlignment(Qt.AlignCenter)
|
||||||
|
utilities_layout.addWidget(utilities_icon_label, alignment=Qt.AlignCenter)
|
||||||
|
|
||||||
|
# Title for utilities
|
||||||
|
utilities_title = QLabel("設備管理工具", self)
|
||||||
|
utilities_title.setAlignment(Qt.AlignCenter)
|
||||||
|
utilities_title.setStyleSheet("font-size: 22px; font-weight: bold; color: #34495E;")
|
||||||
|
utilities_layout.addWidget(utilities_title)
|
||||||
|
|
||||||
|
# Description for utilities
|
||||||
|
utilities_desc = QLabel("連接、管理和更新您的 Kneron 設備", self)
|
||||||
|
utilities_desc.setAlignment(Qt.AlignCenter)
|
||||||
|
utilities_desc.setWordWrap(True)
|
||||||
|
utilities_desc.setStyleSheet("font-size: 14px; color: #7F8C8D;")
|
||||||
|
utilities_layout.addWidget(utilities_desc)
|
||||||
|
|
||||||
|
# Connect the card to the signal
|
||||||
|
utilities_card.mousePressEvent = lambda event: self.open_utilities.emit()
|
||||||
|
|
||||||
|
# Demo App card button
|
||||||
|
demo_card = QFrame(self)
|
||||||
|
demo_card.setMinimumSize(300, 250)
|
||||||
|
demo_card.setCursor(Qt.PointingHandCursor)
|
||||||
|
demo_card.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QFrame:hover {
|
||||||
background-color: #388E3C;
|
background-color: #F5FFF7;
|
||||||
|
border: 1px solid #7DCEA0;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
demo_button.clicked.connect(self.open_demo_app.emit)
|
|
||||||
|
|
||||||
# Add buttons to layout
|
demo_layout = QVBoxLayout(demo_card)
|
||||||
button_layout.addWidget(utilities_button)
|
demo_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
button_layout.addWidget(demo_button)
|
demo_layout.setSpacing(15)
|
||||||
|
demo_layout.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
layout.addWidget(button_container)
|
# Icon for demo app
|
||||||
layout.addStretch(1) # Push everything up
|
demo_icon_label = QLabel()
|
||||||
|
demo_icon_path = os.path.join(UXUI_ASSETS, "Assets_svg/ic_recording_camera.svg")
|
||||||
|
if os.path.exists(demo_icon_path):
|
||||||
|
demo_icon = QPixmap(demo_icon_path)
|
||||||
|
scaled_icon = demo_icon.scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
demo_icon_label.setPixmap(scaled_icon)
|
||||||
|
demo_icon_label.setAlignment(Qt.AlignCenter)
|
||||||
|
demo_layout.addWidget(demo_icon_label, alignment=Qt.AlignCenter)
|
||||||
|
|
||||||
|
# Title for demo app
|
||||||
|
demo_title = QLabel("AI 演示應用", self)
|
||||||
|
demo_title.setAlignment(Qt.AlignCenter)
|
||||||
|
demo_title.setStyleSheet("font-size: 22px; font-weight: bold; color: #34495E;")
|
||||||
|
demo_layout.addWidget(demo_title)
|
||||||
|
|
||||||
|
# Description for demo app
|
||||||
|
demo_desc = QLabel("使用 AI 工具箱探索和測試 Kneron 設備的功能", self)
|
||||||
|
demo_desc.setAlignment(Qt.AlignCenter)
|
||||||
|
demo_desc.setWordWrap(True)
|
||||||
|
demo_desc.setStyleSheet("font-size: 14px; color: #7F8C8D;")
|
||||||
|
demo_layout.addWidget(demo_desc)
|
||||||
|
|
||||||
|
# Connect the card to the signal
|
||||||
|
demo_card.mousePressEvent = lambda event: self.open_demo_app.emit()
|
||||||
|
|
||||||
|
# Add cards to layout
|
||||||
|
button_layout.addWidget(utilities_card)
|
||||||
|
button_layout.addWidget(demo_card)
|
||||||
|
|
||||||
|
content_layout.addWidget(button_container)
|
||||||
|
|
||||||
|
# Add content container to main layout
|
||||||
|
layout.addWidget(content_container, 1)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
footer_label = QLabel(" 2025 Innovedus Inc. All rights reserved.", self)
|
||||||
|
footer_label.setAlignment(Qt.AlignCenter)
|
||||||
|
footer_label.setStyleSheet("font-size: 12px; color: #95A5A6;")
|
||||||
|
layout.addWidget(footer_label)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -33,3 +33,42 @@ Times: 8.5 Hours
|
|||||||
3. 修改 popup windows 中顯示的 dongle 型號 -> 使用類似 完成事項4 的方式使用之前的 mapping
|
3. 修改 popup windows 中顯示的 dongle 型號 -> 使用類似 完成事項4 的方式使用之前的 mapping
|
||||||
4. 修改 inference output format including bb and single model
|
4. 修改 inference output format including bb and single model
|
||||||
5. multi-dongle inference
|
5. multi-dongle inference
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 20250324
|
||||||
|
1. 推論引擎優化
|
||||||
|
幀尺寸不匹配問題修復
|
||||||
|
修復了在暫停視頻模式後上傳圖片時出現的尺寸不匹配錯誤
|
||||||
|
在 InferenceWorkerThread 的 run 方法中添加了幀尺寸檢查
|
||||||
|
實現了比較當前幀與上一幀尺寸的機制,當尺寸不同時重置緩存
|
||||||
|
解決了 "operands could not be broadcast together with shapes" 錯誤
|
||||||
|
推論佇列管理優化
|
||||||
|
添加了 _clear_inference_queue 方法,實現安全清空推論佇列
|
||||||
|
在模式切換時清空佇列,避免使用舊數據
|
||||||
|
在處理新上傳圖片前清空佇列
|
||||||
|
重構了 process_uploaded_image 方法,提高可讀性和錯誤處理
|
||||||
|
2. Utilities 畫面功能增強
|
||||||
|
驅動程式安裝功能
|
||||||
|
添加了紫色的「Install Driver」按鈕
|
||||||
|
實現了 install_drivers 方法,支援多種 Kneron 裝置
|
||||||
|
使用 kp.core.install_driver_for_windows API 安裝驅動程式
|
||||||
|
添加了進度條顯示和詳細的錯誤處理
|
||||||
|
Purchased Items 頁面改進
|
||||||
|
移除了登入要求,讓用戶無需登入即可查看已購買項目
|
||||||
|
修改了頁面佈局,使其在同一個視窗中顯示,而非彈出視窗
|
||||||
|
添加了表格顯示已購買項目,包含產品、模型、版本和相容裝置資訊
|
||||||
|
實現了「Refresh Items」按鈕功能
|
||||||
|
表格功能增強
|
||||||
|
添加了勾選框功能,允許用戶選擇多個項目
|
||||||
|
實現了整行選擇功能,點擊任一單元格時整行變色
|
||||||
|
修改了「Download Selected Items」按鈕功能,實現批量下載選中項目
|
||||||
|
簡化了表格結構,移除了單獨的「Action」列和下載按鈕
|
||||||
|
3. 用戶界面優化
|
||||||
|
視覺設計改進
|
||||||
|
統一了頁面風格和配色
|
||||||
|
改進了按鈕和表格的樣式
|
||||||
|
優化了佈局結構,提供更一致的用戶體驗
|
||||||
|
導航優化
|
||||||
|
實現了 Utilities 和 Purchased Items 頁面之間的無縫切換
|
||||||
|
改進了頁面標題和描述,提供更清晰的功能說明
|
||||||
Loading…
x
Reference in New Issue
Block a user