Python получает значение пикселя экрана в OS X

Я занимаюсь созданием автоматизированного игрового бота в Python на OS X 10.8.2 и в процессе исследования автоматизации Python GUI я обнаружил автопилот. API манипуляции с мышью велик, но кажется, что методы захвата экрана основаны на устаревших методах OpenGL...

Есть ли эффективные способы получения значения цвета пикселя в OS X? Единственный способ, о котором я могу думать сейчас, - использовать os.system("screencapture foo.png"), но процесс, кажется, имеет ненужные накладные расходы, поскольку я буду опросить очень быстро.

Ответ 1

Небольшое улучшение, но использование параметра сжатия TIFF для screencapture немного быстрее:

$ time screencapture -t png /tmp/test.png
real        0m0.235s
user        0m0.191s
sys         0m0.016s
$ time screencapture -t tiff /tmp/test.tiff
real        0m0.079s
user        0m0.028s
sys         0m0.026s

У этого есть много накладных расходов, как вы говорите (создание подпроцесса, запись/чтение с диска, сжатие/распаковка).

Вместо этого вы можете использовать PyObjC для захвата экрана с помощью CGWindowListCreateImage. Я нашел, что потребовалось около 70 мс (~ 14 кадров в секунду) для захвата экрана с разрешением 1680x1050 пикселей и наличия значений, доступных в памяти

Несколько случайных заметок:

  • Импортирование модуля Quartz.CoreGraphics является самой медленной частью, около 1 секунды. То же самое верно для импорта большинства модулей PyObjC. В этом случае маловероятно, но для недолговечных процессов вы можете лучше написать инструмент в ObjC
  • Указание меньшей области немного быстрее, но не очень (~ 40 мс для блока 100x100px, ~ 70 мс для 1680x1050). Большую часть времени, кажется, расходуется только на вызов CGDataProviderCopyData - мне интересно, есть ли способ прямого доступа к данным, так как нам не нужно его изменять?
  • Функция ScreenPixel.pixel довольно быстрая, но доступ к большим количествам пикселей по-прежнему медленный (поскольку 0.01ms * 1650*1050 составляет около 17 секунд) - если вам нужно получить доступ к множеству пикселей, возможно, быстрее, чем struct.unpack_from их всех в один раз.

Здесь код:

import time
import struct

import Quartz.CoreGraphics as CG


class ScreenPixel(object):
    """Captures the screen using CoreGraphics, and provides access to
    the pixel values.
    """

    def capture(self, region = None):
        """region should be a CGRect, something like:

        >>> import Quartz.CoreGraphics as CG
        >>> region = CG.CGRectMake(0, 0, 100, 100)
        >>> sp = ScreenPixel()
        >>> sp.capture(region=region)

        The default region is CG.CGRectInfinite (captures the full screen)
        """

        if region is None:
            region = CG.CGRectInfinite
        else:
            # TODO: Odd widths cause the image to warp. This is likely
            # caused by offset calculation in ScreenPixel.pixel, and
            # could could modified to allow odd-widths
            if region.size.width % 2 > 0:
                emsg = "Capture region width should be even (was %s)" % (
                    region.size.width)
                raise ValueError(emsg)

        # Create screenshot as CGImage
        image = CG.CGWindowListCreateImage(
            region,
            CG.kCGWindowListOptionOnScreenOnly,
            CG.kCGNullWindowID,
            CG.kCGWindowImageDefault)

        # Intermediate step, get pixel data as CGDataProvider
        prov = CG.CGImageGetDataProvider(image)

        # Copy data out of CGDataProvider, becomes string of bytes
        self._data = CG.CGDataProviderCopyData(prov)

        # Get width/height of image
        self.width = CG.CGImageGetWidth(image)
        self.height = CG.CGImageGetHeight(image)

    def pixel(self, x, y):
        """Get pixel value at given (x,y) screen coordinates

        Must call capture first.
        """

        # Pixel data is unsigned char (8bit unsigned integer),
        # and there are for (blue,green,red,alpha)
        data_format = "BBBB"

        # Calculate offset, based on
        # http://www.markj.net/iphone-uiimage-pixel-color/
        offset = 4 * ((self.width*int(round(y))) + int(round(x)))

        # Unpack data from string into Python'y integers
        b, g, r, a = struct.unpack_from(data_format, self._data, offset=offset)

        # Return BGRA as RGBA
        return (r, g, b, a)


