Improve assets folder selection and fix macOS tkinter crash

- Replace tkinter with PyQt5 QFileDialog as primary folder selector to fix macOS crashes
- Add specialized assets_folder property handling in dashboard with validation
- Integrate improved folder dialog utility with ExactModelNode
- Provide detailed validation feedback and user-friendly tooltips
- Maintain backward compatibility with tkinter as fallback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mason 2025-08-14 11:26:23 +08:00
parent 48acae9c74
commit ec940c3f2f
4 changed files with 152 additions and 244 deletions

View File

@ -305,47 +305,46 @@ class ExactModelNode(BaseNode):
print(f"Warning: Could not setup custom property handlers: {e}") print(f"Warning: Could not setup custom property handlers: {e}")
def select_assets_folder(self): 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: if not NODEGRAPH_AVAILABLE:
return "" return ""
try: try:
import tkinter as tk from utils.folder_dialog import select_assets_folder
from tkinter import filedialog
# Create a root window but keep it hidden # Get current folder path as initial directory
root = tk.Tk() current_folder = ""
root.withdraw() # Hide the main window try:
root.attributes('-topmost', True) # Bring dialog to front current_folder = self.get_property('assets_folder') or ""
except:
pass
# Open folder selection dialog # Use the specialized assets folder dialog with validation
folder_path = filedialog.askdirectory( result = select_assets_folder(initial_dir=current_folder)
title="Select Assets Folder",
initialdir="",
mustexist=True
)
# Destroy the root window if result['path']:
root.destroy() # Set the property
if NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', result['path'])
if folder_path: # Print validation results
# Validate the selected folder structure if result['valid']:
if self._validate_assets_folder(folder_path): print(f"✓ Valid Assets folder set to: {result['path']}")
# Set the property if 'details' in result and 'available_series' in result['details']:
if NODEGRAPH_AVAILABLE: series = result['details']['available_series']
self.set_property('assets_folder', folder_path) print(f" Available series: {', '.join(series)}")
print(f"Assets folder set to: {folder_path}")
return folder_path
else: else:
print(f"Warning: Selected folder does not have the expected structure") print(f"⚠ Assets folder set to: {result['path']}")
print("Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders") print(f" Warning: {result['message']}")
# Still set it, but warn user print(" Expected structure: Assets/Firmware/ and Assets/Models/ with series subfolders")
if NODEGRAPH_AVAILABLE:
self.set_property('assets_folder', folder_path) return result['path']
return folder_path else:
print("No folder selected")
return ""
except ImportError: 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 # Fallback to manual input
folder_path = input("Enter Assets folder path: ").strip() folder_path = input("Enter Assets folder path: ").strip()
if folder_path and NODEGRAPH_AVAILABLE: if folder_path and NODEGRAPH_AVAILABLE:

View File

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

View File

@ -43,6 +43,7 @@ except ImportError:
from config.theme import HARMONIOUS_THEME_STYLESHEET from config.theme import HARMONIOUS_THEME_STYLESHEET
from config.settings import get_settings from config.settings import get_settings
from utils.folder_dialog import select_assets_folder
try: try:
from core.nodes import ( from core.nodes import (
InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode, InputNode, ModelNode, PreprocessNode, PostprocessNode, OutputNode,
@ -1323,8 +1324,74 @@ class IntegratedPipelineDashboard(QMainWindow):
if hasattr(node, '_property_options') and prop_name in node._property_options: if hasattr(node, '_property_options') and prop_name in node._property_options:
prop_options = node._property_options[prop_name] prop_options = node._property_options[prop_name]
# Check for file path properties first (from prop_options or name pattern) # Special handling for assets_folder property
if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \ 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']: prop_name in ['model_path', 'source_path', 'destination']:
# File path property with smart truncation and width limits # File path property with smart truncation and width limits
display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...' display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...'

View File

@ -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 import os
def select_folder(title="Select Folder", initial_dir="", must_exist=True): 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: Args:
title (str): Dialog window title title (str): Dialog window title
@ -18,33 +16,70 @@ def select_folder(title="Select Folder", initial_dir="", must_exist=True):
Returns: Returns:
str: Selected folder path, or empty string if cancelled str: Selected folder path, or empty string if cancelled
""" """
# Try PyQt5 first (more reliable on macOS)
try: try:
# Create a root window but keep it hidden from PyQt5.QtWidgets import QApplication, QFileDialog
root = tk.Tk() import sys
root.withdraw() # Hide the main window
root.attributes('-topmost', True) # Bring dialog to front # Create QApplication if it doesn't exist
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# Set initial directory # Set initial directory
if not initial_dir: if not initial_dir:
initial_dir = os.getcwd() initial_dir = os.getcwd()
elif not os.path.exists(initial_dir):
initial_dir = os.getcwd()
# Open folder selection dialog # Open folder selection dialog
folder_path = filedialog.askdirectory( folder_path = QFileDialog.getExistingDirectory(
title=title, None,
initialdir=initial_dir, title,
mustexist=must_exist initial_dir,
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
) )
# Destroy the root window
root.destroy()
return folder_path if folder_path else "" return folder_path if folder_path else ""
except ImportError: except ImportError:
print("tkinter not available") print("PyQt5 not available, trying tkinter...")
return ""
# 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: except Exception as e:
print(f"Error opening folder dialog: {e}") print(f"Error opening PyQt5 folder dialog: {e}")
return "" return ""
def select_assets_folder(initial_dir=""): def select_assets_folder(initial_dir=""):