初版测试
This commit is contained in:
parent
82e8c1d43f
commit
9d8d7444b4
134
main/CSS/control.css
Normal file
134
main/CSS/control.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
10
main/CSS/record.css
Normal file
10
main/CSS/record.css
Normal file
@ -0,0 +1,10 @@
|
||||
/* 录制回放相关样式 */
|
||||
.btn-record {
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-playback {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
79
main/CSS/style.css
Normal file
79
main/CSS/style.css
Normal file
@ -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;
|
||||
}
|
||||
145
main/Javascript/control.js
Normal file
145
main/Javascript/control.js
Normal file
@ -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 = `<p>正在停止...</p>`;
|
||||
} else {
|
||||
status.innerHTML = `<p>正在${direction === 'forward' ? '前进' : '后退'}...</p>`;
|
||||
}
|
||||
|
||||
// 记录操作(如果正在录制)
|
||||
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 = `<p>${data.message}</p>`;
|
||||
} else {
|
||||
status.innerHTML = `<p>错误: ${data.message}</p>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 按钮按下和松开事件
|
||||
|
||||
// 前进按钮 - 支持鼠标和触摸事件
|
||||
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 = `<p>错误: ${data.message}</p>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
112
main/Javascript/record.js
Normal file
112
main/Javascript/record.js
Normal file
@ -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 = `<p>录制完成,共记录 ${window.recordedActions.length} 个操作</p>`;
|
||||
console.log('Recording stopped, actions:', window.recordedActions);
|
||||
} else {
|
||||
// 开始录制
|
||||
window.isRecording = true;
|
||||
window.recordedActions = [];
|
||||
window.recordingStartTime = Date.now();
|
||||
recordBtn.textContent = '停止录制';
|
||||
status.innerHTML = `<p>开始录制...</p>`;
|
||||
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 = `<p>没有可回放的操作</p>`;
|
||||
console.log('No actions to playback');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前录制状态并禁用录制
|
||||
const originalRecordingState = window.isRecording;
|
||||
window.isRecording = false;
|
||||
|
||||
status.innerHTML = `<p>开始回放...</p>`;
|
||||
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 = `<p>回放完成</p>`;
|
||||
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();
|
||||
});
|
||||
});
|
||||
38
main/Javascript/script.js
Normal file
38
main/Javascript/script.js
Normal file
@ -0,0 +1,38 @@
|
||||
// 获取按钮和状态元素
|
||||
const forwardBtn = document.getElementById('forwardBtn');
|
||||
const backwardBtn = document.getElementById('backwardBtn');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
// 实际控制函数
|
||||
function controlMotor(direction) {
|
||||
status.innerHTML = `<p>正在${direction === 'forward' ? '前进' : '后退'}...</p>`;
|
||||
|
||||
// 发送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 = `<p>${data.message}</p>`;
|
||||
} else {
|
||||
status.innerHTML = `<p>错误: ${data.message}</p>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 按钮点击事件
|
||||
forwardBtn.addEventListener('click', () => {
|
||||
controlMotor('forward');
|
||||
});
|
||||
|
||||
backwardBtn.addEventListener('click', () => {
|
||||
controlMotor('backward');
|
||||
});
|
||||
BIN
main/__pycache__/motor.cpython-313.pyc
Normal file
BIN
main/__pycache__/motor.cpython-313.pyc
Normal file
Binary file not shown.
244
main/camera.py
244
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()
|
||||
cv2.destroyAllWindows()
|
||||
motor.stop()
|
||||
42
main/index.html
Normal file
42
main/index.html
Normal file
@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>垃圾桶控制界面</title>
|
||||
<link rel="stylesheet" href="CSS/control.css">
|
||||
<link rel="stylesheet" href="CSS/record.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="title">垃圾桶控制界面</h1>
|
||||
|
||||
<!-- 手柄风格控制器 -->
|
||||
<div class="controller">
|
||||
<!-- 移动控制区域 -->
|
||||
<div class="movement-controls">
|
||||
<div class="movement-row">
|
||||
<button class="btn btn-forward" id="forwardBtn"><span>前进</span></button>
|
||||
</div>
|
||||
<div class="movement-row">
|
||||
<button class="btn btn-backward" id="backwardBtn"><span>后退</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 录制回放控制区域 -->
|
||||
<div class="record-controls">
|
||||
<button class="btn btn-record" id="recordBtn"><span>开始录制</span></button>
|
||||
<button class="btn btn-playback" id="playbackBtn"><span>回放</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态显示区域 -->
|
||||
<div class="status" id="status">
|
||||
<p>就绪</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="Javascript/control.js"></script>
|
||||
<script src="Javascript/record.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
239
main/motor.py
239
main/motor.py
@ -1,104 +1,143 @@
|
||||
from smbus2 import SMBus
|
||||
import time
|
||||
from smbus2 import SMBus
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
|
||||
bus = SMBus(1)
|
||||
addr = 0x60
|
||||
|
||||
# ======================
|
||||
# PCA9685 初始化
|
||||
# ======================
|
||||
bus.write_byte_data(addr, 0x00, 0x00)
|
||||
bus.write_byte_data(addr, 0xFE, 60)
|
||||
|
||||
# ======================
|
||||
# PWM输出(0~4095)
|
||||
# ======================
|
||||
def set_pwm(ch, val):
|
||||
val = max(0, min(4095, val))
|
||||
base = 0x06 + 4 * ch
|
||||
bus.write_byte_data(addr, base, 0)
|
||||
bus.write_byte_data(addr, base+1, 0)
|
||||
bus.write_byte_data(addr, base+2, val & 0xFF)
|
||||
bus.write_byte_data(addr, base+3, val >> 8)
|
||||
|
||||
# ======================
|
||||
# ⭐ 你的真实映射(8通道版本)
|
||||
# ======================
|
||||
MOTORS = {
|
||||
"M1": {"in1": 0, "in2": 1},
|
||||
"M2": {"in1": 2, "in2": 3},
|
||||
"M3": {"in1": 4, "in2": 5},
|
||||
"M4": {"in1": 6, "in2": 7},
|
||||
}
|
||||
|
||||
# ======================
|
||||
# 电机控制(重点)
|
||||
# ======================
|
||||
def motor(name, speed=0, direction="stop"):
|
||||
m = MOTORS[name]
|
||||
|
||||
# 速度限制
|
||||
speed = max(0, min(4095, speed))
|
||||
|
||||
if direction == "f":
|
||||
set_pwm(m["in1"], speed)
|
||||
set_pwm(m["in2"], 0)
|
||||
|
||||
elif direction == "b":
|
||||
set_pwm(m["in1"], 0)
|
||||
set_pwm(m["in2"], speed)
|
||||
|
||||
else:
|
||||
set_pwm(m["in1"], 0)
|
||||
set_pwm(m["in2"], 0)
|
||||
|
||||
# ======================
|
||||
# 四轮控制
|
||||
# ======================
|
||||
def all_motors(speed, direction):
|
||||
for m in MOTORS:
|
||||
motor(m, speed, direction)
|
||||
|
||||
def forward(speed=3500):
|
||||
all_motors(speed, "f")
|
||||
|
||||
def backward(speed=3500):
|
||||
all_motors(speed, "b")
|
||||
|
||||
def stop():
|
||||
all_motors(0, "stop")
|
||||
|
||||
# ======================
|
||||
# 转向(差速)
|
||||
# ======================
|
||||
def turn_left(speed=3500):
|
||||
motor("M1", speed, "b")
|
||||
motor("M3", speed, "b")
|
||||
motor("M2", speed, "f")
|
||||
motor("M4", speed, "f")
|
||||
|
||||
def turn_right(speed=3500):
|
||||
motor("M1", speed, "f")
|
||||
motor("M3", speed, "f")
|
||||
motor("M2", speed, "b")
|
||||
motor("M4", speed, "b")
|
||||
|
||||
# ======================
|
||||
# 测试
|
||||
# ======================
|
||||
if __name__ == "__main__":
|
||||
|
||||
print("前进")
|
||||
forward(3500)
|
||||
time.sleep(3)
|
||||
|
||||
print("停止")
|
||||
# 信号处理函数,用于捕获Ctrl+C
|
||||
def signal_handler(sig, frame):
|
||||
print("\n接收到中断信号,正在停止电机...")
|
||||
stop()
|
||||
time.sleep(1)
|
||||
print("电机已停止,程序退出")
|
||||
sys.exit(0)
|
||||
|
||||
print("后退")
|
||||
backward(3500)
|
||||
time.sleep(3)
|
||||
# 注册信号处理函数
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
print("停止")
|
||||
stop()
|
||||
# ======================
|
||||
# PCA9685 初始化
|
||||
# ======================
|
||||
bus = SMBus(1)
|
||||
addr = 0x60
|
||||
|
||||
MODE1 = 0x00
|
||||
PRESCALE = 0xFE
|
||||
|
||||
bus.write_byte_data(addr, MODE1, 0x00)
|
||||
bus.write_byte_data(addr, PRESCALE, 60)
|
||||
|
||||
# ======================
|
||||
# 电机通道映射(每个电机 IN1 / IN2)
|
||||
# ======================
|
||||
MOTOR = {
|
||||
"M1": (0, 1), # 前左
|
||||
"M2": (2, 3), # 前右
|
||||
"M3": (4, 5), # 后左
|
||||
"M4": (6, 7), # 后右
|
||||
}
|
||||
|
||||
# ======================
|
||||
# 方向修正(麦克纳姆关键)
|
||||
# 根据你 / \ + \ / 结构校正
|
||||
# ======================
|
||||
DIR = {
|
||||
"M1": 1,
|
||||
"M2": -1,
|
||||
"M3": -1,
|
||||
"M4": 1,
|
||||
}
|
||||
|
||||
# ======================
|
||||
# PWM输出函数
|
||||
# ======================
|
||||
def set_pwm(ch, value):
|
||||
value = max(0, min(4095, value))
|
||||
bus.write_byte_data(addr, 0x06 + 4 * ch, 0)
|
||||
bus.write_byte_data(addr, 0x07 + 4 * ch, 0)
|
||||
bus.write_byte_data(addr, 0x08 + 4 * ch, value & 0xFF)
|
||||
bus.write_byte_data(addr, 0x09 + 4 * ch, value >> 8)
|
||||
|
||||
# ======================
|
||||
# 单电机控制
|
||||
# speed: -1 ~ 1
|
||||
# ======================
|
||||
def motor_drive(name, speed):
|
||||
in1, in2 = MOTOR[name]
|
||||
|
||||
speed *= DIR[name]
|
||||
pwm = int(abs(speed) * 4095)
|
||||
|
||||
if speed > 0:
|
||||
set_pwm(in1, pwm)
|
||||
set_pwm(in2, 0)
|
||||
|
||||
elif speed < 0:
|
||||
set_pwm(in1, 0)
|
||||
set_pwm(in2, pwm)
|
||||
|
||||
else:
|
||||
# 刹车(关键,不是0!)
|
||||
set_pwm(in1, 4095)
|
||||
set_pwm(in2, 4095)
|
||||
|
||||
# ======================
|
||||
# 基础运动控制
|
||||
# ======================
|
||||
def forward(speed=0.6):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", speed)
|
||||
|
||||
def backward(speed=0.6):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
# ======================
|
||||
# 麦克纳姆左右移动
|
||||
# ======================
|
||||
def move_left(speed=0.6):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def move_right(speed=0.6):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", speed)
|
||||
|
||||
# ======================
|
||||
# 停止(刹车模式)
|
||||
# ======================
|
||||
def stop():
|
||||
motor_drive("M1", 0)
|
||||
motor_drive("M2", 0)
|
||||
motor_drive("M3", 0)
|
||||
motor_drive("M4", 0)
|
||||
|
||||
# ======================
|
||||
# 测试程序
|
||||
# ======================
|
||||
if __name__ == "__main__":
|
||||
|
||||
print("前进")
|
||||
forward(0.5)
|
||||
time.sleep(1)
|
||||
|
||||
print("停止")
|
||||
stop()
|
||||
time.sleep(1)
|
||||
|
||||
print("后退")
|
||||
backward(0.5)
|
||||
time.sleep(1)
|
||||
|
||||
print("停止")
|
||||
stop()
|
||||
time.sleep(1)
|
||||
|
||||
print("停止")
|
||||
stop()
|
||||
|
||||
51
main/server.py
Normal file
51
main/server.py
Normal file
@ -0,0 +1,51 @@
|
||||
from flask import Flask, send_file, request, jsonify, send_from_directory
|
||||
import motor
|
||||
import time
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 静态文件路由
|
||||
@app.route('/CSS/<path:filename>')
|
||||
def serve_css(filename):
|
||||
return send_from_directory('CSS', filename)
|
||||
|
||||
@app.route('/Javascript/<path:filename>')
|
||||
def serve_js(filename):
|
||||
return send_from_directory('Javascript', filename)
|
||||
|
||||
# 主页路由
|
||||
@app.route('/')
|
||||
def index():
|
||||
return send_file('index.html')
|
||||
|
||||
# 控制电机路由
|
||||
@app.route('/control', methods=['POST'])
|
||||
def control():
|
||||
data = request.get_json()
|
||||
direction = data.get('direction')
|
||||
|
||||
# forward 与 backward 反向调整
|
||||
try:
|
||||
if direction == 'forward':
|
||||
print("控制垃圾桶前进")
|
||||
motor.backward(speed=0.6)
|
||||
# 不使用time.sleep,让电机持续运行
|
||||
return jsonify({'status': 'success', 'message': '前进中'})
|
||||
elif direction == 'backward':
|
||||
print("控制垃圾桶后退")
|
||||
motor.forward(speed=0.6)
|
||||
# 不使用time.sleep,让电机持续运行
|
||||
return jsonify({'status': 'success', 'message': '后退中'})
|
||||
elif direction == 'stop':
|
||||
print("停止垃圾桶")
|
||||
motor.stop()
|
||||
return jsonify({'status': 'success', 'message': '已停止'})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': '无效的方向'})
|
||||
except Exception as e:
|
||||
print(f"控制出错: {e}")
|
||||
return jsonify({'status': 'error', 'message': f'控制出错: {e}'})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
Loading…
x
Reference in New Issue
Block a user