230 lines
10 KiB
Python
230 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
压缩相机客户端Python实现
|
||
功能:连接服务器接收JPEG压缩视频流,解码并显示
|
||
"""
|
||
|
||
import socket
|
||
import cv2
|
||
import numpy as np
|
||
import sys
|
||
import time
|
||
from PIL import Image
|
||
import io
|
||
|
||
|
||
def main():
|
||
print("Starting Compressed Camera Client...")
|
||
|
||
if len(sys.argv) != 3:
|
||
print(f"Usage: {sys.argv[0]} <server_ip> <port>")
|
||
return 1
|
||
|
||
server_ip = sys.argv[1]
|
||
port = int(sys.argv[2])
|
||
|
||
print(f"Configuration: Server IP={server_ip}, Port={port}")
|
||
|
||
# 创建TCP套接字
|
||
print("Creating TCP socket...")
|
||
sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
|
||
# 设置套接字超时
|
||
sockfd.settimeout(5.0)
|
||
print("TCP socket created successfully")
|
||
|
||
try:
|
||
# 连接到服务器
|
||
print(f"Connecting to server {server_ip}:{port}...")
|
||
sockfd.connect((server_ip, port))
|
||
print(f"Connected to server: {server_ip}:{port}")
|
||
|
||
# 关闭超时,改为非阻塞模式
|
||
sockfd.setblocking(True)
|
||
print("Socket set to blocking mode")
|
||
|
||
# OpenCV窗口
|
||
print("Creating OpenCV window...")
|
||
# 尝试使用不同的窗口参数
|
||
cv2.namedWindow("Compressed Camera Stream", cv2.WINDOW_GUI_NORMAL)
|
||
# 设置固定的窗口大小
|
||
cv2.resizeWindow("Compressed Camera Stream", 640, 480)
|
||
# 确保窗口大小是固定的
|
||
cv2.setWindowProperty("Compressed Camera Stream", cv2.WND_PROP_ASPECT_RATIO, cv2.WINDOW_KEEPRATIO)
|
||
cv2.setWindowProperty("Compressed Camera Stream", cv2.WND_PROP_AUTOSIZE, cv2.WINDOW_AUTOSIZE)
|
||
|
||
# 显示初始信息
|
||
info_frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||
cv2.putText(info_frame, "Connecting to server...", (50, 240),
|
||
cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
||
cv2.imshow("Compressed Camera Stream", info_frame)
|
||
cv2.waitKey(100)
|
||
|
||
frame_count = 0
|
||
buffer = b'' # 用于累积数据的缓冲区
|
||
total_bytes_received = 0
|
||
jpeg_found_count = 0
|
||
|
||
# 上次收到数据的时间
|
||
last_receive_time = time.time()
|
||
|
||
while True:
|
||
try:
|
||
# 接收数据
|
||
data = sockfd.recv(8192)
|
||
if not data:
|
||
print("Server closed connection")
|
||
return 0
|
||
|
||
last_receive_time = time.time()
|
||
total_bytes_received += len(data)
|
||
print(f"Received {len(data)} bytes, total: {total_bytes_received} bytes")
|
||
|
||
# 将新数据添加到缓冲区
|
||
buffer += data
|
||
|
||
# 仅保留最近的500KB数据,防止内存溢出
|
||
if len(buffer) > 500 * 1024:
|
||
print(f"Buffer size exceeded 500KB, trimming to 500KB")
|
||
buffer = buffer[-500 * 1024:]
|
||
|
||
# 检查缓冲区中是否有JPEG标记
|
||
has_jpeg = b'\xff\xd8' in buffer
|
||
print(f"Buffer contains JPEG SOI: {has_jpeg}, buffer size: {len(buffer)}")
|
||
|
||
# JPEG文件格式标记
|
||
JPEG_START = b'\xff\xd8' # SOI (Start of Image)
|
||
JPEG_END = b'\xff\xd9' # EOI (End of Image)
|
||
|
||
# 查找所有完整的JPEG帧
|
||
while True:
|
||
# 查找JPEG开始标记
|
||
start_idx = buffer.find(JPEG_START)
|
||
if start_idx == -1:
|
||
# 没有找到开始标记
|
||
print(f"No JPEG SOI found, buffer size: {len(buffer)}")
|
||
# 清空大部分缓冲区,只保留末尾可能包含部分JPEG头的数据
|
||
if len(buffer) > 1024:
|
||
buffer = buffer[-1024:]
|
||
print(f"Trimmed buffer to {len(buffer)} bytes")
|
||
break
|
||
|
||
# 查找当前开始标记后的结束标记
|
||
end_idx = buffer.find(JPEG_END, start_idx + 2)
|
||
if end_idx == -1:
|
||
# 没有找到完整的结束标记
|
||
print(f"Found JPEG SOI but no EOI, buffer size from SOI: {len(buffer) - start_idx}")
|
||
# 保留从开始标记到缓冲区末尾的数据
|
||
buffer = buffer[start_idx:]
|
||
break
|
||
|
||
# 提取完整的JPEG帧(包括结束标记)
|
||
jpeg_frame = buffer[start_idx:end_idx + 2]
|
||
jpeg_found_count += 1
|
||
print(f"Extracted JPEG frame {jpeg_found_count}, size: {len(jpeg_frame)} bytes")
|
||
|
||
# 从缓冲区中移除已处理的JPEG帧
|
||
buffer = buffer[end_idx + 2:]
|
||
print(f"Buffer remaining after extraction: {len(buffer)} bytes")
|
||
|
||
# 只处理合理大小的JPEG帧(1KB到500KB)
|
||
if 1024 <= len(jpeg_frame) <= 500 * 1024:
|
||
try:
|
||
# 使用PIL库来解码和显示图像
|
||
image_stream = io.BytesIO(jpeg_frame)
|
||
pil_image = Image.open(image_stream)
|
||
|
||
if pil_image is not None:
|
||
# 解码成功,显示帧
|
||
frame_count += 1
|
||
print(f"Frame {frame_count} decoded successfully, size: {pil_image.width}x{pil_image.height}")
|
||
|
||
# 转换为OpenCV格式
|
||
opencv_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
|
||
|
||
# 检查图像是否有重复的模式
|
||
# 从截图来看,图像被分割成了3x3的网格
|
||
# 这可能是因为图像高度是正常的3倍
|
||
if opencv_image.shape[0] == 1440 and opencv_image.shape[1] == 640:
|
||
# 高度是1440,是正常的3倍
|
||
# 只显示图像的前480行
|
||
fixed_image = opencv_image[:480, :, :]
|
||
cv2.imshow("Compressed Camera Stream", fixed_image)
|
||
print(f"Fixed frame by cropping to 640x480")
|
||
elif opencv_image.shape[0] == 640 and opencv_image.shape[1] == 1920:
|
||
# 宽度是1920,是正常的3倍
|
||
# 只显示图像的前640列
|
||
fixed_image = opencv_image[:, :640, :]
|
||
cv2.imshow("Compressed Camera Stream", fixed_image)
|
||
print(f"Fixed frame by cropping to 640x480")
|
||
else:
|
||
# 直接显示原始大小的图像
|
||
cv2.imshow("Compressed Camera Stream", opencv_image)
|
||
|
||
# 只在显示新帧后调用一次waitKey,确保图像刷新
|
||
key = cv2.waitKey(1)
|
||
if key == 27:
|
||
print("ESC key pressed, exiting...")
|
||
return 1
|
||
else:
|
||
print(f"Failed to decode JPEG frame {jpeg_found_count}")
|
||
except Exception as e:
|
||
print(f"Error decoding JPEG frame {jpeg_found_count}: {e}")
|
||
# 尝试使用OpenCV的方式解码
|
||
try:
|
||
np_data = np.frombuffer(jpeg_frame, dtype=np.uint8)
|
||
frame = cv2.imdecode(np_data, cv2.IMREAD_COLOR)
|
||
if frame is not None:
|
||
frame_count += 1
|
||
print(f"Frame {frame_count} decoded with OpenCV, size: {frame.shape[1]}x{frame.shape[0]}")
|
||
# 检查是否是3x3网格
|
||
if frame.shape[0] > 480 and frame.shape[1] == 640:
|
||
# 只显示前480行
|
||
cv2.imshow("Compressed Camera Stream", frame[:480, :, :])
|
||
else:
|
||
cv2.imshow("Compressed Camera Stream", frame)
|
||
key = cv2.waitKey(1)
|
||
if key == 27:
|
||
print("ESC key pressed, exiting...")
|
||
return 1
|
||
except Exception as e2:
|
||
print(f"Failed with OpenCV too: {e2}")
|
||
else:
|
||
print(f"Invalid JPEG frame size: {len(jpeg_frame)} bytes, skipped")
|
||
|
||
except socket.timeout:
|
||
print("Socket timeout, no data received")
|
||
except Exception as e:
|
||
print(f"Error in receive loop: {e}")
|
||
|
||
# 检查连接是否超时
|
||
if time.time() - last_receive_time > 10:
|
||
print("Connection timeout: No data received for 10 seconds")
|
||
return 1
|
||
|
||
# 检查是否按下ESC键退出
|
||
key = cv2.waitKey(1)
|
||
if key == 27:
|
||
print("ESC key pressed, exiting...")
|
||
break
|
||
|
||
except socket.timeout as e:
|
||
print(f"Connection timeout: {e}")
|
||
return 1
|
||
except Exception as e:
|
||
print(f"Error: {e}")
|
||
return 1
|
||
finally:
|
||
# 清理资源
|
||
print("Cleaning up resources...")
|
||
sockfd.close()
|
||
cv2.destroyAllWindows()
|
||
print(f"Client exiting. Total bytes received: {total_bytes_received}, JPEG frames found: {jpeg_found_count}, Successful frames: {frame_count}")
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|