使用Python从屏幕中捕获视频数据

57

有没有一种使用Python(可能是通过OpenCV或PIL)连续抓取整个屏幕或部分屏幕帧的方法,至少以15fps或更高的速度?我已经看到其他语言实现过这个功能,所以理论上应该是可能的。

我不需要将图像数据保存到文件中。实际上,我只想输出包含原始RGB数据的数组(例如numpy数组),因为我打算将其发送到一个大型LED显示屏(可能在重新调整大小后)。


https://dev59.com/KGAf5IYBdhLWcg3w7mY_#24213099 - furas
11个回答

38

尝试了以上所有解决方案后,我无法获得可用的帧率,直到我按以下方式修改了我的代码:

import numpy as np
import cv2
from mss import mss
from PIL import Image

bounding_box = {'top': 100, 'left': 0, 'width': 400, 'height': 300}

sct = mss()

while True:
    sct_img = sct.grab(bounding_box)
    cv2.imshow('screen', np.array(sct_img))

    if (cv2.waitKey(1) & 0xFF) == ord('q'):
        cv2.destroyAllWindows()
        break

使用这个解决方案,我可以轻松获得20+帧/秒。

参考链接:OpenCV/Numpy example with mss


4
在Mac Mini上达到65fps——太棒了! - Wolfgang Fahl
2
将文件保存为.npy格式非常容易,我每10-15毫秒获取一帧。结果是100FPS - 67FPS。我猜SSD在这里也扮演了重要的角色。 - Mridul Pandey
1
太棒了,这应该是最佳答案。 - Daniel Chin

33

使用 mss 可以提供更好的帧率,是另一种解决方案。(在MacOS Sierra上使用Macbook Pro测试过)

import numpy as np
import cv2
from mss import mss
from PIL import Image

mon = {'left': 160, 'top': 160, 'width': 200, 'height': 200}

with mss() as sct:
    while True:
        screenShot = sct.grab(mon)
        img = Image.frombytes(
            'RGB', 
            (screenShot.width, screenShot.height), 
            screenShot.rgb, 
        )
        cv2.imshow('test', np.array(img))
        if cv2.waitKey(33) & 0xFF in (
            ord('q'), 
            27, 
        ):
            break

1
当我使用Image.frombytes时,出现了“ValueError: not enough image data”的错误。 - ypicard
6
截至2019年7月,这段代码将会触发以下错误:"AttributeError: 'MSS' object has no attribute 'get_pixels'"。 - YakovK
2
为了避免这个问题,安装一个旧版本的mss: pip uninstall msspip install mss==2.0.22 - Nicolas Forstner
4
另一个解决方法是使用 mss().grab(window)。代码类似于这样: screenshot = mss.mss().grab(window) img = Image.frombytes("RGB", (screenshot.width, screenshot.height), screenshot.rgb) - qdtroemner
1
给所有未来的读者:你必须在底部(while / end之后)添加cv2.destroyAllWindows(),否则你的窗口将关闭(当你按q键时(ln.7 if语句),尽管窗口本身不会被“销毁”/“关闭”)。在使用cv2和cv2.imshow时,永远不要忘记在代码末尾加上cv2.destroyAllWindows()。如果你正在使用jupyter笔记本,并且因为忘记它而导致cv2窗口冻结,只需重新启动内核(Kernel -> Restart)。 - anon
显示剩余2条评论

20
你需要使用Pillow(PIL)库中的ImageGrab并将截屏转换为numpy数组。当你有了该数组后,可以使用opencv对其进行任意处理。我将截屏转换为灰色,并使用imshow()作为演示。
以下是一个快速入门代码:
from PIL import ImageGrab
import numpy as np
import cv2

img = ImageGrab.grab(bbox=(100,10,400,780)) #bbox specifies specific region (bbox= x,y,width,height *starts top-left)
img_np = np.array(img) #this is the array obtained from conversion
frame = cv2.cvtColor(img_np, cv2.COLOR_BGR2GRAY)
cv2.imshow("test", frame)
cv2.waitKey(0)
cv2.destroyAllWindows()

