From bd13a5aeb23a1897133c081ad50c0a3357212ce2 Mon Sep 17 00:00:00 2001 From: Cx330 <1487537121@qq.com> Date: Sat, 18 Apr 2026 16:46:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A7=E5=88=B6=E7=95=8C?= =?UTF-8?q?=E9=9D=A2UI=E6=A0=B7=E5=BC=8F=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=A4=96=E7=95=8C=E6=89=8B=E6=9F=84=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/CSS/control.css | 191 ++++++++++++----- main/CSS/style.css | 10 + main/Javascript/control.js | 5 +- main/Javascript/joycon.js | 247 ++++++++++++++++++++++ main/Javascript/script.js | 57 +++++ main/__pycache__/motor.cpython-313.pyc | Bin 4033 -> 0 bytes main/__pycache__/motor.cpython-38.pyc | Bin 0 -> 3564 bytes main/camera_test.py | 278 +++++++++++++++++++++++++ main/index.html | 50 +++-- main/motor.py | 20 +- 10 files changed, 779 insertions(+), 79 deletions(-) create mode 100644 main/Javascript/joycon.js delete mode 100644 main/__pycache__/motor.cpython-313.pyc create mode 100644 main/__pycache__/motor.cpython-38.pyc create mode 100644 main/camera_test.py diff --git a/main/CSS/control.css b/main/CSS/control.css index 20f9626..5830af6 100644 --- a/main/CSS/control.css +++ b/main/CSS/control.css @@ -6,14 +6,16 @@ justify-content: center; min-height: 80vh; padding: 20px; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); } .title { - font-size: 24px; + font-size: 28px; font-weight: bold; color: #333; margin-bottom: 40px; text-align: center; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); } /* 手柄风格布局 */ @@ -23,46 +25,66 @@ align-items: center; gap: 30px; width: 100%; - max-width: 350px; + max-width: 600px; } -/* 移动控制按钮区域 */ -.movement-controls { +/* 控制器布局 */ +.controller-layout { + display: flex; + align-items: center; + justify-content: space-around; + width: 100%; + padding: 30px; + background: linear-gradient(145deg, #e6e6e6, #ffffff); + border-radius: 20px; + box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.1), -10px -10px 20px rgba(255, 255, 255, 0.7); +} + +/* 方向键区域 */ +.dpad { display: flex; flex-direction: column; align-items: center; - gap: 15px; - width: 100%; + gap: 10px; } -.movement-row { +.dpad-row { display: flex; - gap: 15px; - justify-content: center; + gap: 10px; } -/* 录制回放按钮区域 */ -.record-controls { +/* 中间录制回放控制 */ +.center-controls { display: flex; + flex-direction: column; gap: 15px; - justify-content: center; - width: 100%; - margin-top: 20px; + align-items: center; +} + +/* 右侧旋转控制 */ +.rotate-controls { + display: flex; + flex-direction: column; + gap: 15px; + align-items: center; } /* 按钮样式 */ .btn { - width: 120px; - height: 50px; + width: 70px; + height: 70px; border: none; - border-radius: 25px; - font-size: 16px; + border-radius: 15px; + font-size: 20px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7); + display: flex; + align-items: center; + justify-content: center; } .btn::before { @@ -81,93 +103,168 @@ z-index: 2; } +/* 方向按钮样式 */ .btn-forward { - background-color: #4CAF50; + background: linear-gradient(145deg, #4CAF50, #45a049); color: white; } .btn-backward { - background-color: #f44336; + background: linear-gradient(145deg, #f44336, #da190b); color: white; } .btn-left { - background-color: #2196F3; + background: linear-gradient(145deg, #2196F3, #1976D2); color: white; } .btn-right { - background-color: #FF9800; - color: white; -} - -.btn-rotate-left { - background-color: #9C27B0; - color: white; -} - -.btn-rotate-right { - background-color: #607D8B; + background: linear-gradient(145deg, #FF9800, #F57C00); color: white; } +/* 斜向按钮样式 */ .btn-forward-left { - background-color: #4CAF50; + background: linear-gradient(145deg, #4CAF50, #45a049); color: white; } .btn-forward-right { - background-color: #4CAF50; + background: linear-gradient(145deg, #4CAF50, #45a049); color: white; } .btn-backward-left { - background-color: #f44336; + background: linear-gradient(145deg, #f44336, #da190b); color: white; } .btn-backward-right { - background-color: #f44336; + background: linear-gradient(145deg, #f44336, #da190b); color: white; } +/* 旋转按钮样式 */ +.btn-rotate-left { + background: linear-gradient(145deg, #9C27B0, #7B1FA2); + color: white; +} + +.btn-rotate-right { + background: linear-gradient(145deg, #607D8B, #455A64); + color: white; +} + +/* 中心方块样式 */ +.btn-center { + background: linear-gradient(145deg, #666666, #444444); + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7); + cursor: default; +} + +.btn-center:hover { + transform: none; + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7); +} + +.btn-center:active { + transform: none; + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1), -5px -5px 10px rgba(255, 255, 255, 0.7); +} + +/* 录制回放按钮样式 */ +.btn-record { + background: linear-gradient(145deg, #FF5722, #E64A19); + color: white; + width: 120px; + height: 50px; + font-size: 16px; + border-radius: 25px; +} + +.btn-playback { + background: linear-gradient(145deg, #00BCD4, #0097A7); + color: white; + width: 120px; + height: 50px; + font-size: 16px; + border-radius: 25px; +} + +/* 按钮动画效果 */ .btn:hover { transform: translateY(-3px); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); + box-shadow: 8px 8px 16px rgba(0, 0, 0, 0.15), -8px -8px 16px rgba(255, 255, 255, 0.8); } .btn:active { transform: translateY(0); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.2), -3px -3px 6px rgba(255, 255, 255, 0.6); } /* 状态显示 */ .status { margin-top: 30px; - padding: 15px; - background-color: #f5f5f5; - border-radius: 10px; - min-height: 50px; + padding: 20px; + background: linear-gradient(145deg, #f5f5f5, #e0e0e0); + border-radius: 15px; + min-height: 60px; width: 100%; - max-width: 350px; + max-width: 400px; text-align: center; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: inset 5px 5px 10px rgba(0, 0, 0, 0.1), inset -5px -5px 10px rgba(255, 255, 255, 0.7); } .status p { color: #666; margin: 0; - font-size: 16px; + font-size: 18px; + font-weight: 500; } /* 响应式设计 */ -@media (max-width: 400px) { +@media (max-width: 650px) { + .controller-layout { + flex-direction: column; + gap: 20px; + padding: 20px; + } + + .dpad-row { + gap: 8px; + } + .btn { + width: 60px; + height: 60px; + font-size: 18px; + } + + .btn-record, .btn-playback { width: 100px; height: 45px; font-size: 14px; } + .title { + font-size: 24px; + } +} + +@media (max-width: 400px) { + .btn { + width: 50px; + height: 50px; + font-size: 16px; + } + + .btn-record, .btn-playback { + width: 90px; + height: 40px; + font-size: 13px; + } + .title { font-size: 20px; } diff --git a/main/CSS/style.css b/main/CSS/style.css index 1e43f28..533da69 100644 --- a/main/CSS/style.css +++ b/main/CSS/style.css @@ -56,6 +56,16 @@ h1 { color: white; } +.btn-record { + background-color: #ff9800; + color: white; +} + +.btn-playback { + background-color: #2196f3; + color: white; +} + .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); diff --git a/main/Javascript/control.js b/main/Javascript/control.js index b1da833..96f5cc8 100644 --- a/main/Javascript/control.js +++ b/main/Javascript/control.js @@ -424,6 +424,8 @@ function stopMotor() { const timestamp = Date.now() - window.recordingStartTime; window.recordedActions.push({ direction: 'stop', timestamp }); console.log('Recorded action:', { direction: 'stop', timestamp }); + } else { + console.log('Not recording, action:', 'stop'); } // 发送停止请求到服务器 @@ -437,7 +439,8 @@ function stopMotor() { .then(response => response.json()) .then(data => { if (data.status === 'success') { - // 停止成功,不显示消息 + // 停止成功,显示已停止消息 + status.innerHTML = `

${data.message}

`; } else { status.innerHTML = `

错误: ${data.message}

`; } diff --git a/main/Javascript/joycon.js b/main/Javascript/joycon.js new file mode 100644 index 0000000..88982bd --- /dev/null +++ b/main/Javascript/joycon.js @@ -0,0 +1,247 @@ +// 确保在DOM加载完成后执行 +window.addEventListener('DOMContentLoaded', function() { + console.log('DOMContentLoaded - Joycon.js executing'); + + // 游戏手柄相关变量 + let gamepad = null; + let lastDirection = 'stop'; + let isRecording = false; + let isPlaying = false; + let recordedActions = []; + let recordingStartTime = 0; + + // 按键状态标志(用于检测按键按下事件) + let recordButtonPressed = false; + let playbackButtonPressed = false; + + // 获取状态元素 + const status = document.getElementById('status'); + + // 检查浏览器是否支持游戏手柄API + if (!('getGamepads' in navigator)) { + status.innerHTML = '

您的浏览器不支持游戏手柄API

'; + console.log('Gamepad API not supported'); + return; + } + + // 连接游戏手柄 + window.addEventListener('gamepadconnected', function(e) { + gamepad = e.gamepad; + status.innerHTML = `

已连接游戏手柄: ${gamepad.id}

`; + console.log('Gamepad connected:', gamepad); + // 开始监听游戏手柄输入 + requestAnimationFrame(updateGamepad); + }); + + // 断开游戏手柄 + window.addEventListener('gamepaddisconnected', function(e) { + status.innerHTML = '

游戏手柄已断开连接

'; + console.log('Gamepad disconnected:', e.gamepad); + gamepad = null; + }); + + // 游戏手柄输入更新函数 + function updateGamepad() { + if (!gamepad) { + requestAnimationFrame(updateGamepad); + return; + } + + // 获取游戏手柄状态 + const gamepads = navigator.getGamepads(); + gamepad = gamepads[gamepad.index]; + + if (!gamepad) { + requestAnimationFrame(updateGamepad); + return; + } + + // 处理摇杆输入 (通常是0号摇杆,左摇杆) + const leftStickX = gamepad.axes[0]; + const leftStickY = gamepad.axes[1]; + + // 处理按键输入 + const buttons = gamepad.buttons; + + // 摇杆死区 + const deadzone = 0.3; + + // 确定移动方向 + let direction = 'stop'; + + if (Math.abs(leftStickX) > deadzone || Math.abs(leftStickY) > deadzone) { + // 计算角度 + const angle = Math.atan2(leftStickY, leftStickX) * 180 / Math.PI; + + // 根据角度确定方向(修复:交换前后方向,修复:使用下划线分隔的方向值) + if (angle >= -22.5 && angle < 22.5) { + direction = 'right'; // 右 + } else if (angle >= 22.5 && angle < 67.5) { + direction = 'backward_right'; // 右后 + } else if (angle >= 67.5 && angle < 112.5) { + direction = 'backward'; // 后 + } else if (angle >= 112.5 && angle < 157.5) { + direction = 'backward_left'; // 左后 + } else if (angle >= 157.5 || angle < -157.5) { + direction = 'left'; // 左 + } else if (angle >= -157.5 && angle < -112.5) { + direction = 'forward_left'; // 左前 + } else if (angle >= -112.5 && angle < -67.5) { + direction = 'forward'; // 前 + } else if (angle >= -67.5 && angle < -22.5) { + direction = 'forward_right'; // 右前 + } + } + + // 处理旋转按钮 (X键和Y键) + if (buttons[2].pressed) { // X键 - 左旋转 + direction = 'rotate_left'; + } else if (buttons[3].pressed) { // Y键 - 右旋转 + direction = 'rotate_right'; + } + + // 处理录制按钮 (十字键上键) - 修复:检测按键按下事件 + if (buttons[12].pressed) { // 十字键上键 - 录制 + if (!recordButtonPressed) { + recordButtonPressed = true; + if (!isPlaying) { + if (isRecording) { + // 停止录制 + isRecording = false; + status.innerHTML = `

录制完成,共记录 ${recordedActions.length} 个操作

`; + console.log('Recording stopped, actions:', recordedActions); + } else { + // 开始录制 + isRecording = true; + recordedActions = []; + recordingStartTime = Date.now(); + status.innerHTML = '

开始录制...

'; + console.log('Recording started'); + } + } + } + } else { + recordButtonPressed = false; + } + + // 处理回放按钮 (十字键左键) - 修复:检测按键按下事件 + if (buttons[14].pressed) { // 十字键左键 - 回放 + if (!playbackButtonPressed) { + playbackButtonPressed = true; + if (!isRecording && !isPlaying && recordedActions.length > 0) { + isPlaying = true; + status.innerHTML = '

开始回放...

'; + console.log('Starting playback'); + + // 按时间顺序回放操作 + let currentTime = 0; + const totalDuration = recordedActions[recordedActions.length - 1].timestamp; + + recordedActions.forEach((action, index) => { + if (index === 0) { + // 第一个操作立即执行 + console.log('Executing action immediately:', action); + controlMotor(action.direction); + } else { + // 计算与前一个操作的时间差 + const prevAction = recordedActions[index - 1]; + const delay = action.timestamp - prevAction.timestamp; + currentTime += delay; + + console.log('Scheduling action:', action, 'at delay:', currentTime); + setTimeout(() => { + console.log('Executing action:', action); + controlMotor(action.direction); + }, currentTime); + } + }); + + // 回放完成后,确保发送停止请求 + setTimeout(() => { + console.log('Playback completed, sending stop command'); + controlMotor('stop'); + status.innerHTML = '

回放完成

'; + console.log('Playback completed'); + isPlaying = false; + }, totalDuration + 2000); + } + } + } else { + playbackButtonPressed = false; + } + + // 如果方向改变,发送控制命令 + if (direction !== lastDirection) { + // 如果正在录制,记录操作 + if (isRecording) { + const action = { + direction: direction, + timestamp: Date.now() - recordingStartTime + }; + recordedActions.push(action); + } + controlMotor(direction); + lastDirection = direction; + } + + // 继续监听 + requestAnimationFrame(updateGamepad); + } + + // 控制电机的函数(与control.js中的相同) + function controlMotor(direction) { + // 根据方向设置状态消息 + if (direction === 'stop') { + status.innerHTML = `

正在停止...

`; + } else { + let actionText = ''; + switch (direction) { + case 'forward': actionText = '前进'; break; + case 'backward': actionText = '后退'; break; + case 'left': actionText = '左移'; break; + case 'right': actionText = '右移'; break; + case 'rotate_left': actionText = '左旋转'; break; + case 'rotate_right': actionText = '右旋转'; break; + case 'forward_left': actionText = '左前移动'; break; + case 'forward_right': actionText = '右前移动'; break; + case 'backward_left': actionText = '左后移动'; break; + case 'backward_right': actionText = '右后移动'; break; + default: actionText = '移动'; break; + } + status.innerHTML = `

正在${actionText}...

`; + } + + // 记录操作(如果正在录制) + if (isRecording) { + const timestamp = Date.now() - recordingStartTime; + // 注意:这里不再重复记录,因为已经在updateGamepad中记录了 + console.log('Recorded action:', { direction, timestamp }); + } else { + console.log('Not recording, action:', direction); + } + + // 发送AJAX请求到后端服务器 + fetch('/control', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ direction: direction }) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + status.innerHTML = `

${data.message}

`; + } else { + status.innerHTML = `

错误: ${data.message}

`; + } + }) + .catch(error => { + status.innerHTML = `

通信错误: ${error.message}

`; + }); + } + + // 初始化 + status.innerHTML = '

请连接游戏手柄...

'; + console.log('Joycon.js initialized'); +}); diff --git a/main/Javascript/script.js b/main/Javascript/script.js index 502ec25..fadfe1e 100644 --- a/main/Javascript/script.js +++ b/main/Javascript/script.js @@ -1,12 +1,25 @@ // 获取按钮和状态元素 const forwardBtn = document.getElementById('forwardBtn'); const backwardBtn = document.getElementById('backwardBtn'); +const recordBtn = document.getElementById('recordBtn'); +const playbackBtn = document.getElementById('playbackBtn'); const status = document.getElementById('status'); +// 录制相关变量 +let isRecording = false; +let recordedActions = []; +let recordingStartTime = 0; + // 实际控制函数 function controlMotor(direction) { status.innerHTML = `

正在${direction === 'forward' ? '前进' : '后退'}...

`; + // 记录操作(如果正在录制) + if (isRecording) { + const timestamp = Date.now() - recordingStartTime; + recordedActions.push({ direction, timestamp }); + } + // 发送AJAX请求到后端服务器 fetch('/control', { method: 'POST', @@ -28,6 +41,50 @@ function controlMotor(direction) { }); } +// 录制按钮点击事件 +recordBtn.addEventListener('click', () => { + if (isRecording) { + // 停止录制 + isRecording = false; + recordBtn.textContent = '开始录制'; + status.innerHTML = `

录制完成,共记录 ${recordedActions.length} 个操作

`; + } else { + // 开始录制 + isRecording = true; + recordedActions = []; + recordingStartTime = Date.now(); + recordBtn.textContent = '停止录制'; + status.innerHTML = `

开始录制...

`; + } +}); + +// 回放按钮点击事件 +playbackBtn.addEventListener('click', () => { + if (recordedActions.length === 0) { + status.innerHTML = `

没有可回放的操作

`; + return; + } + + status.innerHTML = `

开始回放...

`; + + // 按时间顺序回放操作 + let lastTimestamp = 0; + recordedActions.forEach((action, index) => { + setTimeout(() => { + controlMotor(action.direction); + + // 最后一个操作完成后显示回放完成 + if (index === recordedActions.length - 1) { + setTimeout(() => { + status.innerHTML = `

回放完成

`; + }, 2000); + } + }, action.timestamp - lastTimestamp); + + lastTimestamp = action.timestamp; + }); +}); + // 按钮点击事件 forwardBtn.addEventListener('click', () => { controlMotor('forward'); diff --git a/main/__pycache__/motor.cpython-313.pyc b/main/__pycache__/motor.cpython-313.pyc deleted file mode 100644 index 564929a45ff85d2932dd9182d35b9dabcf73fb2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4033 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 literal 0 HcmV?d00001 diff --git a/main/camera_test.py b/main/camera_test.py new file mode 100644 index 0000000..d479a39 --- /dev/null +++ b/main/camera_test.py @@ -0,0 +1,278 @@ +import cv2 +import numpy as np +import time +import signal +import sys + +# 信号处理函数,用于捕获Ctrl+C +def signal_handler(sig, frame): + print("\n接收到中断信号,程序退出") + sys.exit(0) + +# 注册信号处理函数 +signal.signal(signal.SIGINT, signal_handler) + +print("开始运行摄像头测试程序...") +print(f"OpenCV版本: {cv2.__version__}") + +# 尝试打开摄像头 +cap = cv2.VideoCapture(0) + +if not cap.isOpened(): + print("无法打开摄像头!") + exit() + +print("摄像头打开成功") + +# 设置摄像头参数 +cap.set(3, 320) +cap.set(4, 240) +cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + +print(f"摄像头分辨率: {cap.get(3)}x{cap.get(4)}") + +# 初始化稳定阶段 +print("初始化摄像头...") +success_count = 0 +for i in range(10): + ret, frame = cap.read() + if not ret: + print(f"第{i+1}帧读取失败") + continue + success_count += 1 + print(f"第{i+1}帧读取成功") + time.sleep(0.1) +print(f"摄像头初始化完成,成功读取{success_count}帧") + +# 读取初始帧 +ret, prev_frame = cap.read() +if not ret: + print("无法读取初始帧!") + cap.release() + exit() + +print("初始帧读取成功") +prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY) +prev_gray = cv2.GaussianBlur(prev_gray, (5,5), 0) +print("初始帧处理完成") + +# 记录上一帧目标的位置和面积 +prev_cx = None +prev_cy = None +prev_area = None +# 目标跟踪状态 +tracking_target = None + +while True: + ret, frame = cap.read() + if not ret: + break + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + gray = cv2.GaussianBlur(gray, (5,5), 0) + + # 帧差 + diff = cv2.absdiff(prev_gray, gray) + diff = cv2.convertScaleAbs(diff, alpha=2.5) + + # 应用形态学操作,去除噪声 + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + diff = cv2.morphologyEx(diff, cv2.MORPH_OPEN, kernel) + diff = cv2.morphologyEx(diff, cv2.MORPH_CLOSE, kernel) + + _, thresh = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) + + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # ===== 核心:目标检测和跟踪 ===== + target = None + max_area = 0 + + # 找到所有符合条件的轮廓 + valid_contours = [] + for cnt in contours: + area = cv2.contourArea(cnt) + + if area < 200: # 过滤噪声,增大阈值 + continue + + if area > 8000: # 过滤大面积假目标(如整个画面),减小阈值 + continue + + # 计算轮廓的宽高比,过滤不符合垃圾特征的目标 + x, y, w, h = cv2.boundingRect(cnt) + aspect_ratio = float(w) / h if h > 0 else 0 + if aspect_ratio > 3 or aspect_ratio < 0.3: # 过滤过于狭长的目标 + continue + + valid_contours.append((area, cnt, x, y, w, h)) + + # 如果有有效的轮廓 + if valid_contours: + # 如果正在跟踪目标,优先选择与上一目标位置接近的轮廓 + if tracking_target is not None and prev_cx is not None and prev_cy is not None: + best_match = None + min_distance = float('inf') + + search_radius = 80 # 目标锁定区域半径 + + for area, cnt, x, y, w, h in valid_contours: + cx = x + w // 2 + cy = y + h // 2 + # 计算与上一目标的距离 + distance = ((cx - prev_cx) ** 2 + (cy - prev_cy) ** 2) ** 0.5 + + # 只考虑在锁定区域内的目标 + if distance > search_radius: + continue + + # 如果距离小于阈值,认为是同一个目标 + if distance < 50: # 距离阈值 + if distance < min_distance: + min_distance = distance + best_match = (area, cnt, x, y, w, h) + + # 如果找到匹配的目标 + if best_match: + area, cnt, x, y, w, h = best_match + target = cnt + max_area = area + else: + # 如果没有找到匹配的目标,选择最大的轮廓 + valid_contours.sort(key=lambda x: x[0], reverse=True) + area, cnt, x, y, w, h = valid_contours[0] + target = cnt + max_area = area + # 开始跟踪新目标 + tracking_target = True + else: + # 如果没有正在跟踪的目标,选择最大的轮廓 + valid_contours.sort(key=lambda x: x[0], reverse=True) + area, cnt, x, y, w, h = valid_contours[0] + target = cnt + max_area = area + # 开始跟踪新目标 + tracking_target = True + + # ===== 只处理一个目标 ===== + if target is not None: + x, y, w, h = cv2.boundingRect(target) + cx = x + w // 2 + cy = y + h // 2 + + # 目标位置平滑处理 + alpha = 0.6 + if prev_cx is not None and prev_cy is not None: + cx = int(alpha * prev_cx + (1 - alpha) * cx) + cy = int(alpha * prev_cy + (1 - alpha) * cy) + + # 绘制画面中心 + center_x = 160 # 画面中心x坐标 + center_y = 120 # 画面中心y坐标 + cv2.circle(frame, (center_x, center_y), 5, (255, 0, 0), -1) + + # 绘制目标矩形和中心 + cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) + cv2.circle(frame, (cx, cy), 5, (0, 0, 255), -1) + + print("唯一目标:", cx, cy, "area:", max_area) + + # 检测目标是否突然跳变 + target_jumped = False + if prev_cx is not None and prev_cy is not None and prev_area is not None: + # 计算位置变化 + dx = abs(cx - prev_cx) + dy = abs(cy - prev_cy) + # 计算面积变化比例 + area_ratio = max_area / prev_area if prev_area > 0 else 0 + + # 如果位置变化过大或面积变化过大,认为目标跳变 + if dx > 100 or dy > 100 or area_ratio < 0.3 or area_ratio > 3: + target_jumped = True + print("目标突然跳变,可能已离开视野范围") + # 重置跟踪状态 + tracking_target = None + + # 控制逻辑:根据x轴和y轴坐标控制垃圾桶移动 + center_x = 160 # 画面中心x坐标 + center_y = 120 # 画面中心y坐标 + threshold = 20 # 阈值范围 + + if target_jumped: + # 目标跳变,停止移动 + print("目标跳变,停止移动") + # motor.stop() + else: + # 计算目标与中心的偏差 + dx = cx - center_x + dy = cy - center_y + + # 确定移动方向 + if abs(dx) < threshold and abs(dy) < threshold: + # 目标在中心附近,停止移动 + print("目标在中心,停止移动") + # motor.stop() + elif abs(dx) < threshold: + # 只在y轴方向有偏差 + if dy > threshold: + # 目标在下侧,垃圾桶向前移动 + print("目标在下侧,向前移动") + # motor.forward(speed=0.6) + else: + # 目标在上侧,垃圾桶向后移动 + print("目标在上侧,向后移动") + # motor.backward(speed=0.6) + elif abs(dy) < threshold: + # 只在x轴方向有偏差 + if dx > threshold: + # 目标在右侧,垃圾桶向右移动 + print("目标在右侧,向右移动") + # motor.move_right(speed=0.6) + else: + # 目标在左侧,垃圾桶向左移动 + print("目标在左侧,向左移动") + # motor.move_left(speed=0.6) + else: + # 在x轴和y轴方向都有偏差,使用斜向移动 + if dx > threshold and dy > threshold: + # 目标在右下侧,垃圾桶向右前移动 + print("目标在右下侧,向右前移动") + # motor.move_right_forward(speed=0.6) + elif dx > threshold and dy < -threshold: + # 目标在右上侧,垃圾桶向右后移动 + print("目标在右上侧,向右后移动") + # motor.move_right_backward(speed=0.6) + elif dx < -threshold and dy > threshold: + # 目标在左下侧,垃圾桶向左前移动 + print("目标在左下侧,向左前移动") + # motor.move_left_forward(speed=0.6) + else: + # 目标在左上侧,垃圾桶向左后移动 + print("目标在左上侧,向左后移动") + # motor.move_left_backward(speed=0.6) + + # 更新上一帧的目标信息 + prev_cx = cx + prev_cy = cy + prev_area = max_area + else: + # 没有检测到目标,停止移动 + print("未检测到目标,停止移动") + # motor.stop() + # 重置上一帧的目标信息 + prev_cx = None + prev_cy = None + prev_area = None + # 重置跟踪状态 + tracking_target = None + + cv2.imshow("tracking", frame) + + prev_gray = gray.copy() + + if cv2.waitKey(1) & 0xFF == 27: + break + +cap.release() +cv2.destroyAllWindows() +# motor.stop() \ No newline at end of file diff --git a/main/index.html b/main/index.html index fd85fd4..13e8a7b 100644 --- a/main/index.html +++ b/main/index.html @@ -13,31 +13,38 @@
- -
-
- - - +
+ +
+
+ + + +
+
+ +
+ +
+
+ + + +
-
- - - + + +
+ +
-
- - - - + + +
+ +
- - -
- - -
@@ -48,5 +55,6 @@ + \ No newline at end of file diff --git a/main/motor.py b/main/motor.py index bf7ef06..2153187 100644 --- a/main/motor.py +++ b/main/motor.py @@ -72,25 +72,25 @@ def motor_drive(name, speed): # ====================== # 基础移动 # ====================== -def forward(speed=0.6): +def forward(speed=0.8): motor_drive("M1", -speed) motor_drive("M2", -speed) motor_drive("M3", speed) motor_drive("M4", speed) -def backward(speed=0.6): +def backward(speed=0.8): motor_drive("M1", speed) motor_drive("M2", speed) motor_drive("M3", -speed) motor_drive("M4", -speed) -def move_left(speed=0.6): +def move_left(speed=0.8): motor_drive("M1", -speed) motor_drive("M2", speed) motor_drive("M3", speed) motor_drive("M4", -speed) -def move_right(speed=0.6): +def move_right(speed=0.8): motor_drive("M1", speed) motor_drive("M2", -speed) motor_drive("M3", -speed) @@ -99,13 +99,13 @@ def move_right(speed=0.6): # ====================== # 旋转 # ====================== -def rotate_left(speed=0.6): +def rotate_left(speed=0.8): motor_drive("M1", -speed) motor_drive("M2", -speed) motor_drive("M3", -speed) motor_drive("M4", -speed) -def rotate_right(speed=0.6): +def rotate_right(speed=0.8): motor_drive("M1", speed) motor_drive("M2", speed) motor_drive("M3", speed) @@ -114,25 +114,25 @@ def rotate_right(speed=0.6): # ====================== # 斜向移动 # ====================== -def move_left_forward(speed=0.6): +def move_left_forward(speed=0.8): motor_drive("M1", 0) motor_drive("M2", speed) motor_drive("M3", 0) motor_drive("M4", -speed) -def move_right_forward(speed=0.6): +def move_right_forward(speed=0.8): motor_drive("M1", speed) motor_drive("M2", 0) motor_drive("M3", -speed) motor_drive("M4", 0) -def move_left_backward(speed=0.6): +def move_left_backward(speed=0.8): motor_drive("M1", -speed) motor_drive("M2", 0) motor_drive("M3", speed) motor_drive("M4", 0) -def move_right_backward(speed=0.6): +def move_right_backward(speed=0.8): motor_drive("M1", 0) motor_drive("M2", -speed) motor_drive("M3", 0)