Як реалізувати попередній перегляд камери за допомогою API Windows Media Foundation у C++

У попередній статті ми розробили бібліотеку LiteCam для доступу до камери на Linux. Щоб розширити її функціональність для Windows, ми скористаємося API Media Foundation.
Ця стаття розглядає, як використовувати Media Foundation для доступу до камери на Windows, інтегрувати її з бібліотекою LiteCam і повторно використовувати існуючий код для сканування штрих-кодів для створення сканера штрих-кодів на базі камери для Windows.

Демонстраційне відео з камери для Windows

Реалізація функцій, пов'язаних з камерою, для Windows

Оновлення заголовочного файлу для підтримки як Windows, так і Linux

Щоб підтримати як Windows, так і Linux, заголовочний файл Camera.h потребує наступних оновлень:

  • Додати специфічні для платформи заголовки:
#ifdef _WIN32  
 #include   
 #include   
 #include   
 #include   
 #include   
 #include   
 #include   

 #elif __linux__  
 #include   
 #include   
 #include   
 #include   
 #include   

 struct Buffer  
 {  
 void *start;  
 size_t length;  
 };  

 #endif

- Визначте макрос `CAMERA_API` для специфічного експортного доступу на різних платформах:

ifdef _WIN32

#ifdef CAMERAEXPORTS
#define CAMERA
API declspec(dllexport)
#else
#define CAMERAAPI _declspec(dllimport)
#endif
#elif defined(
linux) || defined(APPLE)
#define CAMERAAPI _attribute
((visibility("default")))
#else
#define CAMERA_API
#endif
```

  • Змініть структури MediaTypeInfo та CaptureDeviceInfo для використання відповідних типів рядків:
struct CAMERA_API MediaTypeInfo  
 {  
 uint32_t width;  
 uint32_t height;  
 #ifdef _WIN32  
 wchar_t subtypeName[512];   
 #else  
 char subtypeName[512];   
 #endif  
 };  

 struct CAMERA_API CaptureDeviceInfo  
 {  

 #ifdef _WIN32  
 wchar_t friendlyName[512];  
 #else  
 char friendlyName[512];   
 #endif  
 };
  • Коригуйте логіку конвертації пікселів у функції ConvertYUY2ToRGB:
void ConvertYUY2ToRGB(const unsigned char *yuy2Data, unsigned char *rgbData, int width, int height)  
 {
int rgbIndex = 0;  
 for (int i = 0; i < width * height * 2; i += 4)  
 {  
 unsigned char y1 = yuy2Data[i];  
 unsigned char u = yuy2Data[i + 1];  
 unsigned char y2 = yuy2Data[i + 2];  
 unsigned char v = yuy2Data[i + 3];  

 #ifdef _WIN32  
 rgbData[rgbIndex++] = clamp(y1 + 1.772 * (u - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y1 - 0.344136 * (u - 128) - 0.714136 * (v - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y1 + 1.402 * (v - 128), 0.0, 255.0);  

 rgbData[rgbIndex++] = clamp(y2 + 1.772 * (u - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y2 - 0.344136 * (u - 128) - 0.714136 * (v - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y2 + 1.402 * (v - 128), 0.0, 255.0);  
 #else  
 rgbData[rgbIndex++] = clamp(y1 + 1.402 * (v - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y1 - 0.344136 * (u - 128) - 0.714136 * (v - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y1 + 1.772 * (u - 128), 0.0, 255.0);
rgbData[rgbIndex++] = clamp(y2 + 1.402 * (v - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y2 - 0.344136 * (u - 128) - 0.714136 * (v - 128), 0.0, 255.0);  
 rgbData[rgbIndex++] = clamp(y2 + 1.772 * (u - 128), 0.0, 255.0);  
 #endif  
 }  
 }

Порядок пікселів для червоного та синього каналів змінюється між Windows та Linux.

Визначення класу Camera з платформонезалежними членами та методами

class CAMERA_API Camera  
 {  
 public:  
 #ifdef _WIN32  
 Camera();  
 ~Camera();  
 #elif __linux__  
 Camera() : fd(-1), frameWidth(640), frameHeight(480), buffers(nullptr), bufferCount(0) {}  
 ~Camera() { Release(); }  
 #endif  

 private:  
 #ifdef _WIN32  
 void *reader;  

 bool initialized;  
 void InitializeMediaFoundation();  
 void ShutdownMediaFoundation();  
 #endif  

 #ifdef __linux__  
 int fd;  
 Buffer *buffers;  
 unsigned int bufferCount;  

 bool InitDevice();  
 void UninitDevice();  
 bool StartCapture();  
 void StopCapture();  
 #endif  

 };  



## Запит камер

Використовуйте API `Media Foundation` для перерахунку доступних камер:

std::vector ListCaptureDevices()
{
HRESULT hr = S_OK;
ComPtr attributes;
std::vector devicesInfo;

hr = MFCreateAttributes(&attributes, 1);
if (FAILED(hr))
{
std::cerr << "Не вдалося створити атрибути." << std::endl;
return devicesInfo;
}

hr = attributes->SetGUID(MFDEVSOURCEATTRIBUTESOURCETYPE, MFDEVSOURCEATTRIBUTESOURCETYPEVIDCAPGUID);
if (FAILED(hr))
{
std::cerr << "Не вдалося встановити атрибут пристрою відеозахоплення." << std::endl;
return devicesInfo;
}

UINT32 count = 0;
IMFActivate **devices = nullptr;

hr = MFEnumDeviceSources(attributes.Get(), &devices, &count);
if (FAILED(hr) || count == 0)
{
std::cerr << "Не знайдено пристроїв відеозахоплення." << std::endl;
return devicesInfo;
}

for (UINT32 i = 0; i < count; ++i)
{

WCHAR *friendlyName = nullptr;
UINT32 nameLength = 0;

hr = devices[i]->GetAllocatedString(MFDEVSOURCEATTRIBUTEFRIENDLYNAME, &friendlyName, &nameLength);
if (SUCCEEDED(hr))
{
CaptureDeviceInfo info = {};
wcsncpy(info.friendlyName, friendlyName, nameLength);
devicesInfo.push_back(info);
CoTaskMemFree(friendlyName);
}
devices[i]->Release();
}

CoTaskMemFree(devices);
return devicesInfo;
}
```

