3

I have a short program in C++ to capture the pixels from this BlueStacks emulator window, and then I'll be manipulating the pixels using OpenCV and then sending mouse input (win32api) based on some decisions here and there. I'm working in Visual Studio Enterprise 2017.

Only problem is, the function seems to be capturing a smaller region of pixels compared to the full window.

Here's an example image. Original window is on the left, mirrored output is on the right.

example

How can I fix this? I've already enabled High DPI awareness in my project settings, not sure what other steps to take other than adding random magic numbers into the function.

I've done this program already in Python but I'd like to redo this in C++ for the performance boost.

Here's a video (warning: noise). And another one.

Here's my current code:

#include "stdafx.h"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <Windows.h>
#include <iostream>

using namespace std;
using namespace cv;

Mat hwnd2mat(HWND hwnd);

int main(int argc, char **argv)
{
    printf("Hello, world!\n");
    Sleep(1000);

    HWND hwndDesktop;

    hwndDesktop = GetForegroundWindow();
    // namedWindow("output", WINDOW_NORMAL);
    int key = 0;
    Mat src;

    while (key != 27)
    {
        src = hwnd2mat(hwndDesktop);
        // you can do some image processing here
        imshow("output", src);
        key = waitKey(1); // you can change wait time
    }

}

Mat hwnd2mat(HWND hwnd)
{
    HDC hwindowDC, hwindowCompatibleDC;

    int height, width, srcheight, srcwidth;
    HBITMAP hbwindow;
    Mat src;
    BITMAPINFOHEADER  bi;

    hwindowDC = GetDC(hwnd);
    hwindowCompatibleDC = CreateCompatibleDC(hwindowDC);
    SetStretchBltMode(hwindowCompatibleDC, COLORONCOLOR);

    RECT windowsize;    // get the height and width of the screen
    GetClientRect(hwnd, &windowsize);

    srcheight = windowsize.bottom;
    srcwidth = windowsize.right;
    height = windowsize.bottom / 1;  //change this to whatever size you want to resize to
    width = windowsize.right / 1;

    src.create(height, width, CV_8UC4);

    // create a bitmap
    hbwindow = CreateCompatibleBitmap(hwindowDC, width, height);
    bi.biSize = sizeof(BITMAPINFOHEADER);    //http://msdn.microsoft.com/en-us/library/windows/window/dd183402%28v=vs.85%29.aspx
    bi.biWidth = width;
    bi.biHeight = -height;  //this is the line that makes it draw upside down or not
    bi.biPlanes = 1;
    bi.biBitCount = 32;
    bi.biCompression = BI_RGB;
    bi.biSizeImage = 0;
    bi.biXPelsPerMeter = 0;
    bi.biYPelsPerMeter = 0;
    bi.biClrUsed = 0;
    bi.biClrImportant = 0;

    // use the previously created device context with the bitmap
    SelectObject(hwindowCompatibleDC, hbwindow);
    // copy from the window device context to the bitmap device context
    StretchBlt(hwindowCompatibleDC, 0, 0, width, height, hwindowDC, 0, 0, srcwidth, srcheight, SRCCOPY); //change SRCCOPY to NOTSRCCOPY for wacky colors !
    GetDIBits(hwindowCompatibleDC, hbwindow, 0, height, src.data, (BITMAPINFO *)&bi, DIB_RGB_COLORS);  //copy from hwindowCompatibleDC to hbwindow

                                                                                                       // avoid memory leak
    DeleteObject(hbwindow);
    DeleteDC(hwindowCompatibleDC);
    ReleaseDC(hwnd, hwindowDC);

    return src;
}
Ripi2
  • 7,031
  • 1
  • 17
  • 33
Hexus_One
  • 33
  • 4

2 Answers2

3

The stretching is due to DPI scaling. Your own program is not DPI aware, the other program seems to be DPI aware. The easiest way to make your program DPI aware is by calling SetProcessDPIAware(); at the start of the program.

Aside note, HBITMAP handle should not be selected in a device context when calling GetDIBits. You can rewrite your code as

SetProcessDPIAware();
...

Mat hwnd2mat(HWND hwnd)
{
    RECT rc;
    GetClientRect(hwnd, &rc);
    int width = rc.right;
    int height = rc.bottom;

    Mat src;
    src.create(height, width, CV_8UC4);

    HDC hdc = GetDC(hwnd);
    HDC memdc = CreateCompatibleDC(hdc);
    HBITMAP hbitmap = CreateCompatibleBitmap(hdc, width, height);
    HBITMAP oldbmp = (HBITMAP)SelectObject(memdc, hbitmap);

    BitBlt(memdc, 0, 0, width, height, hdc, 0, 0, SRCCOPY);
    SelectObject(memdc, oldbmp);

    BITMAPINFOHEADER  bi = { sizeof(BITMAPINFOHEADER), width, -height, 1, 32, BI_RGB };
    GetDIBits(hdc, hbitmap, 0, height, src.data, (BITMAPINFO*)&bi, DIB_RGB_COLORS);

    DeleteObject(hbitmap);
    DeleteDC(memdc);
    ReleaseDC(hwnd, hdc);

    return src;
}

This code will only work for native Win32 programs. For other programs like Chrome, WPF, Qt apps, this will show blank screen. You would need to take screen shot of desktop window instead.

Barmak Shemirani
  • 30,904
  • 6
  • 40
  • 77
  • Thanks for the tip :) Curiously though, is the following project setting not sufficient? https://i.imgur.com/NoPyDgA.png (in Visual Studio Enterprise 2017) I'll try your suggestions right now – Hexus_One May 19 '18 at 10:04
  • Okay I've tried adding the method as well as the new function - program output is indentical to the image in the original post :/ I've added a constant value to height and width in the function which is sufficient for my needs, although I'm not sure if I need to change any other values. ie `int width = rc.right + 100` `int height = rc.bottom + 100;` – Hexus_One May 19 '18 at 10:11
  • Your DPI method is preferred, it sets the manifest. Don't add `SetProcessDPIAware`. There is something else which you are not showing in the question. – Barmak Shemirani May 19 '18 at 13:30
  • Strange... I restarted my laptop and then it worked fine (as in, my existing function would scan 100 pixels larger than the window, and removing the constant would scan the window correctly). I can tell you that I do indeed have DPI set to 200%, also I'm working with a 3840x2160 screen. I also just found out now that the BlueStacks window itself is resizable. I'll link my current code here: https://pastebin.com/12L56jXx – Hexus_One May 20 '18 at 09:46
  • DPI changes sometimes don't take effect unless you restart or logout and login – Barmak Shemirani May 20 '18 at 15:46
  • Fixed `DeleteDC(memdc)`, note `oldbmp` should not be deleted, because it was never allocated by this program. – Barmak Shemirani May 22 '18 at 07:30
0

I think you want GetActiveWindow() instead of GetForegroundWindow()

See this SO answer

Ripi2
  • 7,031
  • 1
  • 17
  • 33
  • Thanks :) I'm using GetForegroundWindow() as a placeholder to find the window title of the program - I'll be replacing it with FindWindow() as soon as possible. Thanks for the link though, I'll have a read through as it seems to make an important distinction. – Hexus_One May 17 '18 at 15:57
  • I've tried your suggestion (directly replacing GetForegroundWindow() with GetActiveWindow()) and it seems to evaluate to NULL no matter what :/ – Hexus_One May 17 '18 at 16:03
  • Turns out I made a bad cast from const char* to LPCSTR - I should be using the TEXT() macro to cast. Still wondering though, why would GetActiveWindow() return NULL in my case? Surely I have at least one active window. – Hexus_One May 17 '18 at 16:06