使用Python OpenCV将实时视频帧发送到网络上

27

我正在尝试将通过摄像机捕获的实时视频帧发送到服务器并进行处理。我使用OpenCV进行图像处理,Python作为编程语言。这是我的代码:

client_cv.py

import cv2
import numpy as np
import socket
import sys
import pickle
cap=cv2.VideoCapture(0)
clientsocket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
clientsocket.connect(('localhost',8089))
while True:
    ret,frame=cap.read()
    print sys.getsizeof(frame)
    print frame
    clientsocket.send(pickle.dumps(frame))

server_cv.py

import socket
import sys
import cv2
import pickle
import numpy as np
HOST=''
PORT=8089

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print 'Socket created'

s.bind((HOST,PORT))
print 'Socket bind complete'
s.listen(10)
print 'Socket now listening'

conn,addr=s.accept()

while True:
    data=conn.recv(80)
    print sys.getsizeof(data)
    frame=pickle.loads(data)
    print frame
    cv2.imshow('frame',frame)

这段代码给我返回了文件结尾错误,这是合理的,因为数据一直在服务器上流动,而pickle不知道何时结束。我的互联网搜索让我使用pickle,但到目前为止它还没有起作用。

注意:我将conn.recv设置为80,因为当我说print sys.getsizeof(frame)时,这就是我得到的数字。


1
最好使用cv2.imencode() / cv2.imdecode()而不是pickle。 - berak
尝试过直播的人,将其发送到服务器是否可行?还是最好在本地处理帧?例如,连续的人脸识别,一旦检测到人脸就应该发出ping信号吗? - TheSHETTY-Paradise
7个回答

28

几点建议:

  • 使用sendall而不是send,因为不能保证一次性发送所有内容;
  • pickle适用于数据序列化,但必须为客户端和服务器之间交换的消息制定自己的协议,这样可以提前知道用于反序列化的要读取的数据量(见下文);
  • 对于recv,如果接收大块数据,您将获得更好的性能,因此将80替换为4096甚至更多;
  • 要注意sys.getsizeof:它返回内存中对象的大小,这与要通过网络发送的字节的大小(长度)不同;对于Python字符串,这两个值完全不同;
  • 请注意您要发送的帧的大小。以下代码支持长达65535个字节的帧。如果有较大的帧,请将“H”更改为“L”。

协议示例:

client_cv.py

import cv2
import numpy as np
import socket
import sys
import pickle
import struct ### new code
cap=cv2.VideoCapture(0)
clientsocket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
clientsocket.connect(('localhost',8089))
while True:
    ret,frame=cap.read()
    data = pickle.dumps(frame) ### new code
    clientsocket.sendall(struct.pack("H", len(data))+data) ### new code

server_cv.py

import socket
import sys
import cv2
import pickle
import numpy as np
import struct ## new

HOST=''
PORT=8089

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print('Socket created')

s.bind((HOST,PORT))
print('Socket bind complete')
s.listen(10)
print('Socket now listening')

conn,addr=s.accept()

### new
data = ""
payload_size = struct.calcsize("H") 
while True:
    while len(data) < payload_size:
        data += conn.recv(4096)
    packed_msg_size = data[:payload_size]
    data = data[payload_size:]
    msg_size = struct.unpack("H", packed_msg_size)[0]
    while len(data) < msg_size:
        data += conn.recv(4096)
    frame_data = data[:msg_size]
    data = data[msg_size:]
    ###

    frame=pickle.loads(frame_data)
    print frame
    cv2.imshow('frame',frame)

你可能可以对这一切进行优化 (减少复制、使用缓冲区接口等),但至少你可以得到这个想法。


1
len(data)是多少?尝试使用L而不是H - H表示无符号短整型。 - mguijarr
1
是的,当我把H改成了L时,它起作用了。现在服务器端在我输入print frame时打印数组。最后一个问题是修复cv2.imread('frame',frame),因为它不打开任何窗口,也不会给出任何错误,但我认为我可以自己解决它。感谢您的帮助,非常感激。 - atakanyenel
1
这段代码在Python 3中无法正常工作,尽管它在Python 2中完美运行。 - Saikat
2
@Saikat,我很乐意将Python 3版本添加到答案中,或者至少发布您自己的Python 3答案以帮助其他人 :) - mguijarr
2
请问您能否发布Python 3版本的代码? - Rohan Sawant
显示剩余7条评论

18

在数月的互联网搜索后,这就是我找到的东西,我已经整理成类,并附带单元测试和文档,链接为SmoothStream,可以查看一下,这是我能在任何地方找到的唯一简单且可用的流媒体版本。

我使用了这段代码并将自己的代码包装在其周围。

Viewer.py

import cv2
import zmq
import base64
import numpy as np

context = zmq.Context()
footage_socket = context.socket(zmq.SUB)
footage_socket.bind('tcp://*:5555')
footage_socket.setsockopt_string(zmq.SUBSCRIBE, np.unicode(''))