Пояснення

  • Створіть об'єкт IMFAttributes, щоб вказати пристрій відеозахоплення.
  • Перерахуйте пристрої відеозахоплення за допомогою MFEnumDeviceSources.
  • Отримайте зручну назву кожного пристрою за допомогою GetAllocatedString.

Відкриття камери

1.
Активуйте вказану камеру за індексом:

ComPtr mediaSource;  
hr = devices[cameraIndex]->ActivateObject(IID_PPV_ARGS(&mediaSource));  
for (UINT32 i = 0; i < count; i++)  
 devices[i]->Release();  
CoTaskMemFree(devices);  

if (FAILED(hr))  
 return false;  

ComPtr mfReader;  
hr = MFCreateSourceReaderFromMediaSource(mediaSource.Get(), nullptr, &mfReader);  
if (FAILED(hr))  
 return false;

Об'єкт IMFSourceReader використовується для зчитування відеоданих з камери.

  1. Налаштуйте ширину відео, висоту та формат пікселів.
    Наприклад, формат YUY2 з розміром кадру 640x480:
ComPtr mediaType;  
 hr = MFCreateMediaType(&mediaType);  
 if (FAILED(hr))  
 return false;  

 hr = mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);  
 hr = mediaType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_YUY2);  
 hr = MFSetAttributeSize(mediaType.Get(), MF_MT_FRAME_SIZE, frameWidth, frameHeight);  

 if (SUCCEEDED(hr))  
 {  
 hr = mfReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, mediaType.Get());  

 if (SUCCEEDED(hr))  
 {  
 reader = reinterpret_cast(mfReader.Detach());  
 return true;  
 }  
 }

Після налаштування типу медіа, об'єкт IMFSourceReader зберігається у змінній-члені reader.

Зйомка кадру

1.
Зчитування зразка з камери:

