diff --git a/main/CSS/control.css b/main/CSS/control.css new file mode 100644 index 0000000..936b41d --- /dev/null +++ b/main/CSS/control.css @@ -0,0 +1,134 @@ +/* 移动控制相关样式 */ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 80vh; + padding: 20px; +} + +.title { + font-size: 24px; + font-weight: bold; + color: #333; + margin-bottom: 40px; + text-align: center; +} + +/* 手柄风格布局 */ +.controller { + display: flex; + flex-direction: column; + align-items: center; + gap: 30px; + width: 100%; + max-width: 350px; +} + +/* 移动控制按钮区域 */ +.movement-controls { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; + width: 100%; +} + +.movement-row { + display: flex; + gap: 15px; + justify-content: center; +} + +/* 录制回放按钮区域 */ +.record-controls { + display: flex; + gap: 15px; + justify-content: center; + width: 100%; + margin-top: 20px; +} + +/* 按钮样式 */ +.btn { + width: 120px; + height: 50px; + border: none; + border-radius: 25px; + font-size: 16px; + 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); +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0)); + z-index: 1; +} + +.btn span { + position: relative; + z-index: 2; +} + +.btn-forward { + background-color: #4CAF50; + color: white; +} + +.btn-backward { + background-color: #f44336; + color: white; +} + +.btn:hover { + transform: translateY(-3px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +/* 状态显示 */ +.status { + margin-top: 30px; + padding: 15px; + background-color: #f5f5f5; + border-radius: 10px; + min-height: 50px; + width: 100%; + max-width: 350px; + text-align: center; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.status p { + color: #666; + margin: 0; + font-size: 16px; +} + +/* 响应式设计 */ +@media (max-width: 400px) { + .btn { + width: 100px; + height: 45px; + font-size: 14px; + } + + .title { + font-size: 20px; + } +} \ No newline at end of file diff --git a/main/CSS/record.css b/main/CSS/record.css new file mode 100644 index 0000000..2b8e710 --- /dev/null +++ b/main/CSS/record.css @@ -0,0 +1,10 @@ +/* 录制回放相关样式 */ +.btn-record { + background-color: #ff9800; + color: white; +} + +.btn-playback { + background-color: #2196f3; + color: white; +} \ No newline at end of file diff --git a/main/CSS/style.css b/main/CSS/style.css new file mode 100644 index 0000000..1e43f28 --- /dev/null +++ b/main/CSS/style.css @@ -0,0 +1,79 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: Arial, sans-serif; + background-color: #f0f0f0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +.container { + background-color: white; + border-radius: 10px; + padding: 30px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + text-align: center; + width: 90%; + max-width: 400px; +} + +h1 { + color: #333; + margin-bottom: 30px; +} + +.controls { + display: flex; + justify-content: space-around; + margin-bottom: 30px; +} + +.btn { + width: 120px; + height: 50px; + border: none; + border-radius: 5px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-forward { + background-color: #4CAF50; + color: white; +} + +.btn-backward { + background-color: #f44336; + color: white; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.btn:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.status { + margin-top: 20px; + padding: 10px; + background-color: #f5f5f5; + border-radius: 5px; + min-height: 40px; +} + +.status p { + color: #666; +} \ No newline at end of file diff --git a/main/Javascript/control.js b/main/Javascript/control.js new file mode 100644 index 0000000..f307d4c --- /dev/null +++ b/main/Javascript/control.js @@ -0,0 +1,145 @@ +// 获取按钮和状态元素 +const forwardBtn = document.getElementById('forwardBtn'); +const backwardBtn = document.getElementById('backwardBtn'); +const status = document.getElementById('status'); + +// 实际控制函数 +function controlMotor(direction) { + // 根据方向设置状态消息 + if (direction === 'stop') { + status.innerHTML = `
正在停止...
`; + } else { + status.innerHTML = `正在${direction === 'forward' ? '前进' : '后退'}...
`; + } + + // 记录操作(如果正在录制) + if (window.isRecording) { + const timestamp = Date.now() - window.recordingStartTime; + window.recordedActions.push({ direction, timestamp }); + 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}
`; + }); +} + +// 按钮按下和松开事件 + +// 前进按钮 - 支持鼠标和触摸事件 +forwardBtn.addEventListener('mousedown', () => { + // 发送前进请求 + controlMotor('forward'); +}); + +forwardBtn.addEventListener('mouseup', () => { + // 停止电机 + stopMotor(); +}); + +forwardBtn.addEventListener('mouseleave', () => { + // 鼠标离开按钮时也停止 + stopMotor(); +}); + +// 前进按钮 - 触摸事件 +forwardBtn.addEventListener('touchstart', (e) => { + e.preventDefault(); // 防止默认行为 + // 发送前进请求 + controlMotor('forward'); +}); + +forwardBtn.addEventListener('touchend', (e) => { + e.preventDefault(); // 防止默认行为 + // 停止电机 + stopMotor(); +}); + +forwardBtn.addEventListener('touchcancel', (e) => { + e.preventDefault(); // 防止默认行为 + // 触摸被取消时停止电机 + stopMotor(); +}); + +// 后退按钮 - 支持鼠标和触摸事件 +backwardBtn.addEventListener('mousedown', () => { + // 发送后退请求 + controlMotor('backward'); +}); + +backwardBtn.addEventListener('mouseup', () => { + // 停止电机 + stopMotor(); +}); + +backwardBtn.addEventListener('mouseleave', () => { + // 鼠标离开按钮时也停止 + stopMotor(); +}); + +// 后退按钮 - 触摸事件 +backwardBtn.addEventListener('touchstart', (e) => { + e.preventDefault(); // 防止默认行为 + // 发送后退请求 + controlMotor('backward'); +}); + +backwardBtn.addEventListener('touchend', (e) => { + e.preventDefault(); // 防止默认行为 + // 停止电机 + stopMotor(); +}); + +backwardBtn.addEventListener('touchcancel', (e) => { + e.preventDefault(); // 防止默认行为 + // 触摸被取消时停止电机 + stopMotor(); +}); + +// 停止电机函数 +function stopMotor() { + // 记录操作(如果正在录制) + if (window.isRecording) { + const timestamp = Date.now() - window.recordingStartTime; + window.recordedActions.push({ direction: 'stop', timestamp }); + console.log('Recorded action:', { direction: 'stop', timestamp }); + } + + // 发送停止请求到服务器 + fetch('/control', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ direction: 'stop' }) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + // 停止成功,不显示消息 + } else { + status.innerHTML = `错误: ${data.message}
`; + } + }) + .catch(error => { + status.innerHTML = `通信错误: ${error.message}
`; + }); +} \ No newline at end of file diff --git a/main/Javascript/record.js b/main/Javascript/record.js new file mode 100644 index 0000000..171aa7e --- /dev/null +++ b/main/Javascript/record.js @@ -0,0 +1,112 @@ +// 确保在DOM加载完成后执行 +window.addEventListener('DOMContentLoaded', function() { + console.log('DOMContentLoaded - Record.js executing'); + + // 录制相关全局变量 + window.isRecording = false; + window.recordedActions = []; + window.recordingStartTime = 0; + + // 获取录制和回放按钮 + const recordBtn = document.getElementById('recordBtn'); + const playbackBtn = document.getElementById('playbackBtn'); + const status = document.getElementById('status'); + + // 调试信息 + console.log('Record.js loaded'); + console.log('recordBtn:', recordBtn); + console.log('playbackBtn:', playbackBtn); + console.log('status:', status); + + // 录制按钮点击事件 - 支持鼠标和触摸事件 + function toggleRecording() { + console.log('toggleRecording called, current isRecording:', window.isRecording); + if (window.isRecording) { + // 停止录制 + window.isRecording = false; + recordBtn.textContent = '开始录制'; + status.innerHTML = `录制完成,共记录 ${window.recordedActions.length} 个操作
`; + console.log('Recording stopped, actions:', window.recordedActions); + } else { + // 开始录制 + window.isRecording = true; + window.recordedActions = []; + window.recordingStartTime = Date.now(); + recordBtn.textContent = '停止录制'; + status.innerHTML = `开始录制...
`; + console.log('Recording started'); + } + } + + console.log('Adding click event listener to recordBtn'); + recordBtn.addEventListener('click', toggleRecording); + + // 录制按钮 - 触摸事件 + console.log('Adding touchstart event listener to recordBtn'); + recordBtn.addEventListener('touchstart', (e) => { + console.log('Touchstart event on recordBtn'); + e.preventDefault(); // 防止默认行为 + toggleRecording(); + }); + + // 回放按钮点击事件 - 支持鼠标和触摸事件 + function startPlayback() { + console.log('startPlayback called, recordedActions:', window.recordedActions); + if (window.recordedActions.length === 0) { + status.innerHTML = `没有可回放的操作
`; + console.log('No actions to playback'); + return; + } + + // 保存当前录制状态并禁用录制 + const originalRecordingState = window.isRecording; + window.isRecording = false; + + status.innerHTML = `开始回放...
`; + console.log('Starting playback'); + + // 按时间顺序回放操作 + let lastTimestamp = 0; + const totalDuration = window.recordedActions[window.recordedActions.length - 1].timestamp; + + // 确保至少有一个操作 + if (window.recordedActions.length > 0) { + // 执行第一个操作 + console.log('Executing first action immediately:', window.recordedActions[0]); + controlMotor(window.recordedActions[0].direction); + + // 执行剩余的操作 + for (let i = 1; i < window.recordedActions.length; i++) { + const action = window.recordedActions[i]; + const delay = action.timestamp - window.recordedActions[i-1].timestamp; + console.log('Scheduling action:', action, 'at delay:', delay); + setTimeout(() => { + // 调用控制函数执行操作 + console.log('Executing action:', action); + controlMotor(action.direction); + }, delay); + } + } + + // 回放完成后,确保发送停止请求 + setTimeout(() => { + console.log('Playback completed, sending stop command'); + controlMotor('stop'); + status.innerHTML = `回放完成
`; + console.log('Playback completed'); + // 恢复原始录制状态 + window.isRecording = originalRecordingState; + }, totalDuration + 1000); + } + + console.log('Adding click event listener to playbackBtn'); + playbackBtn.addEventListener('click', startPlayback); + + // 回放按钮 - 触摸事件 + console.log('Adding touchstart event listener to playbackBtn'); + playbackBtn.addEventListener('touchstart', (e) => { + console.log('Touchstart event on playbackBtn'); + e.preventDefault(); // 防止默认行为 + startPlayback(); + }); +}); \ No newline at end of file diff --git a/main/Javascript/script.js b/main/Javascript/script.js new file mode 100644 index 0000000..502ec25 --- /dev/null +++ b/main/Javascript/script.js @@ -0,0 +1,38 @@ +// 获取按钮和状态元素 +const forwardBtn = document.getElementById('forwardBtn'); +const backwardBtn = document.getElementById('backwardBtn'); +const status = document.getElementById('status'); + +// 实际控制函数 +function controlMotor(direction) { + status.innerHTML = `正在${direction === 'forward' ? '前进' : '后退'}...
`; + + // 发送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}
`; + }); +} + +// 按钮点击事件 +forwardBtn.addEventListener('click', () => { + controlMotor('forward'); +}); + +backwardBtn.addEventListener('click', () => { + controlMotor('backward'); +}); \ No newline at end of file diff --git a/main/__pycache__/motor.cpython-313.pyc b/main/__pycache__/motor.cpython-313.pyc new file mode 100644 index 0000000..564929a Binary files /dev/null and b/main/__pycache__/motor.cpython-313.pyc differ diff --git a/main/camera.py b/main/camera.py index 709b25c..3338e24 100644 --- a/main/camera.py +++ b/main/camera.py @@ -1,91 +1,229 @@ import cv2 import numpy as np +import motor +import time +import signal +import sys +# 信号处理函数,用于捕获Ctrl+C +def signal_handler(sig, frame): + print("\n接收到中断信号,正在停止电机...") + motor.stop() + print("电机已停止,程序退出") + 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) -# ===== 核心改动:用背景减法器替代帧差法 ===== -# MOG2: 能适应背景变化,适合移动摄像头场景 -# history: 保留多少帧来建立背景模型(越大适应越慢,但越稳定) -# varThreshold: 判断前景的灵敏度(越小越敏感,但噪声越多) -fgbg = cv2.createBackgroundSubtractorMOG2(history=100, varThreshold=25, detectShadows=False) +print(f"摄像头分辨率: {cap.get(3)}x{cap.get(4)}") -# 可选:用KNN背景减法器(效果类似,速度稍快) -# fgbg = cv2.createBackgroundSubtractorKNN(history=100, dist2Threshold=400, detectShadows=False) +# 初始化稳定阶段 +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}帧") -# 形态学核(用于去噪) -kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) +# 读取初始帧 +ret, prev_frame = cap.read() +if not ret: + print("无法读取初始帧!") + cap.release() + exit() -# 用于平滑追踪的卡尔曼滤波器(可选,减少抖动) -kalman = cv2.KalmanFilter(4, 2) -kalman.measurementMatrix = np.array([[1,0,0,0], [0,1,0,0]], np.float32) -kalman.transitionMatrix = np.array([[1,0,1,0], [0,1,0,1], [0,0,1,0], [0,0,0,1]], np.float32) -kalman.processNoiseCov = np.eye(4, dtype=np.float32) * 0.03 -kalman.measurementNoiseCov = np.eye(2, dtype=np.float32) * 0.5 -kalman.statePost = np.zeros((4,1), np.float32) +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 - - # 1. 背景减法:得到前景掩码(白色=运动物体) - fgmask = fgbg.apply(frame) - - # 2. 形态学处理:去噪 + 填充空洞 - fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel) # 先开运算去噪点 - fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_CLOSE, kernel) # 再闭运算填充空洞 - - # 3. 找轮廓 - contours, _ = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - # ===== 核心:选最大目标 ===== + + 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 < 300: # 过滤噪声 + + if area < 500: # 过滤噪声,增大阈值 continue - if area > max_area: - max_area = area - target = cnt + 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') + + 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 < 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 - - # 卡尔曼滤波预测/更新(可选,让追踪更平滑) - measurement = np.array([[np.float32(cx)], [np.float32(cy)]]) - kalman.correct(measurement) - prediction = kalman.predict() - pred_x, pred_y = int(prediction[0]), int(prediction[1]) - - # 画原始检测框 + cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.circle(frame, (cx, cy), 5, (0, 0, 255), -1) - # 画卡尔曼平滑后的位置(黄色) - cv2.circle(frame, (pred_x, pred_y), 5, (0, 255, 255), -1) + + print("唯一目标:", cx, cy, "area:", max_area) - print(f"目标位置: ({cx}, {cy}), 面积: {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 + + # 控制逻辑:根据y轴坐标控制垃圾桶前后移动 + center_y = 120 # 画面中心y坐标 + threshold = 20 # 阈值范围 + speed = 3000 # 电机速度 + + if target_jumped: + # 目标跳变,停止移动 + print("目标跳变,停止移动") + motor.stop() + elif cy < center_y - threshold: + # 目标在上侧,垃圾桶向后移动 + print("目标在上侧,向后移动") + # 使用新的电机控制函数 + motor.backward(speed=0.6) + elif cy > center_y + threshold: + # 目标在下侧,垃圾桶向前移动 + print("目标在下侧,向前移动") + # 使用新的电机控制函数 + motor.forward(speed=0.6) + else: + # 目标在中心附近,停止移动 + print("目标在中心,停止移动") + motor.stop() + + # 更新上一帧的目标信息 + prev_cx = cx + prev_cy = cy + prev_area = max_area else: - # 没有检测到目标时,只预测不更新 - prediction = kalman.predict() - # 可选:根据预测位置保持追踪(即使被短暂遮挡) - - # 可选:显示前景掩码(调试用) - cv2.imshow("foreground mask", fgmask) + # 没有检测到目标,停止移动 + 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() \ No newline at end of file +cv2.destroyAllWindows() +motor.stop() \ No newline at end of file diff --git a/main/index.html b/main/index.html new file mode 100644 index 0000000..05b6029 --- /dev/null +++ b/main/index.html @@ -0,0 +1,42 @@ + + + + + +就绪
+