while True:
    try:
        frame = footage_socket.recv_string()
        img = base64.b64decode(frame)
        npimg = np.fromstring(img, dtype=np.uint8)
        source = cv2.imdecode(npimg, 1)
        cv2.imshow("Stream", source)
        cv2.waitKey(1)

    except KeyboardInterrupt:
        cv2.destroyAllWindows()
        break

流媒体.py

import base64
import cv2
import zmq

context = zmq.Context()
footage_socket = context.socket(zmq.PUB)
footage_socket.connect('tcp://localhost:5555')

camera = cv2.VideoCapture(0)  # init the camera

while True:
    try:
        grabbed, frame = camera.read()  # grab the current frame
        frame = cv2.resize(frame, (640, 480))  # resize the frame
        encoded, buffer = cv2.imencode('.jpg', frame)
        jpg_as_text = base64.b64encode(buffer)
        footage_socket.send(jpg_as_text)

    except KeyboardInterrupt:
        camera.release()
        cv2.destroyAllWindows()
        break

2
将JPEG数据编码为Base64会使载荷乘以3倍左右... 为什么不发送原始的JPEG二进制数据? - mguijarr
老实说,我不知道该如何做,你能给我演示一下吗?将numpy数组转换为字符串? - Rohan Sawant
1
我们的朋友 @mguijarr 注意到的问题是,您使用 imencode + b64encode 两次编码帧时的开销。 如果您关心性能,并且您的流媒体只需要将图像发送给客户端/观众。我建议您直接发送原始帧数据。 然后,在客户端/观众端,您只需要按照您的意愿对原始帧进行 imencode 编码为 jpeg。 - meetnick
在 b64decode 过程中,我遇到了“TypeError: Incorrect padding”错误。 - M Y

11

我将代码从@mguijarr更改为适用于Python 3的代码。对代码所做的更改如下:

  • data现在是字节文字而不是字符串文字
  • 将“H”更改为“L”以发送更大的帧大小。根据文档,我们现在可以发送大小为2 ^ 32而不仅仅是2 ^ 16的帧。

Server.py

import pickle
import socket
import struct

import cv2

HOST = ''
PORT = 8089

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('Socket created')

s.bind((HOST, PORT))
print('Socket bind complete')
s.listen(10)
print('Socket now listening')

conn, addr = s.accept()

data = b'' ### CHANGED
payload_size = struct.calcsize("L") ### CHANGED

while True:

    # Retrieve message size
    while len(data) < payload_size:
        data += conn.recv(4096)

    packed_msg_size = data[:payload_size]
    data = data[payload_size:]
    msg_size = struct.unpack("L", packed_msg_size)[0] ### CHANGED

    # Retrieve all data based on message size
    while len(data) < msg_size:
        data += conn.recv(4096)

    frame_data = data[:msg_size]
    data = data[msg_size:]

    # Extract frame
    frame = pickle.loads(frame_data)

    # Display
    cv2.imshow('frame', frame)
    cv2.waitKey(1)

客户端.py

import cv2
import numpy as np
import socket
import sys
import pickle
import struct

cap=cv2.VideoCapture(0)
clientsocket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
clientsocket.connect(('localhost',8089))

while True:
    ret,frame=cap.read()
    # Serialize frame
    data = pickle.dumps(frame)

    # Send message length first
    message_size = struct.pack("L", len(data)) ### CHANGED

    # Then data
    clientsocket.sendall(message_size + data)

3
我认为这是更好的做事方式!如果你能够友善地向 https://github.com/CT83/SmoothStream/ 提交一个PR,其他许多人也会受益。 - Rohan Sawant
目前,我似乎对ZeroMQ有不必要的依赖,你的代码解决了这个问题! - Rohan Sawant
3
请注意,不同的架构中'L'的含义是不同的,如果你想让树莓派向电脑发送消息,可以使用'=L'代替。 - Charles-Édouard Coste
谢谢@Charles-ÉdouardCoste,你的回复真是救了我一命。在我尝试从树莓派发送数据时,我不知道为什么数据大小会不同,直到我看到了你的评论。非常感谢你,伙计。 - MMH

3
我有点晚了,但是我强大而线程化的VidGear视频处理Python库现在提供NetGear API,专门设计用于在网络上实时同步传输连接系统之间的视频帧。以下是一个例子:

A. 服务器端:(最简单的例子)

打开您喜欢的终端并执行以下Python代码:

注意:您可以通过在服务器端键入键盘上的[Ctrl+c]随时结束服务器和客户端上的流!

Translated text:
我有点晚了,但是我强大而线程化的 VidGear 视频处理 Python 库现在提供 NetGear API,专门设计用于在网络上实时同步传输连接系统之间的视频帧。以下是一个例子:
A. 服务器端: (最简单的例子)
打开您喜欢的终端并执行以下 Python 代码:
注意: 您可以通过在服务器端按下键盘上的 [Ctrl+c] 随时结束服务器和客户端上的流!
# import libraries
from vidgear.gears import VideoGear
from vidgear.gears import NetGear

stream = VideoGear(source='test.mp4').start() #Open any video stream
server = NetGear() #Define netgear server with default settings