HRESULT hr;  
 DWORD streamIndex, flags;  
 LONGLONG timestamp;  
 ComPtr sample;  
 FrameData frame;  

 frame.width = frameWidth;  
 frame.height = frameHeight;  
 frame.rgbData = nullptr;  

 IMFSourceReader *mfReader = reinterpret_cast(reader);  
 hr = mfReader->ReadSample(  
 MF_SOURCE_READER_FIRST_VIDEO_STREAM,  
 0,  
 &streamIndex,  
 &flags,  
 &timestamp,  
 &sample);  

 if (FAILED(hr))  
 {  
 std::cerr << "Не вдалося зчитати зразок." << std::endl;  
 return frame;   
 }

2.
Отримання сирих даних з зразка та їх конвертація в формат RGB888:

if (sample)  
 {  
 ComPtr buffer;  
 hr = sample->ConvertToContiguousBuffer(&buffer);  
 if (FAILED(hr))  
 {  
 std::cerr << "Не вдалося конвертувати зразок в безперервний буфер." << std::endl;  
 return frame;   
 }  

 BYTE *rawData = nullptr;  
 DWORD maxLength = 0, currentLength = 0;  
 hr = buffer->Lock(&rawData, &maxLength, &currentLength);  
 if (SUCCEEDED(hr))  
 {  
 frame.size = frameWidth * frameHeight * 3;  
 frame.rgbData = new unsigned char[frame.size];  
 if (!frame.rgbData)  
 {  
 std::cerr << "Не вдалося виділити пам'ять для RGB даних." << std::endl;  
 return frame;   
 }  

 ConvertYUY2ToRGB(rawData, frame.rgbData, frameWidth, frameHeight);  

 buffer->Unlock();  
 }  
 }

Закриття камери

Звільнення об'єкта IMFSourceReader та ресурсів Media Foundation:

if (reader)  
{  
 ComPtr mfReader(static_cast<IMFSourceReader*>(reader));  
 reader = nullptr;  
}  

if (initialized)  
{  
 MFShutdown();  
 initialized = false;  
}

Реалізація функцій для відображення в Windows

Оновлення заголовочного файлу для підтримки Windows та Linux

Для підтримки кросплатформної сумісності було оновлено заголовочний файл CameraPreview.h наступним чином:

  • Визначення макроса CAMERA_API для Windows та Linux.
#ifdef _WIN32  
 #include <windows.h>  
 #elif __linux__  
 #include <X11/Xlib.h>  
 #include <X11/Xutil.h>  
 #elif __APPLE__  
 #include <Cocoa/Cocoa.h>  
 #endif  

 #ifdef _WIN32  
 #ifdef CAMERA_EXPORTS  
 #define CAMERA_API __declspec(dllexport)  
 #else  
 #define CAMERA_API __declspec(dllimport)  
 #endif  
 #elif defined(__linux__) || defined(__APPLE__)  
 #define CAMERA_API __attribute__((visibility("default")))  
 #else  
 #define CAMERA_API  
 #endif
  • Додавання платформозалежних компонентів для вікна та рендерингу:
class CAMERA_API CameraWindow  
 {  

 private:  

 #ifdef _WIN32  
 static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);  

 HWND hwnd;  
 HDC hdc;  
 WNDCLASS wc;  
 HINSTANCE hInstance;  
 #elif __linux__  
 Display *display;  
 Window window;  
 GC gc;   
 Atom wmDeleteMessage;  
 #endif  
 };

Конструктор та Деструктор

Конструктор ініціалізує клас вікна та зворотний виклик подій.
Деструктор очищає ресурси:

CameraWindow::CameraWindow(int w, int h, const std::string &t)  
 : width(w), height(h), title(t), hwnd(nullptr), hdc(nullptr)  
{  

 hInstance = GetModuleHandle(nullptr);  

 wc = {};  
 wc.lpfnWndProc = WindowProc;   
 wc.hInstance = hInstance;  
 wc.lpszClassName = "CameraWindowClass";  
}  

CameraWindow::~CameraWindow()  
{  
 if (hdc)  
 {  
 ReleaseDC(hwnd, hdc);  
 }  
 if (hwnd)  
 {  
 DestroyWindow(hwnd);  
 }  
 UnregisterClass("CameraWindowClass", hInstance);  
}  

LRESULT CALLBACK CameraWindow::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)  
{  
 switch (uMsg)  
 {  
 case WM_DESTROY:  
 PostQuitMessage(0);  
 return 0;  
 }  
 return DefWindowProc(hwnd, uMsg, wParam, lParam);  
}

