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

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

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


所以本项目主要有以下两部分:
 图像发送端的搭建
 图像接收端的搭建

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


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的项目,点击安装即可。



因为这个项目只用到了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);}
}

最终效果如下:



需要在开头添加以下代码

//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,开启监听,然后客户端也创建一个套接字,连接服务端就可以了,详见后面的代码。直接介绍数据传输流程,如下图:

由于TCP是以字节流的形式发送数据的,不能预知数据的大小,所以客户端在发送图像数据之前,需要先发送数据长度等信息。同时为了防止粘包(服务端接收到的数据会先缓存在缓冲区,在接收一次数据后,如果不及时处理,下一次接收到的数据也会送到缓冲区。由于这些数据都是字节流形式的,这样两次接收到的数据就会黏在一起,无法分开),客户端在发送完数据长度信息后,不能马上发送图像数据,需要等待服务端返回的应答信号。客户端接收到应答信号后,就可以开始发送图像字节流数据了。服务端完成图像数据接收后,还要返回给客户端一个应答信号,通知客户端开始下一帧图像的传输

服务端

#-*- coding: UTF-8 -*-
import socket
import cv2
import numpy as npHOST = ''
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 += datacnt += 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("已断开!")breakfinally: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)的图像实时传输。

本文中的高清晰度图像传输就是在前文方法的基础上,在发送端添加了切片压缩传输以及并行加速的步骤,而接收端则相应地使用多线程进行数据接收,分别接收压缩后的切片数据,再拼接起来进行显示。流程如下

发送端
在发送端我们需要达到的效果如下,左边用来显示原始图像,右上角用来显示各个切片,右下角用来处理接收端的连接请求。

首先设置一些参数

// 实例化一个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 列范围colimg[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();

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

首先进行参数设置

 // 切片数量,与发送端保持一致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所示,左边是发送端,右边是接收端

参考文献:
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实时传输图像相关推荐

  1. UDP/TCP网络传输方式

    网络传输方式 1.面向无连接型: 不要求建立和断开连接,发送端可于任何时候自由发送数据. 反之, 接收端也永远不知道自己会在何时从那里接收到数据. 因此, 面向无连接的情况下, 接收端需要时常确认是否 ...

  2. tcp实时传输kafka数据_将物联网数据和MQTT消息流式传输到Apache Kafka

    Apache Kafka是一个实时流媒体平台,在大型和小型组织中得到广泛采用.Kafka的分布式微服务架构和发布/订阅协议使其成为在企业系统和应用程序之间移动实时数据的理想选择.据一些人称,超过三分之 ...

  3. tcp实时传输kafka数据_tcp怎么传输大数据

    {"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],&q ...

  4. tcp实时传输kafka数据_关于Kafka producer管理TCP连接的讨论

    在Kafka中,TCP连接的管理交由底层的Selector类(org.apache.kafka.common.network)来维护.Selector类定义了很多数据结构,其中最核心的当属java.n ...

  5. 用udp实现可靠传输python_一种基于UDP协议实时可靠图像传输方案的制作方法

    本发明设计属于数据传输与通信技术领域,具体涉及一种基于UDP协议(User Datagram Protocol)实时可靠图像传输方案. 背景技术: 随着网络技术的高速发展,无线数据传输在通信.控制等领 ...

  6. C,C++网络编程实现图像实时传输

    导师让我把项目里OpenGl生成的图片通过UDP网络编程实时传输到电脑另一端,从一点不了解到实现图片单张传输,再到最后用TCP编程实现多张图片实时传输;没有能达到老师的要求,用UDP实现多张图片传输, ...

  7. Linux----网络传输层UDP/TCP

    网络传输层UDP/TCP 引入 ①端口号 ②端口号划分 ③知名端口号 ④netstat.pidof.killall.ps. awk.xargs???? ⑤所有协议需要解决的两个问题: 1)UDP ①U ...

  8. 最大可传输单元 MTU 对 UDP/TCP 包的大小限制

    目录 一.MTU 简述 - 分包后数据包最大长度 1.定义 2.网络中 MTU 值的由来: 1>.最大值: 2>.最佳值的推导: 3>.最佳值: 4>.最小值: 5>.碎 ...

  9. 在局域网内实现图像的实时传输

    作者:bat603 网址:http://blog.csdn.net/bat603/(本文可以随意转载及修改并可用于任何用处,但须注明作者和网址) 在局域网内实现图像的实时传输(实现环境bcb6.0+M ...

最新文章

  1. 学以致用三十五-----像素的困惑
  2. python signal模块的使用(自定义超时异常)
  3. kl散度度量分布_论“邻里关系”的学问:度量和改进图信息在图神经网络中的使用 | AI Time PhD ICLR...
  4. mysql数据库引擎调优
  5. 从XaaS到Java EE – 2012年哪一种该死的云最适合我?
  6. mysql 主从备份 主服务器配置_同一服务器配置Mysql主从备份
  7. 软件测试除了边界值还有什么,在软件测试中,假定 X 为整数,10≤X≤100,用边界值分析法,那么 X 在测试 中应该取( )边界值...
  8. Table表格横竖线实现Css
  9. Linux下Socket网络编程send和recv使用注意事项
  10. 关于EasyRecovery的一些高级设置
  11. Markdown 编辑器 Editor.md 使用
  12. 主汛期到达!水库大坝如何利用北斗短报文等应用防灾减灾?
  13. tulater成语词典 chm格式 和 EXE格式
  14. 计算机考试界面没有验证码,win10系统下网页验证码显示不了的两种解决方法
  15. 商场管理系统实战项目
  16. HDOJ Saving HDU JAVA 2111
  17. 网络拨测DialTest简单介绍
  18. SQL SERVER 发邮箱
  19. 网络组建大全(包括企业网、校园网、网吧和基础知识)(转)
  20. 白硕:区块链技术与数据隐私(附视频)

热门文章

  1. 互联网集体下沉,PKQ后又一匹下沉黑马出现了
  2. 三、Redis在SpringBoot中使用案例
  3. 如何让你的网站变黑白?
  4. 总结30个Python赚钱的接单平台!兼职月入5000+,成年人的世界,钱是活下去的筹码
  5. 《C++ Concurrency in Action》笔记
  6. 在浏览器上打开、预览Excel xlsx表格文件
  7. cesium中长度测量和面积测量
  8. 实用命令-pv: 管道查看器
  9. MySQL必知必会总结
  10. canal.deployer部署