【问题标题】:Python frame data optimization before sending through socket通过socket发送前的Python帧数据优化
【发布时间】:2018-09-21 21:33:38
【问题描述】:

按照Adrian's guide 和其他一些步骤的第 6 步,我设法以 10 fps 的速度和 0.1 秒的延迟将 320x240 帧从我的树莓派传输到我的笔记本电脑。问题是,当我在我的实验室(配备古董路由器)测试这个系统时,它只能以 1-1.5 秒的延迟流式传输 1-2 fps,这对于我打算用那些做的事情来说是完全不可接受的帧。

现在,我的方法很简单直接:树莓派上的服务器捕获一个帧并将其存储为 320x240x3 矩阵,就像上面提到的指南一样,然后腌制该矩阵并继续通过 TCP 套接字将其泵送。笔记本电脑上的客户端不断接收这些帧,对它们进行一些处理,最后用imshow显示结果。我的代码对于一篇文章来说相当长(大约 200 行),所以如果可以的话,我宁愿避免展示它。

有什么方法可以减小每帧数据的大小(腌制的 320x240x3 矩阵,其长度为 230 kB)还是有更好的方法来传输该数据?

编辑:

好的,伙计们,pickled 数组的确切呈现长度是 230563 字节,有效负载数据应至少为 230400 字节,因此开销不应超过总包大小的 0.07%。我认为这将问题缩小到无线连接质量和将数据编码为字节的方法(酸洗似乎很慢)。无线问题可以通过创建 ad-hoc 网络(听起来很有趣但我还没有尝试过)或简单地购买更好的路由器来解决,编码问题可以通过 Aaron 的解决方案来解决。希望这将有助于未来的读者:)

【问题讨论】:

  • cv2.imencode 可以用 jpeg 或 png 压缩为 ecample。您也可以使用 gzip 进行压缩...但是视频流通常由视频编解码器完成,例如 h264
  • 当速度是您的问题时,分析就是答案。如果我猜的话,是 pickle 让你慢了下来。
  • 我第二个@Aaron。酸洗对于预处理和存储大量文件很有用,但没有理由酸洗来流式传输数据。 Pickling 用于快速解包数据,但它可以比数据本身更大的字节数。流式传输数据本身会更好。还有 numpy 的内置转换器。
  • 取决于应用程序,帧之间的差异可能包含比每个单独帧少得多的熵。将其视为实际的图像流并沿时间维度对其进行压缩可能是有意义的,例如,如果帧主要由静态背景组成。
  • 也许考虑在机器人和执行处理的计算机之间创建一个 ad-hoc 网络。基本上是两台机器之间的直接链接。当像您的情况一样,它是控制回路的一部分时,可靠的通信通道将是必不可少的。

标签: python opencv raspberry-pi


【解决方案1】:

tl;drstruct 实际上很慢.. 而不是 pickle 使用 np.ndarray.tobytes() 结合 np.frombuffer() 来消除开销。

我不太熟悉opencv,这可能是最好的答案,但加快传输速度的一种直接方法可能是使用struct 来打包和解包要通过网络发送的数据而不是pickle

这是一个使用struct通过套接字发送已知维度的numpy数组的示例

import numpy as np
import socket
import struct

#----- server ------
conn = socket.socket()
#connect socket somewhere
arr = np.random.randint(0,256,(320,240,3), dtype="B") # unsigned bytes "B": camera likely returns 0-255 pixel values
conn.write(struct.pack('230400B', *arr.flat)) #230400 unsigned bytes

#----- client ------
conn = socket.socket()
#connect socket somewhere
data = conn.read(230400) #read 230400 bytes
arr = np.array(struct.unpack('230400B', data), dtype='B').reshape((320,240,3),)

编辑

一点点挖掘表明 numpy 有一个 tobytes 函数,它将数据的内存视图公开为 bytes 对象。这基本上完成了 struct 的工作,而无需在函数调用中解包参数以进行编码。这促使我也想看看我们是否也可以进行拆包,只要您可以坐在裤子座位上飞行一点(不会优雅地捕捉到中断或错误),我们就可以打包和拆包几乎零开销的数据是您网络的唯一限制因素。

测试脚本:

arr = np.random.randint(0,256,(320,240,3), dtype="B") # unsigned bytes "B": camera likely returns 0-255 pixel values

t = time()
for _ in range(100):
    arr2 = pickle.loads(pickle.dumps(arr))
print(f'pickle pack, pickle unpack: {time()-t} sec')

t = time()
for _ in range(100):
    arr2 = np.array(struct.unpack('230400B', struct.pack('230400B', *arr.flat)), dtype='B').reshape((320,240,3),)
print(f'struct pack, struct unpack: {time()-t} sec')

t = time()
for _ in range(100):
    arr2 = np.array(struct.unpack('230400B', arr.tobytes()), dtype='B').reshape((320,240,3),)
print(f'numpy pack, struct unpack: {time()-t} sec')

t = time()
for _ in range(100):
    arr2 = np.frombuffer(arr.tobytes(), dtype="B").reshape((320,240,3),)
print(f'numpy pack, numpy unpack: {time()-t} sec')

