優化 Inference process, 增加Utilities頁面和功能, 修改UI介面 ( 20250324 in update diary)

This commit is contained in:
Mason Huang 2025-03-24 02:55:31 +08:00
parent d58cec837b
commit 527b183576
18 changed files with 2236 additions and 566 deletions

View File

@ -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", "")

View File

@ -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(
(index for (index, d) in enumerate(self.connected_devices)
if d['usb_port_id'] == new_device['usb_port_id']),
None
)
if existing_device_index is not None:
self.connected_devices[existing_device_index] = new_device
else:
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()
existing_device_index = next( if not devices:
(index for (index, d) in enumerate(self.connected_devices) print("No devices to display")
if d['usb_port_id'] == new_device['usb_port_id']), return
None
) for device in devices:
try:
if existing_device_index is not None: product_id = hex(device.product_id).strip().lower()
self.connected_devices[existing_device_index] = new_device icon_path = os.path.join(UXUI_ASSETS, DongleIconMap.get(product_id, "unknown_dongle.png"))
else:
self.connected_devices.append(new_device) 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;")

View File

@ -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", "")
model_name = tool_config.get("model_name", "")
# Load detailed model configuration
model_path = os.path.join(UTILS_DIR, mode, model_name)
model_config_path = os.path.join(model_path, "config.json")
if os.path.exists(model_config_path):
try:
with open(model_config_path, "r", encoding="utf-8") as f:
detailed_config = json.load(f)
tool_config = {**tool_config, **detailed_config}
except Exception as e:
print(f"Error reading model config: {e}")
# Get tool input type
input_info = tool_config.get("input_info", {})
tool_type = input_info.get("type", "video")
once_mode = True if tool_type == "image" else False
# Prepare input parameters
input_params = tool_config.get("input_parameters", {}).copy()
# Configure device-related settings
selected_device = self.device_controller.get_selected_device()
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", []) mode = tool_config.get("mode", "")
if compatible_devices and dongle not in compatible_devices: model_name = tool_config.get("model_name", "")
msgBox = QMessageBox(self.main_window)
msgBox.setIcon(QMessageBox.Warning) # 載入詳細模型配置
msgBox.setWindowTitle("Device Incompatible") model_path = os.path.join(UTILS_DIR, mode, model_name)
msgBox.setText(f"The selected model does not support {dongle} device.\nSupported devices: {', '.join(compatible_devices)}") model_config_path = os.path.join(model_path, "config.json")
msgBox.setStyleSheet("QLabel { color: white; } QMessageBox { background-color: #2b2b2b; }")
msgBox.exec_() if os.path.exists(model_config_path):
return False try:
with open(model_config_path, "r", encoding="utf-8") as f:
detailed_config = json.load(f)
tool_config = {**tool_config, **detailed_config}
except Exception as e:
print(f"讀取模型配置時發生錯誤: {e}")
# 獲取工具輸入類型
input_info = tool_config.get("input_info", {})
tool_type = input_info.get("type", "video")
once_mode = True if tool_type == "image" else False
# 檢查是否從視訊模式切換到圖片模式,或從圖片模式切換到視訊模式
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")
# 清空推論佇列,確保在模式切換時不會使用舊數據
self._clear_inference_queue()
# 儲存當前工具類型以供下次比較
self.previous_tool_config = tool_config
# 準備輸入參數
input_params = tool_config.get("input_parameters", {}).copy()
# Configure device-related settings
selected_device = self.device_controller.get_selected_device()
if selected_device:
# Get usb_port_id (check if it's a dictionary or object)
if isinstance(selected_device, dict):
input_params["usb_port_id"] = selected_device.get("usb_port_id", 0)
product_id = selected_device.get("product_id", "unknown")
else:
input_params["usb_port_id"] = getattr(selected_device, "usb_port_id", 0)
product_id = getattr(selected_device, "product_id", "unknown")
# Configure firmware paths # Ensure product_id is in the right format for lookup
scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin") # Convert to lowercase hex string if it's a number
ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") if isinstance(product_id, int):
input_params["scpu_path"] = scpu_path product_id = hex(product_id).lower()
input_params["ncpu_path"] = ncpu_path # If it's a string but doesn't start with '0x', add it
else: elif isinstance(product_id, str) and not product_id.startswith('0x'):
# Default device handling try:
devices = self.device_controller.connected_devices # Try to convert to int first, then to hex format
if devices and len(devices) > 0: product_id = hex(int(product_id, 0)).lower()
input_params["usb_port_id"] = devices[0].get("usb_port_id", 0) except ValueError:
print("Warning: No device specifically selected, using first available device") # 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"] = ""
else: print(f"Warning: {tool_type} mode requires a file input, but no file has been uploaded.")
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":
self.main_window.media_controller.start_camera()
else:
print("Tool mode is not video, camera not started")
return True # 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:
# For image tools, temporarily pause the camera but don't stop it completely
# 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
return True
except Exception as e:
print(f"選擇工具時發生錯誤: {e}")
import traceback
print(traceback.format_exc())
def _clear_inference_queue(self):
"""清空推論佇列中的所有數據"""
try:
# 清空現有佇列
while not self.inference_queue.empty():
try:
self.inference_queue.get_nowait()
except queue.Empty:
break
print("推論佇列已清空")
except Exception as e:
print(f"清空推論佇列時發生錯誤: {e}")
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())

