Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

OpenCV ve WinAPI - obrázek ve vlastním okně

10.6.2018

V minulém příspěvku věnovaném přípravě vývojového prostředí a úvodu do OpenCV jsme si ukázali tu nejjednodušší aplikaci která zobrazila obrázek zadaný jako cesta k souboru ve vlastním okně. Šlo o standardní konzolovou aplikaci. K častým požadavkům (a dotazům na internetu) patří otázka jak zobrazovat obrázek (samozřejmě myšleno obrázek načtený do OpenCV třídy Mat) zobrazit ve vlastním okně aplikace s uživatelským rozhraním (GUI). Protože existují různé GUI frameworky, existuje také více různým možností řešení. V tomto článku si ukážeme možnost pro windows aplikaci bez použití nějaké GUI nadstavby, tj. napsanou ve Windows API.

Vstupní funkcí bude tedy WinMain a aplikace bude mít jedno okno, do kterého budeme vykreslovat načtený obrázek v obsluze zprávy WM_PAINT. Pro plné pochopení dále uvedeného kódu je nutné znát alespoň základní principy programování ve Win API.

Nejprve se podíváme na funkci která načte zadaný soubor do objektu cv::Mat.

void otevrit_soubor(const char* sz_cesta) noexcept
{
        if (!_cv_mat.empty())
                _cv_mat.release();
        _cv_mat = cv::imread(sz_cesta, -1);
        if (_cv_mat.empty())
                return;
        _bi_pocet_bitu = (int)((_cv_mat.dataend - _cv_mat.datastart) / (_cv_mat.cols * _cv_mat.rows) * 8);
        RedrawWindow(_hwnd, nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE | RDW_ERASENOW);
}

Při vykreslování budeme potřebovat znát bitovou hloubku obrázku. Abychom ji nemuseli vypočítávat při každém překreslení, uložíme si ji do globální proměnné

unsigned int _bi_pocet_bitu;

Tím se dostáváme k další klíčové funkci, kterou je obsluha zprávy WM_PAINT, tedy překreslení okna vykreslením obrázku do jeho klientské oblasti. V tomto ukázkovém případě budeme obrázek roztahovat do celé klientské oblasti bez ohledu na zachování poměru stran. Vykreslení se zachováním poměru je o několika operacích násobení a dělení a to není tématem tohoto článku. Celá obsluha zprávy WM_PAINT vypadá takto:

void wm_paint(HWND hwnd) noexcept
{
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);
        RECT rect;
        GetClientRect(hwnd, &rect);
        BITMAPINFO bmi = { 0 };
        bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
        bmi.bmiHeader.biCompression = BI_RGB;
        bmi.bmiHeader.biWidth = _cv_mat.cols;
        bmi.bmiHeader.biHeight = _cv_mat.rows * -1;
        bmi.bmiHeader.biPlanes = 1;
        bmi.bmiHeader.biBitCount = _bi_pocet_bitu;
        StretchDIBits(hdc, 0, 0, rect.right - rect.left, rect.bottom - rect.top,
                0, 0, bmi.bmiHeader.biWidth, abs(bmi.bmiHeader.biHeight),
                (RGBTRIPLE*)_cv_mat.data,
                &bmi, DIB_RGB_COLORS, SRCCOPY);
        EndPaint(hwnd, &ps);
}

Jak je vidět z kódu, využijeme členy třídy cv::Mat ve kterých jsou uloženy hodnoty potřebné pro parametry použité funkce StretchDIBits, a předem vypočítaná bitová hloubka, která bude buď 24 (standardní RGB) nebo 32 (to v případě že obrázek obsahuje alfa-kanál).

V ukázkovém projektu je pro ilustraci ukázka použití OpenCV pro aplikaci jednoho z mnoha dostupných filtrů, zde konkrétně rozostření obrázku, tj funkce cv::blur aplikované na objekt cv::Mat, jak vidíte na kódu funkce volané při výběru položky menu.

void obr_blur() noexcept
{
        if (_cv_mat.empty())
                return;
        cv::blur(_cv_mat, _cv_mat, cv::Size(15, 15));
        RedrawWindow(_hwnd, nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE | RDW_ERASENOW);
}

Celý ukázkový projekt (ve Visual Studiu 2017 Community) si můžete stáhnout zde.

Na závěr kompletní výpis zdrojového kódu:

//
// hlavičkový soubor pch.h
//
#pragma once

#include 

#include 
#include 
using namespace cv;

#ifdef _DEBUG
#pragma comment (lib, "opencv_world341d.lib")
#else
#pragma comment (lib, "opencv_world341.lib")
#endif // DEBUG

#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"")

//
// zdrojový kód opencv-winapi.cpp
//

#include "resource.h"

