initial commit
This commit is contained in:
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
.vscode/
|
||||
|
||||
|
||||
/static/received/left/
|
||||
/static/received/right/
|
||||
|
||||
# 数据文件及数据库
|
||||
*.zip
|
||||
*.db
|
||||
|
||||
208
cam_cap.py
Normal file
208
cam_cap.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import cv2
|
||||
import pynng
|
||||
import struct
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from threading import Thread, Lock
|
||||
from queue import Queue, Empty
|
||||
import logging
|
||||
|
||||
HTTP_SERVER_URL = "http://127.0.0.1:5000/upload"
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ImageBuffer:
|
||||
def __init__(self, max_size=2):
|
||||
self.left_queue = Queue(maxsize=max_size)
|
||||
self.right_queue = Queue(maxsize=max_size)
|
||||
self.lock = Lock()
|
||||
|
||||
def update_frames(self, left_frame, right_frame):
|
||||
"""更新缓冲区中的帧,如果缓冲区满则丢弃旧帧"""
|
||||
with self.lock:
|
||||
try:
|
||||
if not self.left_queue.full():
|
||||
self.left_queue.put_nowait(left_frame)
|
||||
else:
|
||||
try:
|
||||
self.left_queue.get_nowait() # 丢弃旧帧
|
||||
self.left_queue.put_nowait(left_frame)
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
if not self.right_queue.full():
|
||||
self.right_queue.put_nowait(right_frame)
|
||||
else:
|
||||
try:
|
||||
self.right_queue.get_nowait() # 丢弃旧帧
|
||||
self.right_queue.put_nowait(right_frame)
|
||||
except Empty:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_latest_frames(self):
|
||||
"""获取最新的帧对"""
|
||||
with self.lock:
|
||||
try:
|
||||
left_frames = []
|
||||
right_frames = []
|
||||
|
||||
# 获取所有可用的左帧
|
||||
while not self.left_queue.empty():
|
||||
try:
|
||||
left_frames.append(self.left_queue.get_nowait())
|
||||
except Empty:
|
||||
break
|
||||
|
||||
# 获取所有可用的右帧
|
||||
while not self.right_queue.empty():
|
||||
try:
|
||||
right_frames.append(self.right_queue.get_nowait())
|
||||
except Empty:
|
||||
break
|
||||
|
||||
if left_frames and right_frames:
|
||||
# 返回最新的帧对
|
||||
return left_frames[-1], right_frames[-1]
|
||||
except:
|
||||
pass
|
||||
return None, None
|
||||
|
||||
# 全局缓冲区
|
||||
image_buffer = ImageBuffer()
|
||||
|
||||
def capture_thread():
|
||||
"""异步捕获线程"""
|
||||
cam_left = cv2.VideoCapture('/dev/videoL')
|
||||
cam_right = cv2.VideoCapture('/dev/videoR')
|
||||
|
||||
# # 设置摄像头分辨率
|
||||
cam_left.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||||
cam_left.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||||
cam_right.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||||
cam_right.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||||
|
||||
while True:
|
||||
ret_left, frame_left = cam_left.read()
|
||||
ret_right, frame_right = cam_right.read()
|
||||
|
||||
if ret_left and ret_right:
|
||||
image_buffer.update_frames(frame_left.copy(), frame_right.copy())
|
||||
# print(f"num of left frames: {image_buffer.left_queue.qsize()}")
|
||||
# print(f"num of right frames: {image_buffer.right_queue.qsize()}")
|
||||
else:
|
||||
print(f"Error capture thread, ret_left: {ret_left}, ret_right: {ret_right}")
|
||||
|
||||
time.sleep(0.03) # ~30fps
|
||||
|
||||
cam_left.release()
|
||||
cam_right.release()
|
||||
|
||||
async def send_image_to_web_server(frame_left, frame_right, metadata):
|
||||
"""异步发送图片到Web服务器(留空实现)"""
|
||||
try:
|
||||
# 编码为JPEG
|
||||
_, jpeg_left = cv2.imencode('.jpg', frame_left)
|
||||
_, jpeg_right = cv2.imencode('.jpg', frame_right)
|
||||
jpeg_left = jpeg_left.tobytes()
|
||||
jpeg_right = jpeg_right.tobytes()
|
||||
|
||||
# HTTP POST请求实现留空
|
||||
print(f"Would send frame {metadata['timestamp']} to web server via HTTP POST")
|
||||
# print(f"Client ID: {metadata['client_id']}")
|
||||
cv2.imwrite(f"./saved/left_{metadata['timestamp']}.jpg", frame_left)
|
||||
cv2.imwrite(f"./saved/right_{metadata['timestamp']}.jpg", frame_right)
|
||||
# 这里可以添加实际的HTTP POST请求代码
|
||||
# 示例:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field('left_image', jpeg_left, filename='left.jpg', content_type='image/jpeg')
|
||||
data.add_field('right_image', jpeg_right, filename='right.jpg', content_type='image/jpeg')
|
||||
data.add_field('metadata', json.dumps(metadata))
|
||||
|
||||
async with session.post(HTTP_SERVER_URL, data=data) as response:
|
||||
if response.status == 200:
|
||||
print("Images sent successfully")
|
||||
else:
|
||||
print(f"Failed to send images: {response.status}")
|
||||
|
||||
# 模拟发送延迟
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending images to web server: {e}")
|
||||
|
||||
class ImageServer:
|
||||
def __init__(self, address="tcp://0.0.0.0:54321"):
|
||||
self.address = address
|
||||
self.socket = None
|
||||
|
||||
async def handle_client_request(self, client_id):
|
||||
"""处理客户端请求:获取最新帧并发送到Web服务器"""
|
||||
try:
|
||||
# 从缓冲区获取最新帧
|
||||
frame_left, frame_right = image_buffer.get_latest_frames()
|
||||
if frame_left is not None and frame_right is not None:
|
||||
# 创建元数据
|
||||
metadata = {
|
||||
"timestamp": time.time(),
|
||||
# "client_id": client_id,
|
||||
# "resolution": {
|
||||
# "width": frame_left.shape[1],
|
||||
# "height": frame_left.shape[0]
|
||||
# }
|
||||
}
|
||||
|
||||
# 异步发送图片到Web服务器
|
||||
asyncio.create_task(send_image_to_web_server(frame_left, frame_right, metadata))
|
||||
|
||||
logger.info(f"Processed request from client {client_id}, frames sent to web server")
|
||||
return b'ACK'
|
||||
else:
|
||||
logger.warning(f"No frames available for client {client_id}")
|
||||
return b'NO_FRAMES'
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing request for client {client_id}: {e}")
|
||||
return b'ERROR'
|
||||
|
||||
async def run(self):
|
||||
"""运行服务器"""
|
||||
self.socket = pynng.Rep0()
|
||||
self.socket.listen(self.address)
|
||||
logger.info(f"Image server listening on {self.address}...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
# 接收客户端请求(客户端ID)
|
||||
data = await self.socket.arecv()
|
||||
|
||||
# 解析客户端ID
|
||||
client_id = data.decode('utf-8') if data else "unknown"
|
||||
# logger.info(f"Received request from client: {client_id}")
|
||||
|
||||
# 处理请求并发送响应
|
||||
response = await self.handle_client_request(client_id)
|
||||
await self.socket.asend(response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Server error: {e}")
|
||||
break
|
||||
|
||||
async def main():
|
||||
# 启动捕获线程
|
||||
capture_thread_obj = Thread(target=capture_thread, daemon=True)
|
||||
capture_thread_obj.start()
|
||||
print("Capture thread started")
|
||||
|
||||
# 启动图像服务器
|
||||
server = ImageServer()
|
||||
await server.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
314
cam_web.py
Normal file
314
cam_web.py
Normal file
@@ -0,0 +1,314 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from flask import Flask, render_template, request, jsonify, send_file
|
||||
from flask_socketio import SocketIO, emit
|
||||
import io
|
||||
import base64
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
import zipfile
|
||||
import tempfile
|
||||
|
||||
from cap_trigger import ImageClient
|
||||
|
||||
|
||||
|
||||
# --- 配置 ---
|
||||
SAVE_PATH_LEFT = "./static/received/left"
|
||||
SAVE_PATH_RIGHT = "./static/received/right"
|
||||
FLASK_HOST = "0.0.0.0"
|
||||
FLASK_PORT = 5000
|
||||
MAX_LIVE_FRAMES = 2 # 保留最新的几帧用于实时显示
|
||||
DATABASE_PATH = "received_images.db" # SQLite 数据库文件路径
|
||||
# --- 配置 ---
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- 数据库初始化 ---
|
||||
def init_db():
|
||||
"""初始化 SQLite 数据库和表"""
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
cursor = conn.cursor()
|
||||
# 创建表,存储图片信息
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
left_filename TEXT NOT NULL,
|
||||
right_filename TEXT NOT NULL,
|
||||
timestamp REAL NOT NULL,
|
||||
metadata TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f"Database {DATABASE_PATH} initialized.")
|
||||
|
||||
# --- 全局变量 ---
|
||||
# 用于存储最新的左右帧,用于实时显示
|
||||
latest_left_frame = None
|
||||
latest_right_frame = None
|
||||
latest_timestamp = None
|
||||
frame_lock = threading.Lock() # 保护全局帧变量
|
||||
|
||||
# --- Flask & SocketIO 应用 ---
|
||||
app = Flask(__name__)
|
||||
# 为生产环境配置 SECRET_KEY
|
||||
app.config['SECRET_KEY'] = 'your-secret-key-change-this'
|
||||
# 配置异步模式,如果需要异步处理可以调整
|
||||
socketio = SocketIO(app, cors_allowed_origins="*") # 允许所有来源,生产环境请具体配置
|
||||
|
||||
# 初始化图像客户端
|
||||
image_client = ImageClient("tcp://127.0.0.1:54321", client_id="local")
|
||||
|
||||
# 初始化数据库
|
||||
init_db()
|
||||
|
||||
# --- Flask 路由 ---
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页,加载实时图像页面"""
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/list') # 新增路由用于显示图片列表
|
||||
def list_images():
|
||||
"""加载图片列表页面"""
|
||||
return render_template('list_images.html')
|
||||
|
||||
@app.route('/api/images', methods=['GET'])
|
||||
def get_images_api():
|
||||
"""API: 获取图片列表 (JSON 格式)"""
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
cursor = conn.cursor()
|
||||
# 按时间倒序排列
|
||||
cursor.execute("SELECT id, left_filename, right_filename, timestamp, metadata, created_at FROM images ORDER BY timestamp DESC")
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
images = []
|
||||
for row in rows:
|
||||
images.append({
|
||||
"id": row[0],
|
||||
"left_filename": row[1],
|
||||
"right_filename": row[2],
|
||||
"timestamp": row[3],
|
||||
"metadata": row[4],
|
||||
"created_at": row[5]
|
||||
})
|
||||
return jsonify(images)
|
||||
|
||||
@app.route('/api/images', methods=['DELETE'])
|
||||
def delete_image_api():
|
||||
"""API: 删除单张图片记录及其文件"""
|
||||
image_id = request.json.get('id')
|
||||
if not image_id:
|
||||
return jsonify({"error": "Image ID is required"}), 400
|
||||
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
cursor = conn.cursor()
|
||||
# 查询文件名
|
||||
cursor.execute("SELECT left_filename, right_filename FROM images WHERE id = ?", (image_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return jsonify({"error": "Image not found"}), 404
|
||||
|
||||
left_filename, right_filename = row
|
||||
# 删除数据库记录
|
||||
cursor.execute("DELETE FROM images WHERE id = ?", (image_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# 删除对应的文件
|
||||
left_path = os.path.join(SAVE_PATH_LEFT, left_filename)
|
||||
right_path = os.path.join(SAVE_PATH_RIGHT, right_filename)
|
||||
try:
|
||||
if os.path.exists(left_path):
|
||||
os.remove(left_path)
|
||||
logger.info(f"Deleted file: {left_path}")
|
||||
if os.path.exists(right_path):
|
||||
os.remove(right_path)
|
||||
logger.info(f"Deleted file: {right_path}")
|
||||
except OSError as e:
|
||||
logger.error(f"Error deleting files: {e}")
|
||||
# 即使删除文件失败,数据库记录也已删除,返回成功
|
||||
pass
|
||||
|
||||
return jsonify({"message": f"Image {image_id} deleted successfully"})
|
||||
|
||||
@app.route('/api/images/export', methods=['POST'])
|
||||
def export_images_api():
|
||||
"""API: 打包导出选中的图片"""
|
||||
selected_ids = request.json.get('ids', [])
|
||||
if not selected_ids:
|
||||
return jsonify({"error": "No image IDs selected"}), 400
|
||||
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
cursor = conn.cursor()
|
||||
placeholders = ','.join('?' * len(selected_ids))
|
||||
cursor.execute(f"SELECT left_filename, right_filename FROM images WHERE id IN ({placeholders})", selected_ids)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not rows:
|
||||
return jsonify({"error": "No matching images found"}), 404
|
||||
|
||||
# 创建临时 ZIP 文件
|
||||
temp_zip_fd, temp_zip_path = tempfile.mkstemp(suffix='.zip')
|
||||
os.close(temp_zip_fd) # 关闭文件描述符,让 zipfile 模块管理
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for left_fn, right_fn in rows:
|
||||
left_path = os.path.join(SAVE_PATH_LEFT, left_fn)
|
||||
right_path = os.path.join(SAVE_PATH_RIGHT, right_fn)
|
||||
if os.path.exists(left_path):
|
||||
zipf.write(left_path, os.path.join('left', left_fn))
|
||||
if os.path.exists(right_path):
|
||||
zipf.write(right_path, os.path.join('right', right_fn))
|
||||
|
||||
logger.info(f"Exported {len(rows)} image pairs to {temp_zip_path}")
|
||||
# 返回 ZIP 文件给客户端
|
||||
return send_file(temp_zip_path, as_attachment=True, download_name='exported_images.zip')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating export ZIP: {e}")
|
||||
if os.path.exists(temp_zip_path):
|
||||
os.remove(temp_zip_path)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload_images():
|
||||
"""接收左右摄像头图片,保存并推送更新"""
|
||||
try:
|
||||
# 从 multipart/form-data 中获取文件
|
||||
left_file = request.files.get('left_image')
|
||||
right_file = request.files.get('right_image')
|
||||
metadata_str = request.form.get('metadata') # 如果需要处理元数据
|
||||
|
||||
if not left_file or not right_file:
|
||||
logger.warning("Received request without required image files.")
|
||||
return jsonify({"error": "Missing left_image or right_image"}), 400
|
||||
|
||||
# 读取图片数据
|
||||
left_img_bytes = left_file.read()
|
||||
right_img_bytes = right_file.read()
|
||||
|
||||
# 解码图片用于后续处理 (如显示、保存)
|
||||
nparr_left = np.frombuffer(left_img_bytes, np.uint8)
|
||||
nparr_right = np.frombuffer(right_img_bytes, np.uint8)
|
||||
img_left = cv2.imdecode(nparr_left, cv2.IMREAD_COLOR)
|
||||
img_right = cv2.imdecode(nparr_right, cv2.IMREAD_COLOR)
|
||||
|
||||
if img_left is None or img_right is None:
|
||||
logger.error("Failed to decode received images.")
|
||||
return jsonify({"error": "Could not decode images"}), 400
|
||||
|
||||
# 解析元数据 (如果提供)
|
||||
metadata = {}
|
||||
if metadata_str:
|
||||
try:
|
||||
metadata = json.loads(metadata_str)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Could not parse metadata: {e}")
|
||||
|
||||
timestamp_str = str(metadata.get("timestamp", str(int(time.time()))))
|
||||
timestamp_str_safe = timestamp_str.replace(".", "_") # 避免文件名中的点号问题
|
||||
|
||||
# 生成文件名
|
||||
left_filename = f"left_{timestamp_str_safe}.jpg"
|
||||
right_filename = f"right_{timestamp_str_safe}.jpg"
|
||||
|
||||
# 保存图片到本地
|
||||
left_path = os.path.join(SAVE_PATH_LEFT, left_filename)
|
||||
right_path = os.path.join(SAVE_PATH_RIGHT, right_filename)
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(SAVE_PATH_LEFT, exist_ok=True)
|
||||
os.makedirs(SAVE_PATH_RIGHT, exist_ok=True)
|
||||
|
||||
cv2.imwrite(left_path, img_left)
|
||||
cv2.imwrite(right_path, img_right)
|
||||
logger.info(f"Saved images: {left_path}, {right_path}")
|
||||
|
||||
# 将图片信息写入数据库
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO images (left_filename, right_filename, timestamp, metadata)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (left_filename, right_filename, float(timestamp_str), json.dumps(metadata)))
|
||||
conn.commit()
|
||||
image_id = cursor.lastrowid # 获取新插入记录的 ID
|
||||
conn.close()
|
||||
logger.info(f"Recorded image pair (ID: {image_id}) in database.")
|
||||
|
||||
# 将 OpenCV 图像编码为 base64 字符串,用于 WebSocket 传输
|
||||
_, left_encoded = cv2.imencode('.jpg', img_left)
|
||||
_, right_encoded = cv2.imencode('.jpg', img_right)
|
||||
left_b64 = base64.b64encode(left_encoded).decode('utf-8')
|
||||
right_b64 = base64.b64encode(right_encoded).decode('utf-8')
|
||||
|
||||
# 更新用于实时显示的全局变量 (如果需要)
|
||||
with frame_lock:
|
||||
global latest_left_frame, latest_right_frame, latest_timestamp
|
||||
latest_left_frame = img_left
|
||||
latest_right_frame = img_right
|
||||
latest_timestamp = timestamp_str
|
||||
|
||||
socketio.emit('update_image', {
|
||||
'left_image': left_b64,
|
||||
'right_image': right_b64,
|
||||
'timestamp': timestamp_str
|
||||
})
|
||||
|
||||
return jsonify({"message": "Images received, saved, and pushed via WebSocket", "timestamp": timestamp_str, "id": image_id})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing upload: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# --- 可选:添加一个简单的状态检查路由 ---
|
||||
@app.route('/status')
|
||||
def status():
|
||||
with frame_lock:
|
||||
has_frames = latest_left_frame is not None and latest_right_frame is not None
|
||||
timestamp = latest_timestamp if has_frames else "N/A"
|
||||
return jsonify({"has_frames": has_frames, "latest_timestamp": timestamp})
|
||||
|
||||
# --- SocketIO 事件处理程序 ---
|
||||
@socketio.event
|
||||
def connect():
|
||||
"""客户端连接事件"""
|
||||
logger.info("Client connected")
|
||||
|
||||
@socketio.event
|
||||
def disconnect():
|
||||
"""客户端断开连接事件"""
|
||||
logger.info("Client disconnected")
|
||||
|
||||
@socketio.event
|
||||
def capture_button(data):
|
||||
"""
|
||||
SocketIO 事件处理器,当客户端发送 'button_pressed' 事件时触发
|
||||
"""
|
||||
try:
|
||||
image_client.request_sync()
|
||||
logger.info("Request sent to server.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending request: {e}")
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info(f"Starting Flask-SocketIO server on {FLASK_HOST}:{FLASK_PORT}")
|
||||
socketio.run(app, host=FLASK_HOST, port=FLASK_PORT, debug=False, allow_unsafe_werkzeug=True)
|
||||
102
cap_trigger.py
Normal file
102
cap_trigger.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import pynng
|
||||
import asyncio
|
||||
import uuid
|
||||
import time
|
||||
|
||||
class ImageClient:
|
||||
def __init__(self, server_address="tcp://10.42.70.1:54321", client_id=None):
|
||||
self.server_address = server_address
|
||||
self.client_id = client_id or f"client_{uuid.uuid4().hex[:8]}"
|
||||
self.socket = None
|
||||
|
||||
async def connect(self):
|
||||
"""连接到服务器"""
|
||||
self.socket = pynng.Req0()
|
||||
self.socket.dial(self.server_address, block=False)
|
||||
print(f"Client {self.client_id} connected to server")
|
||||
|
||||
async def send_request(self):
|
||||
"""发送请求到服务器"""
|
||||
try:
|
||||
# 发送客户端 ID 作为请求
|
||||
client_id_bytes = self.client_id.encode('utf-8')
|
||||
self.socket.send(client_id_bytes)
|
||||
|
||||
# 等待响应
|
||||
response = self.socket.recv()
|
||||
print(f"Client {self.client_id}: Received response: {response}")
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} error: {e}")
|
||||
return None
|
||||
|
||||
async def request(self):
|
||||
"""发送请求并等待响应"""
|
||||
try:
|
||||
await self.connect()
|
||||
response = await self.send_request()
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} error: {e}")
|
||||
|
||||
def request_sync(self):
|
||||
"""发送同步请求到服务器"""
|
||||
socket = None
|
||||
try:
|
||||
# 每次都创建一个新的socket实例以避免复用已关闭的socket
|
||||
socket = pynng.Req0()
|
||||
socket.dial(self.server_address, block=True) # 使用阻塞模式确保连接建立
|
||||
client_id_bytes = self.client_id.encode('utf-8')
|
||||
socket.send(client_id_bytes)
|
||||
response = socket.recv()
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} error: {e}")
|
||||
return None
|
||||
finally:
|
||||
# 确保socket在使用后被正确关闭
|
||||
if socket:
|
||||
try:
|
||||
socket.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
async def run(self, request_interval=0.033): # ~30fps
|
||||
"""运行客户端请求循环"""
|
||||
await self.connect()
|
||||
|
||||
request_count = 0
|
||||
while True:
|
||||
try:
|
||||
response = await self.send_request()
|
||||
if response:
|
||||
print(f"Client {self.client_id}: Request {request_count} completed")
|
||||
|
||||
request_count += 1
|
||||
await asyncio.sleep(request_interval)
|
||||
if request_count >= 1:
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"Client {self.client_id} interrupted by user")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} error in main loop: {e}")
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
async def main():
|
||||
# 创建多个客户端实例
|
||||
num_clients = 1 # 可以根据需要调整客户端数量
|
||||
tasks = []
|
||||
|
||||
for i in range(num_clients):
|
||||
client = ImageClient(client_id=f"client_{i}_{uuid.uuid4().hex[:8]}")
|
||||
task = asyncio.create_task(client.run())
|
||||
tasks.append(task)
|
||||
|
||||
# 等待所有客户端任务完成
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
0
static/received/.gitkeep
Normal file
0
static/received/.gitkeep
Normal file
128
templates/index.html
Normal file
128
templates/index.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Real-time Image Display via WebSocket</title>
|
||||
<!-- 引入 Socket.IO 客户端库 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.image-container {
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.image-container img {
|
||||
width: 400px;
|
||||
height: auto;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
#status {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
#button-container {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Live Stereo Camera Feed (via WebSocket)</h1>
|
||||
<!-- 添加导航链接 -->
|
||||
<p style="text-align: center;"><a href="/list">View Saved Images</a></p>
|
||||
<div id="status">Connecting to server...</div>
|
||||
|
||||
<!-- 新增按钮容器 -->
|
||||
<div id="button-container">
|
||||
<button id="capBtn">触发采集</button>
|
||||
<!-- <div id="button-response">Button response will appear here.</div> -->
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="image-container">
|
||||
<h3>Left Camera</h3>
|
||||
<img id="left-img" src="" alt="No Left Image">
|
||||
<div class="timestamp" id="left-ts">No timestamp</div>
|
||||
</div>
|
||||
<div class="image-container">
|
||||
<h3>Right Camera</h3>
|
||||
<img id="right-img" src="" alt="No Right Image">
|
||||
<div class="timestamp" id="right-ts">No timestamp</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 建立 Socket.IO 连接
|
||||
const socket = io('http://' + document.domain + ':' + location.port);
|
||||
|
||||
socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
document.getElementById('status').innerHTML = 'Connected to server. Waiting for images...';
|
||||
});
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
console.log('Disconnected from server');
|
||||
document.getElementById('status').innerHTML = 'Disconnected from server.';
|
||||
});
|
||||
|
||||
// 监听 'update_image' 事件
|
||||
socket.on('update_image', function(data) {
|
||||
// 更新左图
|
||||
if (data.left_image) {
|
||||
document.getElementById('left-img').src = 'data:image/jpeg;base64,' + data.left_image;
|
||||
}
|
||||
// 更新右图
|
||||
if (data.right_image) {
|
||||
document.getElementById('right-img').src = 'data:image/jpeg;base64,' + data.right_image;
|
||||
}
|
||||
// 更新时间戳
|
||||
if (data.timestamp) {
|
||||
document.getElementById('left-ts').textContent = 'Timestamp: ' + data.timestamp;
|
||||
document.getElementById('right-ts').textContent = 'Timestamp: ' + data.timestamp;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', function(data) {
|
||||
console.error('WebSocket Error:', data);
|
||||
document.getElementById('status').innerHTML = 'Error: ' + data.message;
|
||||
});
|
||||
|
||||
// --- 新增:处理按钮点击事件 ---
|
||||
document.getElementById('capBtn').onclick = function() {
|
||||
console.log("触发采集.");
|
||||
socket.emit('capture_button', {
|
||||
client_time: new Date().toISOString(),
|
||||
// button_id: 'myButton'
|
||||
});
|
||||
};
|
||||
|
||||
// --- 新增:监听服务器对按钮点击的响应 ---
|
||||
socket.on('button_response', function(data) {
|
||||
console.log('Received button response from server:', data);
|
||||
document.getElementById('button-response').textContent =
|
||||
`Response: ${data.message} (Server Time: ${new Date(data.server_time * 1000).toISOString()})`;
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
260
templates/list_images.html
Normal file
260
templates/list_images.html
Normal file
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Saved Images List</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js "></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.controls { margin-bottom: 20px; }
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin-right: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background-color: #0056b3; }
|
||||
button:disabled { background-color: #cccccc; cursor: not-allowed; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
.image-preview { width: 100px; height: auto; }
|
||||
input[type="checkbox"] { transform: scale(1.2); }
|
||||
#status { padding: 10px; margin: 10px 0; background-color: #f0f0f0; }
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Saved Images</h1>
|
||||
<!-- 添加导航链接 -->
|
||||
<p><a href="/">Back to Live Feed</a></p>
|
||||
<div id="status">Loading images...</div>
|
||||
<div class="controls">
|
||||
<button id="refreshBtn">Refresh List</button>
|
||||
<button id="exportBtn" disabled>Export Selected</button>
|
||||
<button id="deleteSelectedBtn" disabled>Delete Selected</button>
|
||||
</div>
|
||||
<table id="imagesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" id="selectAllCheckbox">
|
||||
<label for="selectAllCheckbox" style="display: inline-block; margin-left: 5px; cursor: pointer;">Select All</label>
|
||||
</th>
|
||||
<th>ID</th>
|
||||
<th>Left Image</th>
|
||||
<th>Right Image</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Created At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="imagesTableBody">
|
||||
<!-- 动态加载行 -->
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
const socket = io('http://' + document.domain + ':' + location.port);
|
||||
let allImages = []; // 存储所有图片数据
|
||||
|
||||
// 获取图片列表
|
||||
async function loadImages() {
|
||||
document.getElementById('status').textContent = 'Loading images...';
|
||||
try {
|
||||
const response = await fetch('/api/images');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
allImages = await response.json();
|
||||
renderTable(allImages);
|
||||
document.getElementById('status').textContent = `Loaded ${allImages.length} images.`;
|
||||
updateSelectAllCheckbox(); // 加载后更新全选框状态
|
||||
} catch (error) {
|
||||
console.error('Error loading images:', error);
|
||||
document.getElementById('status').textContent = 'Error loading images: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染表格
|
||||
function renderTable(images) {
|
||||
const tbody = document.getElementById('imagesTableBody');
|
||||
tbody.innerHTML = ''; // 清空现有内容
|
||||
|
||||
images.forEach(image => {
|
||||
const row = tbody.insertRow();
|
||||
row.insertCell(0).innerHTML = `<input type="checkbox" class="selectCheckbox" data-id="${image.id}">`;
|
||||
row.insertCell(1).textContent = image.id;
|
||||
|
||||
const leftCell = row.insertCell(2);
|
||||
const leftImg = document.createElement('img');
|
||||
// 修改这里:使用 Flask 静态文件路径
|
||||
leftImg.src = `/static/received/left/${image.left_filename}`;
|
||||
leftImg.alt = "Left Image";
|
||||
leftImg.className = 'image-preview';
|
||||
leftImg.onerror = function() { this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="75" viewBox="0 0 100 75"><rect width="100" height="75" fill="%23eee"/><text x="50" y="40" font-family="Arial" font-size="12" fill="%23999" text-anchor="middle">No Image</text></svg>'; };
|
||||
leftCell.appendChild(leftImg);
|
||||
|
||||
const rightCell = row.insertCell(3);
|
||||
const rightImg = document.createElement('img');
|
||||
// 修改这里:使用 Flask 静态文件路径
|
||||
rightImg.src = `/static/received/right/${image.right_filename}`;
|
||||
rightImg.alt = "Right Image";
|
||||
rightImg.className = 'image-preview';
|
||||
rightImg.onerror = function() { this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="75" viewBox="0 0 100 75"><rect width="100" height="75" fill="%23eee"/><text x="50" y="40" font-family="Arial" font-size="12" fill="%23999" text-anchor="middle">No Image</text></svg>'; };
|
||||
rightCell.appendChild(rightImg);
|
||||
|
||||
row.insertCell(4).textContent = new Date(image.timestamp * 1000).toISOString();
|
||||
row.insertCell(5).textContent = image.created_at;
|
||||
row.insertCell(6).innerHTML = `<button onclick="deleteImage(${image.id})">Delete</button>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 删除图片
|
||||
async function deleteImage(id) {
|
||||
if (!confirm(`Are you sure you want to delete image ID ${id}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/images', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
// 从本地数组中移除
|
||||
allImages = allImages.filter(img => img.id !== id);
|
||||
renderTable(allImages);
|
||||
updateSelectAllCheckbox(); // 删除后更新全选框状态
|
||||
document.getElementById('status').textContent = `Image ID ${id} deleted.`;
|
||||
} catch (error) {
|
||||
console.error('Error deleting image:', error);
|
||||
document.getElementById('status').textContent = 'Error deleting image: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中的图片 ID
|
||||
function getSelectedIds() {
|
||||
const checkboxes = document.querySelectorAll('.selectCheckbox:checked');
|
||||
return Array.from(checkboxes).map(cb => parseInt(cb.dataset.id));
|
||||
}
|
||||
|
||||
// 更新全选复选框的状态
|
||||
function updateSelectAllCheckbox() {
|
||||
const checkboxes = document.querySelectorAll('.selectCheckbox');
|
||||
const allSelected = checkboxes.length > 0 && checkboxes.length === document.querySelectorAll('.selectCheckbox:checked').length;
|
||||
document.getElementById('selectAllCheckbox').checked = allSelected;
|
||||
updateExportDeleteButtons();
|
||||
}
|
||||
|
||||
// 更新导出和删除按钮的启用状态
|
||||
function updateExportDeleteButtons() {
|
||||
const selectedCount = getSelectedIds().length;
|
||||
document.getElementById('exportBtn').disabled = selectedCount === 0;
|
||||
document.getElementById('deleteSelectedBtn').disabled = selectedCount === 0;
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadImages);
|
||||
|
||||
// --- 修改:使用 fetch API 发起导出请求 ---
|
||||
document.getElementById('exportBtn').addEventListener('click', async function() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Please select at least one image to export.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 fetch API 发起 POST 请求
|
||||
const response = await fetch('/api/images/export', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json', // 设置正确的 Content-Type
|
||||
},
|
||||
body: JSON.stringify({ ids: selectedIds }) // 发送 JSON 格式的请求体
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 如果响应是成功的,浏览器会接收到 ZIP 文件,通常会自动触发下载
|
||||
// 我们可以通过创建一个隐藏的链接来模拟下载
|
||||
const blob = await response.blob(); // 获取响应的 blob 数据
|
||||
const url = window.URL.createObjectURL(blob); // 创建一个 URL 对象
|
||||
const a = document.createElement('a'); // 创建一个隐藏的 <a> 标签
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = 'exported_images.zip'; // 设置下载的文件名
|
||||
document.body.appendChild(a); // 将 <a> 标签添加到页面
|
||||
a.click(); // 模拟点击 <a> 标签
|
||||
window.URL.revokeObjectURL(url); // 释放 URL 对象
|
||||
document.body.removeChild(a); // 移除 <a> 标签
|
||||
} catch (error) {
|
||||
console.error('Error exporting images:', error);
|
||||
document.getElementById('status').textContent = 'Error exporting images: ' + error.message;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('deleteSelectedBtn').addEventListener('click', async function() {
|
||||
const selectedIds = getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
alert('Please select at least one image to delete.');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Are you sure you want to delete ${selectedIds.length} selected images?`)) return;
|
||||
|
||||
for (const id of selectedIds) {
|
||||
// 逐个调用删除 API,或者可以优化为批量删除 API
|
||||
try {
|
||||
const response = await fetch('/api/images', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error deleting image ID ${id}:`, error);
|
||||
document.getElementById('status').textContent = `Error deleting image ID ${id}: ${error.message}`;
|
||||
return; // 停止删除后续图片
|
||||
}
|
||||
}
|
||||
// 成功后刷新列表
|
||||
loadImages();
|
||||
});
|
||||
|
||||
// 全选复选框事件监听
|
||||
document.getElementById('selectAllCheckbox').addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
const checkboxes = document.querySelectorAll('.selectCheckbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
});
|
||||
updateExportDeleteButtons();
|
||||
});
|
||||
|
||||
// 监听单个复选框变化以更新全选框和按钮状态
|
||||
document.getElementById('imagesTable').addEventListener('change', function(e) {
|
||||
if (e.target.classList.contains('selectCheckbox')) {
|
||||
updateSelectAllCheckbox();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时获取图片列表
|
||||
loadImages();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user