diff --git a/core/nodes/exact_nodes.py b/core/nodes/exact_nodes.py index 7ae5b4c..46c4e1d 100644 --- a/core/nodes/exact_nodes.py +++ b/core/nodes/exact_nodes.py @@ -305,47 +305,46 @@ class ExactModelNode(BaseNode): print(f"Warning: Could not setup custom property handlers: {e}") def select_assets_folder(self): - """Method to open folder selection dialog for assets folder using tkinter.""" + """Method to open folder selection dialog for assets folder using improved utility.""" if not NODEGRAPH_AVAILABLE: return "" try: - import tkinter as tk - from tkinter import filedialog + from utils.folder_dialog import select_assets_folder - # Create a root window but keep it hidden - root = tk.Tk() - root.withdraw() # Hide the main window - root.attributes('-topmost', True) # Bring dialog to front + # Get current folder path as initial directory + current_folder = "" + try: + current_folder = self.get_property('assets_folder') or "" + except: + pass - # Open folder selection dialog - folder_path = filedialog.askdirectory( - title="Select Assets Folder", - initialdir="", - mustexist=True - ) + # Use the specialized assets folder dialog with validation + result = select_assets_folder(initial_dir=current_folder) - # Destroy the root window - root.destroy() - - if folder_path: - # Validate the selected folder structure - if self._validate_assets_folder(folder_path): - # Set the property - if NODEGRAPH_AVAILABLE: - self.set_property('assets_folder', folder_path) - print(f"Assets folder set to: {folder_path}") - return folder_path + if result['path']: + # Set the property + if NODEGRAPH_AVAILABLE: + self.set_property('assets_folder', result['path']) + + # Print validation results + if result['valid']: + print(f"✓ Valid Assets folder set to: {result['path']}") + if 'details' in result and 'available_series' in result['details']: + series = result['details']['available_series'] + print(f" Available series: {', '.join(series)}") else: - print(f"Warning: Selected folder does not have the expected structure") - print("Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders") - # Still set it, but warn user - if NODEGRAPH_AVAILABLE: - self.set_property('assets_folder', folder_path) - return folder_path + print(f"⚠ Assets folder set to: {result['path']}") + print(f" Warning: {result['message']}") + print(" Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders") + + return result['path'] + else: + print("No folder selected") + return "" except ImportError: - print("tkinter not available, falling back to simple input") + print("utils.folder_dialog not available, falling back to simple input") # Fallback to manual input folder_path = input("Enter Assets folder path: ").strip() if folder_path and NODEGRAPH_AVAILABLE: diff --git a/mutliseries.py b/mutliseries.py deleted file mode 100644 index 2501742..0000000 --- a/mutliseries.py +++ /dev/null @@ -1,193 +0,0 @@ -import kp -from collections import defaultdict -from typing import Union -import os -import sys -import argparse -import time -import threading -import queue -import numpy as np -import cv2 - -# PWD = os.path.dirname(os.path.abspath(__file__)) -# sys.path.insert(1, os.path.join(PWD, '..')) -IMAGE_FILE_PATH = r"c:\Users\mason\Downloads\kneron_plus_v3.1.2\kneron_plus\res\images\people_talk_in_street_640x640.bmp" -LOOP_TIME = 100 - - -def _image_send_function(_device_group: kp.DeviceGroup, - _loop_time: int, - _generic_inference_input_descriptor: kp.GenericImageInferenceDescriptor, - _image: Union[bytes, np.ndarray], - _image_format: kp.ImageFormat) -> None: - for _loop in range(_loop_time): - try: - _generic_inference_input_descriptor.inference_number = _loop - _generic_inference_input_descriptor.input_node_image_list = [kp.GenericInputNodeImage( - image=_image, - image_format=_image_format, - resize_mode=kp.ResizeMode.KP_RESIZE_ENABLE, - padding_mode=kp.PaddingMode.KP_PADDING_CORNER, - normalize_mode=kp.NormalizeMode.KP_NORMALIZE_KNERON - )] - - kp.inference.generic_image_inference_send(device_group=device_groups[1], - generic_inference_input_descriptor=_generic_inference_input_descriptor) - except kp.ApiKPException as exception: - print(' - Error: inference failed, error = {}'.format(exception)) - exit(0) - - -def _result_receive_function(_device_group: kp.DeviceGroup, - _loop_time: int, - _result_queue: queue.Queue) -> None: - _generic_raw_result = None - - for _loop in range(_loop_time): - try: - _generic_raw_result = kp.inference.generic_image_inference_receive(device_group=device_groups[1]) - - if _generic_raw_result.header.inference_number != _loop: - print(' - Error: incorrect inference_number {} at frame {}'.format( - _generic_raw_result.header.inference_number, _loop)) - - print('.', end='', flush=True) - - except kp.ApiKPException as exception: - print(' - Error: inference failed, error = {}'.format(exception)) - exit(0) - - _result_queue.put(_generic_raw_result) - -model_path = ["C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\models\\KL520\\yolov5-noupsample_w640h640_kn-model-zoo\\kl520_20005_yolov5-noupsample_w640h640.nef", r"C:\Users\mason\Downloads\kneron_plus_v3.1.2\kneron_plus\res\models\KL720\yolov5-noupsample_w640h640_kn-model-zoo\kl720_20005_yolov5-noupsample_w640h640.nef"] -SCPU_FW_PATH_520 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL520\\fw_scpu.bin" -NCPU_FW_PATH_520 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL520\\fw_ncpu.bin" -SCPU_FW_PATH_720 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL720\\fw_scpu.bin" -NCPU_FW_PATH_720 = "C:\\Users\\mason\\Downloads\\kneron_plus_v3.1.2\\kneron_plus\\res\\firmware\\KL720\\fw_ncpu.bin" -device_list = kp.core.scan_devices() - -grouped_devices = defaultdict(list) - -for device in device_list.device_descriptor_list: - grouped_devices[device.product_id].append(device.usb_port_id) - -print(f"Found device groups: {dict(grouped_devices)}") - -device_groups = [] - -for product_id, usb_port_id in grouped_devices.items(): - try: - group = kp.core.connect_devices(usb_port_id) - device_groups.append(group) - print(f"Successfully connected to group for product ID {product_id} with ports{usb_port_id}") - except kp.ApiKPException as e: - print(f"Failed to connect to group for product ID {product_id}: {e}") - -print(device_groups) - -print('[Set Device Timeout]') -kp.core.set_timeout(device_group=device_groups[0], milliseconds=5000) -kp.core.set_timeout(device_group=device_groups[1], milliseconds=5000) -print(' - Success') - -try: - print('[Upload Firmware]') - kp.core.load_firmware_from_file(device_group=device_groups[0], - scpu_fw_path=SCPU_FW_PATH_520, - ncpu_fw_path=NCPU_FW_PATH_520) - kp.core.load_firmware_from_file(device_group=device_groups[1], - scpu_fw_path=SCPU_FW_PATH_720, - ncpu_fw_path=NCPU_FW_PATH_720) - print(' - Success') -except kp.ApiKPException as exception: - print('Error: upload firmware failed, error = \'{}\''.format(str(exception))) - exit(0) - -print('[Upload Model]') -model_nef_descriptors = [] -# for group in device_groups: -model_nef_descriptor = kp.core.load_model_from_file(device_group=device_groups[0], file_path=model_path[0]) -model_nef_descriptors.append(model_nef_descriptor) -model_nef_descriptor = kp.core.load_model_from_file(device_group=device_groups[1], file_path=model_path[1]) -model_nef_descriptors.append(model_nef_descriptor) -print(' - Success') - -""" -prepare the image -""" -print('[Read Image]') -img = cv2.imread(filename=IMAGE_FILE_PATH) -img_bgr565 = cv2.cvtColor(src=img, code=cv2.COLOR_BGR2BGR565) -print(' - Success') - -""" -prepare generic image inference input descriptor -""" -print(model_nef_descriptors) -generic_inference_input_descriptor = kp.GenericImageInferenceDescriptor( - model_id=model_nef_descriptors[1].models[0].id, -) - -""" -starting inference work -""" -print('[Starting Inference Work]') -print(' - Starting inference loop {} times'.format(LOOP_TIME)) -print(' - ', end='') -result_queue = queue.Queue() - -send_thread = threading.Thread(target=_image_send_function, args=(device_groups[1], - LOOP_TIME, - generic_inference_input_descriptor, - img_bgr565, - kp.ImageFormat.KP_IMAGE_FORMAT_RGB565)) - -receive_thread = threading.Thread(target=_result_receive_function, args=(device_groups[1], - LOOP_TIME, - result_queue)) - -start_inference_time = time.time() - -send_thread.start() -receive_thread.start() - -try: - while send_thread.is_alive(): - send_thread.join(1) - - while receive_thread.is_alive(): - receive_thread.join(1) -except (KeyboardInterrupt, SystemExit): - print('\n - Received keyboard interrupt, quitting threads.') - exit(0) - -end_inference_time = time.time() -time_spent = end_inference_time - start_inference_time - -try: - generic_raw_result = result_queue.get(timeout=3) -except Exception as exception: - print('Error: Result queue is empty !') - exit(0) -print() - -print('[Result]') -print(" - Total inference {} images".format(LOOP_TIME)) -print(" - Time spent: {:.2f} secs, FPS = {:.1f}".format(time_spent, LOOP_TIME / time_spent)) - -""" -retrieve inference node output -""" -print('[Retrieve Inference Node Output ]') -inf_node_output_list = [] -for node_idx in range(generic_raw_result.header.num_output_node): - inference_float_node_output = kp.inference.generic_inference_retrieve_float_node(node_idx=node_idx, - generic_raw_result=generic_raw_result, - channels_ordering=kp.ChannelOrdering.KP_CHANNEL_ORDERING_CHW) - inf_node_output_list.append(inference_float_node_output) - -print(' - Success') - -print('[Result]') -print(inf_node_output_list) \ No newline at end of file diff --git a/ui/windows/dashboard.py b/ui/windows/dashboard.py index 708fb92..bfe061e 100644 --- a/ui/windows/dashboard.py +++ b/ui/windows/dashboard.py @@ -43,6 +43,7 @@ except ImportError: from config.theme import HARMONIOUS_THEME_STYLESHEET from config.settings import get_settings +from utils.folder_dialog import select_assets_folder try: from core.nodes import ( InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode, @@ -1323,8 +1324,74 @@ class IntegratedPipelineDashboard(QMainWindow): if hasattr(node, '_property_options') and prop_name in node._property_options: prop_options = node._property_options[prop_name] - # Check for file path properties first (from prop_options or name pattern) - if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \ + # Special handling for assets_folder property + if prop_name == 'assets_folder': + # Assets folder property with validation and improved dialog + display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select Assets Folder...' + widget = QPushButton(display_text) + + # Set fixed width and styling to prevent expansion + widget.setMaximumWidth(250) + widget.setMinimumWidth(200) + widget.setStyleSheet(""" + QPushButton { + text-align: left; + padding: 5px 8px; + background-color: #45475a; + color: #cdd6f4; + border: 1px solid #585b70; + border-radius: 4px; + font-size: 10px; + } + QPushButton:hover { + background-color: #585b70; + border-color: #a6e3a1; + } + QPushButton:pressed { + background-color: #313244; + } + """) + + # Store full path for tooltip and internal use + full_path = str(prop_value) if prop_value else '' + widget.setToolTip(f"Full path: {full_path}\n\nClick to browse for Assets folder\n(Should contain Firmware/ and Models/ subfolders)") + + def browse_assets_folder(): + # Use the specialized assets folder dialog with validation + result = select_assets_folder(initial_dir=full_path or '') + + if result['path']: + # Update button text with truncated path + truncated_text = self.truncate_path_smart(result['path']) + widget.setText(truncated_text) + + # Create detailed tooltip with validation results + tooltip_lines = [f"Full path: {result['path']}"] + if result['valid']: + tooltip_lines.append("✓ Valid Assets folder structure detected") + if 'details' in result and 'available_series' in result['details']: + series = result['details']['available_series'] + tooltip_lines.append(f"Available series: {', '.join(series)}") + else: + tooltip_lines.append(f"⚠ {result['message']}") + + tooltip_lines.append("\nClick to browse for Assets folder") + widget.setToolTip('\n'.join(tooltip_lines)) + + # Set property with full path + if hasattr(node, 'set_property'): + node.set_property(prop_name, result['path']) + + # Show validation message to user + if not result['valid']: + QMessageBox.warning(self, "Assets Folder Validation", + f"Selected folder may not have the expected structure:\n\n{result['message']}\n\n" + "Expected structure:\nAssets/\n├── Firmware/\n│ └── KL520/, KL720/, etc.\n└── Models/\n └── KL520/, KL720/, etc.") + + widget.clicked.connect(browse_assets_folder) + + # Check for file path properties (from prop_options or name pattern) + elif (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \ prop_name in ['model_path', 'source_path', 'destination']: # File path property with smart truncation and width limits display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...' diff --git a/utils/folder_dialog.py b/utils/folder_dialog.py index 3180f0c..32cd291 100644 --- a/utils/folder_dialog.py +++ b/utils/folder_dialog.py @@ -1,14 +1,12 @@ """ -Folder selection utilities using tkinter +Folder selection utilities using PyQt5 as primary, tkinter as fallback """ -import tkinter as tk -from tkinter import filedialog import os def select_folder(title="Select Folder", initial_dir="", must_exist=True): """ - Open a folder selection dialog using tkinter + Open a folder selection dialog using PyQt5 (preferred) or tkinter (fallback) Args: title (str): Dialog window title @@ -18,33 +16,70 @@ def select_folder(title="Select Folder", initial_dir="", must_exist=True): Returns: str: Selected folder path, or empty string if cancelled """ + # Try PyQt5 first (more reliable on macOS) try: - # Create a root window but keep it hidden - root = tk.Tk() - root.withdraw() # Hide the main window - root.attributes('-topmost', True) # Bring dialog to front + from PyQt5.QtWidgets import QApplication, QFileDialog + import sys + + # Create QApplication if it doesn't exist + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) # Set initial directory if not initial_dir: initial_dir = os.getcwd() + elif not os.path.exists(initial_dir): + initial_dir = os.getcwd() # Open folder selection dialog - folder_path = filedialog.askdirectory( - title=title, - initialdir=initial_dir, - mustexist=must_exist + folder_path = QFileDialog.getExistingDirectory( + None, + title, + initial_dir, + QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks ) - # Destroy the root window - root.destroy() - return folder_path if folder_path else "" except ImportError: - print("tkinter not available") - return "" + print("PyQt5 not available, trying tkinter...") + + # Fallback to tkinter + try: + import tkinter as tk + from tkinter import filedialog + + # Create a root window but keep it hidden + root = tk.Tk() + root.withdraw() # Hide the main window + root.attributes('-topmost', True) # Bring dialog to front + + # Set initial directory + if not initial_dir: + initial_dir = os.getcwd() + + # Open folder selection dialog + folder_path = filedialog.askdirectory( + title=title, + initialdir=initial_dir, + mustexist=must_exist + ) + + # Destroy the root window + root.destroy() + + return folder_path if folder_path else "" + + except ImportError: + print("tkinter also not available") + return "" + except Exception as e: + print(f"Error opening tkinter folder dialog: {e}") + return "" + except Exception as e: - print(f"Error opening folder dialog: {e}") + print(f"Error opening PyQt5 folder dialog: {e}") return "" def select_assets_folder(initial_dir=""):