UDP/TCP实时传输图像

首先问个问题,为什么要用UDP传输图像,而不是TCP?

TCP是我们经常使用的通信协议,从认识它的第一天起,就应该知道,它非常稳,丢包率超低。但是一切都有双面性,稳定会影响传输的速度。与TCP不同,UDP没有反复确认这个环节,发送端向一个接收端甩一个数据包,不管接收端有没有接收到,所以相较于TCP,其丢包率比较大,但是它的速度就快多了。针对图像传输这种耗时但是不追求准确性的任务,采用UDP是再合适不过的了。目前许多网络直播都采用UDP来传输图像。

接下来描述一下主要内容,使用C#窗体在两台PC上分别创建一个图像发送端和一个图像接收端,发送端采集摄像头图像,压缩为JPEG格式后使用UDP发送至接收端,接收端接收图像并进行显示。

UDP/TCP实时传输图像_第1张图片
所以本项目主要有以下两部分:
 图像发送端的搭建
 图像接收端的搭建

另外这个项目还会用到TCP来确认双方是否都在线,如果接收方还没准备好,发送方就开始发图像了,那就是在做无用功了(虽然对这个项目来说影响不大)。具体的做法就是主机A(发送端)作为TCP服务端,创建一个套接字,绑定一个IP和端口Port,开启监听。主机B作为TCP客户端,连接到主机A创建的服务端。连接后,主机A打开摄像头,并开始向主机B发送图像。

UDP/TCP实时传输图像_第2张图片
TCP服务端(主机A):

Thread threadWatch = null; //负责监听客户端的线程
Socket socketWatch = null;  //负责监听客户端的套接字
 
/****创建套接字*****/
//定义一个套接字用于监听客户端发来的信息  包含3个参数(IP4寻址协议,流式连接,TCP协议)
socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//服务端发送信息 需要1个IP地址和端口号
IPAddress ipaddress = IPAddress.Parse(this.comboBox1.Text.Trim()); //获取文本框输入的IP地址
//将IP地址和端口号绑定到网络节点endpoint上 
IPEndPoint endpoint = new IPEndPoint(ipaddress, int.Parse(this.comboBox2.Text.Trim())); //获取文本框上输入的端口号
//监听绑定的网络节点
socketWatch.Bind(endpoint);
//将套接字的监听队列长度限制为20
socketWatch.Listen(20);
//创建一个监听线程 
threadWatch = new Thread(WatchConnecting);
//将窗体线程设置为与后台同步
threadWatch.IsBackground = true;
//启动线程
threadWatch.Start();

/****监听客户端发来的请求*****/
//创建一个负责和客户端通信的套接字 
Socket socConnection = null;
private void WatchConnecting()
{
    while (true)  //持续不断监听客户端发来的请求
    {
        socConnection = socketWatch.Accept();
	}
}

TCP客户端(主机B):

