更改,精简下位机代码
This commit is contained in:
		
							parent
							
								
									d7a816d993
								
							
						
					
					
						commit
						dc381b81e5
					
				
							
								
								
									
										93
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								README.md
									
									
									
									
									
								
							| @ -9,7 +9,25 @@ | |||||||
| 
 | 
 | ||||||
| ### 目录介绍 | ### 目录介绍 | ||||||
| + src/:下位机源码 | + src/:下位机源码 | ||||||
| + start_robot_client:启动机械臂客户端脚本 | + + arm.py   机械臂控制脚本 | ||||||
|  | + + arm3d_drag_demo.py 机械臂3D拖拽控制脚本 | ||||||
|  | + + robot_client.py 机械臂客户端脚本 | ||||||
|  | + + test_motors.py 电机测试脚本 | ||||||
|  | + + ZDT_Servo.py ZDT步进电机驱动脚本 | ||||||
|  | + start_robot_client.sh:启动机械臂客户端脚本 | ||||||
|  | + pi_video_client.py:树莓派视频流处理脚本 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | **注意:**启动客户端和视频传输之前需要先启动服务端,请参考[服务端部署文档](https://github.com/ricklentz/checkhand/blob/main/README_server.md) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 连接配置以参数传入的方式启动客户端,可以在启动脚本中修改参数 | ||||||
|  | 
 | ||||||
|  | ```sh | ||||||
|  | SERVER="http://192.168.114.26:5000" | ||||||
|  | MOCK="" | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ### 方式一:使用启动脚本(推荐) | ### 方式一:使用启动脚本(推荐) | ||||||
| @ -21,76 +39,3 @@ | |||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| ### 方式二:手动启动 |  | ||||||
| 
 |  | ||||||
| #### 1. 创建虚拟环境并安装依赖 |  | ||||||
| 
 |  | ||||||
| ```bash |  | ||||||
| python3 -m venv venv |  | ||||||
| source venv/bin/activate  # Linux/Mac |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| #### 2. 启动Web服务器 |  | ||||||
| 
 |  | ||||||
| ```bash |  | ||||||
| python run_web_service.py |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| #### 3. 启动机械臂客户端( |  | ||||||
| 
 |  | ||||||
| ```bash |  | ||||||
| cd src |  | ||||||
| python robot_client.py --mock |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### 3. 访问Web界面 |  | ||||||
| 
 |  | ||||||
| 打开浏览器访问: `http://localhost:5000` |  | ||||||
| 
 |  | ||||||
| ## 启动脚本选项 |  | ||||||
| 
 |  | ||||||
| ### Web服务器启动选项: |  | ||||||
| ```bash |  | ||||||
| ./start_service.sh --help                    # 查看帮助 |  | ||||||
| ./start_service.sh                           # 基本启动 |  | ||||||
| ./start_service.sh --host 0.0.0.0 --port 8080  # 自定义地址和端口 |  | ||||||
| ./start_service.sh --debug                   # 调试模式 |  | ||||||
| ./start_service.sh --test-video data/videos/test.mp4  # 本地视频测试 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### 机械臂客户端启动选项: |  | ||||||
| ```bash |  | ||||||
| ./start_robot_client.sh --help               # 查看帮助 |  | ||||||
| ./start_robot_client.sh                      # 基本启动(模拟模式) |  | ||||||
| ./start_robot_client.sh --server http://192.168.1.100:5000  # 连接远程服务器 |  | ||||||
| ./start_robot_client.sh --real               # 真实机械臂模式 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## 功能特性 |  | ||||||
| 
 |  | ||||||
| - ✅ 使用MediaPipe进行实时手部检测   |  | ||||||
| - ✅ WebSocket实时通信 |  | ||||||
| - ✅ 3D坐标计算(X、Y、Z轴角度) |  | ||||||
| - ✅ Web预览界面 |  | ||||||
| - ✅ 机械臂控制接口 |  | ||||||
| - ✅ 本地视频测试支持 |  | ||||||
| 
 |  | ||||||
| ## 系统架构 |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| 视频客户端 → Web服务器 → MediaPipe → 3D坐标计算 → 客户端(Web预览/机械臂) |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## 控制信号格式 |  | ||||||
| 
 |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|   "x_angle": 90.0,      // X轴角度 (0-180°) |  | ||||||
|   "y_angle": 90.0,      // Y轴角度 (0-180°)   |  | ||||||
|   "z_angle": 90.0,      // Z轴角度 (0-180°) |  | ||||||
|   "grip": 0,            // 抓取状态 (0=松开, 1=抓取) |  | ||||||
|   "action": "none",     // 当前动作 |  | ||||||
|   "speed": 5            // 移动速度 (1-10) |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
							
								
								
									
										43
									
								
								pi_video_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								pi_video_client.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | 
 | ||||||
|  | import base64 | ||||||
|  | import socketio | ||||||
|  | import time | ||||||
|  | from picamera2 import Picamera2 | ||||||
|  | import numpy as np | ||||||
|  | 
 | ||||||
|  | SERVER_URL = 'http://192.168.114.26:5000'  # 修改为你的服务端地址和端口 | ||||||
|  | 
 | ||||||
|  | sio = socketio.Client() | ||||||
|  | 
 | ||||||
|  | @sio.event | ||||||
|  | def connect(): | ||||||
|  |     print('已连接到服务端') | ||||||
|  |     sio.emit('register_client', {'type': 'pi_camera'}) | ||||||
|  | 
 | ||||||
|  | @sio.event | ||||||
|  | def disconnect(): | ||||||
|  |     print('与服务端断开连接') | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     sio.connect(SERVER_URL) | ||||||
|  |     picam = Picamera2() | ||||||
|  |     config = picam.create_video_configuration(main={'size': (640, 480)}) | ||||||
|  |     picam.configure(config) | ||||||
|  |     picam.start() | ||||||
|  |     time.sleep(1)  # 等待摄像头启动 | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         while True: | ||||||
|  |             frame = picam.capture_array() | ||||||
|  |             # 转为JPEG并编码为base64 | ||||||
|  |             import cv2 | ||||||
|  |             _, buffer = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) | ||||||
|  |             jpg_as_text = base64.b64encode(buffer).decode('utf-8') | ||||||
|  |             sio.emit('video_frame', {'frame': jpg_as_text}) | ||||||
|  |             time.sleep(1/30)  # 控制帧率 | ||||||
|  |     finally: | ||||||
|  |         picam.close() | ||||||
|  |         sio.disconnect() | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     main() | ||||||
| @ -1,316 +0,0 @@ | |||||||
| import cv2 |  | ||||||
| import numpy as np |  | ||||||
| import math |  | ||||||
| import mediapipe as mp |  | ||||||
| 
 |  | ||||||
| # 初始化MediaPipe Hands |  | ||||||
| mp_hands = mp.solutions.hands |  | ||||||
| mp_drawing = mp.solutions.drawing_utils |  | ||||||
| mp_drawing_styles = mp.solutions.drawing_styles |  | ||||||
| 
 |  | ||||||
| # 加载MediaPipe手部检测模型 |  | ||||||
| def load_mediapipe_model(max_num_hands=1, min_detection_confidence=0.5, min_tracking_confidence=0.5): |  | ||||||
|     """ |  | ||||||
|     加载MediaPipe手部检测模型 |  | ||||||
|      |  | ||||||
|     参数: |  | ||||||
|     max_num_hands: 最大检测手的数量 |  | ||||||
|     min_detection_confidence: 最小检测置信度 |  | ||||||
|     min_tracking_confidence: 最小跟踪置信度 |  | ||||||
|      |  | ||||||
|     返回: |  | ||||||
|     MediaPipe Hands对象 |  | ||||||
|     """ |  | ||||||
|     hands = mp_hands.Hands( |  | ||||||
|         static_image_mode=False, |  | ||||||
|         max_num_hands=max_num_hands, |  | ||||||
|         min_detection_confidence=min_detection_confidence, |  | ||||||
|         min_tracking_confidence=min_tracking_confidence |  | ||||||
|     ) |  | ||||||
|     return hands |  | ||||||
| 
 |  | ||||||