View File

@ -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,44 +15,159 @@ 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, if hasattr(self.main_window, 'current_bounding_box') and self.main_window.current_bounding_box is not None:
Qt.SmoothTransformation painter = QPainter(pixmap)
) pen = QPen(Qt.red)
self.main_window.canvas_label.setPixmap(QPixmap.fromImage(scaled_image)) pen.setWidth(2)
painter.setPen(pen)
# Convert QImage to numpy array and add to inference queue
frame_np = qimage_to_numpy(qt_image) # 獲取邊界框
self.inference_controller.add_frame_to_queue(frame_np) 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"""
if not self.recording: if not self.recording:
@ -163,4 +278,26 @@ class MediaController:
self.main_window.canvas_label.pixmap().save(filename) self.main_window.canvas_label.pixmap().save(filename)
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

View File

@ -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)

View File

@ -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())

View File

@ -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,

View File

@ -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("檔案複製成功")
# 更新主視窗目的地
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 self.destination
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)

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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}")

View File

@ -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())

View File

@ -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):

View File

@ -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,10 +175,16 @@ 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}")
def hide_device_popup(self): def hide_device_popup(self):
try: try:
self.overlay.hide() self.overlay.hide()
@ -188,32 +202,57 @@ 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) # 檢查結果是否包含邊界框資訊
msgBox.setWindowTitle("Inference Result") if isinstance(result, dict) and "bounding box" in result:
# 更新當前邊界框資訊
# Format text based on result type self.current_bounding_box = result["bounding box"]
if isinstance(result, dict):
result_str = "\n".join(f"{key}: {value}" for key, value in result.items()) # 如果結果中有標籤,將其添加到邊界框中
else: if "result" in result:
result_str = str(result) # 將標籤添加為邊界框列表的第5個元素
if isinstance(self.current_bounding_box, list) and len(self.current_bounding_box) >= 4:
msgBox.setText(result_str) # 確保邊界框列表至少有5個元素
msgBox.setStandardButtons(QMessageBox.Ok) while len(self.current_bounding_box) < 5:
self.current_bounding_box.append(None)
# Set style # 設置第5個元素為結果標籤
msgBox.setStyleSheet(""" self.current_bounding_box[4] = result["result"]
QLabel {
color: white; # 不需要顯示彈窗,因為邊界框會直接繪製在畫面上
} return
""")
# 對於非邊界框結果,使用彈窗顯示
msgBox.exec_() # 創建QMessageBox
msgBox = QMessageBox(self)
msgBox.setWindowTitle("推論結果")
# 根據結果類型格式化文字
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"""
try: try:
@ -249,4 +288,23 @@ 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())

View File

@ -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

View File

@ -32,4 +32,43 @@ Times: 8.5 Hours
2. 增加不同 input format for python script -> 目前是用 numpy 的方式 2. 增加不同 input format for python script -> 目前是用 numpy 的方式
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 頁面之間的無縫切換
改進了頁面標題和描述,提供更清晰的功能說明