private void startBtn_Click(object sender, EventArgs e)
{
        //Parse:将一个字符串的ip地址转换成一个IPAddress对象
        IPAddress ipaddress = IPAddress.Parse(comboBox1.Text);
        EndPoint point = new IPEndPoint(ipaddress, int.Parse(comboBox2.Text));
        Thread connect = new Thread(new ParameterizedThreadStart(Connect));
        connect.Start(point);
}
//连接线程
private static void Connect(object point)
{
    try
    {
        Socket tcpClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        tcpClient.Connect((EndPoint)point);//通过IP和端口号来定位一个所要连接的服务器端
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

最后,两个项目都是用VS2017生成的,.NET框架为4.7.1.
接下来进入正文

一、图像发送端的搭建
首先需要获取摄像头图像,这里使用EmguCV来获取图像以及转码。

点击 工具->NuGet包管理器->管理解决方案的NuGet程序包,在浏览那一栏查找Emgu.cv,选择第一个,在右侧选择要安装EmguCV的项目,点击安装即可。

UDP/TCP实时传输图像_第3张图片
UDP/TCP实时传输图像_第4张图片
因为这个项目只用到了EmguCV的读取摄像头功能,就不再赘述了,如果还想深入了解EmguCV,欢迎交流

EmguCV就这样安装好了。然后就可以调用EmguCV中的函数获取摄像头数据了

private VideoCapture capture = new VideoCapture();
Mat currentImage = capture.QueryFrame();

currentImage就是获取的摄像头图像,默认尺寸是640*480,可以通过以下代码更改设置

capture.SetCaptureProperty(CapProp.FrameWidth, 720);
capture.SetCaptureProperty(CapProp.FrameHeight, 1280);

接下来将图像转化成UDP发送的byte[]格式。一个UDP数据包只能发送64k字节数据,也就是65536字节,但是一帧图片就有640x480x3=921600byte=900k字节,所以需要进行图像压缩,这里采用jpeg压缩格式。

Image<Rgb,Byte> img = currentImage.ToImage<Rgb, Byte>();
byte[] bytes = img.ToJpegData(80);

最后使用UDP进行发送。

UdpClient udpClient = new UdpClient();
//接收端绑定的IPAddress、端口号
IPAddress ipaddress = IPAddress.Parse("10.128.14.249");
IPEndPoint endpoint = new IPEndPoint(ipaddress, int.Parse(this.comboBox2.Text.Trim()));
udpClient.Send(bytes, bytes.Length, endpoint);
udpClient.Close();

二、图像接收端的搭建
在设计面板上添加一个PictureBox控件,用来显示接收到的图像,添加一个Button,用来建立和发送端的连接。

接下来创建一个UDP的套接字,绑定本地IPv4地址

Socket udpServer = null;

//创建套接字
udpServer = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
//绑定IP和端口
udpServer.Bind(new IPEndPoint(GetLocalIPv4Address(), 8090));
//开启接收数据线程
new Thread(ReceiveMessage)
{
    IsBackground = true
}.Start();

如果不知道本地IPv4地址,可以使用以下函数自动获取,这样也可以避免以后IPv4地址改变而报错

public IPAddress GetLocalIPv4Address()
{
    IPAddress localIpv4 = null;
    //获取本机所有的IP地址列表
    IPAddress[] IpList = Dns.GetHostAddresses(Dns.GetHostName());
    //循环遍历所有IP地址
    foreach (IPAddress IP in IpList)
    {
        //判断是否是IPv4地址
        if (IP.AddressFamily == AddressFamily.InterNetwork)
        {
            localIpv4 = IP;
        }
        else
        {
            continue;
        }
    }
    return localIpv4;
}

最后就可以开始接收图像了

void ReceiveMessage()
{
    while (true)
    {
        EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
        //设置一个64k的字节数组作为缓存
        byte[] data = new byte[65536];
        int length = udpServer.ReceiveFrom(data, ref remoteEndPoint);//此方法把数据来源ip、port放到第二个参数中

        MemoryStream ms = new MemoryStream(data, 0, length);
        pictureBox1.Image=Image.FromStream(ms);
    }
}

最终效果如下:

UDP/TCP实时传输图像_第5张图片
UDP/TCP实时传输图像_第6张图片
需要在开头添加以下代码

//TCP、UDP
using System.Net;
using System.Net.Sockets;

//多线程
using System.Threading;

//使用EmguCV读取摄像头
using Emgu.CV;
using Emgu.CV.Structure;

//接收端读取图像
using System.IO;

TCP图像传输

TCP对于传输的数据大小没有限制,同时TCP在发送失败时还有重传机制,可以保证传输的可靠性,所以本文将使用TCP协议来进行图像的实时传输。

TCP连接过程见后面的程序,一般服务端创建一个套接字,绑定本地IP,开启监听,然后客户端也创建一个套接字,连接服务端就可以了,详见后面的代码。直接介绍数据传输流程,如下图:
UDP/TCP实时传输图像_第7张图片
由于TCP是以字节流的形式发送数据的,不能预知数据的大小,所以客户端在发送图像数据之前,需要先发送数据长度等信息。同时为了防止粘包(服务端接收到的数据会先缓存在缓冲区,在接收一次数据后,如果不及时处理,下一次接收到的数据也会送到缓冲区。由于这些数据都是字节流形式的,这样两次接收到的数据就会黏在一起,无法分开),客户端在发送完数据长度信息后,不能马上发送图像数据,需要等待服务端返回的应答信号。客户端接收到应答信号后,就可以开始发送图像字节流数据了。服务端完成图像数据接收后,还要返回给客户端一个应答信号,通知客户端开始下一帧图像的传输

服务端

#-*- coding: UTF-8 -*- 
import socket
import cv2
import numpy as np

HOST = ''
PORT = 8080
ADDRESS = (HOST, PORT)
# 创建一个套接字
tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地ip
tcpServer.bind(ADDRESS)
# 开始监听
tcpServer.listen(5)

while True:
    print("等待连接……")
    client_socket, client_address = tcpServer.accept()
    print("连接成功!")
    try:
        while True:
            # 接收标志数据
            data = client_socket.recv(1024)
            if data:
                # 通知客户端“已收到标志数据,可以发送图像数据”
                client_socket.send(b"ok")
                # 处理标志数据
                flag = data.decode().split(",")
                # 图像字节流数据的总长度
                total = int(flag[0])
                # 接收到的数据计数
                cnt = 0
                # 存放接收到的数据
                img_bytes = b""

                while cnt < total:
                    # 当接收到的数据少于数据总长度时,则循环接收图像数据,直到接收完毕
                    data = client_socket.recv(256000)
                    img_bytes += data
                    cnt += len(data)
                    print("receive:" + str(cnt) + "/" + flag[0])
                # 通知客户端“已经接收完毕,可以开始下一帧图像的传输”
                client_socket.send(b"ok")

                # 解析接收到的字节流数据,并显示图像
                img = np.asarray(bytearray(img_bytes), dtype="uint8")
                img = cv2.imdecode(img, cv2.IMREAD_COLOR)
                cv2.imshow("img", img)
                cv2.waitKey(1)
            else:
                print("已断开!")
                break
    finally:
        client_socket.close()

客户端

#-*- coding: UTF-8 -*- 
import cv2
import time
import socket

# 服务端ip地址
HOST = '192.168.0.100'
# 服务端端口号
PORT = 8080
ADDRESS = (HOST, PORT)

# 创建一个套接字
tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接远程ip
tcpClient.connect(ADDRESS)

cap = cv2.VideoCapture(0)
while True:
    # 计时
    start = time.perf_counter()
    # 读取图像
    ref, cv_image = cap.read()
    # 压缩图像
    img_encode = cv2.imencode('.jpg', cv_image, [cv2.IMWRITE_JPEG_QUALITY, 99])[1]
    # 转换为字节流
    bytedata = img_encode.tostring()
    # 标志数据,包括待发送的字节流长度等数据,用‘,’隔开
    flag_data = (str(len(bytedata))).encode() + ",".encode() + " ".encode()
    tcpClient.send(flag_data)
    # 接收服务端的应答
    data = tcpClient.recv(1024)
    if ("ok" == data.decode()):
        # 服务端已经收到标志数据,开始发送图像字节流数据
        tcpClient.send(bytedata)
    # 接收服务端的应答
    data = tcpClient.recv(1024)
    if ("ok" == data.decode()):
        # 计算发送完成的延时
        print("延时:" + str(int((time.perf_counter() - start) * 1000)) + "ms")

1080P图像UDP传输
通过UDP一次性地发送到接收端,由于一个UDP数据包只能发送64k字节的数据,所以该方法的图片传输大小是有限制的,实测只能发送480P视频中的图像。

所以将采取逐帧发送的形式,以1080P的视频为例,实现更高清晰度(1080 × 1920 × 3 1080\times 1920\times 31080×1920×3)的图像实时传输。

本文中的高清晰度图像传输就是在前文方法的基础上,在发送端添加了切片压缩传输以及并行加速的步骤,而接收端则相应地使用多线程进行数据接收,分别接收压缩后的切片数据,再拼接起来进行显示。流程如下
UDP/TCP实时传输图像_第8张图片
发送端
在发送端我们需要达到的效果如下,左边用来显示原始图像,右上角用来显示各个切片,右下角用来处理接收端的连接请求。
UDP/TCP实时传输图像_第9张图片
首先设置一些参数

// 实例化一个VideoCapture,选择从本地文件读取视频
private VideoCapture capture = new VideoCapture("../../video/04.mp4");
// 设置读取的图片宽度
const int WIDTH = 1920;  
// 设置读取的图片高度
const int HEIGHT = 1080; 
// 切片数量
const int NUM_SLICE = 24; 

然后进行图像的显示以及切片。初始化一组显示控件,用来显示切片后的结果:

private void Form1_Load(object sender, EventArgs e)
{
    // 设置图像大小
    capture.SetCaptureProperty(Emgu.CV.CvEnum.CapProp.FrameWidth, WIDTH);
    capture.SetCaptureProperty(Emgu.CV.CvEnum.CapProp.FrameHeight, HEIGHT);

    // 获取面板控件的大小
    int w = panel_imgs.Width;
    int h = panel_imgs.Height;
    // 在面板panel_imgs上添加显示控件,用于显示每个切片
    for (int i = 0; i < NUM_SLICE; i++)
    {
        ImageBox imgb = new ImageBox();
        imgb.Left = 0;
        imgb.Top = i * h / NUM_SLICE;
        imgb.Width = w;
        imgb.Height = h / NUM_SLICE - 1;
        imgb.SizeMode = PictureBoxSizeMode.StretchImage;
        imgb.Visible = true;

        imgbox[i] = imgb;
        panel_imgs.Controls.Add(imgbox[i]);
    }
    // 在下拉文本框cbb_localIP中显示该计算机中的IPv4地址
    cbb_localIP.Text = GetLocalIPv4Address().ToString();
}

最后就是图像的读取、切片、压缩、发送等处理函数,这处理过程中,使用了Parallel.For并行加速功能,相对于串行的for循环,并行速度提高了一倍左右(不知道为啥我四核八线程的处理器只能降低一半的运行时间)

private void ProcessFram() // 图像读取、切片、发送
{
    DateTime startDT = System.DateTime.Now;
    while (true)
    {
        // 计算两次循环间的间隔,并显示在左上角
        DateTime stopDT = System.DateTime.Now;
        TimeSpan ts = stopDT.Subtract(startDT);
        this.Text = "图片处理耗时:" + ts.TotalMilliseconds + "ms";
        startDT = System.DateTime.Now;

        // 读取一张图片
        Mat currentImage = capture.QueryFrame();
        // 显示摄像头/视频流的图像
        imageBox0.Image = currentImage;

        int N = HEIGHT / NUM_SLICE;

        // 对图像进行切片,并将切片压缩后发送到接收端                
        Parallel.For(0, NUM_SLICE, i => // Parallel并行加速
                     {
                         // 从原图中切割,输入参数:原始图片 行范围row 列范围col
                         img[i] = new Mat(currentImage, new Range(i * N, (i + 1) * N - 1), new Range(0, WIDTH));
                         // 显示
                         imgbox[i].Image = img[i];

                         // 转换格式
                         Image<Rgb, Byte> img_trans = img[i].ToImage<Rgb, Byte>();
                         // JPEG压缩
                         byte[] bytes = img_trans.ToJpegData(95);

                         // UDP配置
                         UdpClient udpClient = new UdpClient();
                         //IPAddress ipaddress = IPAddress.Parse("192.168.0.105");
                         IPAddress ipaddress = remoteIP;
                         IPEndPoint endpoint = new IPEndPoint(ipaddress, 8000 + 10 * i);
                         // UDP发送
                         udpClient.Send(bytes, bytes.Length, endpoint);
                         udpClient.Close();
                     }
                    );
    }
}

在初始化函数中添加以下程序就可以执行包含切片、压缩、发送等操作的线程

Thread transFrames = new Thread(ProcessFram);
transFrames.Start();

接收端
接收端比较简单,实现效果如下,因为在接收端没有对图片进行更进一步的处理,所以本文只在接收端添加了若干个显示控件,用来显示每个切片,但是从观感上每个切片依次连接,形成了一张完整的图片。
UDP/TCP实时传输图像_第10张图片
首先进行参数设置

 // 切片数量,与发送端保持一致
 const int NUM_SLICE = 24; 
 // 为每一个切片创建一个显示控件
 PictureBox[] imgbox = new PictureBox[NUM_SLICE];
 // 为每一个切片创建一个UDP套接字
 Socket[] udpServer = new Socket[NUM_SLICE];

在初始化过程中添加显示控件,与发送端类似

int w = panel_imgs.Width;
int h = panel_imgs.Height;
// 在面板panel_imgs上添加显示接收到的图片的控件
for (int i = 0; i < NUM_SLICE; i++)
{
    // 设置PictureBox的位置、大小等参数
    PictureBox imgb = new PictureBox();
    imgb.Left = 0;
    imgb.Top = i * h / NUM_SLICE;
    imgb.Width = w;
    imgb.Height = h / NUM_SLICE + 1;
    imgb.SizeMode = PictureBoxSizeMode.StretchImage;
    imgb.Visible = true;

    // 添加到面板panel_imgs上
    imgbox[i] = imgb;                
    panel_imgs.Controls.Add(imgbox[i]);
}

接下来需要为每个切片创建一个接收线程

for (int i = 0; i < NUM_SLICE; i++)
{
	new Thread(new ParameterizedThreadStart(ImgReceive))
    {
        IsBackground = true
    }.Start(8000 + i * 10); // 输入参数为端口号,依次增加
}

最后就是接收线程的入口函数ImgReceive的内容

private void ImgReceive(object arg)
{
    // 网络端口号
    int port = (int)arg;
    int index = port % 8000 / 10;

    // 创建套接字
    udpServer[index] = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    // 绑定IP和端口
    udpServer[index].Bind(new IPEndPoint(IPAddress.Parse(cbb_remoteIP.Text), port));
    // 开启数据接收线程
    while (true)
    {
        EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
        // 设置一个64k的字节数组作为缓存
        byte[] data = new byte[65536];
        int length = udpServer[index].ReceiveFrom(data, ref remoteEndPoint);//此方法把数据来源ip、port放到第二个参数中

        MemoryStream ms = new MemoryStream(data, 0, length);
        // 将图像显示到对应的PictureBox控件上
        Image img = Image.FromStream(ms);
        imgbox[index].Image = img;
    }
}

测试结果
测试时发送端和接收端都在同一台PC上运行,运行流程与结果如这张GIF所示,左边是发送端,右边是接收端
UDP/TCP实时传输图像_第11张图片
参考文献:
https://blog.csdn.net/qq_42688495/article/details/108279618
https://blog.csdn.net/qq_42688495/article/details/102565452
https://blog.csdn.net/qq_42688495/article/details/106110338

你可能感兴趣的:(工程,计算机视觉,udp,tcp/ip,网络)