Gdk / X11 Захват экрана

Я хочу написать программу, которая непрерывно захватывает экран и вносит некоторые изменения в изображение. Полную тестовую программу можно найти по адресу:

https://gist.github.com/blogsh/eb4dd4b96aca468c8bfa

Однако я столкнулся с некоторыми проблемами. Первый эксперимент, который я провел, заключался в том, чтобы использовать корневое окно Gdk, создать из него контекст Cairo, а затем использовать его цель в качестве источника для другого окна, в котором отображается содержимое:

mScreenContext = Gdk::Screen::get_default()->get_root_window()->create_cairo_context()
...
context->set_source(mScreenContext->get_target(), 0, 0);
context->paint();

Это отлично работает (вариант 1 в источнике выше). Он просто рисует весь экран в другом окне. Поэтому моим следующим шагом было попытаться сохранить содержимое в Cairo ImageSurface, чтобы изменить его:

mImageContext->set_source(mScreenContext->get_target(), 0, 0);
mImageContext->paint();

context->set_source(mImageSurface, 0, 0);
context->paint();

Удивительно то, что при первом рисовании окна Gtk захватывается и рисуется экран. К сожалению, после этого ничего не происходит, но отображается начальный экран. Как можно объяснить такое поведение? Должен признаться, я мало что знаю об основных процессах, поэтому, может быть, кто-то может дать несколько советов?

Третий вариант с использованием Gdk::Pixbuf дает точно такое же поведение:

mScreenBuffer = Gdk::Pixbuf::create(mGdkRootWindow, 0, 0, mScreenWidth, mScreenHeight);
Gdk::Cairo::set_source_pixbuf(context, mScreenBuffer, 0, 0);
context->paint();

Наконец (вариант 4) я попытался использовать X11 напрямую:

Display *display = XOpenDisplay((char*)0);
XImage *image = XGetImage(display, RootWindow(display, DefaultScreen(display)), 0, 0, mScreenWidth, mScreenHeight, AllPlanes, XYPixmap);

mScreenBuffer = Gdk::Pixbuf::create_from_data((const guint8*)image->data, Gdk::COLORSPACE_RGB, 0, 8, mScreenWidth, mScreenHeight, mScreenWidth);
Gdk::Cairo::set_source_pixbuf(context, mScreenBuffer, 0, 0);
context->paint();

XFree(image);

На самом деле, это работает (хотя я пока не пытался правильно подобрать формат пикселей), но ужасно медленно!

Поэтому я был бы признателен за любые подсказки о том, в чем проблема с двумя вариантами Gdk и/или как ускорить подход X11. Или, может быть, кто-то знает совершенно другой подход к быстрому захвату экрана.

К сожалению, я не так хорошо знаком со всей этой темой, но есть еще одна идея - использовать оконный менеджер на основе OpenGL, где я мог бы напрямую читать фреймбуфер? Имеет ли это смысл?

Основная идея программы в том, что у меня есть проектор, который я не могу расположить прямо перед стеной. Итак, моя идея состоит в том, чтобы захватить экран, выполнить билинейное преобразование, чтобы учесть искажение проекции, а затем отобразить измененный экран в другом окне, которое будет отображаться на проекторе...


person blogsh    schedule 06.10.2015    source источник
comment
Я также добавил версию задачи на Python. Исходный код немного короче и, вероятно, его легче понять...   -  person blogsh    schedule 06.10.2015


Ответы (2)


XShmGetImage и XShmPutImage быстрее, чем XGetImage и XPutImage. В следующем примере я создаю два образа: src и dst. На каждой итерации я сохраняю скриншот в src, а затем визуализирую его масштабированную версию в dst.

На изображении ниже показан пример, работающий в окне, озаглавленном «screencap». При низком спросе он работает со скоростью 60 кадров в секунду (как видно на терминале в правом верхнем углу). При высокой нагрузке производительность может упасть до 25 кадров в секунду.

Тестовый компьютер:

