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

OpenCV ve WinAPI - video z kamery ve vlastním okně

14.6.2018

V minulém příspěvku jsem ukázal jak obrázek načtený pomocí knihovny OpenCV zobrazit ve vlastním okně aplikace pro Windows. V tomto příspěvku si ukážeme jak obdobným způsobem zobrazit (živé) video z kamery počítače (integrované nebo připojené přes USB port).

Základní kostra aplikace bude stejná jako v předchozím příkladě zobrazování obrázku. Navíc budeme muset načítat jednotlivé obrázky (frame) z kamery a ve vhodném intervalu aktualizovat okno, tedy překreslit aktuálním obrázkem z kamery.

Ke grabování videa z kamery použijeme třídu knihovny OpenCV cv::VideoCapture. Tato třída umožňuje mimo jiné načítat jednotlivé obrázky do minule zmíněného a použitého objektu cv::Mat, což využijeme pro vykreslování. Dále lze pomočí této třídy zjistit a popřípadě nastavit některé parametry videostreamu, jak si hned ukážeme.

Pro naše potřeby si opět vytvoříme několik globálních proměnných:

HWND _hwnd = NULL;
cv::Mat _cv_mat;
unsigned int _bi_pocet_bitu = 0;
cv::VideoCapture _kamera;
unsigned int _sirka_videa = 0;
unsigned int _vyska_videa = 0;
unsigned int _obr_za_sekundu = 0;

Na začátku programu se pokusíme otevřít výchozí kameru (to určuje ta 0 v parametru metody open, zatímco offset cv::CAP_DSHOW určuje že chceme jako „low-level“ použít rozhraní DirectShow). Dále se pokusíme nastavit snímkovou frekvenci na 25 snímku za sekundu, pokud se to nepodaří zkusíme ještě nižší hodmotu - 15 fps a pokud se ani toto nezdaří, budeme považovat kameru za nepoužitelnou… Pokud se vše podaří, načteme si jeden snímek do objektu cv::Mat, abychom zjistili bitovou hloubku a dále si načteme do globálních proměnných šířku a výšku videa a obnovovací frekvenci. Vše zmíněné je v následujícím fragmentu kódu ze začátku funkce WinMain:

if (!_kamera.open(0 + cv::CAP_DSHOW))
{
        MessageBox(NULL, L"Nepodařilo se spustit žádnou kameru", L"Sorry jako...", MB_ICONERROR);
        return 0;
}
if (!_kamera.read(_cv_mat))
        return 0;
if (!_kamera.set(CAP_PROP_FPS, 25.0))
        if (!_kamera.set(CAP_PROP_FPS, 15.0))
        {
                MessageBox(NULL, L"Tahle kamera je na 2 věci...", L"Sorry jako...", MB_ICONERROR);
                return 0;
        }
_bi_pocet_bitu = (int)((_cv_mat.dataend - _cv_mat.datastart) / (_cv_mat.cols * _cv_mat.rows) * 8);
_sirka_videa = (unsigned int)_kamera.get(CAP_PROP_FRAME_WIDTH);
_vyska_videa = (unsigned int)_kamera.get(CAP_PROP_FRAME_HEIGHT);
_obr_za_sekundu = (unsigned int)_kamera.get(CAP_PROP_FPS);

Zobrazování „živého“ videa dosáhneme tak že spustíme timer jehož interval nastavíme na prodlevu mezi jednotlivými obrázky, vypočítanou z již zjištěné snímkové frekvence. Na tento timer, tedy při přijetí zprávy WM_TIMER v proceduře okna si načteme aktuální obrázek do objektu cv::Mat a vyvoláme okamžité překreslení okna, a v obsluze WM_PAINT pak zobrazíme aktuálně načtený snímek stejně jako v minulém příkladu zobrazení obrázku ze souboru. Navíc ještě jako ukázku možností zápisu do zobrazeného videa zapíšeme do obrázku aktuální čas.

// obsluha zprávy WM_TIMER
void wm_timer() noexcept
{
        if (!_kamera.read(_cv_mat))
        {
                PostMessage(_hwnd, WM_CLOSE, 0, 0);
                return;
        }
        SYSTEMTIME st;
        GetLocalTime(&st);
        char sz_cas[50];
        sprintf_s(sz_cas, sizeof(sz_cas), "%d:%.2d:%.2d", st.wHour, st.wMinute, st.wSecond);
        cv::putText(_cv_mat, sz_cas, cv::Point(20, _vyska_videa - 40), CV_FONT_HERSHEY_PLAIN, 2,
                CV_RGB(255, 255,0), 2);
        RedrawWindow(_hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE);
}