打印:

泡菜包,泡菜解包:0.005013704299926758 秒
结构包,结构解包:3.558577299118042 秒
numpy 包,结构解包:1.2988512516021729 秒
numpy 包,numpy 解包:0.0010025501251220703 秒

【讨论】:

  • 谢谢,我明天早上试试。有一堆跳线连接 pi 和机器人,所以我把它和机器人一起留在了实验室。
  • 我会给你一个加号,因为这避免了酸洗数据的不必要的开销。它也与 OpenCV 非常相关,因为 Python 绑定使用 numpy.arrays 来表示图像。我可能会在像素数组之前打包一些元数据(大小+通道数),以使协议更加灵活(成本不高)。 |总的来说,我真的不认为这是一个基于 cmets 中提到的 OP 的解决方案。不可靠的通信是问题所在,恕我直言,低成本硬件和简单的解决方案并不适合。
  • 我什至会考虑在那里进行一些相对轻量级的压缩,例如在缓冲区上进行zliblz4,甚至将cv2.imencode 放入PNG(这基本上是zlib 的选择)几个简单的预测变量)。
  • @BaoTran testing is king...struct 显然要慢得多,但根据我的测试,numpy.frombuffer(arr.tobytes())pickle 快大约 3 倍。 ymmv,看来网络稳定性是您的更大问题,但每一点都有帮助,对吧?
  • @DanMašek 我想要的只是比较.. 网络实际上将是最慢的部分。我做了进一步的测试,我放弃了中间 2 个条件,并使用形状为 (3200,2400,3) 的数组来获得我的 3 倍速度值,以确保循环开销不是最大的因素。
【解决方案2】:

由于通信速度和延迟良好且通信链路良好,因此您可能已经受限于 Raspberry 在帧速率方面的性能。通过在单独的线程中运行图像采集和通信(在 Python 中可能需要单独的进程来避免 GIL),您可能会有所收获(如果您还没有这样做的话)。

当然,避免任何不必要的开销,例如 Aaron 在他的回答中所描述的,尤其是当它很容易做到时,是值得的。

我什至会考虑使用一些轻量级的压缩。 Python 提供了zlib,您可以调整它以换取 CPU 使用率的压缩率。还有 bzip2 和自 Python 3.3 以来的 lzma,尽管这些是 CPU 密集型的。您还可以获得诸如 lz4snappy 之类的绑定,它们位于频谱的另一端。

另一种选择是使用带有某种压缩格式的cv2.imencode(和另一端的cv2.imdecode)。由于您将处理图像,因此无损编解码器似乎是合适的,因此 PNG 可能是一个不错的选择(基本上是 zlib 和一些简单的预测器)。

与以往一样,只要性能至关重要,请对各种方法进行基准测试,以便找出最适合您的方法。


如果您实际上有一台 320x240 分辨率的相机,我建议您传输原始的 Bayer CFA 数据(通常彩色相机是带有特殊滤色器的单色阵列)。与 RGB 格式相比,这意味着传输 1/3 的数据(并且您将在另一端进行去马赛克)。但是,由于这是来自 5 兆像素相机的已按比例缩小的图像,因此在这里无济于事。


但是,关于你提到的问题的关键:

  • 通过 WiFi 连接

  • 周围还有 10 个其他 WiFi 网络

  • 您使用的 WiFi 路由器很古老(很可能只有 2.4 GHz)

即使以最佳速率传输,每秒也只能传输 3 兆字节。那不是那么多,所以我会考虑以某种方式改善连接。有一些工具可让您扫描所有相邻网络。使用其中一个找到最不拥挤的频道(记住重叠)。也许您只是切换到更好的频道会改善这种情况。

如果可能,您可能希望使用 5GHz,因为这往往不会那么拥挤。然而,古老的路由器可能会阻止这种情况。您可以考虑在两台机器之间创建一个 ad-hoc 网络,这基本上是一个不涉及任何中间路由器的直接连接。

否则,请考虑投资购买更好的路由器,并确保获得可靠的通信链路。如果链接像您的情况一样是控制循环的一部分,我认为没有太多选择。


编辑:哦,也许是固定机器上的定向天线?

