HTTP.UTF_8过时

今天发现项目里org.apache.http.protocol包下的HTTP.UTF_8过时了,而在HTTP里没找到UTF-8的替代品

结果从网络搜索发现可以用这个替代:StandardCharsets.UTF_8

这个类在java.nio.charset下

本人机器jdk版本1.8

#!/usr/bin/env python # -*- coding: utf-8 -*- "修改:将多个回调函数进行了融合,避免冲突的同时更加好打理: 新增最终播报模块" import rospy import actionlib import os import threading import time import numpy as np from cv_bridge import CvBridge, CvBridgeError from sensor_msgs.msg import Image from geometry_msgs.msg import PoseStamped, Twist from my_control.msg import simulation, simulation_result from std_msgs.msg import String, Bool from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal from actionlib_msgs.msg import GoalStatus from pyzbar import pyzbar import cv2 from playsound import playsound from darknet_ros_msgs.msg import BoundingBoxes, BoundingBox from xf_mic_asr_offline.srv import Get_Offline_Result_srv,Get_Offline_Result_srvRequest class AutomationNode: # 状态定义 - 添加新状态 STATE_IDLE = "IDLE" # 空闲状态 STATE_NAVIGATING = "NAVIGATING" # 导航状态 STATE_ARRIVED = "ARRIVED" # 抵达目标点 STATE_SCANNING = "SCANNING" # 搜索二维码 STATE_PLAYING = "PLAYING" # 播放音频 STATE_SEARCHING = "SEARCHING" # 搜索目标物品 STATE_FOUND_TARGET = "FOUND_TARGET" # 找到目标物品 STATE_WAITING_FOR_TASK3 = "WAITING_FOR_TASK3" # 等待任务3指令 STATE_TRAFFIC_LIGHT_DETECTION = "TRAFFIC_LIGHT_DETECTION" # 交通灯识别状态 STATE_TASK3_NAVIGATING = "TASK3_NAVIGATING" # 任务3导航状态 STATE_WAITING_FOR_SIMULATION_RESULT = "WAITING_FOR_SIMULATION_RESULT" # 新增状态:等待模拟结果 STATE_WAITING_FOR_LINE_RESULT = "WAITING_FOR_LINE_RESULT"#等待寻线任务 STATE_SIMULATION = "GO_TO_DO_SIMULATION" def __init__(self): rospy.init_node('automation_node', anonymous=True) # 状态变量 self.current_state = self.STATE_IDLE self.target_waypoint = None self.qr_detected = False self.waypoint_index = 0 self.task3_started = False # 任务3是否已启动 self.sound_state_1 = 1 self.image = None self.light = False # ROS通信设置 self.bridge = CvBridge() self.speech_sub = rospy.Subscriber('/mic/recognise_result', String, self.speech_callback) self.arrival_sub = rospy.Subscriber('/arrival_status', Bool, self.arrival_callback) self.status_pub = rospy.Publisher('/task_status', String, queue_size=10) self.qr_result_pub = rospy.Publisher('/qrcode_result', String, queue_size=10) self.cmd_vel_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=10) self.yolo_sub = rospy.Subscriber('/darknet_ros/bounding_boxes', BoundingBoxes, self.yolo_callback) self.image_sub =rospy.Subscriber('/usb_cam/image_raw', Image, self.image_callback) self.simulation_start_pub = rospy.Publisher('/simulation_start', simulation, queue_size=10) self.simulation_result_sub = rospy.Subscriber('/simulation_result', simulation_result, self.simulation_result_callback) # 二维码参数配置 self.display = rospy.get_param('~display', False) self.min_size = rospy.get_param('~min_size', 100) self.process_counter = 0 self.process_every_n = 5 self.qr_simulation = None # MoveBase动作客户端 - 等待服务器启动 self.move_base_client = actionlib.SimpleActionClient('move_base', MoveBaseAction) rospy.loginfo("等待move_base服务器...") self.move_base_client.wait_for_server() # 无限期等待直到服务器可用 rospy.loginfo("move_base服务器已连接") rospy.loginfo("等待xf_asr_offline_node服务器...") rospy.wait_for_service('/xf_asr_offline_node/get_offline_recognise_result_srv') rospy.loginfo("xf_asr_offline_node服务器已连接") self.speech_recognition = rospy.ServiceProxy('/xf_asr_offline_node/get_offline_recognise_result_srv',Get_Offline_Result_srv) # 预定义航点(根据实际环境配置) self.waypoints = [ # 任务1相关航点 self.create_pose(1.84581,0.56147, 0.0, 0.0, 0.0, 0.999966, -0.00827442), # 新增过渡点0 self.create_pose(1.00982, 0.630106, 0.0, 0.0, 0.0, 0.999848, 0.017445), # 点1: 扫描点,识别二维码 self.create_pose(0.238816, 1.0809, 0.0, 0.0, 0.0, -0.00275711, 0.999996), # 新增过渡点1 self.create_pose(1.91287, 1.5377, 0.0, 0.0, 0.0, 0.999987,0.00516634), # 新增过渡点2 self.create_pose(0.239635, 2.22449, 0.0, 0.0, 0.0,0.625221, 0.780448), # 点2: 任务1出口 # 任务2搜索航点 self.create_pose(1.55117,2.70212, 0.0, 0.0, 0.0, -0.610833, 0.791759), # 点3: 搜索点1 self.create_pose(1.30051, 4.05638, 0.0, 0.0, 0.0, 0.216052, 0.976382), # 点4: 搜索点2 self.create_pose(0.550306, 3.7162, 0.0, 0.0, 0.0, 0.815828, 0.578295), # 点5: 搜索点3 # 任务2终点 self.create_pose(1.09864, 3.57304, 0.0, 0.0, 0.0, 0.21246, 0.97717), # 点6: 任务2的终点 # 任务3相关航点 self.create_pose(2.63748, 3.98935, 0.0, 0.0, 0.0, 0.707858, 0.706355), # 点7: 路灯1 self.create_pose(2.57195, 3.44993, 0.0, 0.0, 0.0, -0.60105, 0.799212), # 点8: 路口1 self.create_pose(4.46442, 4.08407, 0.0, 0.0, 0.0, 0.643035, 0.765837), # 点9: 路灯2) self.create_pose(4.53154, 3.52776, 0.0, 0.0, 0.0, -0.771258, 0.636523) # 点10: 路口2 ] # 航点索引常量(提高可读性) - 更新索引 self.WAYPOINT_TRANSITION0 = 0 # 新增的过渡点0 self.WAYPOINT_SCAN = 1 # 点1: 扫描点,识别二维码 self.WAYPOINT_TRANSITION1 = 2 # 新增过渡点1 self.WAYPOINT_TRANSITION2 = 3 # 新增过渡点2 self.WAYPOINT_TASK1_EXIT = 4 # 点2: 任务1出口 self.WAYPOINT_SEARCH1 = 5 # 点3: 搜索点1 self.WAYPOINT_SEARCH2 = 6 # 点4: 搜索点2 self.WAYPOINT_SEARCH3 = 7 # 点5: 搜索点3 self.WAYPOINT_TASK2_END = 8 # 点6: 任务2的终点 self.WAYPOINT_LIGHT1 = 9 # 点7: 路灯1) self.WAYPOINT_CROSS1 = 10 # 点8: 路口1) self.WAYPOINT_LIGHT2 = 11 # 点9: 路灯2) self.WAYPOINT_CROSS2 = 12 # 点10: 路口2) # 音频文件映射 - 添加新音频 self.audio_files = { "Fruit": "/home/ucar/ucar_ws/src/mp3/Fruit.mp3", "Vegetable": "/home/ucar/ucar_ws/src/mp3/Vegetable.mp3", "Dessert": "/home/ucar/ucar_ws/src/mp3/Dessert.mp3", "apple": "/home/ucar/ucar_ws/src/mp3/apple.mp3", "watermelon": "/home/ucar/ucar_ws/src/mp3/watermelon.mp3", "banana": "/home/ucar/ucar_ws/src/mp3/banana.mp3", "potato": "/home/ucar/ucar_ws/src/mp3/potato.mp3", "tomato": "/home/ucar/ucar_ws/src/mp3/tomato.mp3", "chili": "/home/ucar/ucar_ws/src/mp3/chili.mp3", "cake": "/home/ucar/ucar_ws/src/mp3/cake.mp3", "coco": "/home/ucar/ucar_ws/src/mp3/coco.mp3", "milk": "/home/ucar/ucar_ws/src/mp3/milk.mp3", "load_1": "/home/ucar/ucar_ws/src/mp3/load_1.mp3", "load_2": "/home/ucar/ucar_ws/src/mp3/load_2.mp3", "A": "/home/ucar/ucar_ws/src/mp3/simulation_A.mp3", "B": "/home/ucar/ucar_ws/src/mp3/simulation_B.mp3", "C": "/home/ucar/ucar_ws/src/mp3/simulation_C.mp3" } # 目标搜索相关变量 self.qr_result = 0 self.target_object = None self.search_timeout = 5.0 # 每个搜索点的超时时间(秒) self.search_start_time = None self.search_timer = None self.search_timer_state = True self.search_acount_state = True self.search_acount_current = 0 self.search_acount_target = 80 self.search_target = False # 目标类别映射 self.test1_categories = { 1: ['apple', 'watermelon', 'banana'], 2: ['potato', 'tomato', 'chili'], 3: ['cake', 'coco', 'milk'] } # 任务3相关变量 self.light_timout1 =True self.light_timout2 = True self.traffic_light_detected = False self.traffic_light_result = None # 'red' 或 'green' #仿真任务相关 self.simulation_result_received = False self.simulation_result1 = None self.simulation_result2 = None self.msg_send = True self.simulation_timeout_timer = None # 特殊任务超时定时器 #价格计算相关 self.goods_1 = None self.goods_2 = None #寻线相关任务 self.line_pub = rospy.Publisher('/line_start', String, queue_size=10) self.line_result_sub = rospy.Subscriber('/line_result', String, self.line_result_callback) self.line_sent = True rospy.loginfo("自动化节点已启动,等待语音指令...") rospy.on_shutdown(self.shutdown) def create_pose(self, x, y, z, qx, qy, qz, qw): """创建导航目标位姿""" pose = PoseStamped() pose.header.frame_id = "map" pose.header.stamp = rospy.Time.now() pose.pose.position.x = x pose.pose.position.y = y pose.pose.position.z = z pose.pose.orientation.x = qx pose.pose.orientation.y = qy pose.pose.orientation.z = qz pose.pose.orientation.w = qw return pose def navigate_to_waypoint(self, waypoint_index): """导航到指定航点""" if waypoint_index >= len(self.waypoints): rospy.logwarn("无效的航点索引: %d", waypoint_index) return self.waypoint_index = waypoint_index waypoint = self.waypoints[waypoint_index] rospy.loginfo("导航到航点 %d", waypoint_index + 1) # 根据任务设置状态 if self.task3_started: self.current_state = self.STATE_TASK3_NAVIGATING else: self.current_state = self.STATE_NAVIGATING self.status_pub.publish("NAVIGATING_TO_TARGET") # 使用move_base动作客户端发送目标 goal = MoveBaseGoal() goal.target_pose = waypoint self.move_base_client.send_goal(goal) # 设置定时器检查导航状态 rospy.Timer(rospy.Duration(0.3), self.check_navigation_status, oneshot=False) def check_navigation_status(self, event): """检查导航状态(使用move_base动作)""" state = self.move_base_client.get_state() if state == GoalStatus.SUCCEEDED: rospy.loginfo_once("成功到达航点 {}".format(self.waypoint_index + 1)) self.handle_arrival() # 停止定时器 event.new_message = None # 停止定时器 elif state in [GoalStatus.ABORTED, GoalStatus.PREEMPTED]: rospy.logwarn("导航失败,尝试重新规划...") self.navigate_to_waypoint(self.waypoint_index) # 停止当前定时器 event.new_message = None def handle_arrival(self): """处理到达航点后的逻辑""" self.current_state = self.STATE_ARRIVED self.status_pub.publish("ARRIVED_AT_TARGET") # 发布到达状态 arrival_pub = rospy.Publisher('/arrival_status', Bool, queue_size=10) arrival_pub.publish(True) def speech_callback(self, msg): """处理语音识别结果""" rospy.loginfo("收到语音指令: {}".format(msg.data)) # 处理初始指令 if "ok" in msg.data.lower() and self.current_state == self.STATE_IDLE: rospy.loginfo("收到语音指令,开始导航到航点1") self.navigate_to_waypoint(self.WAYPOINT_TRANSITION0) elif "fail" in msg.data.lower() and self.current_state == self.STATE_IDLE: # 创建服务请求 req = Get_Offline_Result_srvRequest() # 根据实际srv定义设置请求字段 req.offline_recognise_start = 1 #设置1为确认启用 req.confidence_threshold = 40 #置信度 req.time_per_order = 3 #启动持续时间 self.speech_recognition(req) def arrival_callback(self, msg): """处理到达状态""" if not msg.data or self.current_state != self.STATE_ARRIVED: return rospy.loginfo_once("处理航点 {} 的到达逻辑".format(self.waypoint_index+1)) # 新增过渡点0 - 直接导航到下一个点(二维码扫描点) if self.waypoint_index == self.WAYPOINT_TRANSITION0: rospy.loginfo("到达过渡点0, 继续导航到二维码扫描点") self.navigate_to_waypoint(self.WAYPOINT_SCAN) # 任务1: 二维码扫描点 elif self.waypoint_index == self.WAYPOINT_SCAN: rospy.loginfo_once("启动二维码识别...") self.current_state = self.STATE_SCANNING self.status_pub.publish("SCANNING") self.image_sub = rospy.Subscriber('/usb_cam/image_raw', Image, self.image_callback) # 过渡点1 elif self.waypoint_index == self.WAYPOINT_TRANSITION1: rospy.loginfo("继续导航到过渡点2") self.navigate_to_waypoint(self.WAYPOINT_TRANSITION2) # 过渡点2 elif self.waypoint_index == self.WAYPOINT_TRANSITION2: rospy.loginfo("继续导航到任务1出口") self.navigate_to_waypoint(self.WAYPOINT_TASK1_EXIT) # 任务1出口 elif self.waypoint_index == self.WAYPOINT_TASK1_EXIT: rospy.loginfo_once("到达任务1出口,开始任务2") #self.navigate_to_waypoint(self.WAYPOINT_SEARCH1) self.navigate_to_waypoint(self.WAYPOINT_SEARCH1) # 任务2搜索点 elif self.waypoint_index in [self.WAYPOINT_SEARCH1, self.WAYPOINT_SEARCH2, self.WAYPOINT_SEARCH3]: rospy.loginfo_once("在航点 {} 开始搜索目标...".format(self.waypoint_index+1)) self.start_searching() # 任务2终点 elif self.waypoint_index == self.WAYPOINT_TASK2_END: rospy.loginfo_once("到达任务2终点,执行特殊任务...") rospy.sleep(3) self.execute_special_task() # 任务3: 路灯1 elif self.waypoint_index == self.WAYPOINT_LIGHT1: rospy.loginfo("到达路灯1,开始识别交通灯...") self.current_state = self.STATE_TRAFFIC_LIGHT_DETECTION self.status_pub.publish("DETECTING_TRAFFIC_LIGHT") self.traffic_light_detected = False self.traffic_light_result = None # 设置超时定时器 if self.search_timer and self.light_timout1: self.light_timout1 = False self.search_timer.shutdown() self.search_timer = rospy.Timer(rospy.Duration(self.search_timeout), self.handle_traffic_light_timeout, oneshot=True) # 任务3: 路灯2 elif self.waypoint_index == self.WAYPOINT_LIGHT2: rospy.loginfo("到达路灯2,开始识别交通灯...") self.current_state = self.STATE_TRAFFIC_LIGHT_DETECTION self.status_pub.publish("DETECTING_TRAFFIC_LIGHT") self.traffic_light_detected = False self.traffic_light_result = None # 设置超时定时器 if self.search_timer and self.light_timout2: self.light_timout2 = False self.search_timer.shutdown() self.search_timer = rospy.Timer(rospy.Duration(self.search_timeout), self.handle_traffic_light_timeout, oneshot=True) # 任务3: 路口1或路口2 elif self.waypoint_index in [self.WAYPOINT_CROSS1, self.WAYPOINT_CROSS2]: rospy.loginfo("到达路口{},准备进行下一步任务...".format(self.waypoint_index-7)) self.status_pub.publish("TASK3_COMPLETED") self.current_state = self.STATE_IDLE self.task3_started = False # 根据到达的路口播放不同的音频 if self.waypoint_index == self.WAYPOINT_CROSS1: rospy.loginfo("在路口1播放load_1音频") audio_file = self.audio_files.get("load_1") else: rospy.loginfo("在路口2播放load_2音频") audio_file = self.audio_files.get("load_2") if self.sound_state_1 == 1: self.sound_state_1 = 0 if audio_file and os.path.exists(audio_file): threading.Thread(target=self.play_audio_thread, args=(audio_file,)).start() # 发送line话题 if self.line_sent: rospy.loginfo("发送line话题: start") self.line_pub.publish("start") self.line_sent = False self.current_state = self.STATE_WAITING_FOR_LINE_RESULT def start_searching(self): """启动目标搜索""" if self.current_state == self.STATE_FOUND_TARGET: return rospy.loginfo_once("在航点 {} 开始搜索".format(self.waypoint_index+1)) self.current_state = self.STATE_SEARCHING self.status_pub.publish("SEARCHING_TARGET") if self.search_acount_state == True: self.search_acount_current += 1 twist = Twist() twist.angular.z = 0.2 self.cmd_vel_pub.publish(twist) if self.search_acount_current % 10 == 0: rospy.loginfo('当前已搜索{}次, 目标搜索次数为{}'.format(self.search_acount_current,self.search_acount_target)) if self.search_acount_current == self.search_acount_target or self.search_acount_current > self.search_acount_target: self.handle_search_timeout() def handle_search_timeout(self): """处理搜索超时""" if self.current_state != self.STATE_SEARCHING: return # 检查是否已找到目标 if self.current_state == self.STATE_FOUND_TARGET: rospy.loginfo("搜索超时但已找到目标,忽略超时") return rospy.logwarn("航点 {} 搜索超时".format(self.waypoint_index+1)) self.search_acount_state = False self.search_acount_current = 0 if self.waypoint_index == self.WAYPOINT_SEARCH1: rospy.loginfo("前往搜索点2 ") self.search_acount_state = True self.navigate_to_waypoint(self.WAYPOINT_SEARCH2) elif self.waypoint_index == self.WAYPOINT_SEARCH2: rospy.loginfo("前往搜索点3") self.search_acount_state = True self.navigate_to_waypoint(self.WAYPOINT_SEARCH3) else: rospy.logwarn("所有搜索点均未找到目标,前往任务终点") self.search_acount_state = False self.search_acount_current = 0 self.navigate_to_waypoint(self.WAYPOINT_TASK2_END) ##if next_index <= max_search_index: # rospy.loginfo("前往下一个搜索点: {}".format(next_index+1)) # self.navigate_to_waypoint(next_index) #else: # rospy.logwarn("所有搜索点均未找到目标,前往任务终点") # self.navigate_to_waypoint(self.WAYPOINT_TASK2_END) def cancel_current_search(self): if self.search_timer is not None: try: self.search_acount_state = False rospy.loginfo_once("搜索定时器已取消") except Exception as e: rospy.logwarn("取消定时器时出错: {}".format(e)) finally: self.search_acount_current = 0 def handle_traffic_light_timeout(self, event): """处理交通灯识别超时""" if self.current_state == self.STATE_TRAFFIC_LIGHT_DETECTION: rospy.logwarn("交通灯识别超时,执行默认操作") # 如果在路灯1,默认前往路口1 if self.waypoint_index == self.WAYPOINT_LIGHT1: rospy.loginfo("默认前往路口1(点9)") self.navigate_to_waypoint(self.WAYPOINT_CROSS1) # 如果在路灯2,默认前往路口2 elif self.waypoint_index == self.WAYPOINT_LIGHT2: rospy.loginfo("默认前往路口2(点11)") self.navigate_to_waypoint(self.WAYPOINT_CROSS2) def image_callback(self, data): """统一的图像处理回调函数""" try: cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8") # 统一翻转图像 cv_image = cv2.flip(cv_image, 1) self.image = cv_image except CvBridgeError as e: rospy.logerr("图像转换错误: {}".format(e)) return # 状态分发处理 if self.current_state == self.STATE_SCANNING: self._handle_qr_scanning(cv_image) self.process_counter +=1 elif self.current_state == self.STATE_FOUND_TARGET: self._handle_target_tracking(cv_image) elif self.current_state == self.STATE_TRAFFIC_LIGHT_DETECTION: # 交通灯检测仍通过YOLO回调处理,这里只需显示图像 if self.display: cv2.imshow("Traffic Light Detection", cv_image) cv2.waitKey(1) def _handle_qr_scanning(self, cv_image): """处理二维码扫描""" if self.process_counter % self.process_every_n != 0: return # 跳过处理 decoded_objects = pyzbar.decode(cv_image) results = [] for obj in decoded_objects: data = obj.data.decode('utf-8') results.append(data) if results: result_msg = ", ".join(results) rospy.loginfo("识别到二维码: {}".format(result_msg)) self.qr_result_pub.publish(result_msg) self.qr_detected = True self.current_state = self.STATE_PLAYING self.status_pub.publish("PLAYING_AUDIO") # 取消图像订阅(任务完成后) #if self.image_sub: # self.image_sub.unregister() # self.image_sub = None self.play_audio(result_msg) rospy.sleep(3) self.navigate_to_waypoint(self.WAYPOINT_TRANSITION1) # 可选:显示图像 if self.display: for obj in decoded_objects: points = obj.polygon if len(points) > 4: hull = cv2.convexHull(np.array([point for point in points], dtype=np.float32)) hull = list(map(tuple, np.squeeze(hull))) else: hull = points n = len(hull) for j in range(n): cv2.line(cv_image, hull[j], hull[(j+1) % n], (0, 255, 0), 3) cv2.imshow("QR Scanning", cv_image) cv2.waitKey(1) def _handle_target_tracking(self, cv_image): """处理目标跟踪""" if not self.target_object: return self.search_acount_state = False self.search_acount_current = 0 # 计算目标位置 target_center_x = (self.xmin + self.xmax) // 2 target_width = self.xmax - self.xmin frame_center_x = cv_image.shape[1] // 2 # 视觉控制逻辑 twist = Twist() # 横向控制(转向) lateral_error = target_center_x - frame_center_x twist.angular.z = 0.01 * lateral_error if abs(lateral_error) > 30 else 0.0 # 纵向控制(前进/停止) if target_width < 120: twist.linear.x = 0.25 else: # 到达目标面前 twist.linear.x = 0.0 twist.angular.z = 0.0 rospy.loginfo("已到达目标 {} 面前!".format(self.target_object)) self.status_pub.publish("ARRIVED_AT_TARGET") self.goods_1 = self.target_object rospy.loginfo_once("货物1为{}".format(self.goods_1)) # 播放目标音频 audio_file = self.audio_files.get(self.target_object) #self.play_audio(audio_file) if audio_file and os.path.exists(audio_file): threading.Thread(target=playsound, args=(audio_file,)).start() rospy.sleep(2) # 导航到任务2终点 rospy.loginfo("前往任务2终点...") self.navigate_to_waypoint(self.WAYPOINT_TASK2_END) # 重置目标状态 self.target_object = None self.current_state = self.STATE_WAITING_FOR_SIMULATION_RESULT # 发布控制命令 self.cmd_vel_pub.publish(twist) # 可选:显示跟踪框 if self.display: cv2.rectangle(cv_image, (self.xmin, self.ymin), (self.xmax, self.ymax), (0, 255, 0), 2) cv2.circle(cv_image, (target_center_x, (self.ymin + self.ymax)//2), 5, (0, 0, 255), -1) cv2.imshow("Target Tracking", cv_image) cv2.waitKey(1) def play_audio(self, qr_code): """播放音频文件""" rospy.loginfo("播放音频: {}".format(qr_code)) # 查找对应的音频文件 audio_file = None for key in self.audio_files: if key in qr_code: audio_file = self.audio_files[key] break if audio_file and os.path.exists(audio_file): # 播放音频(异步) threading.Thread(target=self.play_audio_thread, args=(audio_file,)).start() # 设置二维码结果 if "Fruit" in qr_code: self.qr_result = 1 self.qr_simulation = "fruit" elif "Vegetable" in qr_code: self.qr_result = 2 self.qr_simulation = "vegetable" elif "Dessert" in qr_code: self.qr_result = 3 self.qr_simulation = "dessert" else: rospy.logwarn("未找到匹配的音频文件: {}".format(qr_code) ) def play_audio_thread(self, audio_file): """线程中播放音频""" try: playsound(audio_file) rospy.loginfo("音频播放完成") except Exception as e: rospy.logerr("音频播放错误: {}".format(e)) def yolo_callback(self, msg): """处理YOLO检测结果""" # 目标搜索状态 if self.current_state == self.STATE_SEARCHING or self.current_state == self.STATE_FOUND_TARGET: test1_targets = self.test1_categories.get(self.qr_result, []) for bbox in msg.bounding_boxes: if bbox.Class in test1_targets: rospy.loginfo_once("发现目标: {}".format(bbox.Class)) self.target_object = bbox.Class self.xmin = bbox.xmin self.ymin = bbox.ymin self.xmax = bbox.xmax self.ymax = bbox.ymax # 取消搜索定时器 #self.cancel_current_search() self.search_acount_state = False self.search_acount_current = 0 # 更新状态 self.current_state = self.STATE_FOUND_TARGET self.status_pub.publish("TARGET_FOUND") # 订阅图像用于目标跟踪 #if not self.image_sub: # self.image_sub = rospy.Subscriber('/usb_cam/image_raw', Image, self.yolo_image_callback) break # 交通灯识别状态 elif self.current_state == self.STATE_TRAFFIC_LIGHT_DETECTION : # 检查交通灯类别 for bbox in msg.bounding_boxes: if bbox.Class in ['red', 'green']: rospy.loginfo("识别到交通灯: {}".format(bbox.Class) ) self.traffic_light_result = bbox.Class self.traffic_light_detected = True if bbox.Class == 'green': self.xmin = bbox.xmin self.ymin = bbox.ymin self.xmax = bbox.xmax self.ymax = bbox.ymax target_center_x = (self.xmin + self.xmax) // 2 target_width = self.xmax - self.xmin frame_center_x = self.image.shape[1] // 2 # 视觉控制逻辑 twist = Twist() # 横向控制(转向) lateral_error = target_center_x - frame_center_x twist.angular.z = 0.005 * lateral_error if abs(lateral_error) > 50 else 0.0 # 纵向控制(前进/停止) if target_width < 140: twist.linear.x = 0.5 else: # 到达目标面前 twist.linear.x = 0.0 twist.angular.z = 0.0 self.light = True # 取消超时定时器 if self.search_timer: self.search_timer.shutdown() self.search_timer = None # 根据识别结果决定下一步 if self.light == True: self.handle_traffic_light_result() elif bbox.Class == 'red' : self.handle_traffic_light_result() break def handle_traffic_light_result(self): """根据交通灯识别结果决定导航路径并播放音频""" if self.waypoint_index == self.WAYPOINT_LIGHT1: # 路灯1 if self.traffic_light_result == 'green': rospy.loginfo("路灯1为绿灯,播放load_1音频并前往路口1") # 播放音频 if self.sound_state_1 == 1: self.sound_state_1 = 0 audio_file = self.audio_files.get("load_1") if audio_file and os.path.exists(audio_file): threading.Thread(target=self.play_audio_thread, args=(audio_file,)).start() else: rospy.logwarn("load_1音频文件不存在!") # 导航到路口1 self.navigate_to_waypoint(self.WAYPOINT_CROSS1) else: # 红灯 rospy.loginfo("路灯1为红灯,前往路灯2") self.navigate_to_waypoint(self.WAYPOINT_LIGHT2) elif self.waypoint_index == self.WAYPOINT_LIGHT2: # 路灯2 if self.traffic_light_result == 'green': rospy.loginfo("路灯2为绿灯,播放load_2音频并前往路口2") # 播放音频 if self.sound_state_1 == 1: self.sound_state_1 = 0 audio_file = self.audio_files.get("load_1") if audio_file and os.path.exists(audio_file): threading.Thread(target=self.play_audio_thread, args=(audio_file,)).start() else: rospy.logwarn("load_1音频文件不存在!") # 导航到路口2 self.navigate_to_waypoint(self.WAYPOINT_CROSS2) else: # 红灯 rospy.loginfo("路灯2为红灯,默认前往路口2") self.navigate_to_waypoint(self.WAYPOINT_CROSS2) def execute_special_task(self): """执行任务二终点的特殊任务""" rospy.loginfo_once("执行特殊任务: 发布simulation_start话题") # 创建并发布simulation_start消息 if self.msg_send == True: self.msg_send = False sim_msg = simulation() sim_msg.start = "start" sim_msg.myclass_send = str(self.qr_simulation) # 二维码结果 self.simulation_start_pub.publish(sim_msg) # 进入等待结果状态 self.current_state = self.STATE_WAITING_FOR_SIMULATION_RESULT self.status_pub.publish("WAITING_FOR_SIMULATION_RESULT") # 重置结果接收标志 self.simulation_result_received = False # 设置超时定时器(2分50秒 = 170秒) rospy.loginfo_once("设置特殊任务超时定时器(170秒)") self.simulation_timeout_timer = rospy.Timer(rospy.Duration(170), self.handle_simulation_timeout, oneshot=True) rospy.loginfo_once("已发布simulation_start,等待simulation_result...") def simulation_result_callback(self, msg): """处理simulation_result消息""" if self.current_state != self.STATE_WAITING_FOR_SIMULATION_RESULT: return rospy.loginfo("收到simulation_result: stop={}, room={}, myclass={}".format(msg.stop,msg.room,msg.myclass)) # 检查消息格式 if msg.stop == 'stop': self.simulation_result1 = msg.room self.simulation_result2 = msg.myclass self.simulation_result_received = True self.goods_2 = self.simulation_result2 # 根据str2的内容播放相应的音频 self.handle_simulation_result() def handle_simulation_result(self): """处理模拟结果并导航到路灯1(点7)""" if not self.simulation_result_received or not self.simulation_result1: return # 取消超时定时器 if self.simulation_timeout_timer: self.simulation_timeout_timer.shutdown() self.simulation_timeout_timer = None rospy.loginfo("处理模拟结果: {}".format(self.simulation_result1)) # 检查结果是否有效(A, B, C) if self.simulation_result1 in ["A", "B", "C"]: # 播放相应的音频 audio_file = self.audio_files.get(self.simulation_result1) if audio_file and os.path.exists(audio_file): rospy.loginfo("播放结果音频: {}".format(self.simulation_result1) ) threading.Thread(target=self.play_audio_thread, args=(audio_file,)).start() else: rospy.logwarn("找不到结果音频文件:{}".format(self.simulation_result1)) rospy.sleep(2) # 导航到路灯1(点7)开始任务3 rospy.loginfo("特殊任务完成,导航到路灯1开始任务3") self.task3_started = True # 标记任务3已开始 self.navigate_to_waypoint(self.WAYPOINT_LIGHT1) # 直接导航到路灯1 def handle_simulation_timeout(self, event): """处理特殊任务超时""" if self.current_state == self.STATE_WAITING_FOR_SIMULATION_RESULT: rospy.logwarn("特殊任务超时(170秒),未收到simulation_result,将直接前往路灯1") # 取消超时定时器 if self.simulation_timeout_timer: self.simulation_timeout_timer.shutdown() self.simulation_timeout_timer = None # 导航到路灯1(点7) rospy.loginfo("导航到路灯1") self.task3_started = True # 标记任务3已开始 self.navigate_to_waypoint(self.WAYPOINT_LIGHT1) def line_result_callback(self, msg): """处理line_result消息""" if self.current_state != self.STATE_WAITING_FOR_LINE_RESULT: return if msg.data == 'over': rospy.loginfo("收到line_result: over, 执行Final_voice") self.Final_voice() rospy.loginfo("等待3秒后关闭节点...") rospy.sleep(3) rospy.signal_shutdown("任务完成,关闭节点") def Final_voice(self): if self.qr_result == 1: if self.goods_1 == 'apple': if self.goods_2 == 'apple': playsound("/home/ucar/ucar_ws/src/mp3/2apple.mp3") elif self.goods_2 == 'banana': playsound("/home/ucar/ucar_ws/src/mp3/apple_banana.mp3") elif self.goods_2 == 'watermelon': playsound("/home/ucar/ucar_ws/src/mp3/apple_watermelon.mp3") elif self.goods_1 == 'banana': if self.goods_2 == 'banana': playsound("/home/ucar/ucar_ws/src/mp3/2banana.mp3") elif self.goods_2 == 'apple': playsound("/home/ucar/ucar_ws/src/mp3/apple_banana.mp3") elif self.goods_2 == 'watermelon': playsound("/home/ucar/ucar_ws/src/mp3/banana_watermelon.mp3") elif self.goods_1 == 'watermelon': if self.goods_2 == 'watermelon': playsound("/home/ucar/ucar_ws/src/mp3/2watermelon.mp3") elif self.goods_2 == 'apple': playsound("/home/ucar/ucar_ws/src/mp3/apple_watermelon.mp3") elif self.goods_2 == 'banana': playsound("/home/ucar/ucar_ws/src/mp3/banana_watermelon.mp3") if self.qr_result == 2: if self.goods_1 == 'cake': if self.goods_2 == 'cake': playsound("/home/ucar/ucar_ws/src/mp3/2cake.mp3") elif self.goods_2 == 'coco': playsound("/home/ucar/ucar_ws/src/mp3/cake_coco.mp3") elif self.goods_2 == 'milk': playsound("/home/ucar/ucar_ws/src/mp3/milk_cake.mp3") elif self.goods_1 == 'coco': if self.goods_2 == 'coco': playsound("/home/ucar/ucar_ws/src/mp3/2coco.mp3") elif self.goods_2 == 'cake': playsound("/home/ucar/ucar_ws/src/mp3/cake_coco.mp3") elif self.goods_2 == 'milk': playsound("/home/ucar/ucar_ws/src/mp3/milk_coco.mp3") elif self.goods_1 == 'milk': if self.goods_2 == 'milk': playsound("/home/ucar/ucar_ws/src/mp3/2milk.mp3") elif self.goods_2 == 'coco': playsound("/home/ucar/ucar_ws/src/mp3/milk_coco.mp3") elif self.goods_2 == 'cake': playsound("/home/ucar/ucar_ws/src/mp3/milk_cake.mp3") if self.qr_result == 3: if self.goods_1 == 'potato': if self.goods_2 == 'potato': playsound("/home/ucar/ucar_ws/src/mp3/2potato.mp3") elif self.goods_2 == 'chili': playsound("/home/ucar/ucar_ws/src/mp3/chili_potato.mp3") elif self.goods_2 == 'tomato': playsound("/home/ucar/ucar_ws/src/mp3/tomato_potato.mp3") elif self.goods_1 == 'chili': if self.goods_2 == 'chili': playsound("/home/ucar/ucar_ws/src/mp3/2chili.mp3") elif self.goods_2 == 'potato': playsound("/home/ucar/ucar_ws/src/mp3/chili_potato.mp3") elif self.goods_2 == 'tomato': playsound("/home/ucar/ucar_ws/src/mp3/chili_tomato.mp3") elif self.goods_1 == 'tomato': if self.goods_2 == 'tomato': playsound("/home/ucar/ucar_ws/src/mp3/2tomato.mp3") elif self.goods_2 == 'chili': playsound("/home/ucar/ucar_ws/src/mp3/chili_tomato.mp3") elif self.goods_2 == 'potato': playsound("/home/ucar/ucar_ws/src/mp3/tomato_potato.mp3") def shutdown(self): """节点关闭时的清理工作""" rospy.loginfo("关闭节点...") if self.image_sub: self.image_sub.unregister() if self.search_timer: self.search_timer.shutdown() if self.simulation_timeout_timer: self.simulation_timeout_timer.shutdown() self.cmd_vel_pub.publish(Twist()) # 停止机器人运动 if __name__ == '__main__': try: node = AutomationNode() rospy.spin() except rospy.ROSInterruptException: pass 我想在识别交通灯的时候也实现目标跟随
最新发布
08-07
# -*- coding: utf-8 -*- import threading import time import sys import inspect import ctypes import os import cv2 import numpy as np import logging from MvCameraControl_class import * from datetime import datetime from ctypes import * from enum import Enum from ctypes import byref, cast, POINTER, c_ubyte from MvCameraControl_class import MvCamera from CameraParams_header import MV_FRAME_OUT_INFO_EX, PixelType_Gvsp_BGR8_Packed, MV_OK # 配置日志系统 def setup_logging(log_level=logging.INFO): """配置全局日志系统""" logging.basicConfig( level=log_level, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("camera_operation.log"), logging.StreamHandler() ] ) logging.info("日志系统初始化完成") # 强制关闭线程 def async_raise(tid, exctype): """安全终止线程""" tid = ctypes.c_long(tid) if not inspect.isclass(exctype): exctype = type(exctype) res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)) if res == 0: raise ValueError("invalid thread id") elif res != 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) raise SystemError("PyThreadState_SetAsyncExc failed") def stop_thread(thread): """停止指定线程""" async_raise(thread.ident, SystemExit) # 触发模式常量定义(兼容不同SDK版本) try: from CameraParams_header import MV_TRIGGER_MODE_ON, MV_TRIGGER_MODE_OFF, MV_TRIGGER_SOURCE_SOFTWARE except ImportError: # 如果导入失败,使用默认值(兼容旧版本) logging.warning("CameraParams_header 导入触发模式常量失败,使用默认值") MV_TRIGGER_MODE_ON = 1 MV_TRIGGER_MODE_OFF = 0 MV_TRIGGER_SOURCE_SOFTWARE = 7 # 像素格式常量定义(兼容不同SDK版本) try: from CameraParams_header import * # 如果导入成功,使用SDK中的常量定义 PIXEL_FORMATS = { "MONO8": PixelType_Gvsp_Mono8, "MONO10": PixelType_Gvsp_Mono10, "MONO12": PixelType_Gvsp_Mono12, "BAYER_BG8": PixelType_Gvsp_BayerBG8, "BAYER_GB8": PixelType_Gvsp_BayerGB8, "BAYER_GR8": PixelType_Gvsp_BayerGR8, "BAYER_RG8": PixelType_Gvsp_BayerRG8, "RGB8": PixelType_Gvsp_RGB8_Packed, "YUV422": PixelType_Gvsp_YUV422_Packed, "YUV422_YUYV": PixelType_Gvsp_YUV422_YUYV_Packed } except ImportError: # 如果导入失败,使用默认值(兼容旧版本) logging.warning("CameraParams_header 导入失败,使用默认像素格式常量") PIXEL_FORMATS = { "MONO8": 0x01080001, "MONO10": 0x01100003, "MONO12": 0x01100005, "BAYER_BG8": 0x0108000B, "BAYER_GB8": 0x0108000A, "BAYER_GR8": 0x01080008, "BAYER_RG8": 0x01080009, "RGB8": 0x02180014, "YUV422": 0x02100032, "YUV422_YUYV": 0x0210001F } # 像素格式枚举 class PixelFormat(Enum): MONO8 = PIXEL_FORMATS["MONO8"] MONO10 = PIXEL_FORMATS["MONO10"] MONO12 = PIXEL_FORMATS["MONO12"] BAYER_BG8 = PIXEL_FORMATS["BAYER_BG8"] BAYER_GB8 = PIXEL_FORMATS["BAYER_GB8"] BAYER_GR8 = PIXEL_FORMATS["BAYER_GR8"] BAYER_RG8 = PIXEL_FORMATS["BAYER_RG8"] RGB8 = PIXEL_FORMATS["RGB8"] YUV422 = PIXEL_FORMATS["YUV422"] YUV422_YUYV = PIXEL_FORMATS["YUV422_YUYV"] def is_mono_data(enGvspPixelType): """判断是否为单色图像格式""" mono_formats = [ PIXEL_FORMATS["MONO8"], PIXEL_FORMATS["MONO10"], PIXEL_FORMATS["MONO12"], 0x010C0004, # Mono10 Packed 0x010C0006, # Mono12 Packed 0x01080002, # Mono8 Signed 0x0110000C # Mono16 ] return enGvspPixelType in mono_formats def is_color_data(enGvspPixelType): """判断是否为彩色图像格式""" color_formats = [ # Bayer格式 PIXEL_FORMATS["BAYER_BG8"], PIXEL_FORMATS["BAYER_GB8"], PIXEL_FORMATS["BAYER_GR8"], PIXEL_FORMATS["BAYER_RG8"], 0x01100011, # BayerBG10 0x01100010, # BayerGB10 0x0110000E, # BayerGR10 0x0110000F, # BayerRG10 0x01100017, # BayerBG12 0x01100016, # BayerGB12 0x01100014, # BayerGR12 0x01100015, # BayerRG12 # YUV格式 PIXEL_FORMATS["YUV422"], PIXEL_FORMATS["YUV422_YUYV"], # RGB格式 PIXEL_FORMATS["RGB8"], 0x0220001E, # RGB10_Packed 0x02300020, # RGB12_Packed 0x02400021, # RGB16_Packed 0x02180032 # BGR8_Packed ] return enGvspPixelType in color_formats def get_pixel_format_name(pixel_value): """根据像素值获取可读的名称""" for name, value in PIXEL_FORMATS.items(): if value == pixel_value: return name return f"未知格式: 0x{pixel_value:08X}" # 相机操作类 class CameraOperation: # 状态码定义 MV_OK = 0 MV_E_CALLORDER = -2147483647 MV_E_PARAMETER = -2147483646 MV_E_NO_DATA = -2147483645 MV_E_SAVE_IMAGE = -2147483644 MV_E_STATE = -2147483643 # 在 CamOperation_class.py 中修改 CameraOperation 类 def __init__(self, obj_cam, st_device_list, n_connect_num=0): """ 初始化相机操作对象 :param obj_cam: 相机对象 :param st_device_list: 设备列表 :param n_connect_num: 连接编号 """ self.obj_cam = obj_cam self.st_device_list = st_device_list self.n_connect_num = n_connect_num # 状态标志 - 确保所有属性初始化 self.b_open_device = False self.b_start_grabbing = False self.b_thread_running = False self.b_exit = False # 帧接收状态 - 关键修复:显式初始化 self.b_frame_received = False # 线程相关 self.h_thread_handle = None # 图像数据 self.st_frame_info = None self.buf_save_image = None self.n_save_image_size = 0 self.current_frame = None # 参数 self.frame_rate = 0.0 self.exposure_time = 0.0 self.gain = 0.0 # 线程安全锁 self.buf_lock = threading.Lock() # 图像缓冲区锁 self.frame_lock = threading.Lock() # 当前帧锁 self.param_lock = threading.Lock() # 参数锁 self.frame_count = 0 # 帧计数 self.last_frame_time = None # 最后帧时间 self.is_streaming = False # 取流状态 logging.info("相机操作对象初始化完成") # 添加 is_frame_available 方法 def is_frame_available(self): """检查是否有可用帧图像""" try: # 检查SDK是否返回有效帧 if hasattr(self, 'cam') and self.cam: return self.cam.is_frame_available() return False except Exception as e: logging.error(f"检查帧可用性失败: {str(e)}") return False def capture_frame(self): """捕获当前帧""" try: return self.cam.get_current_frame() except Exception as e: logging.error(f"捕获帧失败: {str(e)}") return None # 在获取图像的方法中更新 last_frame def get_current_frame(self): """获取当前帧图像""" try: # 假设这是获取图像的实际实现 frame = self.cam.get_image() # 伪代码,实际调用SDK方法 self.last_frame = frame # 更新最后捕获的帧 return frame except Exception as e: logging.error(f"获取图像失败: {str(e)}") return None # 在开始取流的方法中设置标志 def start_grabbing(self, hwnd=None): """开始取流""" ret = self.cam.start_grabbing(hwnd) if ret == MV_OK: self.is_grabbing = True return ret # 在停止取流的方法中重置标志 def Stop_grabbing(self): """停止取流""" ret = self.cam.stop_grabbing() if ret == MV_OK: self.is_grabbing = False self.last_frame = None # 清除最后帧 self.image_buffer = [] # 清空缓冲区 return ret def is_ready(self): """检查设备是否就绪""" return self.b_open_device and not self.b_exit def open_device(self): """打开相机设备""" if self.b_open_device: logging.warning("设备已打开,无需重复操作") return self.MV_OK if self.n_connect_num < 0: logging.error("无效的连接编号") return self.MV_E_CALLORDER try: # 选择设备并创建句柄 nConnectionNum = int(self.n_connect_num) stDeviceList = cast(self.st_device_list.pDeviceInfo[int(nConnectionNum)], POINTER(MV_CC_DEVICE_INFO)).contents self.obj_cam = MvCamera() # 创建相机句柄 ret = self.obj_cam.MV_CC_CreateHandle(stDeviceList) if ret != self.MV_OK: self.obj_cam.MV_CC_DestroyHandle() logging.error(f"创建句柄失败: {hex(ret)}") return ret # 打开设备 ret = self.obj_cam.MV_CC_OpenDevice() if ret != self.MV_OK: logging.error(f"打开设备失败: {hex(ret)}") return ret # 设备已成功打开 self.b_open_device = True self.b_exit = False logging.info("设备打开成功") # 探测网络最佳包大小(仅对GigE相机有效) if stDeviceList.nTLayerType in [MV_GIGE_DEVICE, MV_GENTL_GIGE_DEVICE]: nPacketSize = self.obj_cam.MV_CC_GetOptimalPacketSize() if int(nPacketSize) > 0: ret = self.obj_cam.MV_CC_SetIntValue("GevSCPSPacketSize", nPacketSize) if ret != self.MV_OK: logging.warning(f"设置包大小失败: {hex(ret)}") else: logging.warning(f"获取最佳包大小失败: {hex(nPacketSize)}") # 获取采集帧率使能状态 stBool = c_bool(False) ret = self.obj_cam.MV_CC_GetBoolValue("AcquisitionFrameRateEnable", stBool) if ret != self.MV_OK: logging.warning(f"获取帧率使能状态失败: {hex(ret)}") # 现在设备已打开,可以安全设置触发模式 ret = self.set_continue_mode() if ret != self.MV_OK: logging.warning(f"设置连续模式失败: {hex(ret)}") return self.MV_OK except Exception as e: logging.exception(f"打开设备异常: {str(e)}") # 尝试清理资源 try: if hasattr(self, 'obj_cam'): self.obj_cam.MV_CC_CloseDevice() self.obj_cam.MV_CC_DestroyHandle() except: pass self.b_open_device = False return self.MV_E_STATE def close_device(self): """关闭相机设备""" if not self.b_open_device: logging.warning("设备未打开,无需关闭") return self.MV_OK try: # 停止采集(如果正在采集) if self.b_start_grabbing: self.stop_grabbing() # 关闭设备 ret = self.obj_cam.MV_CC_CloseDevice() if ret != self.MV_OK: logging.error(f"关闭设备失败: {hex(ret)}") # 销毁句柄 ret = self.obj_cam.MV_CC_DestroyHandle() if ret != self.MV_OK: logging.error(f"销毁句柄失败: {hex(ret)}") self.b_open_device = False self.b_exit = True logging.info("设备关闭成功") return self.MV_OK except Exception as e: logging.exception(f"关闭设备异常: {str(e)}") return self.MV_E_STATE def start_grabbing(self, winHandle=None): """开始图像采集""" if not self.b_open_device: logging.error("设备未打开,无法开始采集") return self.MV_E_CALLORDER if self.b_start_grabbing: logging.warning("采集已在进行中") return self.MV_OK try: # 开始采集 ret = self.obj_cam.MV_CC_StartGrabbing() if ret != self.MV_OK: logging.error(f"开始采集失败: {hex(ret)}") return ret self.b_start_grabbing = True self.b_exit = False # 启动采集线程 self.h_thread_handle = threading.Thread( target=self.work_thread, args=(winHandle,), daemon=True ) self.h_thread_handle.start() self.b_thread_running = True logging.info("图像采集已开始") return self.MV_OK except Exception as e: logging.exception(f"开始采集异常: {str(e)}") return self.MV_E_STATE def stop_grabbing(self): """停止图像采集""" if not self.b_open_device: logging.error("设备未打开,无法停止采集") return self.MV_E_CALLORDER if not self.b_start_grabbing: logging.warning("采集未在进行中") return self.MV_OK try: # 设置退出标志 self.b_exit = True # 等待线程结束 if self.b_thread_running and self.h_thread_handle.is_alive(): self.h_thread_handle.join(timeout=2.0) if self.h_thread_handle.is_alive(): logging.warning("采集线程未正常退出,尝试强制终止") stop_thread(self.h_thread_handle) # 停止采集 ret = self.obj_cam.MV_CC_StopGrabbing() if ret != self.MV_OK: logging.error(f"停止采集失败: {hex(ret)}") return ret self.b_start_grabbing = False self.b_thread_running = False logging.info("图像采集已停止") return self.MV_OK except Exception as e: logging.exception(f"停止采集异常: {str(e)}") return self.MV_E_STATE def set_trigger_mode(self, enable=True, source=MV_TRIGGER_SOURCE_SOFTWARE): """ 设置触发模式 :param enable: 是否启用触发模式 :param source: 触发源 (默认软件触发) :return: 操作结果 """ if not self.b_open_device: logging.error("设备未打开,无法设置触发模式") return self.MV_E_CALLORDER try: mode = MV_TRIGGER_MODE_ON if enable else MV_TRIGGER_MODE_OFF ret = self.obj_cam.MV_CC_SetEnumValue("TriggerMode", mode) if ret != self.MV_OK: logging.error(f"设置触发模式失败: {hex(ret)}") return ret if enable: ret = self.obj_cam.MV_CC_SetEnumValue("TriggerSource", source) if ret != self.MV_OK: logging.error(f"设置触发源失败: {hex(ret)}") return ret logging.info(f"触发模式设置成功: {'启用' if enable else '禁用'}") return self.MV_OK except Exception as e: logging.exception(f"设置触发模式异常: {str(e)}") return self.MV_E_STATE def set_continue_mode(self): """设置连续采集模式""" # 添加设备状态检查 if not self.b_open_device: logging.error("设备未打开,无法设置连续模式") return self.MV_E_CALLORDER logging.info("尝试设置连续采集模式") try: # 禁用触发模式 ret = self.obj_cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF) if ret != self.MV_OK: logging.error(f"禁用触发模式失败: {hex(ret)}") return ret # 设置采集模式为连续 ret = self.obj_cam.MV_CC_SetEnumValue("AcquisitionMode", 2) # 2表示连续采集 if ret != self.MV_OK: logging.error(f"设置连续采集模式失败: {hex(ret)}") return ret logging.info("连续采集模式设置成功") return self.MV_OK except Exception as e: logging.exception(f"设置连续模式异常: {str(e)}") return self.MV_E_STATE def trigger_once(self): """执行一次软触发""" if not self.b_open_device: logging.error("设备未打开,无法触发") return self.MV_E_CALLORDER try: ret = self.obj_cam.MV_CC_SetCommandValue("TriggerSoftware") if ret != self.MV_OK: logging.error(f"软触发失败: {hex(ret)}") return ret logging.info("软触发成功") return self.MV_OK except Exception as e: logging.exception(f"软触发异常: {str(e)}") return self.MV_E_STATE def get_parameters(self): """获取相机参数""" if not self.b_open_device: logging.error("设备未打开,无法获取参数") return self.MV_E_CALLORDER try: # 使用锁保护参数访问 with self.param_lock: # 初始化返回值为整数错误码 return_code = self.MV_OK # 帧率 stFrameRate = MVCC_FLOATVALUE() memset(byref(stFrameRate), 0, sizeof(MVCC_FLOATVALUE)) ret = self.obj_cam.MV_CC_GetFloatValue("AcquisitionFrameRate", stFrameRate) if ret == self.MV_OK: self.frame_rate = stFrameRate.fCurValue logging.debug(f"获取帧率成功: {self.frame_rate}") else: logging.warning(f"获取帧率失败: {hex(ret)}") if return_code == self.MV_OK: # 保留第一个错误码 return_code = ret # 曝光时间 stExposure = MVCC_FLOATVALUE() memset(byref(stExposure), 0, sizeof(MVCC_FLOATVALUE)) ret = self.obj_cam.MV_CC_GetFloatValue("ExposureTime", stExposure) if ret == self.MV_OK: self.exposure_time = stExposure.fCurValue logging.debug(f"获取曝光时间成功: {self.exposure_time}") else: logging.warning(f"获取曝光时间失败: {hex(ret)}") if return_code == self.MV_OK: return_code = ret # 增益 stGain = MVCC_FLOATVALUE() memset(byref(stGain), 0, sizeof(MVCC_FLOATVALUE)) ret = self.obj_cam.MV_CC_GetFloatValue("Gain", stGain) if ret == self.MV_OK: self.gain = stGain.fCurValue logging.debug(f"获取增益成功: {self.gain}") else: logging.warning(f"获取增益失败: {hex(ret)}") if return_code == self.MV_OK: return_code = ret # 返回整数错误码 return return_code except Exception as e: logging.exception(f"获取参数异常: {str(e)}") return self.MV_E_STATE # 添加兼容拼写错误的方法 def get_parame(self): """兼容拼写错误的方法名:get_parame""" logging.warning("调用get_parame方法 - 可能是拼写错误,建议使用get_parameters()") return self.get_parameters() # 添加动态属性处理 def __getattr__(self, name): # 处理保存方法 if name == "Save_Image": logging.warning("动态处理Save_Image调用 - 映射到save_image") return self.save_image if name == "Save_Bmp": logging.warning("动态处理Save_Bmp调用 - 映射到save_bmp") return self.save_bmp # 处理其他可能的拼写错误 method_map = { "get_parame": self.get_parame, "get_parm": self.get_parameters, "get_parmeter": self.get_parameters, "get_parma": self.get_parameters, "GetParameter": self.get_parameters, "GetParam": self.get_parameters } if name in method_map: logging.warning(f"动态处理{name}调用 - 可能是拼写错误") return method_map[name] # 关键修复:处理b_frame_received属性 if name == "b_frame_received": logging.warning("动态访问b_frame_received属性 - 提供默认值") return False raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") def set_param(self, frame_rate=None, exposure_time=None, gain=None): """ 设置相机参数 :param frame_rate: 帧率 (None表示不修改) :param exposure_time: 曝光时间 (None表示不修改) :param gain: 增益 (None表示不修改) :return: 操作结果 """ if not self.b_open_device: logging.error("设备未打开,无法设置参数") return self.MV_E_CALLORDER try: # 使用锁保护参数修改 with self.param_lock: # 禁用自动曝光 ret = self.obj_cam.MV_CC_SetEnumValue("ExposureAuto", 0) if ret != self.MV_OK: logging.warning(f"禁用自动曝光失败: {hex(ret)}") # 设置帧率 if frame_rate is not None: ret = self.obj_cam.MV_CC_SetFloatValue("AcquisitionFrameRate", float(frame_rate)) if ret != self.MV_OK: logging.error(f"设置帧率失败: {hex(ret)}") return ret self.frame_rate = float(frame_rate) # 设置曝光时间 if exposure_time is not None: ret = self.obj_cam.MV_CC_SetFloatValue("ExposureTime", float(exposure_time)) if ret != self.MV_OK: logging.error(f"设置曝光时间失败: {hex(ret)}") return ret self.exposure_time = float(exposure_time) # 设置增益 if gain is not None: ret = self.obj_cam.MV_CC_SetFloatValue("Gain", float(gain)) if ret != self.MV_OK: logging.error(f"设置增益失败: {hex(ret)}") return ret self.gain = float(gain) logging.info(f"参数设置成功: 帧率={self.frame_rate}, 曝光={self.exposure_time}, 增益={self.gain}") return self.MV_OK except Exception as e: logging.exception(f"设置参数异常: {str(e)}") return self.MV_E_STATE def work_thread(self, winHandle=None): """图像采集工作线程""" stOutFrame = MV_FRAME_OUT() memset(byref(stOutFrame), 0, sizeof(stOutFrame)) logging.info("采集线程启动") while not self.b_exit: try: # 获取图像缓冲区 ret = self.obj_cam.MV_CC_GetImageBuffer(stOutFrame, 1000) if ret != self.MV_OK: if ret != self.MV_E_NO_DATA: # 忽略无数据错误 logging.warning(f"获取图像缓冲区失败: {hex(ret)}") time.sleep(0.01) continue # 更新帧信息 self.st_frame_info = stOutFrame.stFrameInfo # 关键修复:标记帧已接收 self.b_frame_received = True # 记录像素格式信息 pixel_format = get_pixel_format_name(self.st_frame_info.enPixelType) logging.debug(f"获取到图像: {self.st_frame_info.nWidth}x{self.st_frame_info.nHeight}, " f"格式: {pixel_format}, 大小: {self.st_frame_info.nFrameLen}字节") # 分配/更新图像缓冲区 frame_size = stOutFrame.stFrameInfo.nFrameLen with self.buf_lock: if self.buf_save_image is None or self.n_save_image_size < frame_size: self.buf_save_image = (c_ubyte * frame_size)() self.n_save_image_size = frame_size # 复制图像数据 cdll.msvcrt.memcpy(byref(self.buf_save_image), stOutFrame.pBufAddr, frame_size) # 更新当前帧 self.update_current_frame() # 显示图像(如果指定了窗口句柄) if winHandle is not None: stDisplayParam = MV_DISPLAY_FRAME_INFO() memset(byref(stDisplayParam), 0, sizeof(stDisplayParam)) stDisplayParam.hWnd = int(winHandle) stDisplayParam.nWidth = self.st_frame_info.nWidth stDisplayParam.nHeight = self.st_frame_info.nHeight stDisplayParam.enPixelType = self.st_frame_info.enPixelType stDisplayParam.pData = self.buf_save_image stDisplayParam.nDataLen = frame_size self.obj_cam.MV_CC_DisplayOneFrame(stDisplayParam) # 释放图像缓冲区 self.obj_cam.MV_CC_FreeImageBuffer(stOutFrame) except Exception as e: logging.exception(f"采集线程异常: {str(e)}") time.sleep(0.1) # 清理资源 with self.buf_lock: if self.buf_save_image is not None: del self.buf_save_image self.buf_save_image = None self.n_save_image_size = 0 logging.info("采集线程退出") def update_current_frame(self): """将原始图像数据转换为OpenCV格式并存储""" if not self.st_frame_info or not self.buf_save_image: logging.warning("更新当前帧时缺少帧信息或缓冲区") return try: # 获取图像信息 width = self.st_frame_info.nWidth height = self.st_frame_info.nHeight pixel_type = self.st_frame_info.enPixelType # 复制缓冲区数据 with self.buf_lock: buffer_copy = bytearray(self.buf_save_image) # 转换为numpy数组 np_buffer = np.frombuffer(buffer_copy, dtype=np.uint8) # 根据像素类型处理图像 frame = None if is_mono_data(pixel_type): # 单色图像 frame = np_buffer.reshape(height, width) elif pixel_type == PIXEL_FORMATS["BAYER_BG8"]: # Bayer BG格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerBG2RGB) elif pixel_type == PIXEL_FORMATS["BAYER_GB8"]: # Bayer GB格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGB2RGB) elif pixel_type == PIXEL_FORMATS["BAYER_GR8"]: # Bayer GR格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGR2RGB) elif pixel_type == PIXEL_FORMATS["BAYER_RG8"]: # Bayer RG格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerRG2RGB) elif pixel_type == PIXEL_FORMATS["RGB8"]: # RGB格式 frame = np_buffer.reshape(height, width, 3) elif pixel_type in [PIXEL_FORMATS["YUV422"], PIXEL_FORMATS["YUV422_YUYV"]]: # YUV格式 frame = cv2.cvtColor(np_buffer.reshape(height, width, 2), cv2.COLOR_YUV2RGB_YUYV) else: # 尝试自动处理其他格式 if pixel_type in [PIXEL_FORMATS["MONO10"], PIXEL_FORMATS["MONO12"]]: # 10位或12位单色图像需要特殊处理 frame = self.process_high_bit_depth(np_buffer, width, height, pixel_type) else: logging.warning(f"不支持的像素格式: {get_pixel_format_name(pixel_type)}") return # 更新当前帧 - 使用线程安全锁 with self.frame_lock: self.current_frame = frame logging.debug(f"当前帧更新成功: {frame.shape if frame is not None else '无数据'}") except Exception as e: logging.exception(f"更新当前帧异常: {str(e)}") # 调试:保存原始数据用于分析 try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") debug_path = f"frame_debug_{timestamp}.bin" with open(debug_path, "wb") as f: f.write(buffer_copy) logging.info(f"已保存原始帧数据到: {debug_path}") except: logging.error("保存调试帧数据失败") def process_high_bit_depth(self, buffer, width, height, pixel_type): """处理高位深度图像格式""" try: # 10位或12位图像处理 if pixel_type == PIXEL_FORMATS["MONO10"]: # 将10位数据转换为16位 data_16bit = np.frombuffer(buffer, dtype=np.uint16) # 10位数据存储方式:每个像素占用2字节,但只有10位有效 data_16bit = (data_16bit >> 6) # 右移6位使10位数据对齐到低10位 frame = data_16bit.reshape(height, width).astype(np.uint16) elif pixel_type == PIXEL_FORMATS["MONO12"]: # 将12位数据转换为16位 data_16bit = np.frombuffer(buffer, dtype=np.uint16) # 12位数据存储方式:每个像素占用2字节,但只有12位有效 data_16bit = (data_16bit >> 4) # 右移4位使12位数据对齐到低12位 frame = data_16bit.reshape(height, width).astype(np.uint16) else: logging.warning(f"不支持的高位深度格式: {get_pixel_format_name(pixel_type)}") return None # 归一化到8位用于显示(如果需要) frame_8bit = cv2.convertScaleAbs(frame, alpha=(255.0/4095.0)) return frame_8bit except Exception as e: logging.exception(f"处理高位深度图像异常: {str(e)}") return None def get_current_frame(self): """获取当前帧的副本(线程安全)""" with self.frame_lock: if self.current_frame is None: return None # 返回副本以避免外部修改影响内部状态 return self.current_frame.copy() # 在 CameraOperation 类中添加/修改以下方法 def save_image(self, file_path, save_format="bmp", quality=95): """ 安全保存当前帧到文件 - 使用原始缓冲区数据 """ if not self.b_open_device or not self.b_start_grabbing: logging.error("设备未就绪,无法保存图像") return self.MV_E_CALLORDER # 使用缓冲区锁确保数据一致性 with self.buf_lock: if not self.buf_save_image or not self.st_frame_info: logging.error("无可用图像数据") return self.MV_E_NO_DATA # 获取图像信息 width = self.st_frame_info.nWidth height = self.st_frame_info.nHeight pixel_type = self.st_frame_info.enPixelType frame_size = self.st_frame_info.nFrameLen # 复制缓冲区数据 buffer_copy = bytearray(self.buf_save_image) try: # 确保目录存在 directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): os.makedirs(directory, exist_ok=True) logging.info(f"创建目录: {directory}") # 根据像素类型处理图像 np_buffer = np.frombuffer(buffer_copy, dtype=np.uint8) # 根据像素格式转换图像 if is_mono_data(pixel_type): # 单色图像 frame = np_buffer.reshape(height, width) elif pixel_type == PIXEL_FORMATS["BAYER_BG8"]: # Bayer BG格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerBG2BGR) elif pixel_type == PIXEL_FORMATS["BAYER_GB8"]: # Bayer GB格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGB2BGR) elif pixel_type == PIXEL_FORMATS["BAYER_GR8"]: # Bayer GR格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerGR2BGR) elif pixel_type == PIXEL_FORMATS["BAYER_RG8"]: # Bayer RG格式 frame = cv2.cvtColor(np_buffer.reshape(height, width), cv2.COLOR_BayerRG2BGR) elif pixel_type == PIXEL_FORMATS["RGB8"]: # RGB格式 frame = np_buffer.reshape(height, width, 3) frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) elif pixel_type in [PIXEL_FORMATS["YUV422"], PIXEL_FORMATS["YUV422_YUYV"]]: # YUV格式 frame = cv2.cvtColor(np_buffer.reshape(height, width, 2), cv2.COLOR_YUV2BGR_YUYV) else: # 尝试自动处理其他格式 if pixel_type in [PIXEL_FORMATS["MONO10"], PIXEL_FORMATS["MONO12"]]: # 10位或12位单色图像需要特殊处理 frame = self.process_high_bit_depth(np_buffer, width, height, pixel_type) else: logging.error(f"不支持的像素格式: {get_pixel_format_name(pixel_type)}") return self.MV_E_PARAMETER # 根据格式保存图像 save_format = save_format.lower() try: if save_format == "bmp": cv2.imwrite(file_path, frame) elif save_format in ["jpg", "jpeg"]: cv2.imwrite(file_path, frame, [cv2.IMWRITE_JPEG_QUALITY, quality]) elif save_format == "png": cv2.imwrite(file_path, frame, [cv2.IMWRITE_PNG_COMPRESSION, 9]) elif save_format in ["tiff", "tif"]: cv2.imwrite(file_path, frame, [cv2.IMWRITE_TIFF_COMPRESSION, 1]) else: logging.error(f"不支持的图像格式: {save_format}") return self.MV_E_PARAMETER # 验证保存结果 if not os.path.exists(file_path): logging.error(f"文件保存失败: {file_path}") return self.MV_E_SAVE_IMAGE file_size = os.path.getsize(file_path) if file_size < 1024: # 小于1KB视为无效 logging.error(f"文件大小异常: {file_path} ({file_size} 字节)") os.remove(file_path) # 删除无效文件 return self.MV_E_SAVE_IMAGE logging.info(f"图像已保存: {file_path} ({file_size} 字节)") return self.MV_OK except Exception as e: logging.exception(f"保存图像异常: {str(e)}") return self.MV_E_SAVE_IMAGE except Exception as e: logging.exception(f"图像处理异常: {str(e)}") return self.MV_E_SAVE_IMAGE # 在 CameraOperation 类中添加/修改以下方法 # 兼容旧方法的保存接口 def save_jpg(self, file_path=None, quality=95): """保存为JPEG格式""" if file_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_path = f"capture_{timestamp}.jpg" return self.save_image(file_path, "jpg", quality) def save_bmp(self, file_path=None): """保存为BMP格式""" if file_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_path = f"capture_{timestamp}.bmp" return self.save_image(file_path, "bmp") def save_png(self, file_path=None): """保存为PNG格式""" if file_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_path = f"capture_{timestamp}.png" return self.save_image(file_path, "png") def save_tiff(self, file_path=None): """保存为TIFF格式""" if file_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_path = f"capture_{timestamp}.tiff" return self.save_image(file_path, "tiff") # 新增方法:检查帧是否可用 def is_frame_available(self): """检查是否有有效帧可用""" with self.frame_lock: return self.current_frame is not None and self.current_frame.size > 0 # 新增方法:获取帧状态详细信息 def get_frame_status(self): """获取当前帧状态详细信息""" # 安全访问b_frame_received属性 frame_received = getattr(self, 'b_frame_received', False) status = { "camera_open": self.b_open_device, "grabbing_started": self.b_start_grabbing, "thread_running": self.b_thread_running, "frame_received": frame_received, # 使用安全访问 "frame_size": self.n_save_image_size if self.buf_save_image else 0, "current_frame": self.current_frame is not None } if self.st_frame_info: status.update({ "width": self.st_frame_info.nWidth, "height": self.st_frame_info.nHeight, "pixel_format": get_pixel_format_name(self.st_frame_info.enPixelType), "frame_num": self.st_frame_info.nFrameNum }) return status 这是camoperatto_class.py目前的完整代码在不改变原本已有功能的前提下,把你刚刚提出的改进方案完整展示出来
07-11
import tkinter as tk import cv2 import time import torch from ultralytics import YOLO from PIL import Image, ImageTk import threading import queue import dxcam import traceback import ctypes from tkinter import ttk, messagebox import os import glob import sys import logitech.lg from pynput import mouse class PIDController: """PID控制器实现""" def __init__(self, kp=0.0, ki=0.0, kd=0.0): self.kp = kp self.ki = ki self.kd = kd self.prev_error = (0, 0) self.integral = (0, 0) self.last_time = time.time() def update(self, error): current_time = time.time() dt = current_time - self.last_time self.last_time = current_time if dt <= 0: dt = 0.01 dx, dy = error px = self.kp * dx py = self.kp * dy self.integral = ( self.integral[0] + dx * dt, self.integral[1] + dy * dt ) ix = self.ki * self.integral[0] iy = self.ki * self.integral[1] dx_dt = (dx - self.prev_error[0]) / dt dy_dt = (dy - self.prev_error[1]) / dt ddx = self.kd * dx_dt ddy = self.kd * dy_dt self.prev_error = (dx, dy) output_x = px + ix + ddx output_y = py + iy + ddy return (output_x, output_y) def reset(self): self.prev_error = (0, 0) self.integral = (0, 0) self.last_time = time.time() class ScreenDetector: def __init__(self, config_path): # 解析配置文件 self._parse_config(config_path) # 设备检测与模型加载 self.device = self._determine_device() self.model = YOLO(self.model_path).to(self.device) # 屏幕信息初始化 self._init_screen_info() # 控制参数初始化 self._init_control_params() # 状态管理 self.stop_event = threading.Event() self.camera_lock = threading.Lock() self.target_lock = threading.Lock() self.offset_lock = threading.Lock() self.button_lock = threading.Lock() # 初始化相机 self._init_camera() # 初始化鼠标监听器 self._init_mouse_listener() def _parse_config(self, config_path): """解析并存储配置参数""" self.cfg = self._parse_txt_config(config_path) # 存储常用参数 self.model_path = self.cfg['model_path'] self.model_device = self.cfg['model_device'] self.screen_target_size = int(self.cfg['screen_target_size']) self.detection_conf_thres = float(self.cfg['detection_conf_thres']) self.detection_iou_thres = float(self.cfg['detection_iou_thres']) self.detection_classes = [int(x) for x in self.cfg['detection_classes'].split(',')] self.visualization_color = tuple(map(int, self.cfg['visualization_color'].split(','))) self.visualization_line_width = int(self.cfg['visualization_line_width']) self.visualization_font_scale = float(self.cfg['visualization_font_scale']) self.visualization_show_conf = bool(self.cfg['visualization_show_conf']) self.fov_horizontal = float(self.cfg.get('move_fov_horizontal', '90')) self.mouse_dpi = int(self.cfg.get('move_mouse_dpi', '400')) self.pid_kp = float(self.cfg.get('pid_kp', '0.5')) self.pid_ki = float(self.cfg.get('pid_ki', '0.0')) self.pid_kd = float(self.cfg.get('pid_kd', '0.1')) self.target_offset_x_percent = float(self.cfg.get('target_offset_x', '50')) self.target_offset_y_percent = 100 - float(self.cfg.get('target_offset_y', '50')) self.smooth_alpha = float(self.cfg.get('smooth_alpha', '0.4')) # 平滑系数[1](@ref) def _parse_txt_config(self, path): """解析TXT格式的配置文件""" config = {} with open(path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue if '=' in line: key, value = line.split('=', 1) config[key.strip()] = value.strip() return config def _determine_device(self): """确定运行设备""" if self.model_device == 'auto': return 'cuda' if torch.cuda.is_available() and torch.cuda.device_count() > 0 else 'cpu' return self.model_device def _init_screen_info(self): """初始化屏幕信息""" user32 = ctypes.windll.user32 self.screen_width, self.screen_height = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1) self.screen_center = (self.screen_width // 2, self.screen_height // 2) # 计算截图区域 left = (self.screen_width - self.screen_target_size) // 2 top = (self.screen_height - self.screen_target_size) // 2 self.region = ( max(0, int(left)), max(0, int(top)), min(self.screen_width, int(left + self.screen_target_size)), min(self.screen_height, int(top + self.screen_target_size)) ) def _init_control_params(self): """初始化控制参数""" self.pid_controller = PIDController( kp=self.pid_kp, ki=self.pid_ki, kd=self.pid_kd ) self.previous_target_info = None self.closest_target_absolute = None self.target_offset = None self.right_button_pressed = False # 改为鼠标右键状态 self.smoothed_offset = (0, 0) # 平滑后的偏移量[1](@ref) def _init_camera(self): """初始化相机""" try: with self.camera_lock: self.camera = dxcam.create( output_idx=0, output_color="BGR", region=self.region ) self.camera.start(target_fps=60, video_mode=True) # 稳定帧率减少抖动[1](@ref) except Exception as e: print(f"相机初始化失败: {str(e)}") try: # 降级模式 with self.camera_lock: self.camera = dxcam.create() self.camera.start(target_fps=60, video_mode=True) except Exception as fallback_e: print(f"降级模式初始化失败: {str(fallback_e)}") self.camera = None def _init_mouse_listener(self): """初始化鼠标监听器""" self.mouse_listener = mouse.Listener( on_click=self.on_mouse_click # 监听鼠标点击事件 ) self.mouse_listener.daemon = True self.mouse_listener.start() def on_mouse_click(self, x, y, button, pressed): """处理鼠标点击事件""" try: if button == mouse.Button.right: # 监听鼠标右键 with self.button_lock: self.right_button_pressed = pressed # 更新状态 if pressed: # 当右键按下时重置PID self.pid_controller.reset() except Exception as e: print(f"鼠标事件处理错误: {str(e)}") def calculate_fov_movement(self, dx, dy): """基于FOV算法计算鼠标移动量""" # 计算屏幕对角线长度 screen_diagonal = (self.screen_width ** 2 + self.screen_height ** 2) ** 0.5 # 计算垂直FOV aspect_ratio = self.screen_width / self.screen_height fov_vertical = self.fov_horizontal / aspect_ratio # 计算每像素对应角度 angle_per_pixel_x = self.fov_horizontal / self.screen_width angle_per_pixel_y = fov_vertical / self.screen_height # 计算角度偏移 angle_offset_x = dx * angle_per_pixel_x angle_offset_y = dy * angle_per_pixel_y # 转换为鼠标移动量 move_x = (angle_offset_x / 360) * self.mouse_dpi move_y = (angle_offset_y / 360) * self.mouse_dpi return move_x, move_y def move_mouse_to_target(self): """移动鼠标使准心对准目标点""" if not self.target_offset: return try: # 获取目标点与屏幕中心的偏移量 with self.offset_lock: dx, dy = self.target_offset # 使用FOV算法计算鼠标移动量 move_x, move_y = self.calculate_fov_movement(dx, dy) # 使用PID控制器平滑移动 pid_output = self.pid_controller.update((move_x, move_y)) move_x_pid, move_y_pid = pid_output # 使用罗技API移动鼠标 if move_x_pid != 0 or move_y_pid != 0: logitech.lg.mouse_xy(int(move_x_pid), int(move_y_pid)) except Exception as e: print(f"移动鼠标时出错: {str(e)}") def run(self, frame_queue): """主检测循环""" while not self.stop_event.is_set(): try: # 截图 grab_start = time.perf_counter() screenshot = self._grab_screenshot() grab_time = (time.perf_counter() - grab_start) * 1000 # ms if screenshot is None: time.sleep(0.001) continue # 推理 inference_start = time.perf_counter() results = self._inference(screenshot) inference_time = (time.perf_counter() - inference_start) * 1000 # ms # 处理检测结果 target_info, closest_target_relative, closest_offset = self._process_detection_results(results) # 更新目标信息 self._update_target_info(target_info, closest_offset) # 移动鼠标 self._move_mouse_if_needed() # 可视化处理 annotated_frame = self._visualize_results(results, closest_target_relative) if frame_queue else None # 放入队列 if frame_queue: try: frame_queue.put( (annotated_frame, len(target_info), inference_time, grab_time, target_info), timeout=0.01 ) except queue.Full: pass except Exception as e: print(f"检测循环异常: {str(e)}") traceback.print_exc() self._reset_camera() time.sleep(0.5) def _grab_screenshot(self): """安全获取截图""" with self.camera_lock: if self.camera: return self.camera.grab() return None def _inference(self, screenshot): """执行模型推理""" return self.model.predict( screenshot, conf=self.detection_conf_thres, iou=self.detection_iou_thres, classes=self.detection_classes, device=self.device, verbose=False ) def _process_detection_results(self, results): """处理检测结果""" target_info = [] min_distance = float('inf') closest_target_relative = None closest_target_absolute = None closest_offset = None for box in results[0].boxes: # 获取边界框坐标 x1, y1, x2, y2 = map(int, box.xyxy[0]) # 计算绝对坐标 x1_abs = x1 + self.region[0] y1_abs = y1 + self.region[1] x2_abs = x2 + self.region[0] y2_abs = y2 + self.region[1] # 计算边界框尺寸 width = x2_abs - x1_abs height = y2_abs - y1_abs # 应用偏移百分比计算目标点 target_x = x1_abs + int(width * (self.target_offset_x_percent / 100)) target_y = y1_abs + int(height * (self.target_offset_y_percent / 100)) # 计算偏移量 dx = target_x - self.screen_center[0] dy = target_y - self.screen_center[1] distance = (dx ** 2 + dy ** 2) ** 0.5 # 更新最近目标 if distance < min_distance: min_distance = distance # 计算相对坐标(用于可视化) closest_target_relative = ( x1 + int(width * (self.target_offset_x_percent / 100)), y1 + int(height * (self.target_offset_y_percent / 100)) ) closest_target_absolute = (target_x, target_y) closest_offset = (dx, dy) # 保存目标信息 class_id = int(box.cls) class_name = self.model.names[class_id] target_info.append(f"{class_name}:{x1_abs},{y1_abs},{x2_abs},{y2_abs}") # 应用EMA平滑处理目标偏移量[1](@ref) if closest_offset: dx, dy = closest_offset prev_dx, prev_dy = self.smoothed_offset smoothed_dx = self.smooth_alpha * dx + (1 - self.smooth_alpha) * prev_dx smoothed_dy = self.smooth_alpha * dy + (1 - self.smooth_alpha) * prev_dy closest_offset = (smoothed_dx, smoothed_dy) self.smoothed_offset = closest_offset return target_info, closest_target_relative, closest_offset def _update_target_info(self, target_info, closest_offset): """更新目标信息""" # 检查目标信息是否有变化 if target_info != self.previous_target_info: self.previous_target_info = target_info.copy() print(f"{len(target_info)}|{'|'.join(target_info)}") # 更新目标偏移量 with self.offset_lock: self.target_offset = closest_offset def _visualize_results(self, results, closest_target): """可视化处理结果""" frame = results[0].plot( line_width=self.visualization_line_width, font_size=self.visualization_font_scale, conf=self.visualization_show_conf ) # 绘制最近目标 if closest_target: # 绘制目标中心点 cv2.circle( frame, (int(closest_target[0]), int(closest_target[1])), 3, (0, 0, 255), -1 ) # 计算屏幕中心在截图区域内的相对坐标 screen_center_x = self.screen_center[0] - self.region[0] screen_center_y = self.screen_center[1] - self.region[1] # 绘制中心到目标的连线 cv2.line( frame, (int(screen_center_x), int(screen_center_y)), (int(closest_target[0]), int(closest_target[1])), (0, 255, 0), 1 ) return frame def _move_mouse_if_needed(self): """如果需要则移动鼠标""" with self.button_lock: if self.right_button_pressed and self.target_offset: # 使用right_button_pressed self.move_mouse_to_target() def _reset_camera(self): """重置相机""" print("正在重置相机...") try: self._init_camera() except Exception as e: print(f"相机重置失败: {str(e)}") traceback.print_exc() def stop(self): """安全停止检测器""" self.stop_event.set() self._safe_stop() if hasattr(self, 'mouse_listener') and self.mouse_listener.running: # 改为停止鼠标监听器 self.mouse_listener.stop() def _safe_stop(self): """同步释放资源""" print("正在安全停止相机...") try: with self.camera_lock: if self.camera: self.camera.stop() print("相机已停止") except Exception as e: print(f"停止相机时发生错误: {str(e)}") print("屏幕检测器已停止") class App: def __init__(self, root, detector): self.root = root self.detector = detector self.root.title("DXcam Detection") self.root.geometry(f"{detector.region[2] - detector.region[0]}x{detector.region[3] - detector.region[1] + 50}") self.root.wm_attributes('-topmost', 1) # 界面组件 self.canvas = tk.Canvas(root, highlightthickness=0) self.canvas.pack(fill=tk.BOTH, expand=True) # 性能监控队列 self.frame_queue = queue.Queue(maxsize=3) # 控制面板 self.control_frame = tk.Frame(root) self.control_frame.pack(side=tk.BOTTOM, fill=tk.X) # 性能信息显示 self.info_label = tk.Label(self.control_frame, text="初始化中...", font=("Consolas", 10)) self.info_label.pack(side=tk.TOP, fill=tk.X, padx=5) # 按钮区域 self.toggle_btn = tk.Button(self.control_frame, text="切换可视化", command=self.toggle_visualization) self.toggle_btn.pack(side=tk.LEFT, padx=5) self.settings_btn = tk.Button(self.control_frame, text="设置", command=self.open_settings) self.settings_btn.pack(side=tk.LEFT, padx=5) # 鼠标右键状态显示(替换Shift状态) self.button_status = tk.Label(self.control_frame, text="鼠标右键状态: 未按下", fg="red", font=("Consolas", 10)) self.button_status.pack(side=tk.LEFT, padx=10) # 重命名为button_status # 启动检测线程 self.detection_thread = threading.Thread(target=self.detector.run, args=(self.frame_queue,)) self.detection_thread.daemon = True self.detection_thread.start() # 界面更新 self.visualization_enabled = True self.update_interval = 1 # 1ms更新一次界面 self.update_image() # 窗口关闭处理 self.root.protocol("WM_DELETE_WINDOW", self.safe_exit) # 添加鼠标事件绑定 self.root.bind('<Button-3>', self.update_button_status) # 绑定鼠标右键按下 self.root.bind('<ButtonRelease-3>', self.update_button_status) # 绑定鼠标右键释放 def update_button_status(self, event=None): """更新鼠标右键状态显示""" with self.detector.button_lock: if self.detector.right_button_pressed: self.button_status.config(text="鼠标右键状态: 按下", fg="green") else: self.button_status.config(text="鼠标右键状态: 未按下", fg="red") def toggle_visualization(self): """切换可视化状态""" self.visualization_enabled = not self.visualization_enabled state = "启用" if self.visualization_enabled else "禁用" self.info_label.config(text=f"可视化状态: {state}") self.canvas.delete("all") if not self.visualization_enabled: self.canvas.config(bg="black") def open_settings(self): """打开设置窗口""" SettingsWindow(self.root, self.detector.cfg) def display_target_info(self, target_info): """在画布上显示目标信息""" # 显示标题 title = "目标类别与坐标" self.canvas.create_text(10, 10, text=title, anchor=tk.NW, fill="#00FF00", font=("Consolas", 11, "bold")) # 显示目标信息 y_offset = 40 line_height = 20 if target_info: for i, data in enumerate(target_info): try: parts = data.split(":", 1) if len(parts) == 2: class_name, coords_str = parts coords = list(map(int, coords_str.split(','))) if len(coords) == 4: display_text = f"{class_name}: [{coords[0]}, {coords[1]}, {coords[2]}, {coords[3]}]" else: display_text = f"坐标格式错误: {data}" else: display_text = f"数据格式错误: {data}" except: display_text = f"解析错误: {data}" self.canvas.create_text(15, y_offset, text=display_text, anchor=tk.NW, fill="#00FFFF", font=("Consolas", 10)) y_offset += line_height else: self.canvas.create_text(15, y_offset, text="无检测目标", anchor=tk.NW, fill="#FF0000", font=("Consolas", 10)) def update_image(self): """更新界面显示""" try: # 获取最新数据 latest_data = None while not self.frame_queue.empty(): latest_data = self.frame_queue.get_nowait() if latest_data: # 解包数据 frame, targets_count, inference_time, grab_time, target_info = latest_data # 单位转换 inference_sec = inference_time / 1000 grab_sec = grab_time / 1000 # 更新显示 if self.visualization_enabled and frame is not None: # 显示图像 img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(img) self.tk_image = ImageTk.PhotoImage(image=img) self.canvas.delete("all") self.canvas.create_image(0, 0, image=self.tk_image, anchor=tk.NW) else: # 显示坐标文本 self.canvas.delete("all") self.display_target_info(target_info) # 更新性能信息 info_text = (f"目标: {targets_count} | " f"推理: {inference_sec:.3f}s | " f"截图: {grab_sec:.3f}s") self.info_label.config(text=info_text) except Exception as e: print(f"更新图像时出错: {str(e)}") finally: # 更新鼠标右键状态 self.update_button_status() # 调度下一次更新 self.root.after(self.update_interval, self.update_image) def safe_exit(self): """安全退出程序""" self.detector.stop() self.root.after(100, self.root.destroy) class SettingsWindow(tk.Toplevel): def __init__(self, parent, config): super().__init__(parent) self.title("设置") self.geometry("400x500") self.config = config self.transient(parent) self.grab_set() self.create_widgets() def create_widgets(self): """创建设置窗口界面""" notebook = ttk.Notebook(self) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 模型设置 model_frame = ttk.Frame(notebook) notebook.add(model_frame, text="模型设置") self.create_model_settings(model_frame) # 屏幕设置 screen_frame = ttk.Frame(notebook) notebook.add(screen_frame, text="屏幕设置") self.create_screen_settings(screen_frame) # 检测设置 detection_frame = ttk.Frame(notebook) notebook.add(detection_frame, text="检测设置") self.create_detection_settings(detection_frame) # 移动设置 move_frame = ttk.Frame(notebook) notebook.add(move_frame, text="移动设置") self.create_move_settings(move_frame) # 目标点设置 target_frame = ttk.Frame(notebook) notebook.add(target_frame, text="目标点设置") self.create_target_settings(target_frame) # 按钮区域 btn_frame = ttk.Frame(self) btn_frame.pack(fill=tk.X, padx=10, pady=10) save_btn = tk.Button(btn_frame, text="保存配置", command=self.save_config) save_btn.pack(side=tk.RIGHT, padx=5) cancel_btn = tk.Button(btn_frame, text="取消", command=self.destroy) cancel_btn.pack(side=tk.RIGHT, padx=5) def create_model_settings(self, parent): """创建模型设置页面""" # 获取基础路径 if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.dirname(os.path.abspath(__file__)) # 获取模型文件列表 models_dir = os.path.join(base_path, 'models') model_files = [] if os.path.exists(models_dir): model_files = glob.glob(os.path.join(models_dir, '*.pt')) # 处理模型显示名称 model_display_names = [os.path.basename(f) for f in model_files] if model_files else ["未找到模型文件"] self.model_name_to_path = {os.path.basename(f): f for f in model_files} # 当前配置的模型处理 current_model_path = self.config['model_path'] current_model_name = os.path.basename(current_model_path) # 确保当前模型在列表中 if current_model_name not in model_display_names: model_display_names.append(current_model_name) self.model_name_to_path[current_model_name] = current_model_path # 创建UI组件 ttk.Label(parent, text="选择模型:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) self.model_name = tk.StringVar(value=current_model_name) model_combo = ttk.Combobox(parent, textvariable=self.model_name, state="readonly", width=30) model_combo['values'] = model_display_names model_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="运行设备:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.device_var = tk.StringVar(value=self.config['model_device']) device_combo = ttk.Combobox(parent, textvariable=self.device_var, state="readonly", width=30) device_combo['values'] = ('auto', 'cuda', 'cpu') device_combo.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) def create_screen_settings(self, parent): """创建屏幕设置页面""" ttk.Label(parent, text="显示器编号:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) self.monitor_var = tk.StringVar(value=self.config.get('screen_monitor', '0')) ttk.Entry(parent, textvariable=self.monitor_var, width=10).grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="截屏尺寸:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.target_size_var = tk.StringVar(value=self.config['screen_target_size']) ttk.Entry(parent, textvariable=self.target_size_var, width=10).grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) def create_detection_settings(self, parent): """创建检测设置页面""" ttk.Label(parent, text="置信度阈值:").grid(row=0, column=0, padx=5, pady=5, sticky=极客时间 self.conf_thres_var = tk.DoubleVar(value=float(self.config['detection_conf_thres'])) conf_scale = ttk.Scale(parent, from_=0.1, to=1.0, variable=self.conf_thres_var, orient=tk.HORIZONTAL, length=200) conf_scale.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) self.conf_thres_display = tk.StringVar() self.conf_thres_display.set(f"{self.conf_thres_var.get():.2f}") ttk.Label(parent, textvariable=self.conf_thres_display).grid(row=0, column=2, padx=5, pady=5) self.conf_thres_var.trace_add("write", lambda *args: self.conf_thres_display.set(f"{self.conf_thres_var.get():.2f}")) ttk.Label(parent, text="IOU阈值:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.iou_thres_var = tk.DoubleVar(value=float(self.config['detection_iou_thres'])) iou_scale = ttk.Scale(parent, from_=0.1, to=1.0, variable=self.iou_thres_var, orient=tk.HORIZONTAL, length=200) i极客时间 self.iou_thres_display.set(f"{self.iou_thres_var.get():.2f}") ttk.Label(parent, textvariable=self.iou_thres_display).grid(row=1, column=2, padx=5, pady=5) self.iou_thres_var.trace_add("write", lambda *args: self.iou_thres_display.set(f"{self.iou_thres_var.get():.2f}")) ttk.Label(parent, text="检测类别:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W) self.classes_var = tk.StringVar(value=self.config['detection_classes']) ttk.Entry(parent, textvariable=self.classes_var, width=20).grid(row=2, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="(逗号分隔)").grid(row=2, column=2, padx=5, pady=5, sticky=tk.W) def create_move_settings(self, parent): """创建移动设置页面""" ttk.Label(parent, text="横向FOV(度):").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) self.fov_horizontal_var = tk.StringVar(value=self.config.get('move_fov_horizontal', '90')) fov_entry = ttk.Entry(parent, textvariable=self.fov_horizontal_var, width=10) fov_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="鼠标DPI:").grid(row=1, column=0, padx极客时间 self.mouse_dpi_var = tk.StringVar(value=self.config.get('move_mouse_dpi', '400')) dpi_entry = ttk.Entry(parent, textvariable=self.mouse_dpi_var, width=10) dpi_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) # PID参数设置 ttk.Label(parent, text="PID参数设置", font=("Arial", 10, "bold")).grid(row=2, column=0, columnspan=3, pady=10, sticky=tk.W) ttk.Label(parent, text="比例系数(P):").grid(row=3, column=0, padx=5, pady=5, sticky=tk.W) self.pid_kp_var = tk.StringVar(value=self.config.get('pid_kp', '0.5')) kp_entry = ttk.Entry(parent, textvariable=self.pid_kp_var, width=10) kp_entry.grid(row=3, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="积分系数(I):").grid(row=4, column=0, padx=5, pady=5, sticky=tk.W) self.pid_ki_var = tk.StringVar(value=self.config.get('pid_ki', '0.0')) ki_entry = ttk.Entry(parent, textvariable=self.pid_ki_var, width=10) ki_entry.grid(row=4, column=1, padx=5, pady=5, sticky=tk.W) ttk.Label(parent, text="微分系数(D):").grid(row=5, column=0, padx=5, pady=5, sticky=tk.W) self.pid_kd_var = tk.StringVar(value=self.config.get('pid_kd', '0.1')) kd_entry = ttk.Entry(parent, textvariable=self.pid_kd_var, width=10) kd_entry.grid(row=5, column=1, padx=5, pady=5, sticky=tk.W) # 平滑系数设置[1](@ref) ttk.Label(parent, text="平滑系数(α):").grid(row=6, column=0, padx=5, pady=5, sticky=tk.W) self.smooth_alpha_var = tk.DoubleVar(value=float(self.config.get('smooth_alpha', '0.4'))) smooth_scale = ttk.Scale(parent, from_=0.1, to=0.9, variable=self.smooth_alpha_var, orient=tk.HORIZONTAL, length=200) smooth_scale.grid(row=6, column=1, padx=5, pady=5, sticky=tk.W) self.smooth_alpha_display = tk.StringVar() self.smooth_alpha_display.set(f"{self.smooth_alpha_var.get():.2f}") ttk.Label(parent, textvariable=self.smooth_alpha_display).grid(row=6, column=2, padx=5, pady=5) self.smooth_alpha_var.trace_add("write", lambda *args: self.smooth_alpha_display.set(f"{self.smooth_alpha_var.get():.2f}")) def create_target_settings(self, parent): """创建目标点设置页面 (新增)""" ttk.Label(parent, text="目标点偏移设置", font=("Arial", 10, "bold")).grid( row=0, column=0, columnspan=3, pady=10, sticky=tk.W ) # X轴偏移设置 ttk.Label(parent, text="X轴偏移(%):").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) self.target_offset_x_var = tk.DoubleVar(value=float(self.config.get('target_offset_x', '50'))) offset_x_scale = ttk.Scale(parent, from_=0, to=100, variable=self.target_offset_x_var, orient=tk.HORIZONTAL, length=200) offset_x_scale.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W) self.offset_x_display = tk.StringVar(value=f"{self.target_offset_x_var.get():.0f}") ttk.Label(parent, textvariable=self.offset_x_display).grid(row=1, column=2, padx=5, pady=5) self.target_offset_x_var.trace_add("write", lambda *args: self.offset_x_display.set( f"{self.target_offset_x_var.get():.0f}")) # Y轴偏移设置 ttk.Label(parent, text="Y轴偏移(%):").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W) self.target_offset_y_var = tk.DoubleVar(value=float(self.config.get('target_offset_y', '50'))) offset_y_scale = ttk.Scale(parent, from_=0, to=100, variable=self.target_offset_y_var, orient=tk.HORIZONTAL, length=200) offset_y_scale.grid(row=2, column=1, padx=5, pady=5, sticky=tk.W) self.offset_y_display = tk.StringVar(value=f"{self.target_offset_y_var.get():.0f}") ttk.Label(parent, textvariable=self.offset_y_display).grid(row=2, column=2, padx=5, pady=5) self.target_offset_y_var.trace_add("write", lambda *args: self.offset_y_display.set( f"{self.target_offset_y_var.get():.0f}")) # 添加说明标签 ttk.Label(parent, text="(0% = 左上角, 50% = 中心, 100% = 右下角)").grid( row=3, column=0, columnspan=3, padx=5, pady=5, sticky=tk.W ) def save_config(self): """保存配置到文件""" try: model_name = self.model_name.get() model_path = self.model_name_to_path.get(model_name, model_name) self.config['model_path'] = model_path self.config['model_device'] = self.device_var.get() self.config['screen_monitor'] = self.monitor_var.get() self.config['screen_target_size'] = self.target_size_var.get() self.config['detection_conf_thres'] = str(self.conf_thres_var.get()) self.config['detection_iou_thres'] = str(self.iou_thres_var.get()) self.config['detection_classes'] = self.classes_var.get() # 保存移动设置 self.config['move_fov_horizontal'] = self.fov_horizontal_var.get() self.config['move_mouse_dpi'] = self.mouse_dpi_var.get() # 保存PID参数 self.config['pid_kp'] = self.pid_kp_var.get() self.config['pid_ki'] = self.pid_ki_var.get() self.config['pid_kd'] = self.pid_kd_var.get() # 保存目标点偏移设置 self.config['target_offset_x'] = str(self.target_offset_x_var.get()) self.config['target_offset_y'] = str(self.target_offset_y_var.get()) # 保存平滑系数[1](@ref) self.config['smooth_alpha'] = str(self.smooth_alpha_var.get()) # 保存为TXT格式 with open('detection_config.txt', 'w', encoding='utf-8') as f: for key, value in self.config.items(): f.write(f"{key} = {value}\n") messagebox.showinfo("成功", "配置已保存!重启后生效") self.destroy() except Exception as e: messagebox.showerror("错误", f"保存配置失败: {str(e)}") if __name__ == "__main__": detector = ScreenDetector('detection_config.txt') print(f"\nDXcam检测器初始化完成 | 设备: {detector.device.upper()}") root = tk.Tk() app = App(root, detector) root.mainloop() 目标检测点抖抖动,我希望用最简单最高效的办法处理,并且绘制处理的点是处理后的,请严格按照我的代码风格给我优化后的完整代码
07-04
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值