如何通过TCP Socket正确传输NV21图像?Qt与Python跨端传输问题
解决TCP Socket传输NV21图像数据不完整的问题
看起来你遇到的核心问题是TCP流的无边界特性导致的传输不完整,再加上两端代码的逻辑漏洞,使得服务器只收到了部分图像数据。下面分客户端和服务器端逐一拆解修复:
一、Qt Android客户端的问题与修复
你的客户端代码存在两个关键漏洞:
output变量持续累积帧数据,没有在每次发送后清空,导致发送的是多帧混合的无效数据,首次发送的可能只是部分帧内容。tcpSocket->write不一定能一次性写完所有数据——TCP是流式协议,需要循环确认所有字节都被写入缓冲区。
修正后的客户端代码:
void came::processFrame(const QVideoFrame& frame) { QVideoFrame f(frame); // 先确认能成功映射帧数据 if (!f.map(QAbstractVideoBuffer::ReadOnly)) { qWarning() << "Failed to map video frame"; return; } // 直接使用当前帧的原始数据,不累积旧数据 QByteArray frameData((char*)f.bits(), f.mappedBytes()); f.unmap(); // 用完及时解除映射,避免资源泄漏 // 第一步:先发送数据长度(用4字节大端整数标记,让服务器知道要收多少) quint32 dataLen = frameData.size(); QByteArray lenBytes; QDataStream stream(&lenBytes, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::BigEndian); stream << dataLen; qint64 lenWritten = 0; while (lenWritten < lenBytes.size()) { qint64 written = tcpSocket->write(lenBytes.mid(lenWritten)); if (written == -1) { qWarning() << "Failed to send data length:" << tcpSocket->errorString(); return; } lenWritten += written; tcpSocket->waitForBytesWritten(100); // 等待缓冲区写入 } // 第二步:循环发送完整的帧数据 qint64 bytesWritten = 0; while (bytesWritten < frameData.size()) { qint64 written = tcpSocket->write(frameData.mid(bytesWritten)); if (written == -1) { qWarning() << "Socket write error:" << tcpSocket->errorString(); break; } bytesWritten += written; tcpSocket->waitForBytesWritten(100); } qDebug() << "Successfully sent" << bytesWritten << "bytes of NV21 data"; }
二、Python服务器端的问题与修复
你的服务器接收逻辑完全错误:不能用「单次recv返回的字节数小于blockSize」作为接收完成的标志——TCP传输中,数据会被拆分成多个数据包发送,即使还有剩余数据,某次recv也可能返回小于blockSize的字节数。
正确的做法是遵循「先长度后数据」的协议:先接收数据长度,再根据长度接收完整的图像数据。
修正后的服务器代码:
import socket import numpy as np import cv2 def recv_all(sock, target_length): """ 确保接收指定长度的完整数据,处理TCP拆包问题 """ received_data = b'' while len(received_data) < target_length: # 每次接收剩余长度和4096中的较小值,避免内存浪费 chunk = sock.recv(min(target_length - len(received_data), 4096)) if not chunk: raise ConnectionError("Socket connection was unexpectedly closed") received_data += chunk return received_data def recv_nv21_image(sock, rows=1080, cols=1920): # 1. 先接收4字节的长度数据(大端格式) len_bytes = recv_all(sock, 4) total_length = int.from_bytes(len_bytes, byteorder='big') print(f"Expecting {total_length} bytes of NV21 data") # 2. 接收完整的NV21图像数据 nv21_data = recv_all(sock, total_length) print(f"Received {len(nv21_data)} bytes in total") # 3. 解析NV21并转换为BGR格式 bin_y = nv21_data[:rows * cols] img_y = np.frombuffer(bin_y, np.uint8).reshape((rows, cols)) bin_u = nv21_data[rows * cols::2] img_u = np.frombuffer(bin_u, np.uint8).reshape((rows // 2, cols // 2)) bin_v = nv21_data[rows * cols + 1::2] img_v = np.frombuffer(bin_v, np.uint8).reshape((rows // 2, cols // 2)) # 放大UV通道到与Y通道同尺寸 enlarge_u = cv2.resize(img_u, dsize=(cols, rows), interpolation=cv2.INTER_CUBIC) enlarge_v = cv2.resize(img_v, dsize=(cols, rows), interpolation=cv2.INTER_CUBIC) # 合并并转换颜色空间 yuv_img = cv2.merge([img_y, enlarge_u, enlarge_v]) bgr_img = cv2.cvtColor(yuv_img, cv2.COLOR_YUV2BGR) return bgr_img if __name__ == '__main__': server_addr = ('0.0.0.0', 12345) server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(server_addr) server_socket.listen(1) print(f"Server listening on {server_addr}...") client_sock, client_addr = server_socket.accept() print(f"Connected to client: {client_addr}") try: received_image = recv_nv21_image(client_sock) cv2.imwrite("received_frame.jpg", received_image) print("Image saved successfully") except Exception as e: print(f"Error during transmission: {str(e)}") finally: client_sock.close() server_socket.close()
三、额外注意事项
- 格式验证:确认
QVideoFrame的像素格式确实是NV21,可通过frame.pixelFormat() == QVideoFrameFormat::Format_NV21检查,部分设备可能输出其他YUV变种格式。 - 性能优化:1920*1080的NV21数据约3MB,若网络带宽有限,可考虑在客户端先将图像压缩为JPEG再传输,减少带宽占用。
- 异步处理:如果需要更高的并发性能,Qt客户端可改用
readyRead信号异步处理发送,Python服务器可采用多线程/异步IO框架。
内容的提问来源于stack exchange,提问作者wry
相关产品推荐
相关产品推荐