Створення вікна

Викликаємо CreateWindowEx, щоб створити вікно, та GetDC, щоб отримати контекст пристрою:

bool CameraWindow::Create()  
{  
 if (!RegisterClass(&wc))  
 {  
   std::cerr << "Не вдалося зареєструвати клас вікна." << std::endl;  
   return false;  
 }  

 hwnd = CreateWindowEx(0,  
 wc.lpszClassName,  
 title.c_str(),  
 WS_OVERLAPPEDWINDOW,  
 CW_USEDEFAULT, CW_USEDEFAULT, width, height,  
 nullptr, nullptr, hInstance, nullptr);  

 if (!hwnd)  
 {  
   std::cerr << "Не вдалося створити вікно." << std::endl;  
   return false;  
 }  

 hdc = GetDC(hwnd);  
 if (!hdc)  
 {  
   std::cerr << "Не вдалося отримати контекст пристрою." << std::endl;  
   return false;  
 }  

 return true;  
}

std::cerr << "Не вдалося зареєструвати клас вікна." << std::endl;  
 return false;  
 }  

 hwnd = CreateWindowEx(  
 0, "CameraWindowClass", title.c_str(), WS_OVERLAPPEDWINDOW,  
 CW_USEDEFAULT, CW_USEDEFAULT, width, height,  
 nullptr, nullptr, hInstance, nullptr);  

 if (!hwnd)  
 {  
 std::cerr << "Не вдалося створити вікно." << std::endl;  
 return false;  
 }  

 hdc = GetDC(hwnd);  
 return true;  
}

Показ вікна

Викликаємо ShowWindow, щоб відобразити вікно:

void CameraWindow::Show()  
{  
 ShowWindow(hwnd, SW_SHOW);  
}

Обробка події з клавіатури

Перехоплюємо ввід з клавіатури для виходу з програми:

bool CameraWindow::WaitKey(char key)  
{  
 MSG msg = {};  
 while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))  
 {  
 TranslateMessage(&msg);  
 DispatchMessage(&msg);  

 if (msg.message == WM_QUIT)  
 {  
 return false;   
 }  

 if (msg.message == WM_KEYDOWN)  
 {  
 char keyPressed = static_cast<char>(msg.wParam);  
   if (keyPressed == key)  
   {  
     return true;  
   }  
 }  
 }  
 return false;  
}

if (key != '\0' && (keyPressed == key || keyPressed == std::toupper(key)))  
 {  
 return false;   
 }  
 }  
 }  
 return true;  
}

Відображення кадру з камери

Використовуємо функцію StretchDIBits, щоб відобразити кадр з камери.

void CameraWindow::ShowFrame(const unsigned char *rgbData, int frameWidth, int frameHeight)  
{  
 if (!hdc || !rgbData)  
 return;  

 BITMAPINFO bmpInfo = {};  
 bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);  
 bmpInfo.bmiHeader.biWidth = frameWidth;  
 bmpInfo.bmiHeader.biHeight = -frameHeight;   
 bmpInfo.bmiHeader.biPlanes = 1;  
 bmpInfo.bmiHeader.biBitCount = 24;   
 bmpInfo.bmiHeader.biCompression = BI_RGB;  

 StretchDIBits(  
 hdc,  
 0, 0, frameWidth, frameHeight,   
 0, 0, frameWidth, frameHeight,  
 rgbData,   
 &bmpInfo,   
 DIB_RGB_COLORS,   
 SRCCOPY   
 );  
}

Малювання тексту на вікні

Малюємо текст на вікні за допомогою функції TextOut.

