这个其实要算我当时实习的一项内容了,当时实习去了家老师开的小公司,工作压力相对轻松,很放开的感觉,觉得有什么需求就做什么,做完我之前的两个项目之后我就开始做这个了,做的图像传输,当然我这个到最后也只是浅尝辄止,没有做的太深了,这里也只是记录下这个过程,真正的应用到产品上还有很长的一段路要走,也希望如果有大佬看到这篇文章有更好的办法可以提出一些宝贵的意见!
1、UDP/TCP图像是怎么进行传输的
前面几篇文章刚刚说了,TCP/UDP传输的其实是通过套接字来进行传输的,貌似跟图像没什么关系,甚至我们都在想怎么把一张张图像这样给他传输过去,这也太麻烦了吧。
不过根据之前opencv的学习可以知道其实一张图像,或者说一段视频(视频就是由一张张的图像来组成的对吧),那么图像是啥呢,其实也就是一个矩阵来组成的对吧,这里我们可以加载一张图片并进行显示,可以看到他是一个numpy的矩阵,然后是6404803的一张图片,一般情况下默认加载图像上RGB格式的,这里就是三个通道了,图像大小还是640480的大小,这里的单位是像素
我们干脆就更爽快一点,也不用这些东西了,直接打印这个图片看看到底是什么情况,可以看到她其实也是一个个的数值,这里因为图像一般每个通道八位大小也就是(0-255)了,所以每个数值一般不会超过这个
那么传输这样的一张图像要传输多少数据呢,这里其实也就是在看看他有多大就是了,其实不打印也能知道,就是6404803了
所以我们传输图像的本质其实就是在传输这里面的矩阵,因此这里有一点很明显的就是我们传输的图像尺寸越大,必然会导致我们这个传输的速率变慢,同样的我们传输320240大小的图像和传输640480大小的图像而言,速度上就要减慢四倍,更不要说1080960这样尺寸的了,速度上也会大打折扣的,因此传输的时候需要提前考虑这个隐因素。
那么知道了这些其实传输就简单了,大概流程就是:
- 一端进行图像的获取和解码,然后通过socket将数据发送出去
- 另一端接收socket回传的数据
- 这边再重新转为图片,然后用opencv的方式显示出来
2、UDP传输
这里话不多说,先上代码:
服务器部分:
import cv2
from socket import *
cap = cv2.VideoCapture(0)
# 统一图像大小为640*480
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
Host = '192.168.0.100'
Port = 5555
sock = socket(AF_INET, SOCK_DGRAM) # 创建UDP套接字
while True:
ret, img = cap.read()
ret, send_data = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 50])# 将图片转为byte
sock.sendto(send_data, (Host,Port))
sock.close()
cv2.destroyAllWindows()
客户端部分
import numpy as np
import cv2
from socket import *
sock = socket(AF_INET, SOCK_DGRAM) # 创建UDP套接字
Host = '192.168.0.100'
Port = 5555
sock.bind((Host,Port))
sock.setblocking(0) # 设置为非阻塞模式
# 非阻塞模式 当程序碰到耗时操作,分发给别的线程,主线程继续执行,这样可以提升程序的效率
while True:
data = None
try:
data, address = sock.recvfrom(921600)
receive_data = np.frombuffer(data, dtype='uint8')
img = cv2.imdecode(receive_data, 1)
cv2.imshow('server', img)
except BlockingIOError as e: # 这里UDP传输有错误是正常的,但是影响不大,所以下面直接pass掉
pass
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
一些详情的介绍其实在代码注释里面我已经说明过了,这里就不进行赘述了,还是比较直白的,就直接截图看这个说明吧:
首先是数据发送,也就是客服端这边
下面是接收图像并显示的部分,也就是接收端,这里有一个很重要的点就是接收的字节,可以看到每次接收的字节是921600,这个其实就是最开始的设定了,因为我们传输的是一张6404803的彩色图像,所以这张图像其实大小就是921600这个大小,所以我们这里不用去判断图像的大小然后进行解码了。
这里为了说明这个图像传输还不错我这里用树莓派来测试,效果如下,可以看出来其实这个效果在树莓派上也是很不错的,也是基本上没有什么大的延迟。
3、TCP传输
还是一样,先上代码再说:
客户端部分如下:
import cv2
import socket
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
HOST = '192.168.218.133'
PORT = 12345
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST,PORT))
while True:
ref, img = cap.read()
_,img_encode = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 50])
bytedata = img_encode.tostring() # 转换为字节流
flag_data = (str(len(bytedata))).encode() + ",".encode() + " ".encode() # 将数据用,隔开
sock.send(flag_data)
data = sock.recv(1024)
if('ok' == data.decode()):
sock.send(bytedata) # 确认收到数据就开始发送图像
sock.close()
cv2.destroyAllWindows()
这里相比udp的部分就是多了一个判断,其他都没什么东西
服务器部分如下:
import socket
import cv2
import numpy as np
HOST = '192.168.218.133'
PORT = 12345
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((HOST,PORT))
sock.listen(True)
while True:
client_socket, client_address = sock.accept()
try:
while True:
data = client_socket.recv(1024) # 接收客户端数据
if data: # 如果收到客户端数据
client_socket.send(b'ok')
img_bytes = client_socket.recv(921600)
img = np.asarray(bytearray(img_bytes), dtype="uint8")
img = cv2.imdecode(img, cv2.IMREAD_COLOR)
cv2.imshow("img", img)
cv2.waitKey(1)
else:
print('已断开')
finally:
sock.close()
这里还是一贯的采用我们之前的方法,就是提前计算好图像尺寸,不然就要在图像里面记录了,那样太蛮烦了,网上有很多其他方案都是用的先发送命令在计算图像大小,这样需要专门设计大小计算的函数,不太方便
这里我用我的电脑来进行了测试,感觉效果还是不错的,也基本上看不到什么延迟,效果如下
4、使用树莓派CSI摄像头传输
因为当时用的是树莓派的方案,不过最近树莓派涨价涨的太厉害了,因此这个方案可能有一定的局限性了,看看就行,这个效果其实还是不错的,可以参考下。
将树莓派自身提供的picamera应该是使用硬件的方式进行的摄像头的读取的,这里我们就把这个当作读取视频的方式了,就不用opencv的方式来进行一帧帧的图像读取了,这里我参考了这个博客,感觉还是很不错的,有需要的可以看下原博客,链接如下:
这里还是看源码吧
客户端部分如下所示,这里相关参数都在注释中说明了
import time
import numpy as np
import cv2
import socket
import struct
import time
from picamera.array import PiRGBArray
from picamera import PiCamera
class Camera(object):
def __init__(self):
HOST = '192.168.218.133'
PORT = 8000
self.server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
self.server.connect((HOST,PORT))
def CameraInit(self): # 初始化PiCamera
self.camera = PiCamera()
self.camera.resolution = (640, 480)
self.camera.vflip = True
self.camera.hflip = True
self.camera.framerate = 32
rawCapture = PiRGBArray(self.camera, size=(640, 480))
rawCapture.truncate(0)
time.sleep(2) # wait for camera starting 相机需要时间预热
return self.camera, rawCapture
def VideoTransmission(self, frame): # transmit video from Pi to PC
result, imgencode = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 50]) #编码
try:
self.server.sendall(struct.pack('i',imgencode.shape[0])) # 发送数据长度作为校验
self.server.sendall(imgencode)
except:
print("fail to send the frame")
def CameraCleanup(self):
self.server.sendall(struct.pack('c',1)) #发送关闭消息
self.server.close()
self.camera.close()
if __name__ == '__main__':
try:
cam = Camera()
camera, rawCapture = cam.CameraInit()
for raw_frame in camera.capture_continuous(rawCapture, format="bgr",use_video_port=True):
frame = np.copy(raw_frame.array)
car.VideoTransmission(frame)
rawCapture.truncate(0)
except KeyboardInterrupt:
car.CameraCleanup()
下面是服务器部分
import cv2
import socket
import numpy as np
HOST='192.168.218.133'
PORT= 8000
buffSize=65535
if __name__=='__main__':
server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建socket对象
server.bind((HOST,PORT))
while True:
data,address=server.recvfrom(buffSize) #先接收的是字节长度
if len(data)==1 and data[0]==1: #如果收到关闭消息则停止程序
server.close()
cv2.destroyAllWindows()
exit()
data,address=server.recvfrom(buffSize) #接收编码图像数据
image = cv2.imdecode(np.frombuffer(data, dtype=np.uint8), cv2.IMREAD_COLOR)
cv2.imshow('frames',image,) #窗口显示
key = cv2.waitKey(1)
if cv2.waitKey(1)==27: #按下“ESC”退出
break
server.close()
cv2.destroyAllWindows()
最终效果如下所示,这个延迟其实还是很不错的,我用的测试方式是热点,基本上也看不出来什么
5、降低延迟的一些思路
综合上面的几种传输方式,可以总结其实影响延迟的因素无非就是传输的量的大小还有传输速度,因此个人觉得有这么几个是值得去尝试下的:
- 能用硬件解码就不要用软件的方式,因为软件是用opencv的方式来进行的,这样把图像转成字节本来就要花费一定的时间
- 尽量用小的图片格式,能不用大的就不用,大的图像造成传输的数据量急剧上升,其实一般情况也用不到那么大的图像
- 传输的卡顿也有可能是硬件本身造成的,比如路由器这个中继,因为它取决了网络传输的速度,可能电脑这边已经处理完了,但是路由器那边还没有
- 说到路由器那就可以想到一些硬件了,比如我们用一些其他的硬件例如树莓派等,这个时候影响速度的就有可能是这个设备本身了
好了上面就是本文的全部内容了,后续还将继续把这个传输系统结合qt做成上位机,这也是我当时实习的一部分内容