環境
ラズベリー PI がリアルタイムでリモート クライアントに画像を送信するシステムを作成しています。ラズベリー PI は、ラズベリー PI カメラを使用して画像をキャプチャします。キャプチャされた画像は、すべてのピクセル (行、列、RGB) の 3 次元配列として利用できます。画像を非常に高速に送信して表示することにより、ユーザーにはビデオとして表示されます。
私の目標は、画像の解像度をできるだけ高くして、これらの画像をリアルタイムで送信することです。許容できるフレーム レートは約 30 fps です。TCP ではなく UDP プロトコルを選択しました。オーバーヘッドが少ないため、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 パケットが必要です。ラズベリー PI の c++ アプリケーションは、配列を 1447 バイトのフラグメントにフラグメント化し、パケットのオーバーヘッドとして 1 ~ 717 のフラグメント インデックス番号を追加することでこれを行います。また、以前に送信された画像/配列と区別するために、画像番号も追加します。パケットは次のようになります: udp パケット
問題
クライアント側では、すべてのパケットを受信し、含まれているインデックス番号を使用して配列を再構築する C# アプリケーションを開発しました。EgmuCV ライブラリを使用して、受け取った配列を画像に変換し、GUI に描画します。ただし、受信した画像の一部は黒い線/塊で描かれています。デバッグ中に、この問題は画像の描画が原因ではないことがわかりましたが、黒いチャンクは、実際には到着しなかった配列フラグメントが欠落していることが原因です。配列内のバイト値はデフォルトで 0 として初期化されるため、不足しているフラグメントは黒いチャンクとして表示されます
デバッグ
クライアント側で Wireshark を使用して、欠落しているフラグメントのインデックスを検索したところ、元の状態であることに驚きました。これは、データがトランスポート層で正しく受信され (そして、wireshark で監視され)、アプリケーション層で読み取られないことを意味します。
この画像は、インデックス 174.000 で、受信した配列のチャンクが欠落していることを示しています。パケットには 1447 データ バイトがあるため、この失われたデータのインデックスは、フラグメント インデックス 121 (174.000/1447) を持つ UDP パケットに対応します。121 に相当する 16 進数は 79 です。次の画像は、Wireshark の UDP パケットに対応するパケットを示しており、データがトランスポート層でそのまま残っていることを証明しています。画像
これまでに何を試しましたか
フレーム レートを下げると、黒い塊が少なくなり、多くの場合小さくなります。フレームレートが 3FPS の場合、黒はまったくありません。ただし、このフレーム レートは望ましくありません。これは、約 (3fps * 720x480x3) 3.110.400 ビット/秒 (379kb/s) の速度です。通常のコンピューターは、これよりも多くのビット/秒を読み取ることができるはずです。説明したように、パケット DID は Wireshark に到着し、アプリケーション層で読み取られるだけではありません。
また、UDP ペイロードの長さを 1447 から 500 に変更してみました。これは悪化するだけです。画像 を参照してください。
データが異なるスレッドで読み取られて処理されるように、マルチスレッドを実装しました。
TCPの実装を試みました。画像はそのまま受信されましたが、画像をリアルタイムで転送するには十分な速度ではありませんでした。
「黒いチャンク」は、1447 バイトの単一の欠落フラグメントを表すのではなく、多くの連続するフラグメントを表すことに注意してください。そのため、データを読み取るときのある時点で、多数のパケットが読み取られません。また、すべての画像にこの問題があるわけではありません。一部の画像は無傷で届きます。
この望ましくない効果をもたらす実装の何が問題なのか疑問に思っています。そのため、以下にコードの一部を掲載します。例外「SocketException」が実際にスローされることはなく、「無効なオーバーヘッド」の Console.Writeline も出力されないことに注意してください。_client.Receive は常に 1450 バイトを受け取ります。配列の最後のフラグメントを除いて、これは小さいです。
また
このバグを解決する以外に、これらのアレイをより効率的な方法で (より少ない帯域幅で、品質を落とさずに) 送信するための別の提案があれば、喜んで聞きます。ソリューションが両方のエンドポイントで入力/出力として配列を持っている限り。
最も重要なこと: 不足しているパケットが UdpClient.Receive() メソッドによって返されないことに注意してください。ラズベリー PI で実行する 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);
}
}