你可以将一个数组插入其中,设置你想要保持捕获帧的频率。之后,只需要解码这些帧即可。别忘了在循环之前添加:

fourcc = cv2.VideoWriter_fourcc(*'XVID')
vid = cv2.VideoWriter('output.avi', fourcc, 6, (640,480))

你可以在循环内添加:

vid.write(frame) #the edited frame or the original img_np as you please

更新
最终结果看起来像这样(如果您想要实现一系列帧。将其存储为视频只是使用OpenCV在屏幕捕获上进行演示):

from PIL import ImageGrab
import numpy as np
import cv2
while(True):
    img = ImageGrab.grab(bbox=(100,10,400,780)) #bbox specifies specific region (bbox= x,y,width,height)
    img_np = np.array(img)
    frame = cv2.cvtColor(img_np, cv2.COLOR_BGR2GRAY)
    cv2.imshow("test", frame)
    cv2.waitKey(0)
cv2.destroyAllWindows()
希望有所帮助。

不确定你的意思:“你可以插入一个数组,以任意频率捕获帧”... 数组在哪里?而且我根本不需要进行视频输出...有没有一种指定在屏幕上抓取位置的方法? - Adam Haile
我想说的是添加一个while循环来获取屏幕截图流。我现在会编辑代码。至于指定特定区域,请使用bbox参数。等一下,我会进行更新。 - ibininja
我进行了进一步的更改。希望这有所帮助。 - ibininja
1
这个本质上使用了 ImageGrab.grab(),根据我的经验来看非常非常慢(在我的2015款Macbook Pro上大约只有2帧每秒)。这个解决方案无法达到OP要求的超过15帧每秒的捕获速度。我还尝试过更快的 mss 库,但是我没能保证稳定的帧率或图像之间的规则时间间隔。 - Ray
1
ImageGrab 只支持 macOS 和 Windows。 - Sarath Ak
@ibininja 我在我的 Mac 机器上尝试了上面的代码。它生成了大小为 0 kb 的 output.avi 文件。我不确定出了什么问题。 - SachinB

9
基于这篇文章和其他文章的内容,我制作了类似于这样的东西。 它会在不保存图片的情况下截取屏幕,并将其写入视频文件中。
import cv2
import numpy as np
import os
import pyautogui

output = "video.avi"
img = pyautogui.screenshot()
img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
#get info from img
height, width, channels = img.shape
# Define the codec and create VideoWriter object
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output, fourcc, 20.0, (width, height))

while(True):
 try:
  img = pyautogui.screenshot()
  image = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
  out.write(image)
  StopIteration(0.5)
 except KeyboardInterrupt:
  break

out.release()
cv2.destroyAllWindows()

1
谢谢,朋友。第18行有两个拼写错误,应该传递img而不是image,在第19行应该传递image而不是img。 - Muhammad Younus
谢谢,没注意到,只是想把它放在一起:D(很快就会修复) - szanyi krisztián

6
您可以尝试这段代码,因为它对我来说很有效。我已在Linux上进行了测试。
import numpy as np
import cv2
from mss import mss
from PIL import Image

sct = mss()

while 1:
    w, h = 800, 640
    monitor = {'top': 0, 'left': 0, 'width': w, 'height': h}
    img = Image.frombytes('RGB', (w,h), sct.grab(monitor).rgb)
    cv2.imshow('test', cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR))
    if cv2.waitKey(25) & 0xFF == ord('q'):
        cv2.destroyAllWindows()
        break

请确保已安装以下软件包:

Pillow,opencv-python,numpy,mss


很棒的方法,使用这种方法平均可以获得90帧每秒。 - DankCoder

5
你可以尝试这个=>
import mss
import numpy

with mss.mss() as sct:
    monitor = {'top': 40, 'left': 0, 'width': 800, 'height': 640}
    img = numpy.array(sct.grab(monitor))
    print(img)