Display resolution: 1920x1080
Graphic card: ATI Radeon HD 4200 (integrated)
CPU: AMD Phenom(tm) II X4 945, 3013.85 MHz
Window manager: XFCE 4.12 (compositing off)
Operating system: OpenBSD 5.9
Tested also in Linux (openSUSE Leap 42.1)

работает скриншот

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
#include <sys/shm.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XShm.h>
#ifdef __linux__
    #include <sys/time.h>
#endif

// comment the next line to busy-wait at each frame
//#define __SLEEP__
#define FRAME  16667
#define PERIOD 1000000
#define NAME   "screencap"
#define NAMESP "         "
#define BPP    4

struct shmimage
{
    XShmSegmentInfo shminfo ;
    XImage * ximage ;
    unsigned int * data ; // will point to the image's BGRA packed pixels
} ;

void initimage( struct shmimage * image )
{
    image->ximage = NULL ;
    image->shminfo.shmaddr = (char *) -1 ;
}

void destroyimage( Display * dsp, struct shmimage * image )
{
    if( image->ximage )
    {
        XShmDetach( dsp, &image->shminfo ) ;
        XDestroyImage( image->ximage ) ;
        image->ximage = NULL ;
    }

    if( image->shminfo.shmaddr != ( char * ) -1 )
    {
        shmdt( image->shminfo.shmaddr ) ;
        image->shminfo.shmaddr = ( char * ) -1 ;
    }
}

int createimage( Display * dsp, struct shmimage * image, int width, int height )
{
    // Create a shared memory area 
    image->shminfo.shmid = shmget( IPC_PRIVATE, width * height * BPP, IPC_CREAT | 0600 ) ;
    if( image->shminfo.shmid == -1 )
    {
        perror( NAME ) ;
        return false ;
    }

    // Map the shared memory segment into the address space of this process
    image->shminfo.shmaddr = (char *) shmat( image->shminfo.shmid, 0, 0 ) ;
    if( image->shminfo.shmaddr == (char *) -1 )
    {
        perror( NAME ) ;
        return false ;
    }

    image->data = (unsigned int*) image->shminfo.shmaddr ;
    image->shminfo.readOnly = false ;

    // Mark the shared memory segment for removal
    // It will be removed even if this program crashes
    shmctl( image->shminfo.shmid, IPC_RMID, 0 ) ;

    // Allocate the memory needed for the XImage structure
    image->ximage = XShmCreateImage( dsp, XDefaultVisual( dsp, XDefaultScreen( dsp ) ),
                        DefaultDepth( dsp, XDefaultScreen( dsp ) ), ZPixmap, 0,
                        &image->shminfo, 0, 0 ) ;
    if( !image->ximage )
    {
        destroyimage( dsp, image ) ;
        printf( NAME ": could not allocate the XImage structure\n" ) ;
        return false ;
    }

    image->ximage->data = (char *)image->data ;
    image->ximage->width = width ;
    image->ximage->height = height ;

    // Ask the X server to attach the shared memory segment and sync
    XShmAttach( dsp, &image->shminfo ) ;
    XSync( dsp, false ) ;
    return true ;
}

void getrootwindow( Display * dsp, struct shmimage * image )
{
    XShmGetImage( dsp, XDefaultRootWindow( dsp ), image->ximage, 0, 0, AllPlanes ) ;
}

long timestamp( )
{
   struct timeval tv ;
   struct timezone tz ;
   gettimeofday( &tv, &tz ) ;
   return tv.tv_sec*1000000L + tv.tv_usec ;
}

Window createwindow( Display * dsp, int width, int height )
{
    unsigned long mask = CWBackingStore ;
    XSetWindowAttributes attributes ;
    attributes.backing_store = NotUseful ;
    mask |= CWBackingStore ;
    Window window = XCreateWindow( dsp, DefaultRootWindow( dsp ),
            0, 0, width, height, 0,
            DefaultDepth( dsp, XDefaultScreen( dsp ) ),
            InputOutput, CopyFromParent, mask, &attributes ) ;
    XStoreName( dsp, window, NAME );
    XSelectInput( dsp, window, StructureNotifyMask ) ;
    XMapWindow( dsp, window );
    return window ;
}