# infinite loop until [Ctrl+C] is pressed
while True:
    try: 
        frame = stream.read()
        # read frames

        # check if frame is None
        if frame is None:
            #if True break the infinite loop
            break

        # do something with frame here

        # send frame to server
        server.send(frame)
    
    except KeyboardInterrupt:
        #break the infinite loop
        break

# safely close video stream
stream.stop()
# safely close server
server.close()

B. 客户端:(最简单的例子)

然后在同一系统上打开另一个终端并执行以下Python代码,查看输出:

# import libraries
from vidgear.gears import NetGear
import cv2

#define netgear client with `receive_mode = True` and default settings
client = NetGear(receive_mode = True)

# infinite loop
while True:
    # receive frames from network
    frame = client.recv()

    # check if frame is None
    if frame is None:
        #if True break the infinite loop
        break

    # do something with frame here

    # Show output window
    cv2.imshow("Output Frame", frame)

    key = cv2.waitKey(1) & 0xFF
    # check for 'q' key-press
    if key == ord("q"):
        #if 'q' key-pressed break out
        break

# close output window
cv2.destroyAllWindows()
# safely close client
client.close()

最初的回答:
更高级的用法和相关文档可以在这里找到:https://github.com/abhiTronix/vidgear/wiki/NetGear

感谢您实现了这样一个强大的库。我想在向服务器发送帧后收到响应。您能否提供代码片段以从服务器获取响应。谢谢。 - Bala venkatesh
@Balavenkatesh已经在我们的 gitter 社区频道回复了你。 - abhiTronix
谢谢,我明白了。 - Bala venkatesh

2

正如@Rohan Sawant所说,我使用zmq库而不使用base64编码。以下是新代码:

Streamer.py

import base64
import cv2
import zmq
import numpy as np
import time

context = zmq.Context()
footage_socket = context.socket(zmq.PUB)
footage_socket.connect('tcp://192.168.1.3:5555')

camera = cv2.VideoCapture(0)  # init the camera

while True:
        try:
                grabbed, frame = camera.read()  # grab the current frame
                frame = cv2.resize(frame, (640, 480))  # resize the frame
                encoded, buffer = cv2.imencode('.jpg', frame)
                footage_socket.send(buffer)


        except KeyboardInterrupt:
                camera.release()
                cv2.destroyAllWindows()
                break

Viewer.py

import cv2
import zmq
import base64
import numpy as np

context = zmq.Context()
footage_socket = context.socket(zmq.SUB)
footage_socket.bind('tcp://*:5555')
footage_socket.setsockopt_string(zmq.SUBSCRIBE, np.unicode(''))

while True:
    try:
        frame = footage_socket.recv()
        npimg = np.frombuffer(frame, dtype=np.uint8)
        #npimg = npimg.reshape(480,640,3)
        source = cv2.imdecode(npimg, 1)
        cv2.imshow("Stream", source)
        cv2.waitKey(1)

    except KeyboardInterrupt:
        cv2.destroyAllWindows()
        break

1

我已经成功让它在我的MacOS上工作。

我使用了@mguijarr的代码,并将struct.pack从“H”更改为“L”。

# Server.py:
import socket
import sys
import cv2
import pickle
import numpy as np
import struct ## new


HOST=''
PORT=8089
    
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print 'Socket created'
    
s.bind((HOST,PORT))
print 'Socket bind complete'
s.listen(10)
print 'Socket now listening'
    
conn,addr=s.accept()
    
# new
data = ""
payload_size = struct.calcsize("L") 
while True:
    while len(data) < payload_size:
        data += conn.recv(4096)
    packed_msg_size = data[:payload_size]
    data = data[payload_size:]
    msg_size = struct.unpack("L", packed_msg_size)[0]
    while len(data) < msg_size:
        data += conn.recv(4096)
    frame_data = data[:msg_size]
    data = data[msg_size:]
    
    
    frame=pickle.loads(frame_data)
    print frame
    cv2.imshow('frame',frame)
        
    key = cv2.waitKey(10)
    if (key == 27) or (key == 113):
        break
    
cv2.destroyAllWindows()

# Client.py
import cv2
import numpy as np
import socket
import sys
import pickle
import struct ### new code


cap=cv2.VideoCapture(0)
clientsocket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
clientsocket.connect(('localhost',8089))

while True:
    ret,frame=cap.read()
    data = pickle.dumps(frame) ### new code
    clientsocket.sendall(struct.pack("L", len(data))+data) ### new code

1

最近我发布了 imagiz 包,它使用 OpenCV 和 ZMQ 实现快速且非阻塞的视频实时流传输。

https://pypi.org/project/imagiz/

客户端:

import imagiz
import cv2


client=imagiz.Client("cc1",server_ip="localhost")
vid=cv2.VideoCapture(0)
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90]

while True:
    r,frame=vid.read()
    if r:
        r, image = cv2.imencode('.jpg', frame, encode_param)
        client.send(image)
    else:
        break

服务器:

import imagiz
import cv2

server=imagiz.Server()
while True:
    message=server.recive()
    frame=cv2.imdecode(message.image,1)
    cv2.imshow("",frame)
    cv2.waitKey(1)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接