2

语境

我一直在创建一个系统,其中覆盆子 PI 将图像实时发送到远程客户端。覆盆子 PI 使用覆盆子 PI 相机捕获图像。捕获的图像可用作所有像素(行、列和 rgb)的 3 维数组。通过非常快速地发送和显示图像,它将以视频的形式呈现给用户。

我的目标是以尽可能高的图像分辨率实时发送这些图像。可接受的帧速率约为 30 fps。我选择了协议 UDP 而不是 TCP。我这样做是因为数据可以在 UDP 中传输得更快,因为开销更少。没有必要重新传输单个数据包,因为在我的情况下丢失一些像素是可以接受的。覆盆子 PI 和客户端位于同一网络中,因此无论如何都不会丢弃很多数据包。

考虑到以太网层的最大传输单元(MTU)为1500字节,UDP数据包不应该被分片或丢弃,我选择了最大有效载荷长度为1450字节,其中1447字节为数据,3字节是应用层开销。剩余的 50 个字节保留用于 TCP/IP 和传输层自动添加的开销。

我提到捕获的图像可作为数组使用。假设这个数组的大小例如是 1.036.800 字节(例如 width=720 * height=480 * numberOfColors=3),那么需要 717 (1.036.800 / 1447) 个 UDP 数据包来发送整个数组。树莓派上的 c++ 应用程序通过将数组分割成 1447 字节的片段,并添加一个介于 1-717 之间的片段索引号作为数据包的开销来实现这一点。我们还添加了一个图像编号,以区别于之前发送的图像/数组。数据包看起来像这样: udp 数据包

问题

在客户端,我开发了一个 C# 应用程序,它接收所有数据包并使用包含的索引号重新组合数组。使用 EgmuCV 库,接收到的数组被转换为图像并在 GUI 中绘制。然而,一些接收到的图像是用黑线/块绘制的。调试的时候发现,这个问题不是绘制图像引起的,而是黑色的块其实是缺少了从未到达的数组片段。因为数组中的字节值默认初始化为0,所以缺失的片段显示为黑色块

调试

在客户端使用 Wireshark,我搜索了这样一个丢失片段的索引,并惊讶地发现它完好无损。这意味着数据在传输层上被正确接收(并被wireshark观察到),但从未在应用层上读取。

此图像显示在索引 174.000 处缺少接收到的数组的一个块。因为一个数据包中有 1447 个数据字节,所以这个丢失数据的索引对应一个片段索引为 121 (174.000/1447) 的 UDP 数据包。121 的十六进制等效值为 79。下图显示了在 Wireshark 中对应 UDP 数据包的数据包,证明数据在传输层上仍然完好无损。图片

到目前为止我尝试了什么

  1. 当我降低帧速率时,黑色块会更少,而且它们通常更小。以 3FPS 的帧速率,根本没有黑色。然而,这个帧速率是不希望的。这是大约 (3fps * 720x480x3) 3.110.400 位每秒 (379kb/s) 的速度。一台普通的计算机应该能够每秒读取比这更多的位。正如我所解释的,数据包 DID 到达了 Wireshark,只是在应用层没有读取它们。

  2. 我还尝试将 UDP 有效负载长度从 1447 更改为 500。这只会使情况变得更糟,请参见图片

  3. 我实现了多线程,以便在不同的线程中读取和处理数据。

  4. 我尝试了一个 TCP 实现。图像被完整接收,但速度不够快,无法实时传输图像。

值得注意的是,一个“黑色块”并不代表一个 1447 字节的缺失片段,而是许多连续的片段。因此,在读取数据的某些时候,不会读取许多数据包。也不是每个图像都有这个问题,有些是完好无损的。

我想知道我的实现有什么问题导致这种不良影响。所以我将在下面发布我的一些代码。请注意,从未真正抛出异常“SocketException”,也从未打印过“无效开销”的 Console.Writeline。_client.Receive 总是接收 1450 个字节,除了数组的最后一个片段,它更小。

除了解决这个错误,如果有人有其他建议以更有效的方式传输这些阵列(需要更少的带宽但没有质量损失),我很乐意听到。只要解决方案在两个端点上都将数组作为输入/输出。

最重要的是:请注意,UdpClient.Receive() 方法从未返回丢失的数据包。我没有发布在树莓派上运行的 c++ 应用程序的代码,因为我已经证明了数据确实到达(在 wireshark 中)。所以传输工作正常,但接收不是。

private const int ClientPort = 50000;
private UdpClient _client;
private Thread _receiveThread;
private Thread _processThread;
private volatile bool _started;
private ConcurrentQueue<byte[]> _receivedPackets = new ConcurrentQueue<byte[]>();
private IPEndPoint _remoteEP = new IPEndPoint(IPAddress.Parse("192.168.4.1"), 2371);

public void Start()
{
    if (_started)
    {
         throw new InvalidCastException("Already started");
    }
    _started = true;
    _client = new UdpClient(_clientPort);
    _receiveThread = new Thread(new ThreadStart(ReceiveThread));
    _processThread = new Thread(new ThreadStart(ProcessThread));
    _receiveThread.Start();
    _processThread.Start();
}

public void Stop()
{
    if (!_started)
    {
        return;
    }
    _started = false;
    _receiveThread.Join();
    _receiveThread = null;
    _processThread.Join();
    _processThread = null;
    _client.Close();
}

public void ReceiveThread()
{
    _client.Client.ReceiveTimeout = 100;
    while (_started)
    {
        try
        {
            byte[] data = _client.Receive(ref _remoteEP);
            _receivedPackets.Enqueue(data);
        }
        catch(SocketException ex)
        {
            Console.Writeline(ex.Message);
            continue;
        }
    }
}

private void ProcessThread()
{
    while (_started)
    {
        byte[] data;
        bool dequeued = _receivedPackets.TryDequeue(out data);
        if (!dequeued)
        {
            continue;
        }
        int imgNr = data[0];
        int fragmentIndex = (data[1] << 8) | data[2];
        if (imgNr <= 0 || imgNr > 255 || fragmentIndex <= 0)
        {
            Console.WriteLine("Received data with invalid overhead");
            return;
        }
        // i omitted the code for this method because is does not interfere with the
        // socket and therefore not really relevant to the issue that i described
        ProccessReceivedData(imgNr, fragmentIndex , data);
    }
}
4

0 回答 0