2
我尝试了以上所有方法,但没有实时屏幕更新。您可以尝试这个代码。这段代码已经过测试并成功运行,并且还会给出良好的fps输出。您还可以通过每个循环时间来判断它。
import numpy as np
import cv2
from PIL import ImageGrab as ig
import time

last_time = time.time()
while(True):
    screen = ig.grab(bbox=(50,50,800,640))
    print('Loop took {} seconds',format(time.time()-last_time))
    cv2.imshow("test", np.array(screen))
    last_time = time.time()
    if cv2.waitKey(25) & 0xFF == ord('q'):
        cv2.destroyAllWindows()
        break

1
我在Windows上使用Python 3.6尝试了一下,我的i7 6700k CPU大约获得了10 fps。 - Pablo Canseco
循环花费了{}秒。这是我的FPS吗? - Yaroslav Dukal
不,这是两个帧处理之间的时间差异... 差异越小越好。您可以进行另一种计算,例如分析每60秒处理了多少帧的差异... - Istiyak
1
在MacMini上的帧速率为2.5 fps,而Markoe7的方法则可以渲染超过65 fps。 - Wolfgang Fahl

2

如果有人正在寻找一种更简单和更快的方法来在Python中抓取屏幕作为帧,那么可以尝试使用ScreenGear API,它是我高性能视频处理vidgear库中的一部分,只需几行Python代码即可在任何机器上使用(已在所有平台上进行了测试,包括Windows 10、MacOS Serra、Linux Mint)并享受多线程屏幕广播:

注意:它还支持多个后端和屏幕开箱即用。

# import required libraries
from vidgear.gears import ScreenGear
import cv2

# define dimensions of screen w.r.t to given monitor to be captured
options = {'top': 40, 'left': 0, 'width': 100, 'height': 100}

# open video stream with defined parameters
stream = ScreenGear(logging=True, **options).start()

# loop over
while True:

    # read frames from stream
    frame = stream.read()

    # check for frame if Nonetype
    if frame is None:
        break


    # {do something with the frame here}


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

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

# close output window
cv2.destroyAllWindows()

# safely close video stream
stream.stop()

VidGear库文档: https://abhitronix.github.io/vidgear

ScreenGear API: https://abhitronix.github.io/vidgear/latest/gears/screengear/overview/

更多示例: https://abhitronix.github.io/vidgear/latest/gears/screengear/usage/