void CameraWindow::DrawText(const std::string &text, int x, int y, int fontSize, const Color &color)
{
if (!hdc)
return;

SetTextColor(hdc, RGB(color.r, color.g, color.b));
SetBkMode(hdc, TRANSPARENT);

HFONT hFont = CreateFont(
fontSize,
0,
0,
0,
FWNORMAL,
FALSE,
FALSE,
FALSE,
DEFAULT
CHARSET,
OUTDEFAULTPRECIS,
CLIPDEFAULTPRECIS,
DEFAULTQUALITY,
DEFAULT
PITCH | FF_DONTCARE,
"Arial");

if (!hFont)
return;

HGDIOBJ oldFont = SelectObject(hdc, hFont);

TextOut(hdc, x, y, text.cstr(), staticcast(text.length()));

SelectObject(hdc, oldFont);
DeleteObject(hFont);
}
```

Малювання контурів на вікні

Малюємо контури на вікні за допомогою функцій MoveToEx та LineTo.

void CameraWindow::DrawContour(const std::vector<Point> &points)  
{  
 if (!hdc || points.size() < 4)  
 return;  

 HPEN hPen = CreatePen(PS_SOLID, 2, RGB(0, 255, 0));  

HGDIOBJ oldPen = SelectObject(hdc, hPen);  

 MoveToEx(hdc, points[0].first, points[0].second, nullptr);  
 for (size_t i = 1; i < points.size(); ++i)  
 {  
 LineTo(hdc, points[i].first, points[i].second);  
 }  
 LineTo(hdc, points[0].first, points[0].second);   

 SelectObject(hdc, oldPen);  
 DeleteObject(hPen);  
}

Створення програми для сканера штрих-кодів на Windows

Для створення програми сканера штрих-кодів не потрібно змінювати логіку сканування штрих-кодів.
Дотримуйтесь цих кроків:

  1. Підготуйте бібліотеку для роботи з камерами та Dynamsoft C++ Barcode SDK для Windows.

2.
Оновіть файл CMakeLists.txt, щоб включити конфігурацію, специфічну для Windows.

cmake_minimum_required(VERSION 3.10)  
 project(BarcodeScanner)  

 if(WIN32)  

 if(CMAKE_BUILD_TYPE STREQUAL "Release")  
 link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../dist/lib/windows/release ${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/platforms/win/lib)  
 else()  
 link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../dist/lib/windows/debug ${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/platforms/win/lib)  
 endif()  

 set(DBR_LIBS "DynamsoftCorex64" "DynamsoftLicensex64" "DynamsoftCaptureVisionRouterx64" "DynamsoftUtilityx64")  
 elseif(UNIX)  
 SET(CMAKE_CXX_FLAGS "-std=c++11 -O3 -Wl,-rpath=$ORIGIN")  
 SET(CMAKE_INSTALL_RPATH "$ORIGIN")  
 link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../dist/lib/linux ${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/platforms/linux)  
 set(DBR_LIBS "DynamsoftCore" "DynamsoftLicense" "DynamsoftCaptureVisionRouter" "DynamsoftUtility" pthread)  
 endif()  

 # Створити виконуваний файл  
 add_executable(BarcodeScanner main.cpp)  
 target_include_directories(BarcodeScanner PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../dist/include ${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/include)  
 target_link_libraries(BarcodeScanner litecam ${DBR_LIBS})  

 if(WIN32)  
 if(CMAKE_BUILD_TYPE STREQUAL "Release")  
 add_custom_command(TARGET BarcodeScanner POST_BUILD  
 COMMAND ${CMAKE_COMMAND} -E copy_directory  
 ${CMAKE_CURRENT_SOURCE_DIR}/../../dist/lib/windows/release   
 $)  
 else()  
 add_custom_command(TARGET BarcodeScanner POST_BUILD  
 COMMAND ${CMAKE_COMMAND} -E copy_directory  
 ${CMAKE_CURRENT_SOURCE_DIR}/../../dist/lib/windows/debug   
 $)  
 endif()  

 add_custom_command(TARGET BarcodeScanner POST_BUILD  
 COMMAND ${CMAKE_COMMAND} -E copy_directory  
 ${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/platforms/win/bin/   
 $)  
 elseif(UNIX)  
 add_custom_command(TARGET BarcodeScanner POST_BUILD  
 COMMAND ${CMAKE_COMMAND} -E copy_directory  
 ${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/platforms/linux/   
 $)  
 endif()

3.
Збудуйте додаток за допомогою CMake.

mkdir build  
 cd build  
 cmake ..  
 cmake --build .

pic

Джерело коду

https://github.com/yushulx/cmake-cpp-barcode-qrcode-mrz/tree/main/litecam

Перекладено з: How to Implement Camera Preview with Windows Media Foundation API in C++

Leave a Reply

Your email address will not be published. Required fields are marked *