(五)抠图/抠像服务、图像\视频TCP/IP传输、图像\视频Socket传输、AI实时抠图、AI实时抠像、PaddlePaddle模型、虚拟现实视频会议、沉浸式会议场景、人像去背景、视频背景消除
本文与前几篇博文关联性较强,请事先阅读前几篇。 对此文感兴趣的可以加微深入探讨:herbert156
一、抠图/抠像服务器模式
有时抠图/抠像服务不止是单机使用,需要部署到服务端,给其它的客户端使用,需要利用Socket来进行不同机器之间的图片、视频的传输;此外这种办法也适用于不同编程环境之间的对接,比如服务端用Python部署抠像服务、客户端用其它语言Unity、C/C++、Java等处理抠图之后的图片;
二、服务端处理代码
此函数是服务器端的Python代码, 注释较全,请参考注释即可。
def ReceiveImage():
address = ('0.0.0.0', 55555) # IP地址'0.0.0.0'为等待客户端连接
# 建立socket对象 socket.AF_INET:服务器之间网络通信 socket.SOCK_STREAM:流式socket , for TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(address) # 将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址.
s.listen(1) # 开始监听TCP传入连接。参数指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序5。
def recvall(sock, count):
buf = b'' # buf是一个byte类型
while count:
newbuf = sock.recv(count) # 接受TCP套接字的数据。数据以字符串形式返回,count指定要接收的最大数据量.
if not newbuf: return None
buf += newbuf
count -= len(newbuf)
return buf
# 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。addr是连接客户端的地址。
# 没有连接则等待有连接 conn, addr = s.accept()
conn, addr = s.accept()
print('Receiving Frame From :【' + addr[0] +':' + str(addr[1])+'】')
while True:
try:
length = recvall(conn, 16) # 获得图片文件的长度,16代表获取长度
#print(int(length))
stringData = recvall(conn, int(length)) # 根据获得的文件长度,获取图片文件
except:
print("TCP连接中断,等待客户端重新连接...");
conn, addr = s.accept()
print('Receiving Frame From :【' + addr[0] + ':' + str(addr[1]) + '】')
continue
try:
data = np.frombuffer(stringData, np.uint8) # 将获取到的字符流数据转换成1维数组
decimg = cv2.imdecode(data, cv2.IMREAD_COLOR) # 将数组解码成图像
except:
print("The Date is not image Format!"); continue
if not Debug_On: ret_img = koutu(decimg) #调用抠图函数
else: ret_img = cv2.flip(decimg,1) #调试时使用:镜像图片
#ret_img = cv2.imdecode(np.fromfile('test_pic.png', dtype=np.uint8), -1)
#print(ret_img.shape)
#cv2.imshow('Server', ret_img) # 显示图像
#cv2.waitKey(1)
encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), rates] #rates:1-9
result, imgencode = cv2.imencode('.png', ret_img, encode_param)
data1 = np.array(imgencode)
stringData1 = data1.tobytes()
conn.send(str.encode(str(len(stringData1)).ljust(16)))
conn.send(stringData1) # 发送图片数据
s.close()
三、客户端处理代码
此函数是客户端的Python代码,也可用其它语言编写,与服务端互通;注释较全,请参考注释即可。
代码中用到的函数(如:CV2toPIL、PILtoCV2、two_pic_combine_PIL等),请在我的其它博文中查找。
import socket,cv2,time,sys
import numpy as np
from PIL import Image
def SendImage(IP = '127.0.0.1', PORT = 55555, src_img = []):
address = (IP, PORT) # 建立socket连接,设置连接服务器的IP地址和端口号
def recvall(sock, count):
buf = b'' # buf是一个byte类型
while count:
newbuf = sock.recv(count) # 接受TCP套接字的数据,以字符串形式返回,count指定要接收的最大数据量.
if not newbuf: return None
buf += newbuf
count -= len(newbuf)
return buf
try: #建立socket对象;socket.AF_INET:与服务器之间网络通信;socket.SOCK_STREAM:流式socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(address) # 开启socket连接
except socket.error as msg: print(msg); sys.exit(1)
while True:
#frame = cv2.imdecode(np.fromfile(test_file, dtype=np.uint8), -1)
t1 = time.time() #计算单帧处理时间
try: ret, frame = cap.read() #读取摄像机的1帧
except: print('读取摄像头, 出现错误!'); stop_flag = True; return
#cv2.imshow("Camera Video (Exit Key: 'q')", frame) # 显示摄像机原始视频
# 压缩参数,后面cv2.imencode将会用到,对于jpeg来说,rates越高代表图像质量越好,0-100,默认95
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), rates]
# cv2.imencode将图片格式转换成流数据,赋值到内存缓存中;主要用于图像数据格式的压缩、'.jpg'表示将图片按照jpg格式编码。
result, imgencode = cv2.imencode('.jpg', frame, encode_param)
data = np.array(imgencode) # 建立numpy矩阵
stringData = data.tobytes() # 将numpy矩阵转换成字符形式,以便在网络中传输
# 先发送要发送的数据的长度;ljust()方法返回一个原字符串左对齐,并使用空格填充至指定长度的新字符串
sock.send(str.encode(str(len(stringData)).ljust(16))) #发送图片长度数据
sock.send(stringData) # 发送图片内容数据
length = recvall(sock, 16) # 接受数据,获得图片文件的长度,16代表获取长度
stringData1 = recvall(sock, int(length)) # 根据获得的文件长度,获取图片文件
data = np.frombuffer(stringData1, np.uint8) # 将获取到的字符流数据转换成1维数组
ret_img = cv2.imdecode(data, -1) # 将数组解码成图像:1代表3通道、0代表灰度图像、-1代表4通道
#print(ret_img.shape)
print('单帧处理耗时:%.3f秒\b' % (time.time() - t1), end='\r')
img1 = cv2.resize(bk_img, (size_x, size_y)) #使绿色背景图片大小与摄像机帧大小一致
img2 = two_pic_combine_PIL(img1, ret_img) #使带A通道的图片叠加到背景图片以便显示
img3 = np.concatenate((frame, img2), axis=1) #合并成1张图,以便对比观察
cv2.imshow("Removing Background (Exit Key: 'q')", img3) #显示抠像返回的视频
if cv2.waitKey(1) == ord('q'): break
cap.release() #关闭摄像机
sock.close() #关闭Socket
if __name__ == '__main__':
bk_img = np.zeros((720, 1280, 3), np.uint8) # 建立黑色空白图片,以便与返回的透明图片合成
cam_no_str = input("请输入摄像机编号(回车默认0):") #电脑摄像机的编号:0、1、2等等
if cam_no_str == '': cam_no = 0
else: cam_no = int(cam_no_str)
my_ip = input("请输入服务器IP(回车默认本机地址):")
if my_ip == '': my_ip = '127.0.0.1' #'127.0.0.1'是本机环回地址;如果服务器在另外一台电脑,则输入服务器的IP
try: cap = cv2.VideoCapture(cam_no) # 读取视频文件
except: print('读取摄像头, 出现错误...')
cap.set(3, 640); cap.set(4, 360) #设置摄像机分辨率;不设置的话,就会使用摄像头默认分辨率
size_x = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) # 视频流的帧宽度
size_y = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 视频流的帧高度
if size_x == 0 or size_y == 0: print('读取摄像头(编号:%d), 出现错误...' % cam_no)
rates = 75 #网络传输JPEG压缩率
my_port = 55555 #服务器的接受端口
print("\n摄像机编号:", cam_no)
print("服务器IP:",my_ip)
print("\n在视频窗口里,敲击字母'q'退出程序...\n")
print("摄像机分辨率:", size_x,'x', size_y,'\n')
SendImage(IP = my_ip, PORT = my_port) #调用发送函数
四、效率优化
抠图消耗时间与图片分辨率相关,720p的大概15fps,基本可以满足要求。服务端在传输之前进行了PNG格式的数据压缩,消耗时间很长,影响了实时性。 解决办法:
1、采用多线程处理,抠图的同时进行压缩、传输;
2、采用不压缩方式,直接对原图进行传输,缺点是占用了过多的网络带宽,在本机127.0.0.1对网络无影响,因为数据没有经过网卡,直接环回了。