1
@PaulLam 全部都可以。您可以在任何您选择的平台上安装它:https://abhitronix.github.io/vidgear/latest/installation/#supported-systems - abhiTronix
1
@PaulLam 有人在安卓上尝试过:https://github.com/rubar-tech/vidgear-kivy-android-opencv,但不确定iOS是否可行。如果您能在iOS上安装和运行Python,则是可以的。 - abhiTronix
我试图在安卓上使用并导入vidgear,但在导入vidgear的依赖时出现了一些问题:(第一个是'mss',我通过import mss; mss.mss(display = ":0")来解决它,然后又出现了另一个错误:x11库未找到。我尝试在buildozer.specrequirement中包含python3-xlib,但似乎不起作用:( 我该如何解决它? - Paul Lam
嗯,我正在使用 Windows 操作系统,并使用“Windows 子系统用于 Linux”来使用buildozer构建apk,你是指要编辑源代码吗? 我尝试在我的.py文件中测试使用 try:from vidgear.gears import ScreenGear;ScreenGear(),但又出现了另一个错误:(无法在此系统上抓取任何实例,您是否正在无头运行?我找不到任何在线解决方案.... 你介意打开聊天吗? 我确实需要帮助.... 谢谢。 - Paul Lam
@PaulLam 我们在这里讨论:https://gitter.im/vidgear/community - abhiTronix
显示剩余3条评论

0

我尝试了来自PILImageGrab,它给了我20fps,这还可以,但使用win32库给了我+40fps,这太棒了!

我使用了Frannecklp的this代码,但它并没有完全正常工作,所以我需要修改它:

-首先,在使用库时安装pip install pywin32

-像这样导入库:

import cv2
import numpy as np
from win32 import win32gui
from pythonwin import win32ui
from win32.lib import win32con
from win32 import win32api

获取简单的图像屏幕,请执行以下操作:

from grab_screen import grab_screen
import cv2
img = grab_screen()
cv2.imshow('frame',img)

获取帧:

while(True):
#frame = grab_screen((0,0,100,100))
frame = grab_screen()
cv2.imshow('frame',frame)
if cv2.waitKey(1) & 0xFF == ord('q') or x>150:
    break

0
对于那些想知道如何在纯 macOS 上实现此功能的人,以下窗口捕获代码在 M1 Pro 硅 MacBook Pro 上运行良好。
它通过名称捕获所需的窗口,并使用其 ID 使用 Quartz(与 macOS 的 Core Graphics 框架交互的 Python 接口)创建“屏幕截图”。
import numpy as np
import Quartz as QZ

class WindowCapture:

    # properties
    window_name = None
    window = None
    window_id = None
    window_width = 0
    window_height = 0

    # constructor
    def __init__(self, given_window_name=None):
        if given_window_name is not None:

            self.window_name = given_window_name
            self.window = self.get_window()

            if self.window is None:
                raise Exception('Unable to find window: {}'.format(given_window_name))

            self.window_id = self.get_window_id()

            self.window_width = self.get_window_width()
            self.window_height = self.get_window_height()

            self.window_x = self.get_window_pos_x()
            self.window_y = self.get_window_pos_y()
        else:
            raise Exception('No window name given')

    def get_window(self):
        windows = QZ.CGWindowListCopyWindowInfo(QZ.kCGWindowListOptionAll, QZ.kCGNullWindowID)
        for window in windows:
            name = window.get('kCGWindowName', 'Unknown')
            if name and self.window_name in name:
                
                return window
        return None
    
    def get_window_id(self):
        return self.window['kCGWindowNumber']

    def get_window_width(self):
        return int(self.window['kCGWindowBounds']['Width'])
    
    def get_window_height(self):
        return int(self.window['kCGWindowBounds']['Height'])

    def get_window_pos_x(self):
        return int(self.window['kCGWindowBounds']['X'])

    def get_window_pos_y(self):
        return int(self.window['kCGWindowBounds']['Y'])
    
    def get_image_from_window(self):
        core_graphics_image = QZ.CGWindowListCreateImage(
            QZ.CGRectNull,
            QZ.kCGWindowListOptionIncludingWindow,
            self.window_id,
            QZ.kCGWindowImageBoundsIgnoreFraming | QZ.kCGWindowImageNominalResolution
        )

        bytes_per_row = QZ.CGImageGetBytesPerRow(core_graphics_image)
        width = QZ.CGImageGetWidth(core_graphics_image)
        height = QZ.CGImageGetHeight(core_graphics_image)

        core_graphics_data_provider = QZ.CGImageGetDataProvider(core_graphics_image)
        core_graphics_data = QZ.CGDataProviderCopyData(core_graphics_data_provider)

        np_raw_data = np.frombuffer(core_graphics_data, dtype=np.uint8)

        numpy_data = np.lib.stride_tricks.as_strided(np_raw_data,
                                                shape=(height, width, 3),
                                                strides=(bytes_per_row, 4, 1),
                                                writeable=False)
        
        final_output = np.ascontiguousarray(numpy_data, dtype=np.uint8)

        return final_output

这是一个如何使用它的例子:

import cv2 as cv
from time import time
from windowcapture import WindowCapture

# initialise the WindowCapture class
wincap = WindowCapture('Name of my application window')

loop_time = time()

while(True):
    # get an updated image of the window you want
    screenshot = wincap.get_image_from_window()

    # show that image
    cv.imshow('Computer Vision', screenshot)

    print('FPS {:.2f}'.format(round(1 / (time() - loop_time), 2)))
    loop_time = time()

    # hold 'q' with the output window focused to exit.
    # waits 1 ms every loop to process key presses
    if cv.waitKey(1) == ord('q'):
        cv.destroyAllWindows()
        break

print('Done.')

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