Výsledkem bude „živé“ video v našem okně ve kterém bude dole „běžet“ aktuální čas jako hodiny:minuty:sekundy.

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

Na závěr opět kompletní výpis kódu programu:

//
// hlavička 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='*'\"")

//
// zdojový kód opencv-kamera-winapi.cpp
//
#include "pch.h"
#include "resource.h"

HWND _hwnd = NULL;
cv::Mat _cv_mat;
unsigned int _bi_pocet_bitu = 0;
cv::VideoCapture _kamera;
unsigned int _sirka_videa = 0;
unsigned int _vyska_videa = 0;
unsigned int _obr_za_sekundu = 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 wm_paint() 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, _cv_mat.cols, _cv_mat.rows,
                0, 0, _cv_mat.cols, _cv_mat.rows,
                (RGBTRIPLE*)_cv_mat.data,
                &bmi, DIB_RGB_COLORS, SRCCOPY);
        EndPaint(_hwnd, &ps);
}

void wm_create(HWND hwnd) noexcept
{
        SetTimer(hwnd, 1, 1000 / _obr_za_sekundu, nullptr);
}

// obsluha zprávy WM_TIMER
void wm_timer() noexcept
{
        if (!_kamera.read(_cv_mat))
        {
                PostMessage(_hwnd, WM_CLOSE, 0, 0);
                return;
        }
        SYSTEMTIME st;
        GetLocalTime(&st);
        char sz_cas[50];
        sprintf_s(sz_cas, sizeof(sz_cas), "%d:%.2d:%.2d", st.wHour, st.wMinute, st.wSecond);
        cv::putText(_cv_mat, sz_cas, cv::Point(20, _vyska_videa - 40), CV_FONT_HERSHEY_PLAIN, 2,
                CV_RGB(255, 255,0), 2);
        RedrawWindow(_hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE);
}

LRESULT CALLBACK window_proc(HWND hwnd, UINT zprava, WPARAM wparam, LPARAM lparam) noexcept
{
        switch (zprava)
        {
        case WM_COMMAND:
                switch (LOWORD(wparam))
                {
                case ID_APLIKACE_KONEC:
                        PostMessage(hwnd, WM_CLOSE, 0, 0);
                        break;
                }
                break;
        case WM_CREATE:
                wm_create(hwnd);
                break;
        case WM_CLOSE:
                KillTimer(hwnd, 1);
                return DefWindowProc(hwnd, zprava, wparam, lparam);
        case WM_TIMER:
                wm_timer();
                break;
        case WM_PAINT:
                if (!_cv_mat.empty())
                        wm_paint();
                else
                        return DefWindowProc(hwnd, zprava, wparam, lparam);
                break;
        case WM_ERASEBKGND:
                if (!_cv_mat.empty())
                        return 0;
                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)
{
        if (!_kamera.open(0 + cv::CAP_DSHOW))
        {
                MessageBox(NULL, L"Nepodařilo se spustit žádnou kameru", L"Sorry jako...", MB_ICONERROR);
                return 0;
        }
        if (!_kamera.read(_cv_mat))
                return 0;
        if (!_kamera.set(CAP_PROP_FPS, 25.0))
                if (!_kamera.set(CAP_PROP_FPS, 15.0))
                {
                        MessageBox(NULL, L"Tahle kamera je na 2 věci...", L"Sorry jako...", MB_ICONERROR);
                        return 0;
                }
        _bi_pocet_bitu = (int)((_cv_mat.dataend - _cv_mat.datastart) / (_cv_mat.cols * _cv_mat.rows) * 8);
        _sirka_videa = (unsigned int)_kamera.get(CAP_PROP_FRAME_WIDTH);
        _vyska_videa = (unsigned int)_kamera.get(CAP_PROP_FRAME_HEIGHT);
        _obr_za_sekundu = (unsigned int)_kamera.get(CAP_PROP_FPS);

        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();
        RECT rect = { 0, 0, (LONG)_sirka_videa, (LONG)_vyska_videa };
        AdjustWindowRect(&rect, WS_SYSMENU | WS_SIZEBOX, TRUE);
        SetWindowPos(_hwnd, NULL, 0, 0, rect.right - rect.left,
                rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
        ShowWindow(_hwnd, SW_SHOW);
        UpdateWindow(_hwnd);
        MSG msg;
        while (GetMessage(&msg, nullptr, 0, 0))
        {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
        }
        return (int)msg.wParam;
}

Příloha: opencv-kamera-winapi.zip.