if __name__ == '__main__':
    # Timer helper-function
    import contextlib

    @contextlib.contextmanager
    def timer(msg):
        start = time.time()
        yield
        end = time.time()
        print "%s: %.02fms" % (msg, (end-start)*1000)


    # Example usage
    sp = ScreenPixel()

    with timer("Capture"):
        # Take screenshot (takes about 70ms for me)
        sp.capture()

    with timer("Query"):
        # Get pixel value (takes about 0.01ms)
        print sp.width, sp.height
        print sp.pixel(0, 0)


    # To verify screen-cap code is correct, save all pixels to PNG,
    # using http://the.taoofmac.com/space/projects/PNGCanvas

    from pngcanvas import PNGCanvas
    c = PNGCanvas(sp.width, sp.height)
    for x in range(sp.width):
        for y in range(sp.height):
            c.point(x, y, color = sp.pixel(x, y))

    with open("test.png", "wb") as f:
        f.write(c.dump())

Ответ 2

Я столкнулся с этим сообщением при поиске решения для получения снимка экрана в Mac OS X, используемого для обработки в реальном времени. Я попытался использовать ImageGrab из PIL, как это было предложено в некоторых других сообщениях, но не смог получить данные достаточно быстро (всего около 0,5 кадра в секунду).

Ответ fooobar.com/questions/534283/... в этом сообщении для использования PyObjC спас мой день! Спасибо @dbr!

Однако моя задача требует получить все пиксельные значения, а не только один пиксель, а также прокомментировать третью ноту от @dbr, я добавил новый метод в этом классе, чтобы получить полное изображение, если кто-то еще может понадобиться.

Данные изображения возвращаются в виде массива numpy с размером (высота, ширина, 3), которые могут быть непосредственно использованы для последующей обработки в numpy или opencv и т.д.... получение отдельных значений пикселей из него также становится довольно простым при использовании numpy индексации.

Я протестировал код с снимком экрана 1600 x 1000 - получение данных с использованием capture() заняло ~ 30 мс и преобразование его в массив np. getimage() занимает только 50 мс на моем Macbook. Итак, теперь у меня > 10 fps и даже быстрее для небольших регионов.

import numpy as np

def getimage(self):
    imgdata=np.fromstring(self._data,dtype=np.uint8).reshape(len(self._data)/4,4)
    return imgdata[:self.width*self.height,:-1].reshape(self.height,self.width,3)

note Я выбрасываю "альфа-канал" из канала BGRA 4.

Ответ 3

Все это было очень полезно, поэтому я должен был вернуться к комментарию/однако у меня нет репутации.. У меня, однако, есть пример кода комбинации ответов выше для молниеносного захвата/сохранения экрана благодаря @dbr и @qqg!

import time
import numpy as np
from scipy.misc import imsave
import Quartz.CoreGraphics as CG

image = CG.CGWindowListCreateImage(CG.CGRectInfinite, CG.kCGWindowListOptionOnScreenOnly, CG.kCGNullWindowID, CG.kCGWindowImageDefault)

prov = CG.CGImageGetDataProvider(image)
_data = CG.CGDataProviderCopyData(prov)

width = CG.CGImageGetWidth(image)
height = CG.CGImageGetHeight(image)

imgdata=np.fromstring(_data,dtype=np.uint8).reshape(len(_data)/4,4)
numpy_img = imgdata[:width*height,:-1].reshape(height,width,3)
imsave('test_fast.png', numpy_img)