HWND _hwnd = NULL;
cv::Mat _cv_mat;
RGBTRIPLE* _rgb_triple = nullptr;
unsigned int _bi_pocet_bitu = 0;

inline const wchar_t* trida_hlavni() noexcept
{
        return L"opencv_winapi";
}

inline void __declspec(noreturn) kriticke_ukonceni() noexcept
{
        _CrtDbgBreak();
        FatalAppExit(0, L"Došlo k závažné chybě! Aplikace bude ukončena...");
}

void otevrit_soubor(const char* sz_cesta) noexcept
{
        if (!_cv_mat.empty())
                _cv_mat.release();
        _cv_mat = cv::imread(sz_cesta, -1);
        if (_cv_mat.empty())
                return;
        _bi_pocet_bitu = (int)((_cv_mat.dataend - _cv_mat.datastart) / (_cv_mat.cols * _cv_mat.rows) * 8);
        RedrawWindow(_hwnd, nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE | RDW_ERASENOW);
}

void vybrat_soubor() noexcept
{
        char* soubor = (char*)malloc(32768);
        soubor[0] = 0;
        OPENFILENAMEA ofn;
        memset(&ofn, 0, sizeof(ofn));
        ofn.lStructSize = sizeof(ofn);
        ofn.hwndOwner = _hwnd;
        ofn.lpstrFile = soubor;
        ofn.nMaxFile = 32768;
        ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
        if (!GetOpenFileNameA(&ofn))
        {
                free(soubor);
                return;
        }
        otevrit_soubor(soubor);
        free(soubor);
}

void obr_blur() noexcept
{
        if (_cv_mat.empty())
                return;
        cv::blur(_cv_mat, _cv_mat, cv::Size(15, 15));
        RedrawWindow(_hwnd, nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE | RDW_ERASENOW);
}

void wm_paint(HWND hwnd) noexcept
{
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);
        RECT rect;
        GetClientRect(hwnd, &rect);
        BITMAPINFO bmi = { 0 };
        bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
        bmi.bmiHeader.biCompression = BI_RGB;
        bmi.bmiHeader.biWidth = _cv_mat.cols;
        bmi.bmiHeader.biHeight = _cv_mat.rows * -1;
        bmi.bmiHeader.biPlanes = 1;
        bmi.bmiHeader.biBitCount = _bi_pocet_bitu;
        StretchDIBits(hdc, 0, 0, rect.right - rect.left, rect.bottom - rect.top,
                0, 0, bmi.bmiHeader.biWidth, abs(bmi.bmiHeader.biHeight),
                (RGBTRIPLE*)_cv_mat.data,
                &bmi, DIB_RGB_COLORS, SRCCOPY);
        EndPaint(hwnd, &ps);
}

LRESULT CALLBACK window_proc(HWND hwnd, UINT zprava, WPARAM wparam, LPARAM lparam) noexcept
{
        switch (zprava)
        {
        case WM_COMMAND:
                switch (LOWORD(wparam))
                {
                case ID_OBR_BLUR:
                        obr_blur();
                        break;
                case ID_SOUBOR_OTEVRIT:
                        vybrat_soubor();
                        break;
                case ID_KONEC:
                        PostMessage(hwnd, WM_CLOSE, 0, 0);
                        break;
                }
                break;
        case WM_PAINT:
                if (!_cv_mat.empty())
                        wm_paint(hwnd);
                else
                        return DefWindowProc(hwnd, zprava, wparam, lparam);
                break;
        case WM_DESTROY:
                PostQuitMessage(0);
                break;
        default:
                return DefWindowProc(hwnd, zprava, wparam, lparam);
        }
        return 0;
}

int APIENTRY wWinMain(_In_ HINSTANCE hinstance, _In_opt_ HINSTANCE, _In_ LPWSTR, _In_ int)
{
        WNDCLASSEXW wcex;
        memset(&wcex, 0, sizeof(wcex));
        wcex.cbSize = sizeof(WNDCLASSEX);
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = window_proc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = hinstance;
        wcex.hIcon = LoadIcon(hinstance, MAKEINTRESOURCE(IDI_HLAVNI));
        wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszClassName = trida_hlavni();
        wcex.lpszMenuName = MAKEINTRESOURCEW(IDR_HLAVNI);
        wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_HLAVNI));
        if (!RegisterClassExW(&wcex))
                kriticke_ukonceni();
        _hwnd = CreateWindowEx(0, trida_hlavni(), L"OpenCV ve WinAPI", WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hinstance, nullptr);
        if (!_hwnd)
                kriticke_ukonceni();
        ShowWindow(_hwnd, SW_SHOW);
        UpdateWindow(_hwnd);
        vybrat_soubor();
        MSG msg;
        while (GetMessage(&msg, nullptr, 0, 0))
        {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
        }
        return (int)msg.wParam;
}

Příloha: opencv-winapi.zip.