【讨论】:

    【解决方案3】:

    我一直在研究通过 wifi 从 RaspberryPi 传输图像到 Mac 的所有选项,但接收器可以是任何东西。

    我决定以 YUV420p 格式传输数据,因为这需要最少的网络带宽 - HALF 是 RGB 数据所需的。我尝试禁用降噪以增加 ISO 和我能想象到的一切以使其运行得更快,并使用我以 12 英镑(15 美元)从亚马逊购买的非常酷的小型逻辑分析仪和sigrok 逻辑分析仪软件分析了整个事情。

    这是我如何获得每秒 36 帧的 320x240 像素:

    #!/usr/bin/python3
    import io
    import socket
    import struct
    import time
    import picamera
    
    import RPi.GPIO as GPIO
    
    # Enable GPIO pins as output for Logic Analyser probe-based debugging
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(20, GPIO.OUT)
    GPIO.setup(21, GPIO.OUT)
    GPIO.setup(22, GPIO.OUT)
    GPIO.setup(23, GPIO.OUT)
    
    WIDTH=320
    HEIGHT=240
    BUFFERSIZE=WIDTH*HEIGHT*3//2
    PORT=8000
    SERVER='192.168.0.8'
    client_socket = socket.socket()
    client_socket.connect((SERVER, PORT))
    connection = client_socket.makefile('wb')
    try:
        with picamera.PiCamera() as camera:
            camera.resolution = (WIDTH, HEIGHT)
            camera.framerate = 60
            time.sleep(2)
            start = time.time()
            count = 0
            stream = io.BytesIO()
            # Use the video-port for captures...
            while True:
                GPIO.output(20,GPIO.HIGH)
                frame=next(camera.capture_continuous(stream, format="yuv", use_video_port=True))
                GPIO.output(20,GPIO.LOW)
                stream.seek(0)
                GPIO.output(22,GPIO.HIGH)
                connection.write(stream.read())
                GPIO.output(22,GPIO.LOW)
                count += 1
                if time.time() - start > 15:
                    break
                stream.seek(0)
                stream.truncate()
        #connection.write(struct.pack('<L', 0))
    finally:
        connection.close()
        client_socket.close()
        finish = time.time()
    print('Sent %d images in %d seconds at %.2ffps' % (
        count, finish-start, count / (finish-start)))
    GPIO.cleanup() 
    

    在 Mac 上,作为服务器运行并显示:

    #!/usr/local/bin/python3
    import io
    import socket
    import struct
    import numpy as np
    import cv2
    
    WIDTH=320
    HEIGHT=240
    PORT=8000
    BUFFERSIZE=WIDTH*HEIGHT*3//2
    print("Running on port {}: width={}, height={}".format(PORT,WIDTH,HEIGHT))
    
    # Start a socket listening for connections on all interfaces
    server_socket = socket.socket()
    server_socket.bind(('0.0.0.0', PORT))
    server_socket.listen(0)
    
    frame=0
    
    # Accept a single connection and make a file-like object out of it
    connection = server_socket.accept()[0].makefile('rb')
    try:
        while True:
            # Construct a stream and fill with image data from network
            image_stream = io.BytesIO()
            image_stream.write(connection.read(BUFFERSIZE))
            # Rewind the stream, and process
            image_stream.seek(0)
            img_str=image_stream.read()
            nparr=np.fromstring(img_str,np.uint8)
            # Data layout is here https://en.wikipedia.org/wiki/YUV#/media/File:Yuv420.svg
            Y=nparr[0:WIDTH*HEIGHT].reshape(HEIGHT,WIDTH)
            # ssU = sub-sampled U channel, ssV = sub-sampled V channel
            ssU=nparr[WIDTH*HEIGHT:WIDTH*HEIGHT*5//4].reshape(HEIGHT//2,WIDTH//2)
            ssV=nparr[WIDTH*HEIGHT*5//4:].reshape(HEIGHT//2,WIDTH//2)
            # Up-sample the U and V channels to full size
            U=cv2.resize(ssU,(WIDTH,HEIGHT),cv2.INTER_LINEAR)
            V=cv2.resize(ssV,(WIDTH,HEIGHT),cv2.INTER_LINEAR)
            # Combine the YUV into single image and convert to BGR
            oc_YUV=cv2.merge((Y,U,V))
            oc_BGR=cv2.cvtColor(oc_YUV,cv2.COLOR_YUV2BGR)
            # That's actually it, but I upscale for better viewing on screen
            tmp=cv2.resize(oc_BGR,(WIDTH*2,HEIGHT*2),cv2.INTER_LINEAR)
            cv2.imshow('image',tmp)
            cv2.waitKey(1)
            frame=frame+1
    finally:
        connection.close()
        server_socket.close()
    

    这是它运行的一个小视频。它的响应速度非常快,而且抖动只是因为我将屏幕捕获重新采样到 2fps 以使其低于 StackOverflow 2MB 限制。

    您可以放心地忽略代码中所有与 GPIO 相关的内容 - 只是在逻辑分析仪上调整引脚的高低,这样我就可以准确地计时。

    YUV 数据的布局如下:

    Xburge03 的原始位图版本,Qef 的 SVG 版本。 - http://en.wikipedia.org/wiki/Image:Yuv420.png 的矢量化版本 - 使用讨论页上给出的程序创建。,公共领域,https://en.wikipedia.org/w/index.php?curid=18105371


    鉴于您的问题似乎与网络带宽有关,我的方法是基于发送 YUV 数据,如上所述。我还查看了 Dan 关于压缩数据的建议,虽然带宽减少显然取决于您的照明和主题,但 YUV 数据确实适合一些 lz4 压缩。我看到网络带宽进一步减少了大约 25%,并且延迟没有明显增加。代码如下所示:

    import lz4.frame
    ...
    compressed=lz4.frame.compress(data,compression_level=lz4.frame.COMPRESSIONLEVEL_MINHC))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-05-03
      • 1970-01-01
      • 2021-03-27
      • 2022-06-14
      • 1970-01-01
      • 2019-10-13
      • 2014-10-27
      相关资源
      最近更新 更多