void destroywindow( Display * dsp, Window window )
{
    XDestroyWindow( dsp, window );
}

unsigned int getpixel( struct shmimage * src, struct shmimage * dst,
                       int j, int i, int w, int h )
{
    int x = (float)(i * src->ximage->width) / (float)w ;
    int y = (float)(j * src->ximage->height) / (float)h ;
    return src->data[ y * src->ximage->width + x ] ;
}

int processimage( struct shmimage * src, struct shmimage * dst )
{
    int sw = src->ximage->width ;
    int sh = src->ximage->height ;
    int dw = dst->ximage->width ;
    int dh = dst->ximage->height ;

    // Here you can set the resulting position and size of the captured screen
    // Because of the limitations of this example, it must fit in dst->ximage
    int w = dw / 2 ;
    int h = dh / 2 ;
    int x = ( dw - w ) ;
    int y = ( dh - h ) / 2 ;

    // Just in case...
    if( x < 0 || y < 0 || x + w > dw || y + h > dh || sw < dw || sh < dh )
    {
        printf( NAME   ": This is only a limited example\n" ) ;
        printf( NAMESP "  Please implement a complete scaling algorithm\n" ) ;
        return false ;
    }

    unsigned int * d = dst->data + y * dw + x ;
    int r = dw - w ;
    int j, i ;
    for( j = 0 ; j < h ; ++j )
    {
        for( i = 0 ; i < w ; ++i )
        {
            *d++ = getpixel( src, dst, j, i, w, h ) ;
        }
        d += r ;
    }
    return true ;
}

int run( Display * dsp, Window window, struct shmimage * src, struct shmimage * dst )
{
    XGCValues xgcvalues ;
    xgcvalues.graphics_exposures = False ;
    GC gc = XCreateGC( dsp, window, GCGraphicsExposures, &xgcvalues ) ;

    Atom delete_atom = XInternAtom( dsp, "WM_DELETE_WINDOW", False ) ;
    XSetWMProtocols( dsp, window, &delete_atom, True ) ;

    XEvent xevent ;
    int running = true ;
    int initialized = false ;
    int dstwidth = dst->ximage->width ;
    int dstheight = dst->ximage->height ;
    long framets = timestamp( ) ;
    long periodts = timestamp( ) ;
    long frames = 0 ;
    int fd = ConnectionNumber( dsp ) ;
    while( running )
    {
        while( XPending( dsp ) )
        {
            XNextEvent( dsp, &xevent ) ;
            if( ( xevent.type == ClientMessage && xevent.xclient.data.l[0] == delete_atom )
                || xevent.type == DestroyNotify )
            {
                running = false ;
                break ;
            }
            else if( xevent.type == ConfigureNotify )
            {
                if( xevent.xconfigure.width == dstwidth
                    && xevent.xconfigure.height == dstheight )
                {
                    initialized = true ;
                }
            }
        }
        if( initialized )
        {
            getrootwindow( dsp, src ) ;
            if( !processimage( src, dst ) )
            {
                return false ;
            }
            XShmPutImage( dsp, window, gc, dst->ximage,
                          0, 0, 0, 0, dstwidth, dstheight, False ) ;
            XSync( dsp, False ) ;

            int frameus = timestamp( ) - framets ;
            ++frames ;
            while( frameus < FRAME )
            {
                #if defined( __SLEEP__ )
                usleep( FRAME - frameus ) ;
                #endif
                frameus = timestamp( ) - framets ;
            }
            framets = timestamp( ) ;

            int periodus = timestamp( ) - periodts ;
            if( periodus >= PERIOD )
            {
                printf( "fps: %d\n", (int)round( 1000000.0L * frames / periodus ) ) ;
                frames = 0 ;
                periodts = framets ;
            }
        }
    }
    return true ;
}