| # 处理帧并返回三轴控制信号 |  | ||||||
| def process_frame_3d(frame, hands_model, prev_hand_data=None): |  | ||||||
|     """ |  | ||||||
|     处理视频帧,检测手部并计算三轴控制信号 |  | ||||||
|      |  | ||||||
|     参数: |  | ||||||
|     frame: 输入的BGR格式帧 |  | ||||||
|     hands_model: MediaPipe Hands模型 |  | ||||||
|     prev_hand_data: 前一帧的手部数据 |  | ||||||
|      |  | ||||||
|     返回: |  | ||||||
|     三轴控制信号字典和当前手部数据 |  | ||||||
|     """ |  | ||||||
|     # 获取帧尺寸 |  | ||||||
|     frame_height, frame_width = frame.shape[:2] |  | ||||||
|      |  | ||||||
|     # 初始化三轴控制信号 |  | ||||||
|     control_signal = { |  | ||||||
|         "x_angle": 90,     # 水平角度 (左右) |  | ||||||
|         "y_angle": 90,     # 垂直角度 (上下) |  | ||||||
|         "z_angle": 90,     # 深度角度 (前后) |  | ||||||
|         "grip": 0,         # 抓取状态 (0=松开, 1=抓取) |  | ||||||
|         "action": "none",  # 当前动作 |  | ||||||
|         "speed": 5         # 移动速度 (1-10) |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     # MediaPipe需要RGB格式的图像 |  | ||||||
|     rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) |  | ||||||
|      |  | ||||||
|     # 处理图像 |  | ||||||
|     results = hands_model.process(rgb_frame) |  | ||||||
|      |  | ||||||
|     # 如果没有检测到手,返回默认控制信号和前一帧数据 |  | ||||||
|     if not results.multi_hand_landmarks: |  | ||||||
|         return control_signal, prev_hand_data |  | ||||||
|      |  | ||||||
|     # 获取第一只手的关键点 |  | ||||||
|     hand_landmarks = results.multi_hand_landmarks[0] |  | ||||||
|      |  | ||||||
|     # 计算手部中心点 (使用所有关键点的平均位置) |  | ||||||
|     center_x, center_y, center_z = 0, 0, 0 |  | ||||||
|     for landmark in hand_landmarks.landmark: |  | ||||||
|         center_x += landmark.x |  | ||||||
|         center_y += landmark.y |  | ||||||
|         center_z += landmark.z |  | ||||||
|      |  | ||||||
|     num_landmarks = len(hand_landmarks.landmark) |  | ||||||
|     center_x /= num_landmarks |  | ||||||
|     center_y /= num_landmarks |  | ||||||
|     center_z /= num_landmarks |  | ||||||
|      |  | ||||||
|     # 转换为像素坐标 |  | ||||||
|     hand_center = { |  | ||||||
|         'x': center_x * frame_width, |  | ||||||
|         'y': center_y * frame_height, |  | ||||||
|         'z': center_z  # 保留归一化的z值 |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     # 获取更精确的手部姿态信息 |  | ||||||
|     wrist = hand_landmarks.landmark[mp_hands.HandLandmark.WRIST] |  | ||||||
|     index_finger_tip = hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP] |  | ||||||
|     pinky_tip = hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_TIP] |  | ||||||
|     thumb_tip = hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_TIP] |  | ||||||
|      |  | ||||||
|     # 计算手部方向向量 (从手腕到手中心) |  | ||||||
|     direction_vector = { |  | ||||||
|         'x': center_x - wrist.x, |  | ||||||
|         'y': center_y - wrist.y, |  | ||||||
|         'z': center_z - wrist.z |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     # 计算屏幕中心点 |  | ||||||
|     frame_center = { |  | ||||||
|         'x': frame_width / 2, |  | ||||||
|         'y': frame_height / 2, |  | ||||||
|         'z': 0  # 参考点 |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     # 计算手部相对于屏幕中心的位移 |  | ||||||
|     dx = hand_center['x'] - frame_center['x'] |  | ||||||
|     dy = hand_center['y'] - frame_center['y'] |  | ||||||
|     dz = hand_center['z'] * 10  # 缩放z值使其更明显 |  | ||||||
|      |  | ||||||
|     # 转换为机械臂的三个轴的角度 |  | ||||||
|     # X轴角度 (左右移动) - 范围0-180度,中间是90度 |  | ||||||
|     x_angle = map_to_angle(dx, frame_width / 2) |  | ||||||
|      |  | ||||||
|     # Y轴角度 (上下移动) - 范围0-180度,中间是90度 |  | ||||||
|     y_angle = map_to_angle(-dy, frame_height / 2)  # 注意负号:图像y轴向下,但机械臂y轴向上 |  | ||||||
|      |  | ||||||
|     # Z轴角度 (前后移动) - 范围0-180度,中间是90度 |  | ||||||
|     # Z值需要正确映射,这里我们假设z值范围在-0.5至0.5之间 |  | ||||||
|     # 越靠近摄像头z值越小(更负),越远离摄像头z值越大(更正) |  | ||||||
|     z_angle = 90 + (dz * 90)  # 将z值映射到0-180范围 |  | ||||||
|     z_angle = max(0, min(180, z_angle))  # 确保在有效范围内 |  | ||||||
|      |  | ||||||
|     # 检测抓取动作 (拇指和食指距离) |  | ||||||
|     pinch_distance = calculate_3d_distance( |  | ||||||
|         thumb_tip.x, thumb_tip.y, thumb_tip.z, |  | ||||||
|         index_finger_tip.x, index_finger_tip.y, index_finger_tip.z |  | ||||||
|     ) |  | ||||||
|      |  | ||||||
|     # 如果拇指和食指距离小于阈值,认为是抓取状态 |  | ||||||
|     grip = 1 if pinch_distance < 0.05 else 0 |  | ||||||
|      |  | ||||||
|     # 检测手部动作 (如果有前一帧数据) |  | ||||||
|     action = "none" |  | ||||||
|     speed = 5  # 默认中等速度 |  | ||||||
|      |  | ||||||
|     if prev_hand_data: |  | ||||||
|         # 计算移动向量 |  | ||||||
|         move_x = hand_center['x'] - prev_hand_data['x'] |  | ||||||
|         move_y = hand_center['y'] - prev_hand_data['y'] |  | ||||||
|         move_z = hand_center['z'] - prev_hand_data['z'] |  | ||||||
|          |  | ||||||
|         # 计算移动距离 |  | ||||||
|         move_distance = math.sqrt(move_x**2 + move_y**2 + move_z**2) |  | ||||||
|          |  | ||||||
|         # 如果移动足够大,确定主要移动方向 |  | ||||||
|         if move_distance > 0.01: |  | ||||||
|             # 计算移动速度 (1-10范围) |  | ||||||
|             speed = min(10, max(1, int(move_distance * 200))) |  | ||||||
|              |  | ||||||
|             # 确定主导移动方向 |  | ||||||
|             abs_move = [abs(move_x), abs(move_y), abs(move_z)] |  | ||||||
|             max_move = max(abs_move) |  | ||||||
|              |  | ||||||
|             if max_move == abs_move[0]:  # X轴移动 |  | ||||||
|                 action = "right" if move_x > 0 else "left" |  | ||||||
|             elif max_move == abs_move[1]:  # Y轴移动 |  | ||||||
|                 action = "down" if move_y > 0 else "up" |  | ||||||
|             else:  # Z轴移动 |  | ||||||
|                 action = "forward" if move_z < 0 else "backward" |  | ||||||
|      |  | ||||||
|     # 更新控制信号 |  | ||||||
|     control_signal = { |  | ||||||
|         "x_angle": x_angle, |  | ||||||
|         "y_angle": y_angle, |  | ||||||
|         "z_angle": z_angle, |  | ||||||
|         "grip": grip, |  | ||||||
|         "action": action, |  | ||||||
|         "speed": speed |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     # 在帧上绘制手部关键点和连接线 |  | ||||||
|     mp_drawing.draw_landmarks( |  | ||||||
|         frame, |  | ||||||
|         hand_landmarks, |  | ||||||
|         mp_hands.HAND_CONNECTIONS, |  | ||||||
|         mp_drawing_styles.get_default_hand_landmarks_style(), |  | ||||||
|         mp_drawing_styles.get_default_hand_connections_style() |  | ||||||
|     ) |  | ||||||
|      |  | ||||||
|     # 绘制三轴控制指示器 |  | ||||||
|     draw_3d_control_visualization(frame, control_signal, hand_center) |  | ||||||
|      |  | ||||||
|     return control_signal, hand_center |  | ||||||
| 
 |  | ||||||
| def calculate_3d_distance(x1, y1, z1, x2, y2, z2): |  | ||||||
|     """计算3D空间中两点之间的欧几里得距离""" |  | ||||||
|     return math.sqrt((x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2) |  | ||||||
| 
 |  | ||||||
| def map_to_angle(value, max_value): |  | ||||||
|     """将-max_value到max_value范围内的值映射到0-180度范围的角度""" |  | ||||||
|     # 计算相对位置 (-1到1) |  | ||||||
|     relative_position = value / max_value |  | ||||||
|      |  | ||||||
|     # 映射到角度 (90度为中点) |  | ||||||
|     angle = 90 + (relative_position * 45)  # 使用45度作为最大偏移量 |  | ||||||
|      |  | ||||||
|     # 确保角度在有效范围内 |  | ||||||
|     return max(0, min(180, angle)) |  | ||||||
| 
 |  | ||||||
| def draw_3d_control_visualization(frame, control_signal, hand_center): |  | ||||||
|     """在帧上绘制三轴控制可视化""" |  | ||||||
|     height, width = frame.shape[:2] |  | ||||||
|      |  | ||||||
|     # 绘制坐标轴 |  | ||||||
|     origin_x, origin_y = width - 150, height - 150 |  | ||||||
|     axis_length = 100 |  | ||||||
|      |  | ||||||
|     # X轴 (红色) |  | ||||||
|     cv2.line(frame, (origin_x, origin_y), (origin_x + axis_length, origin_y), (0, 0, 255), 2) |  | ||||||
|     cv2.putText(frame, "X", (origin_x + axis_length + 5, origin_y + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) |  | ||||||
|      |  | ||||||
|     # Y轴 (绿色) |  | ||||||
|     cv2.line(frame, (origin_x, origin_y), (origin_x, origin_y - axis_length), (0, 255, 0), 2) |  | ||||||
|     cv2.putText(frame, "Y", (origin_x - 15, origin_y - axis_length - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) |  | ||||||
|      |  | ||||||
|     # Z轴 (蓝色) - 以45度角向右上方 |  | ||||||
|     z_x = int(origin_x + axis_length * 0.7) |  | ||||||
|     z_y = int(origin_y - axis_length * 0.7) |  | ||||||
|     cv2.line(frame, (origin_x, origin_y), (z_x, z_y), (255, 0, 0), 2) |  | ||||||
|     cv2.putText(frame, "Z", (z_x + 5, z_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2) |  | ||||||
|      |  | ||||||
|     # 绘制控制角度值 |  | ||||||
|     x_text = f"X: {control_signal['x_angle']:.1f}°" |  | ||||||
|     y_text = f"Y: {control_signal['y_angle']:.1f}°" |  | ||||||
|     z_text = f"Z: {control_signal['z_angle']:.1f}°" |  | ||||||
|     grip_text = f"抓取: {'开' if control_signal['grip'] == 0 else '关'}" |  | ||||||
|     action_text = f"动作: {control_signal['action']}" |  | ||||||
|     speed_text = f"速度: {control_signal['speed']}" |  | ||||||
|      |  | ||||||
|     cv2.putText(frame, x_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) |  | ||||||
|     cv2.putText(frame, y_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) |  | ||||||
|     cv2.putText(frame, z_text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2) |  | ||||||
|     cv2.putText(frame, grip_text, (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2) |  | ||||||
|     cv2.putText(frame, action_text, (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) |  | ||||||
|     cv2.putText(frame, speed_text, (10, 180), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 255), 2) |  | ||||||
|      |  | ||||||
|     # 绘制手部位置到屏幕中心的连接线 |  | ||||||
|     if hand_center: |  | ||||||
|         center_x, center_y = int(width/2), int(height/2) |  | ||||||
|         hand_x, hand_y = int(hand_center['x']), int(hand_center['y']) |  | ||||||
|          |  | ||||||
|         # 绘制屏幕中心点 |  | ||||||
|         cv2.circle(frame, (center_x, center_y), 5, (0, 0, 255), -1) |  | ||||||
|          |  | ||||||
|         # 绘制手部位置点 |  | ||||||
|         cv2.circle(frame, (hand_x, hand_y), 10, (0, 255, 0), -1) |  | ||||||
|          |  | ||||||
|         # 绘制连接线 |  | ||||||
|         cv2.line(frame, (center_x, center_y), (hand_x, hand_y), (255, 0, 0), 2) |  | ||||||
| 
 |  | ||||||
| def analyze_hand_gesture(hand_landmarks): |  | ||||||
|     """分析手势以确定机械臂控制模式""" |  | ||||||
|     # 获取指尖和手掌关键点 |  | ||||||
|     thumb_tip = hand_landmarks.landmark[mp_hands.HandLandmark.THUMB_TIP] |  | ||||||
|     index_tip = hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP] |  | ||||||
|     middle_tip = hand_landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP] |  | ||||||
|     ring_tip = hand_landmarks.landmark[mp_hands.HandLandmark.RING_FINGER_TIP] |  | ||||||
|     pinky_tip = hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_TIP] |  | ||||||
|      |  | ||||||
|     # 获取手掌底部关键点 |  | ||||||
|     wrist = hand_landmarks.landmark[mp_hands.HandLandmark.WRIST] |  | ||||||
|     index_mcp = hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_MCP] |  | ||||||
|     pinky_mcp = hand_landmarks.landmark[mp_hands.HandLandmark.PINKY_MCP] |  | ||||||
|      |  | ||||||
|     # 检测拳头 - 所有手指弯曲 |  | ||||||
|     fist = ( |  | ||||||
|         thumb_tip.y > index_mcp.y and |  | ||||||
|         index_tip.y > index_mcp.y and |  | ||||||
|         middle_tip.y > index_mcp.y and |  | ||||||
|         ring_tip.y > index_mcp.y and |  | ||||||
|         pinky_tip.y > pinky_mcp.y |  | ||||||
|     ) |  | ||||||
|      |  | ||||||
|     # 检测手掌打开 - 所有手指伸直 |  | ||||||
|     open_palm = ( |  | ||||||
|         thumb_tip.y < wrist.y and |  | ||||||
|         index_tip.y < index_mcp.y and |  | ||||||
|         middle_tip.y < index_mcp.y and |  | ||||||
|         ring_tip.y < index_mcp.y and |  | ||||||
|         pinky_tip.y < pinky_mcp.y |  | ||||||
|     ) |  | ||||||
|      |  | ||||||
|     # 检测指向手势 - 食指伸出,其他手指弯曲 |  | ||||||
|     pointing = ( |  | ||||||
|         index_tip.y < index_mcp.y and |  | ||||||
|         middle_tip.y > index_mcp.y and |  | ||||||
|         ring_tip.y > index_mcp.y and |  | ||||||
|         pinky_tip.y > pinky_mcp.y |  | ||||||
|     ) |  | ||||||
|      |  | ||||||
|     # 检测"OK"手势 - 拇指和食指形成圆圈,其他手指伸出 |  | ||||||
|     pinch_distance = calculate_3d_distance( |  | ||||||
|         thumb_tip.x, thumb_tip.y, thumb_tip.z, |  | ||||||
|         index_tip.x, index_tip.y, index_tip.z |  | ||||||
|     ) |  | ||||||
|     ok_gesture = (pinch_distance < 0.05 and  |  | ||||||
|                  middle_tip.y < index_mcp.y and |  | ||||||
|                  ring_tip.y < index_mcp.y and |  | ||||||
|                  pinky_tip.y < pinky_mcp.y) |  | ||||||
|      |  | ||||||
|     # 返回识别的手势 |  | ||||||
|     if fist: |  | ||||||
|         return "fist" |  | ||||||
|     elif open_palm: |  | ||||||
|         return "open_palm" |  | ||||||
|     elif pointing: |  | ||||||
|         return "pointing" |  | ||||||
|     elif ok_gesture: |  | ||||||
|         return "ok" |  | ||||||
|     else: |  | ||||||
|         return "unknown" |  | ||||||
| @ -1,168 +0,0 @@ | |||||||
| import cv2 |  | ||||||
| import numpy as np |  | ||||||
| import time |  | ||||||
| import os |  | ||||||
| 
 |  | ||||||
| # 导入我们的3D手部检测模块 |  | ||||||
| from src.hand_detection_3d import load_mediapipe_model, process_frame_3d |  | ||||||
| 
 |  | ||||||
| # 尝试导入 Picamera2,如果不可用则使用标准OpenCV |  | ||||||
| try: |  | ||||||
|     from picamera2 import Picamera2 |  | ||||||
|     PICAMERA_AVAILABLE = True |  | ||||||
| except ImportError: |  | ||||||
|     PICAMERA_AVAILABLE = False |  | ||||||
| 
 |  | ||||||
| def process_camera_3d(camera_id=0, output_path=None, use_picamera=True): |  | ||||||
|     """ |  | ||||||
|     使用摄像头进行3D手部检测 - 简化版本 |  | ||||||
|      |  | ||||||
|     参数: |  | ||||||
|     camera_id: 摄像头ID (默认为0) |  | ||||||
|     output_path: 输出视频文件路径 (可选) |  | ||||||
|     use_picamera: 是否优先使用Picamera2 (树莓派摄像头) |  | ||||||
|     """ |  | ||||||
|     # 加载MediaPipe手部检测模型 |  | ||||||
|     hands_model = load_mediapipe_model(max_num_hands=1) |  | ||||||
|      |  | ||||||
|     # 初始化摄像头 |  | ||||||
|     cap = None |  | ||||||
|     picam2 = None |  | ||||||
|      |  | ||||||
|     if use_picamera and PICAMERA_AVAILABLE: |  | ||||||
|         # 使用 Picamera2 (树莓派摄像头) |  | ||||||
|         try: |  | ||||||
|             print("正在初始化 Picamera2...") |  | ||||||
|             picam2 = Picamera2() |  | ||||||
|             video_config = picam2.create_video_configuration(main={"size": (640, 480)}) |  | ||||||
|             picam2.configure(video_config) |  | ||||||
|             picam2.start() |  | ||||||
|             print("✅ Picamera2 启动成功") |  | ||||||
|             frame_width, frame_height = 640, 480 |  | ||||||
|         except Exception as e: |  | ||||||
|             print(f"❌ Picamera2 启动失败: {e}") |  | ||||||
|             print("⚠️  回退到 OpenCV VideoCapture") |  | ||||||
|             picam2 = None |  | ||||||
|             use_picamera = False |  | ||||||
|      |  | ||||||
|     if not use_picamera or not PICAMERA_AVAILABLE: |  | ||||||
|         # 使用标准 OpenCV VideoCapture |  | ||||||
|         cap = cv2.VideoCapture(camera_id) |  | ||||||
|         if not cap.isOpened(): |  | ||||||
|             print(f"无法打开摄像头 ID: {camera_id}") |  | ||||||
|             return |  | ||||||
|         frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) |  | ||||||
|         frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) |  | ||||||
|      |  | ||||||
|     fps = 30 |  | ||||||
|      |  | ||||||
|     # 创建视频写入器(如果需要保存) |  | ||||||
|     video_writer = None |  | ||||||
|     if output_path: |  | ||||||
|         fourcc = cv2.VideoWriter_fourcc(*'XVID') |  | ||||||
|         video_writer = cv2.VideoWriter(output_path, fourcc, fps, (frame_width, frame_height)) |  | ||||||
|      |  | ||||||
|     # 前一帧的手部数据 |  | ||||||
|     prev_hand_data = None |  | ||||||
|      |  | ||||||
|     # 初始化性能计数器 |  | ||||||
|     frame_count = 0 |  | ||||||
|     start_time = time.time() |  | ||||||
|      |  | ||||||
|     print("控制说明:") |  | ||||||
|     print("- 移动手部显示控制信号") |  | ||||||
|     print("- 拇指和食指捏合显示抓取状态")  |  | ||||||
|     print("- 按 Ctrl+C 退出") |  | ||||||
|      |  | ||||||
|     try: |  | ||||||
|         while True: |  | ||||||
|             # 读取一帧 |  | ||||||
|             ret = False |  | ||||||
|             frame = None |  | ||||||
|              |  | ||||||
|             if picam2: |  | ||||||
|                 try: |  | ||||||
|                     frame = picam2.capture_array() |  | ||||||
|                     frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) |  | ||||||
|                     ret = True |  | ||||||
|                 except Exception as e: |  | ||||||
|                     print(f"Picamera2 读取帧失败: {e}") |  | ||||||
|                     break |  | ||||||
|             else: |  | ||||||
|                 ret, frame = cap.read() |  | ||||||
|              |  | ||||||
|             if not ret or frame is None: |  | ||||||
|                 break |  | ||||||
|              |  | ||||||
|             frame_count += 1 |  | ||||||
|              |  | ||||||
|             # 处理帧并获取三轴控制信号 |  | ||||||
|             control_signal, prev_hand_data = process_frame_3d(frame, hands_model, prev_hand_data) |  | ||||||
|              |  | ||||||
|             # 显示控制信号 |  | ||||||
|             if control_signal and frame_count % 30 == 0:  # 每秒显示一次 |  | ||||||
|                 print_control_signal(control_signal) |  | ||||||
|              |  | ||||||
|             # 写入输出视频 |  | ||||||
|             if video_writer: |  | ||||||
|                 video_writer.write(frame) |  | ||||||
|                  |  | ||||||
|     except KeyboardInterrupt: |  | ||||||
|         print("\n收到中断信号,正在退出...") |  | ||||||
|     except Exception as e: |  | ||||||
|         print(f"程序运行时出错: {e}") |  | ||||||
|     finally: |  | ||||||
|         print("正在清理资源...") |  | ||||||
|          |  | ||||||
|         # 释放资源 |  | ||||||
|         if picam2: |  | ||||||
|             try: |  | ||||||
|                 picam2.stop() |  | ||||||
|                 picam2.close() |  | ||||||
|             except: |  | ||||||
|                 pass |  | ||||||
|          |  | ||||||
|         if cap: |  | ||||||
|             try: |  | ||||||
|                 cap.release() |  | ||||||
|             except: |  | ||||||
|                 pass |  | ||||||
|          |  | ||||||
|         if video_writer: |  | ||||||
|             try: |  | ||||||
|                 video_writer.release() |  | ||||||
|                 print(f"✅ 视频已保存: {output_path}") |  | ||||||
|             except: |  | ||||||
|                 pass |  | ||||||
|          |  | ||||||
|         # 显示统计信息 |  | ||||||
|         elapsed_time = time.time() - start_time |  | ||||||
|         if frame_count > 0: |  | ||||||
|             avg_fps = frame_count / elapsed_time |  | ||||||
|             print(f"📊 处理了 {frame_count} 帧,平均FPS: {avg_fps:.1f}") |  | ||||||
|          |  | ||||||
|         print("✅ 程序已退出") |  | ||||||
| 
 |  | ||||||
| def print_control_signal(control_signal): |  | ||||||
|     """显示手部控制信号信息""" |  | ||||||
|     print(f"手部控制信号:") |  | ||||||
|     print(f"  X轴角度: {control_signal['x_angle']:.1f}°") |  | ||||||
|     print(f"  Y轴角度: {control_signal['y_angle']:.1f}°")  |  | ||||||
|     print(f"  Z轴角度: {control_signal['z_angle']:.1f}°") |  | ||||||
|     print(f"  抓取状态: {'抓取' if control_signal['grip'] == 1 else '释放'}") |  | ||||||
|     print(f"  动作: {control_signal['action']}") |  | ||||||
|     print("-------------------------------") |  | ||||||
| 
 |  | ||||||
| if __name__ == "__main__": |  | ||||||
|     import argparse |  | ||||||
|      |  | ||||||
|     parser = argparse.ArgumentParser(description='简化版3D手部检测') |  | ||||||
|     parser.add_argument('--camera-id', '-i', type=int, default=0, help='摄像头ID (默认为0)') |  | ||||||
|     parser.add_argument('--output', '-o', help='输出视频文件路径') |  | ||||||
|      |  | ||||||
|     args = parser.parse_args() |  | ||||||
|      |  | ||||||
|     process_camera_3d( |  | ||||||
|         camera_id=args.camera_id, |  | ||||||
|         output_path=args.output |  | ||||||
|     )  |  | ||||||
| @ -1,213 +0,0 @@ | |||||||
| #!/usr/bin/env python3 |  | ||||||
| """ |  | ||||||
| 网页预览版手部检测系统 |  | ||||||
| 通过Flask提供网页界面,解决树莓派图形界面问题 |  | ||||||
| """ |  | ||||||
| 
 |  | ||||||
| import cv2 |  | ||||||
| import threading |  | ||||||
| import time |  | ||||||
| from flask import Flask, render_template_string, Response |  | ||||||
| from picamera2 import Picamera2 |  | ||||||
| import sys |  | ||||||
| import os |  | ||||||
| 
 |  | ||||||
| # 导入手部检测模块 |  | ||||||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |  | ||||||
| from src.hand_detection_3d import load_mediapipe_model, process_frame_3d |  | ||||||
| 
 |  | ||||||
| app = Flask(__name__) |  | ||||||
| 
 |  | ||||||
| class WebPreviewCamera: |  | ||||||
|     def __init__(self): |  | ||||||
|         self.picam2 = None |  | ||||||
|         self.hands_model = load_mediapipe_model(max_num_hands=1) |  | ||||||
|         self.prev_hand_data = None |  | ||||||
|         self.latest_control_signal = None |  | ||||||
|          |  | ||||||
|     def start_camera(self): |  | ||||||
|         """启动摄像头""" |  | ||||||
|         self.picam2 = Picamera2() |  | ||||||
|         video_config = self.picam2.create_video_configuration(main={"size": (640, 480)}) |  | ||||||
|         self.picam2.configure(video_config) |  | ||||||
|         self.picam2.start() |  | ||||||
|          |  | ||||||
|     def get_frame(self): |  | ||||||
|         """获取处理后的帧""" |  | ||||||
|         if not self.picam2: |  | ||||||
|             return None |  | ||||||
|              |  | ||||||
|         # 获取原始帧 |  | ||||||
|         frame = self.picam2.capture_array() |  | ||||||
|         frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) |  | ||||||
|          |  | ||||||
|         # 进行手部检测 |  | ||||||
|         control_signal, self.prev_hand_data = process_frame_3d( |  | ||||||
|             frame, self.hands_model, self.prev_hand_data |  | ||||||
|         ) |  | ||||||
|         self.latest_control_signal = control_signal |  | ||||||
|          |  | ||||||
|         # 在帧上添加控制信号信息 |  | ||||||
|         if control_signal: |  | ||||||
|             y_offset = 30 |  | ||||||
|             cv2.putText(frame, f"X: {control_signal['x_angle']:.1f}°",  |  | ||||||
|                        (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) |  | ||||||
|             cv2.putText(frame, f"Y: {control_signal['y_angle']:.1f}°",  |  | ||||||
|                        (10, y_offset + 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) |  | ||||||
|             cv2.putText(frame, f"Z: {control_signal['z_angle']:.1f}°",  |  | ||||||
|                        (10, y_offset + 50), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) |  | ||||||
|             cv2.putText(frame, f"Grip: {'ON' if control_signal['grip'] else 'OFF'}",  |  | ||||||
|                        (10, y_offset + 75), cv2.FONT_HERSHEY_SIMPLEX, 0.6,  |  | ||||||
|                        (0, 0, 255) if control_signal['grip'] else (0, 255, 0), 2) |  | ||||||
|          |  | ||||||
|         return frame |  | ||||||
|          |  | ||||||
|     def generate_frames(self): |  | ||||||
|         """生成视频流""" |  | ||||||
|         while True: |  | ||||||
|             frame = self.get_frame() |  | ||||||
|             if frame is None: |  | ||||||
|                 continue |  | ||||||
|                  |  | ||||||
|             # 编码为JPEG |  | ||||||
|             ret, buffer = cv2.imencode('.jpg', frame) |  | ||||||
|             frame_bytes = buffer.tobytes() |  | ||||||
|              |  | ||||||
|             yield (b'--frame\r\n' |  | ||||||
|                    b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n') |  | ||||||
|              |  | ||||||
|             time.sleep(0.033)  # ~30 FPS |  | ||||||
| 
 |  | ||||||
| # 全局摄像头实例 |  | ||||||
| camera = WebPreviewCamera() |  | ||||||
| 
 |  | ||||||
| @app.route('/') |  | ||||||
| def index(): |  | ||||||
|     """主页面""" |  | ||||||
|     return render_template_string(''' |  | ||||||
|     <!DOCTYPE html> |  | ||||||
|     <html> |  | ||||||
|     <head> |  | ||||||
|         <title>🍓 树莓派手部检测预览</title> |  | ||||||
|         <style> |  | ||||||
|             body {  |  | ||||||
|                 font-family: Arial, sans-serif;  |  | ||||||
|                 text-align: center;  |  | ||||||
|                 background-color: #f0f0f0; |  | ||||||
|                 margin: 0; |  | ||||||
|                 padding: 20px; |  | ||||||
|             } |  | ||||||
|             .container { |  | ||||||
|                 max-width: 800px; |  | ||||||
|                 margin: 0 auto; |  | ||||||
|                 background: white; |  | ||||||
|                 padding: 20px; |  | ||||||
|                 border-radius: 10px; |  | ||||||
|                 box-shadow: 0 4px 6px rgba(0,0,0,0.1); |  | ||||||
|             } |  | ||||||
|             h1 { color: #333; } |  | ||||||
|             .video-container { |  | ||||||
|                 margin: 20px 0; |  | ||||||
|                 border: 2px solid #ddd; |  | ||||||
|                 border-radius: 10px; |  | ||||||
|                 overflow: hidden; |  | ||||||
|             } |  | ||||||
|             img {  |  | ||||||
|                 width: 100%;  |  | ||||||
|                 height: auto;  |  | ||||||
|                 display: block; |  | ||||||
|             } |  | ||||||
|             .info { |  | ||||||
|                 background: #e7f3ff; |  | ||||||
|                 padding: 15px; |  | ||||||
|                 border-radius: 5px; |  | ||||||
|                 margin: 20px 0; |  | ||||||
|             } |  | ||||||
|             .controls { |  | ||||||
|                 margin: 20px 0; |  | ||||||
|             } |  | ||||||
|             button { |  | ||||||
|                 background: #007bff; |  | ||||||
|                 color: white; |  | ||||||
|                 border: none; |  | ||||||
|                 padding: 10px 20px; |  | ||||||
|                 border-radius: 5px; |  | ||||||
|                 cursor: pointer; |  | ||||||
|                 margin: 0 10px; |  | ||||||
|             } |  | ||||||
|             button:hover { |  | ||||||
|                 background: #0056b3; |  | ||||||
|             } |  | ||||||
|         </style> |  | ||||||
|     </head> |  | ||||||
|     <body> |  | ||||||
|         <div class="container"> |  | ||||||
|             <h1>🍓 树莓派3D手部检测系统</h1> |  | ||||||
|              |  | ||||||
|             <div class="info"> |  | ||||||
|                 <p><strong>操作说明:</strong></p> |  | ||||||
|                 <p>• 将手部放在摄像头前</p> |  | ||||||
|                 <p>• 移动手部查看三轴角度变化</p> |  | ||||||
|                 <p>• 拇指和食指捏合触发抓取检测</p> |  | ||||||
|                 <p>• 实时显示控制信号</p> |  | ||||||
|             </div> |  | ||||||
|              |  | ||||||
|             <div class="video-container"> |  | ||||||
|                 <img src="{{ url_for('video_feed') }}" alt="手部检测预览"> |  | ||||||
|             </div> |  | ||||||
|              |  | ||||||
|             <div class="controls"> |  | ||||||
|                 <button onclick="location.reload()">🔄 刷新页面</button> |  | ||||||
|                 <button onclick="window.close()">❌ 关闭窗口</button> |  | ||||||
|             </div> |  | ||||||
|              |  | ||||||
|             <div class="info"> |  | ||||||
|                 <p><strong>访问地址:</strong> http://树莓派IP:5000</p> |  | ||||||
|                 <p><strong>提示:</strong> 可以在手机、电脑等任意设备上打开此页面查看预览</p> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|          |  | ||||||
|         <script> |  | ||||||
|             // 自动刷新控制信号信息 |  | ||||||
|             setInterval(() => { |  | ||||||
|                 // 这里可以添加AJAX获取最新控制信号的代码 |  | ||||||
|             }, 1000); |  | ||||||
|         </script> |  | ||||||
|     </body> |  | ||||||
|     </html> |  | ||||||
|     ''') |  | ||||||
| 
 |  | ||||||
| @app.route('/video_feed') |  | ||||||
| def video_feed(): |  | ||||||
|     """视频流路由""" |  | ||||||
|     return Response(camera.generate_frames(), |  | ||||||
|                    mimetype='multipart/x-mixed-replace; boundary=frame') |  | ||||||
| 
 |  | ||||||
| def start_web_preview(): |  | ||||||
|     """启动网页预览服务""" |  | ||||||
|     print("🍓 启动树莓派网页预览系统...") |  | ||||||
|     print("📷 初始化摄像头...") |  | ||||||
|      |  | ||||||
|     try: |  | ||||||
|         camera.start_camera() |  | ||||||
|         print("✅ 摄像头启动成功") |  | ||||||
|         print("🌐 启动网页服务器...") |  | ||||||
|         print("📱 请在浏览器中访问: http://localhost:5000") |  | ||||||
|         print("📱 或在其他设备访问: http://树莓派IP:5000") |  | ||||||
|         print("🛑 按 Ctrl+C 停止服务") |  | ||||||
|          |  | ||||||
|         # 启动Flask应用 |  | ||||||
|         app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) |  | ||||||
|          |  | ||||||
|     except KeyboardInterrupt: |  | ||||||
|         print("\n🛑 用户停止服务") |  | ||||||
|     except Exception as e: |  | ||||||
|         print(f"❌ 错误: {e}") |  | ||||||
|     finally: |  | ||||||
|         if camera.picam2: |  | ||||||
|             camera.picam2.stop() |  | ||||||
|             camera.picam2.close() |  | ||||||
|         print("✅ 网页预览系统已停止") |  | ||||||
| 
 |  | ||||||
| if __name__ == "__main__": |  | ||||||
|     start_web_preview()  |  | ||||||
| @ -1,401 +0,0 @@ | |||||||
| #!/usr/bin/env python3 |  | ||||||
| """ |  | ||||||
| 实时手部检测Web服务器 |  | ||||||
| 支持WebSocket通信,实时视频流处理和机械臂控制 |  | ||||||
| """ |  | ||||||
| 
 |  | ||||||
| import asyncio |  | ||||||
| import base64 |  | ||||||
| import json |  | ||||||
| import logging |  | ||||||
| import os |  | ||||||
| import time |  | ||||||
| from threading import Thread |  | ||||||
| from typing import Dict, Optional, Any |  | ||||||
| 
 |  | ||||||
| import cv2 |  | ||||||
| import numpy as np |  | ||||||
| from flask import Flask, render_template, request |  | ||||||
| from flask_socketio import SocketIO, emit |  | ||||||
| from PIL import Image |  | ||||||
| import io |  | ||||||
| 
 |  | ||||||
| from hand_detection_3d import load_mediapipe_model, process_frame_3d |  | ||||||
| 
 |  | ||||||
| # 配置日志 |  | ||||||
| logging.basicConfig(level=logging.INFO) |  | ||||||
| logger = logging.getLogger(__name__) |  | ||||||
| 
 |  | ||||||
| class HandDetectionWebServer: |  | ||||||
|     """实时手部检测Web服务器""" |  | ||||||
|      |  | ||||||
|     def __init__(self, host='0.0.0.0', port=5000): |  | ||||||
|         self.host = host |  | ||||||
|         self.port = port |  | ||||||
|          |  | ||||||
|         # Flask应用和SocketIO |  | ||||||
|         # 设置模板和静态文件路径 |  | ||||||
|         import os |  | ||||||
|         template_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'templates') |  | ||||||
|         self.app = Flask(__name__, template_folder=template_dir) |  | ||||||
|         self.app.config['SECRET_KEY'] = 'hand_detection_secret_key' |  | ||||||
|         self.socketio = SocketIO(self.app, cors_allowed_origins="*", async_mode='threading') |  | ||||||
|          |  | ||||||
|         # MediaPipe模型 |  | ||||||
|         self.hands_model = load_mediapipe_model() |  | ||||||
|          |  | ||||||
|         # 状态管理 |  | ||||||
|         self.clients = {}  # 连接的客户端 |  | ||||||
|         self.current_frame = None |  | ||||||
|         self.previous_hand_data = None |  | ||||||
|         self.detection_results = { |  | ||||||
|             'x_angle': 90, |  | ||||||
|             'y_angle': 90, |  | ||||||
|             'z_angle': 90, |  | ||||||
|             'grip': 0, |  | ||||||
|             'action': 'none', |  | ||||||
|             'speed': 5, |  | ||||||
|             'timestamp': time.time() |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         # 性能监控 |  | ||||||
|         self.fps_counter = 0 |  | ||||||
|         self.last_fps_time = time.time() |  | ||||||
|         self.current_fps = 0 |  | ||||||
|          |  | ||||||
|         # 配置路由和事件处理 |  | ||||||
|         self._setup_routes() |  | ||||||
|         self._setup_socket_events() |  | ||||||
|      |  | ||||||
|     def _setup_routes(self): |  | ||||||
|         """设置HTTP路由""" |  | ||||||
|          |  | ||||||
|         @self.app.route('/') |  | ||||||
|         def index(): |  | ||||||
|             """主页面""" |  | ||||||
|             return render_template('index.html') |  | ||||||
|          |  | ||||||
|         @self.app.route('/api/status') |  | ||||||
|         def api_status(): |  | ||||||
|             """获取系统状态""" |  | ||||||
|             return { |  | ||||||
|                 'status': 'running', |  | ||||||
|                 'fps': self.current_fps, |  | ||||||
|                 'clients': len(self.clients), |  | ||||||
|                 'detection_results': self.detection_results |  | ||||||
|             } |  | ||||||
|      |  | ||||||
|     def _setup_socket_events(self): |  | ||||||
|         """设置WebSocket事件处理""" |  | ||||||
|          |  | ||||||
|         @self.socketio.on('connect') |  | ||||||
|         def handle_connect(): |  | ||||||
|             """客户端连接""" |  | ||||||
|             client_id = request.sid |  | ||||||
|             self.clients[client_id] = { |  | ||||||
|                 'connected_at': time.time(), |  | ||||||
|                 'type': 'unknown', |  | ||||||
|                 'last_ping': time.time() |  | ||||||
|             } |  | ||||||
|             logger.info(f"客户端 {client_id} 已连接") |  | ||||||
|              |  | ||||||
|             # 发送欢迎消息和当前状态 |  | ||||||
|             emit('status', { |  | ||||||
|                 'message': '连接成功', |  | ||||||
|                 'client_id': client_id, |  | ||||||
|                 'current_results': self.detection_results |  | ||||||
|             }) |  | ||||||
|          |  | ||||||
|         @self.socketio.on('disconnect') |  | ||||||
|         def handle_disconnect(): |  | ||||||
|             """客户端断开连接""" |  | ||||||
|             client_id = request.sid |  | ||||||
|             if client_id in self.clients: |  | ||||||
|                 del self.clients[client_id] |  | ||||||
|                 logger.info(f"客户端 {client_id} 已断开连接") |  | ||||||
|          |  | ||||||
|         @self.socketio.on('register_client') |  | ||||||
|         def handle_register_client(data): |  | ||||||
|             """注册客户端类型""" |  | ||||||
|             client_id = request.sid |  | ||||||
|             client_type = data.get('type', 'unknown') |  | ||||||
|              |  | ||||||
|             if client_id in self.clients: |  | ||||||
|                 self.clients[client_id]['type'] = client_type |  | ||||||
|                 logger.info(f"客户端 {client_id} 注册为: {client_type}") |  | ||||||
|                  |  | ||||||
|                 emit('registration_success', { |  | ||||||
|                     'client_id': client_id, |  | ||||||
|                     'type': client_type |  | ||||||
|                 }) |  | ||||||
|          |  | ||||||
|         @self.socketio.on('video_frame') |  | ||||||
|         def handle_video_frame(data): |  | ||||||
|             """处理视频帧""" |  | ||||||
|             try: |  | ||||||
|                 # 解码base64图像 |  | ||||||
|                 frame_data = base64.b64decode(data['frame']) |  | ||||||
|                 frame = self._decode_frame(frame_data) |  | ||||||
|                  |  | ||||||
|                 if frame is not None: |  | ||||||
|                     # 处理帧并检测手部 |  | ||||||
|                     control_signal, hand_data = process_frame_3d( |  | ||||||
|                         frame, self.hands_model, self.previous_hand_data |  | ||||||
|                     ) |  | ||||||
|                      |  | ||||||
|                     # 更新检测结果 |  | ||||||
|                     self.detection_results = control_signal |  | ||||||
|                     self.detection_results['timestamp'] = time.time() |  | ||||||
|                     self.previous_hand_data = hand_data |  | ||||||
|                      |  | ||||||
|                     # 编码处理后的帧 |  | ||||||
|                     processed_frame_data = self._encode_frame(frame) |  | ||||||
|                      |  | ||||||
|                     # 发送结果给web预览客户端 |  | ||||||
|                     self.socketio.emit('detection_results', { |  | ||||||
|                         'control_signal': control_signal, |  | ||||||
|                         'processed_frame': processed_frame_data, |  | ||||||
|                         'fps': self.current_fps |  | ||||||
|                     }, room=None) |  | ||||||
|                      |  | ||||||
|                     # 发送控制信号给机械臂客户端 |  | ||||||
|                     self._send_to_robot_clients(control_signal) |  | ||||||
|                      |  | ||||||
|                     # 更新FPS |  | ||||||
|                     self._update_fps() |  | ||||||
|                      |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.error(f"处理视频帧时出错: {e}") |  | ||||||
|                 emit('error', {'message': str(e)}) |  | ||||||
|          |  | ||||||
|         @self.socketio.on('ping') |  | ||||||
|         def handle_ping(): |  | ||||||
|             """处理ping请求""" |  | ||||||
|             client_id = request.sid |  | ||||||
|             if client_id in self.clients: |  | ||||||
|                 self.clients[client_id]['last_ping'] = time.time() |  | ||||||
|             emit('pong', {'timestamp': time.time()}) |  | ||||||
|          |  | ||||||
|         @self.socketio.on('get_detection_results') |  | ||||||
|         def handle_get_detection_results(): |  | ||||||
|             """获取最新的检测结果""" |  | ||||||
|             emit('detection_results', { |  | ||||||
|                 'control_signal': self.detection_results, |  | ||||||
|                 'fps': self.current_fps |  | ||||||
|             }) |  | ||||||
|          |  | ||||||
|         @self.socketio.on('start_local_test') |  | ||||||
|         def handle_start_local_test(data=None): |  | ||||||
|             """处理开始本地测试请求""" |  | ||||||
|             try: |  | ||||||
|                 # 如果提供了视频路径,使用指定的视频 |  | ||||||
|                 if data and 'video_path' in data: |  | ||||||
|                     test_video = data['video_path'] |  | ||||||
|                     if not os.path.exists(test_video): |  | ||||||
|                         emit('test_error', { |  | ||||||
|                             'message': f'视频文件不存在: {test_video}' |  | ||||||
|                         }) |  | ||||||
|                         return |  | ||||||
|                 else: |  | ||||||
|                     # 检查是否有默认测试视频 |  | ||||||
|                     test_videos = [ |  | ||||||
|                         'data/videos/test_basic.mp4', |  | ||||||
|                         'data/videos/test_gesture.mp4' |  | ||||||
|                     ] |  | ||||||
|                      |  | ||||||
|                     # 找到第一个存在的测试视频 |  | ||||||
|                     test_video = None |  | ||||||
|                     for video_path in test_videos: |  | ||||||
|                         if os.path.exists(video_path): |  | ||||||
|                             test_video = video_path |  | ||||||
|                             break |  | ||||||
|                      |  | ||||||
|                     if not test_video: |  | ||||||
|                         # 没有找到测试视频,提供帮助信息 |  | ||||||
|                         emit('test_error', { |  | ||||||
|                             'message': '未找到测试视频文件', |  | ||||||
|                             'help': '请先生成测试视频:python create_test_video.py' |  | ||||||
|                         }) |  | ||||||
|                         return |  | ||||||
|                  |  | ||||||
|                 logger.info(f"开始本地测试,使用视频: {test_video}") |  | ||||||
|                 self.start_local_video_test(test_video) |  | ||||||
|                  |  | ||||||
|                 emit('test_started', { |  | ||||||
|                     'message': f'本地测试已开始,使用视频: {os.path.basename(test_video)}', |  | ||||||
|                     'video_path': test_video |  | ||||||
|                 }) |  | ||||||
|                      |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.error(f"启动本地测试时出错: {e}") |  | ||||||
|                 emit('test_error', { |  | ||||||
|                     'message': f'启动本地测试失败: {str(e)}' |  | ||||||
|                 }) |  | ||||||
|          |  | ||||||
|         @self.socketio.on('get_video_list') |  | ||||||
|         def handle_get_video_list(): |  | ||||||
|             """获取可用的视频文件列表""" |  | ||||||
|             try: |  | ||||||
|                 video_dirs = ['data/videos', 'videos', '.'] |  | ||||||
|                 video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv'] |  | ||||||
|                 videos = [] |  | ||||||
|                  |  | ||||||
|                 for video_dir in video_dirs: |  | ||||||
|                     if os.path.exists(video_dir): |  | ||||||
|                         for file in os.listdir(video_dir): |  | ||||||
|                             if any(file.lower().endswith(ext) for ext in video_extensions): |  | ||||||
|                                 file_path = os.path.join(video_dir, file) |  | ||||||
|                                 try: |  | ||||||
|                                     file_size = os.path.getsize(file_path) |  | ||||||
|                                     size_mb = file_size / (1024 * 1024) |  | ||||||
|                                     videos.append({ |  | ||||||
|                                         'path': file_path, |  | ||||||
|                                         'name': file, |  | ||||||
|                                         'size': f'{size_mb:.1f}MB' |  | ||||||
|                                     }) |  | ||||||
|                                 except OSError: |  | ||||||
|                                     continue |  | ||||||
|                  |  | ||||||
|                 # 按文件名排序 |  | ||||||
|                 videos.sort(key=lambda x: x['name']) |  | ||||||
|                  |  | ||||||
|                 emit('video_list', { |  | ||||||
|                     'videos': videos |  | ||||||
|                 }) |  | ||||||
|                  |  | ||||||
|             except Exception as e: |  | ||||||
|                 logger.error(f"获取视频列表时出错: {e}") |  | ||||||
|                 emit('video_list', { |  | ||||||
|                     'videos': [] |  | ||||||
|                 }) |  | ||||||
|      |  | ||||||
|     def _decode_frame(self, frame_data: bytes) -> Optional[np.ndarray]: |  | ||||||
|         """解码图像帧""" |  | ||||||
|         try: |  | ||||||
|             # 使用PIL解码 |  | ||||||
|             image = Image.open(io.BytesIO(frame_data)) |  | ||||||
|             frame = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) |  | ||||||
|             return frame |  | ||||||
|         except Exception as e: |  | ||||||
|             logger.error(f"解码帧时出错: {e}") |  | ||||||
|             return None |  | ||||||
|      |  | ||||||
|     def _encode_frame(self, frame: np.ndarray) -> str: |  | ||||||
|         """编码图像帧为base64""" |  | ||||||
|         try: |  | ||||||
|             # 转换为RGB格式 |  | ||||||
|             frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) |  | ||||||
|             image = Image.fromarray(frame_rgb) |  | ||||||
|              |  | ||||||
|             # 编码为JPEG |  | ||||||
|             buffer = io.BytesIO() |  | ||||||
|             image.save(buffer, format='JPEG', quality=80) |  | ||||||
|             frame_data = base64.b64encode(buffer.getvalue()).decode('utf-8') |  | ||||||
|              |  | ||||||
|             return f"data:image/jpeg;base64,{frame_data}" |  | ||||||
|         except Exception as e: |  | ||||||
|             logger.error(f"编码帧时出错: {e}") |  | ||||||
|             return "" |  | ||||||
|      |  | ||||||
|     def _send_to_robot_clients(self, control_signal: Dict[str, Any]): |  | ||||||
|         """发送控制信号给机械臂客户端""" |  | ||||||
|         robot_clients = [ |  | ||||||
|             client_id for client_id, info in self.clients.items()  |  | ||||||
|             if info.get('type') == 'robot' |  | ||||||
|         ] |  | ||||||
|          |  | ||||||
|         if robot_clients: |  | ||||||
|             for client_id in robot_clients: |  | ||||||
|                 self.socketio.emit('robot_control', control_signal, room=client_id) |  | ||||||
|      |  | ||||||
|     def _update_fps(self): |  | ||||||
|         """更新FPS计数""" |  | ||||||
|         self.fps_counter += 1 |  | ||||||
|         current_time = time.time() |  | ||||||
|          |  | ||||||
|         if current_time - self.last_fps_time >= 1.0:  # 每秒更新一次 |  | ||||||
|             self.current_fps = self.fps_counter |  | ||||||
|             self.fps_counter = 0 |  | ||||||
|             self.last_fps_time = current_time |  | ||||||
|      |  | ||||||
|     def start_local_video_test(self, video_path: str): |  | ||||||
|         """启动本地视频测试""" |  | ||||||
|         def video_test_thread(): |  | ||||||
|             cap = cv2.VideoCapture(video_path) |  | ||||||
|              |  | ||||||
|             while cap.isOpened(): |  | ||||||
|                 ret, frame = cap.read() |  | ||||||
|                 if not ret: |  | ||||||
|                     break |  | ||||||
|                  |  | ||||||
|                 # 处理帧 |  | ||||||
|                 control_signal, hand_data = process_frame_3d( |  | ||||||
|                     frame, self.hands_model, self.previous_hand_data |  | ||||||
|                 ) |  | ||||||
|                  |  | ||||||
|                 # 更新状态 |  | ||||||
|                 self.detection_results = control_signal |  | ||||||
|                 self.detection_results['timestamp'] = time.time() |  | ||||||
|                 self.previous_hand_data = hand_data |  | ||||||
|                  |  | ||||||
|                 # 编码帧 |  | ||||||
|                 processed_frame_data = self._encode_frame(frame) |  | ||||||
|                  |  | ||||||
|                 # 广播结果 |  | ||||||
|                 self.socketio.emit('detection_results', { |  | ||||||
|                     'control_signal': control_signal, |  | ||||||
|                     'processed_frame': processed_frame_data, |  | ||||||
|                     'fps': self.current_fps |  | ||||||
|                 }) |  | ||||||
|                  |  | ||||||
|                 # 发送给机械臂 |  | ||||||
|                 self._send_to_robot_clients(control_signal) |  | ||||||
|                  |  | ||||||
|                 # 更新FPS |  | ||||||
|                 self._update_fps() |  | ||||||
|                  |  | ||||||
|                 # 控制帧率 |  | ||||||
|                 time.sleep(1/30)  # 30 FPS |  | ||||||
|              |  | ||||||
|             cap.release() |  | ||||||
|          |  | ||||||
|         thread = Thread(target=video_test_thread) |  | ||||||
|         thread.daemon = True |  | ||||||
|         thread.start() |  | ||||||
|         logger.info(f"本地视频测试已启动: {video_path}") |  | ||||||
|      |  | ||||||
|     def run(self, debug=False): |  | ||||||
|         """启动Web服务器""" |  | ||||||
|         logger.info(f"启动手部检测Web服务器 http://{self.host}:{self.port}") |  | ||||||
|         self.socketio.run( |  | ||||||
|             self.app, |  | ||||||
|             host=self.host, |  | ||||||
|             port=self.port, |  | ||||||
|             debug=debug, |  | ||||||
|             allow_unsafe_werkzeug=True |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
| def main(): |  | ||||||
|     """主函数""" |  | ||||||
|     import argparse |  | ||||||
|      |  | ||||||
|     parser = argparse.ArgumentParser(description='实时手部检测Web服务器') |  | ||||||
|     parser.add_argument('--host', default='0.0.0.0', help='服务器地址') |  | ||||||
|     parser.add_argument('--port', type=int, default=5000, help='端口号') |  | ||||||
|     parser.add_argument('--debug', action='store_true', help='调试模式') |  | ||||||
|     parser.add_argument('--test-video', help='本地测试视频路径') |  | ||||||
|      |  | ||||||
|     args = parser.parse_args() |  | ||||||
|      |  | ||||||
|     # 创建服务器实例 |  | ||||||
|     server = HandDetectionWebServer(host=args.host, port=args.port) |  | ||||||
|      |  | ||||||
|     # 如果指定了测试视频,启动本地视频测试 |  | ||||||
|     if args.test_video: |  | ||||||
|         server.start_local_video_test(args.test_video) |  | ||||||
|      |  | ||||||
|     # 启动服务器 |  | ||||||
|     server.run(debug=args.debug) |  | ||||||
| 
 |  | ||||||
| if __name__ == '__main__': |  | ||||||
|     main() |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user