int main( int argc, char * argv[] )
{
    Display * dsp = XOpenDisplay( NULL ) ;
    if( !dsp )
    {
        printf( NAME ": could not open a connection to the X server\n" ) ;
        return 1 ;
    }

    if( !XShmQueryExtension( dsp ) )
    {
        XCloseDisplay( dsp ) ;
        printf( NAME ": the X server does not support the XSHM extension\n" ) ;
        return 1 ;
    }

    int screen = XDefaultScreen( dsp ) ;
    struct shmimage src, dst ;
    initimage( &src ) ;
    int width = XDisplayWidth( dsp, screen ) ;
    int height = XDisplayHeight( dsp, screen ) ;
    if( !createimage( dsp, &src, width, height ) )
    {
        XCloseDisplay( dsp ) ;
        return 1 ;
    }
    initimage( &dst ) ;
    int dstwidth = width / 2 ;
    int dstheight = height / 2 ;
    if( !createimage( dsp, &dst, dstwidth, dstheight ) )
    {
        destroyimage( dsp, &src ) ;
        XCloseDisplay( dsp ) ;
        return 1 ;
    }

    if( dst.ximage->bits_per_pixel != 32 )
    {
        destroyimage( dsp, &src ) ;
        destroyimage( dsp, &dst ) ;
        XCloseDisplay( dsp ) ;
        printf( NAME   ": This is only a limited example\n" ) ;
        printf( NAMESP "  Please add support for all pixel formats using: \n" ) ;
        printf( NAMESP "      dst.ximage->bits_per_pixel\n" ) ;
        printf( NAMESP "      dst.ximage->red_mask\n" ) ;
        printf( NAMESP "      dst.ximage->green_mask\n" ) ;
        printf( NAMESP "      dst.ximage->blue_mask\n" ) ;
        return 1 ;
    }

    Window window = createwindow( dsp, dstwidth, dstheight ) ;
    run( dsp, window, &src, &dst ) ;
    destroywindow( dsp, window ) ;  

    destroyimage( dsp, &src ) ;
    destroyimage( dsp, &dst ) ;
    XCloseDisplay( dsp ) ;
    return 0 ;
}

Это всего лишь пример. Если вам нравится, как он работает, вам следует рассмотреть возможность добавления более подходящего алгоритма масштабирования и поддержки всех форматов пикселей.

Вы можете скомпилировать пример следующим образом:

gcc screencap.c -o screencap -std=c99 -I/usr/X11R6/include -L/usr/X11R6/lib -lX11 -lXext -lm
person Community    schedule 12.07.2016
comment
Спасибо! Прошло некоторое время с тех пор, как я задал вопрос, и тем временем я остановился на XGetImage, копируя данные изображения, а затем выполняя преобразование с использованием OpenGL и шейдеров для преобразования. Расширение Shm может немного ускорить процесс, так что я посмотрю! - person blogsh; 13.07.2016
comment
я получаю screencap.c: In function ‘timestamp’: screencap.c:105:20: error: storage size of ‘tz’ isn’t known - person Tom; 26.12.2017
comment
Вы можете закомментировать эту строку и заменить следующую строку на эту: gettimeofday( &tv, NULL ) ; Часовой пояс здесь не используется, и этот системный вызов принимает NULL (не заполняет его) - person Dankó Dávid; 06.12.2019

Вызовите cairo_surface_mark_dirty() на поверхности cairo, которая относится к корневому окну. Поскольку у вас, кажется, есть только контекст cairo, а не поверхность напрямую: вы можете использовать cairo_surface_get_target(), чтобы получить поверхность из контекста cairo.

Cairo предполагает, что вы рисуете на поверхность только через cairo API, если только вы не сообщите ему, что вы что-то изменили за пределами cairo. Загрузка снимка экрана стоит дорого, поэтому cairo кэширует результат, чтобы его можно было повторно использовать позже.

Скорее всего, вам также придется вызывать cairo_surface_flush() перед вызовом cairo_surface_mark_dirty()...

person Uli Schlachter    schedule 17.07.2016