Це остання стаття в серії, присвяченій створенню легковагової бібліотеки для роботи з камерами на C++ для кількох платформ. Ми вже охопили Windows та Linux, а тепер настав час зануритись у macOS. У цій статті ми скористаємося AVFoundation для обробки захоплення відео в фоновому режимі та використаємо Cocoa для основних елементів інтерфейсу користувача.
Ви побачите, як поєднувати Objective-C з C++, щоб побудувати бібліотеку для роботи з камерами, а потім інтегрувати її з Dynamsoft Barcode Reader SDK для створення сканера штрих-кодів на macOS.
Демо відео камери для macOS
Реалізація функцій, що пов'язані з камерою для macOS
Давайте розглянемо функції, пов'язані з камерою, які потрібно реалізувати для macOS:
std::vector ListCaptureDevices()
: Перелічує доступні камери.bool Open(int cameraIndex)
: Активує вказану камеру.void Release()
: Вивільняє камеру.FrameData CaptureFrame()
: Захоплює кадр з камери.
Оновлення заголовного файлу для macOS
Відкрийте заголовний файл Camera.h
і додайте наступні зміни:
class CAMERA_API Camera
{
public:
#ifdef _WIN32
...
#elif __APPLE__
Camera() noexcept;
~Camera();
#endif
...
private:
...
#ifdef __APPLE__
void *captureSession;
void *videoOutput;
#endif
};
#endif
Пояснення
noexcept
використовується для позначення того, що функція не викидає виключень.captureSession
таvideoOutput
використовуються для керування сесією захоплення з камери та відео виведенням.
Запит камер
Перелічимо доступні камери за допомогою API AVFoundation:
std::vector ListCaptureDevices()
{
@autoreleasepool {
std::vector devicesInfo;
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices)
{
CaptureDeviceInfo info = {};
strncpy(info.friendlyName, [[device localizedName] UTF8String], sizeof(info.friendlyName) - 1);
devicesInfo.push_back(info);
}
return devicesInfo;
}
}
Пояснення
AVCaptureDevice
представляє фізичний пристрій захоплення.AVMediaTypeVideo
вказує на тип медіа відео.localizedName
отримує ім'я пристрою.
Відкриття камери
Кроки для відкриття камери:
1.
Отримати доступні пристрої захоплення.
2. Створити вхідний пристрій та сесію захоплення.
3. Налаштувати відео виведення.
4.
Почати сесію захоплення.
bool Camera::Open(int cameraIndex)
{
@autoreleasepool {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
if (cameraIndex >= [devices count])
{
std::cerr << "Індекс камери поза межами допустимого діапазону." << std::endl;
return false;
}
AVCaptureDevice *device = devices[cameraIndex];
NSError *error = nil;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!input)
{
std::cerr << "Помилка при створенні вхідного пристрою: " << [[error localizedDescription] UTF8String] << std::endl;
return false;
}
AVCaptureSession *cs = [[AVCaptureSession alloc] init];
captureSession = (void *)cs;
if (![cs canAddInput:input])
{
std::cerr << "Не можна додати вхідний пристрій до сесії." << std::endl;
return false;
}
[cs addInput:input];
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
output.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};
output.alwaysDiscardsLateVideoFrames = YES;
videoOutput = (void *)output;
if (![cs canAddOutput:output])
{
std::cerr << "Не можна додати відеовихід до сесії." << std::endl;
return false;
}
[cs addOutput:output];
[cs startRunning];
return true;
}
}
Пояснення
AVCaptureDeviceInput
захоплює дані з вибраного пристрою камери.AVCaptureSession
керує потоком даних від пристрою до виходу.
Ми зберігаємо його вcaptureSession
.AVCaptureVideoDataOutput
надає сирі відео кадри з пристрою захоплення.
Ми зберігаємо це вvideoOutput
.kCVPixelBufferPixelFormatTypeKey
вказує тип формату пікселів.
Закриття камери
Зупиніть сесію захоплення та звільніть ресурси:
void Camera::Release()
{
if (captureSession)
{
AVCaptureSession *session = (__bridge AVCaptureSession *)captureSession;
if (videoOutput)
{
AVCaptureVideoDataOutput *output = (__bridge AVCaptureVideoDataOutput *)videoOutput;
[session removeOutput:output];
videoOutput = nil;
}
[session stopRunning];
captureSession = nil;
}
}
Захоплення кадру
Захоплення кадру з камери за допомогою протоколу AVCaptureVideoDataOutputSampleBufferDelegate
:
@interface CaptureDelegate : NSObject
{
FrameData *frame;
dispatch_semaphore_t semaphore;
}
- (instancetype)initWithFrame:(FrameData *)frame semaphore:(dispatch_semaphore_t)semaphore;
@end
@implementation CaptureDelegate
- (instancetype)initWithFrame:(FrameData *)frame semaphore:(dispatch_semaphore_t)semaphore {
self = [super init];
if (self) {
self->frame = frame;
self->semaphore = semaphore;
}
return self;
}
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if (!imageBuffer) {
std::cerr << "Не вдалося отримати буфер зображення." << std::endl;
dispatch_semaphore_signal(semaphore);
return;
}
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
frame->width = width;
frame->height = height;
frame->size = width * height * 3;
frame->rgbData = new unsigned char[frame->size];
OSType pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer);
if (pixelFormat == kCVPixelFormatType_32BGRA) {
unsigned char *src = (unsigned char *)baseAddress;
unsigned char *dst = frame->rgbData;
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
size_t offset = y * bytesPerRow + x * 4;
dst[0] = src[offset + 2];
dst[1] = src[offset + 1];
dst[2] = src[offset + 0];
dst += 3;
}
}
} else {
std::cerr << "Непідтримуваний формат пікселів." << std::endl;
}
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
dispatch_semaphore_signal(semaphore);
}
@end
FrameData Camera::CaptureFrame()
{
@autoreleasepool {
FrameData frame = {};
if (!captureSession || !videoOutput) {
std::cerr << "Сесію захоплення не ініціалізовано." << std::endl;
return frame;
}
AVCaptureSession *session = (__bridge AVCaptureSession *)captureSession;
AVCaptureVideoDataOutput *vo = (__bridge AVCaptureVideoDataOutput *)videoOutput;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[vo setSampleBufferDelegate:[[CaptureDelegate alloc] initWithFrame:&frame semaphore:semaphore]
queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
frameWidth = frame.width;
frameHeight = frame.height;
return frame;
}
}
Пояснення
setSampleBufferDelegate
вказує об'єкт, який буде отримувати кадри відAVCaptureVideoDataOutput
.FrameData *frame
вказує на структуру, в якій зберігаються дані зображення (ширина, висота та RGB-буфер).dispatch_semaphore_t semaphore
використовується для синхронізації.
Коли кадр оброблений, делегат сигналізує семафору, щоб викликаючий код знав, що кадр готовий.- У
captureOutput:didOutputSampleBuffer:fromConnection
ми отримуємо піксельний буфер і конвертуємо BGRA в RGB (3 байти на піксель). CMSampleBufferGetImageBuffer(sampleBuffer)
повертаєCVImageBufferRef
, що містить піксельні дані.CVPixelBufferLockBaseAddress
таCVPixelBufferUnlockBaseAddress
забезпечують безпеку потоків під час читання.dispatch_semaphore_signal(semaphore)
сповіщає того, хто чекає на цей семафор, що дані кадру тепер готові.
Реалізація функцій для відображення на macOS
Оновлення заголовного файлу для macOS
Додайте специфічні для macOS елементи до заголовного файлу CameraPreview.h
:
class CAMERA_API CameraWindow
{
public:
...
#ifdef _WIN32
...
#elif __APPLE__
void *nsWindow;
void *contentView;
#endif
};
Пояснення
nsWindow
таcontentView
використовуються для керування вікном і відповідним контентом на macOS.
Спеціальний NSView для малювання кадрів камери, контурів і тексту
Створіть підклас NSView
, щоб обробляти малювання:
struct CameraContentViewImpl {
std::vector<unsigned char> rgbData;
int frameWidth = 0;
int frameHeight = 0;
int x = 0;
int y = 0;
int fontSize = 0;
std::vector<std::pair<int, int>> contourPoints;
std::string displayText;
CameraWindow::Color textColor;
};
@interface CameraContentView : NSView
{
CameraContentViewImpl* impl;
}
- (void)updateFrame:(const unsigned char*)data width:(int)width height:(int)height;
- (void)updateContour:(const std::vector<std::pair<int, int>>&)points;
- (void)updateText:(const std::string&)text
x:(int)x
y:(int)y
fontSize:(int)fontSize
color:(const CameraWindow::Color&)color;
@end
@implementation CameraContentView
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
impl = new CameraContentViewImpl();
}
return self;
}
- (void)dealloc {
delete impl;
[super dealloc];
}
- (void)updateFrame:(const unsigned char*)data width:(int)width height:(int)height {
impl->rgbData.assign(data, data + (width * height * 3));
impl->frameWidth = width;
impl->frameHeight = height;
[self setNeedsDisplay:YES];
}
- (void)updateContour:(const std::vector<std::pair<int, int>>&)points {
impl->contourPoints = points;
[self setNeedsDisplay:YES];
}
- (void)updateText:(const std::string&)text
x:(int)x
y:(int)y
fontSize:(int)fontSize
color:(const CameraWindow::Color&)color {
impl->displayText = text;
impl->textColor = color;
impl->x = x;
impl->y = y;
impl->fontSize = fontSize;
impl->textColor = color;
[self setNeedsDisplay:YES];
}
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSRect bounds = [self bounds];
CGContextRef context = [[NSGraphicsContext currentContext] CGContext];
if (impl->rgbData.empty() || impl->frameWidth == 0 || impl->frameHeight == 0) {
return;
}
CGFloat scaleX = bounds.size.width / impl->frameWidth;
CGFloat scaleY = bounds.size.height / impl->frameHeight;
CGFloat scale = MIN(scaleX, scaleY);
CGFloat offsetX = (bounds.size.width - (impl->frameWidth * scale)) / 2.0;
CGFloat offsetY = (bounds.size.height - (impl->frameHeight * scale)) / 2.0;
CGContextSaveGState(context);
CGContextTranslateCTM(context, offsetX, offsetY);
CGContextScaleCTM(context, scale, scale);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, impl->rgbData.data(), impl->rgbData.size(), NULL);
CGImageRef image = CGImageCreate(impl->frameWidth, impl->frameHeight, 8, 24, impl->frameWidth * 3, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaNone, provider, NULL, false, kCGRenderingIntentDefault);
CGRect rect = CGRectMake(0, 0, impl->frameWidth, impl->frameHeight);
CGContextDrawImage(context, rect, image);
CGImageRelease(image);
CGDataProviderRelease(provider);
CGColorSpaceRelease(colorSpace);
if (!impl->contourPoints.empty()) {
CGContextSaveGState(context);
CGContextSetLineWidth(context, 3.0 / scale);
CGContextSetStrokeColorWithColor(context, [[NSColor yellowColor] CGColor]);
auto firstPoint = impl->contourPoints[0];
CGContextMoveToPoint(context, firstPoint.first, impl->frameHeight - firstPoint.second);
for (size_t i = 1; i < impl->contourPoints.size(); ++i) {
auto point = impl->contourPoints[i];
CGContextAddLineToPoint(context, point.first, impl->frameHeight - point.second);
}
CGContextClosePath(context);
CGContextStrokePath(context);
CGContextRestoreGState(context);
impl->contourPoints.clear();
}
CGContextRestoreGState(context);
if (!impl->displayText.empty()) {
CGContextSaveGState(context);
CGFloat scaledX = impl->x * scale + offsetX;
CGFloat scaledY = impl->y * scale + offsetY;
NSColor *color = [NSColor colorWithRed:impl->textColor.r / 255.0 green:impl->textColor.g / 255.0 blue:impl->textColor.b / 255.0 alpha:1.0];
NSDictionary *attributes = @{
NSFontAttributeName : [NSFont systemFontOfSize:impl->fontSize * scale],
NSForegroundColorAttributeName : color
};
NSPoint point = NSMakePoint(scaledX, bounds.size.height - scaledY - (impl->fontSize * scale));
NSString *nsText = [NSString stringWithUTF8String:impl->displayText.c_str()];
[nsText drawAtPoint:point withAttributes:attributes];
CGContextRestoreGState(context);
impl->displayText.clear();
}
}
@end
**Пояснення**
- `CameraContentViewImpl` містить усі дані, необхідні для відображення (дані кадру, текст тощо).
- `CameraContentView` — це підклас `NSView`, який відображає кадр камери, контури та текст.
- Виклик `setNeedsDisplay:` ініціює перерисовку, яка, в свою чергу, викликає `drawRect`.
- У `drawRect:` обчислюються коефіцієнти масштабування та зміщення, щоб зберегти співвідношення сторін і центризувати зображення.
## Делегат вікна для обробки подій вікна
Створіть власний `NSWindowDelegate` для обробки подій вікна:
@interface CameraWindowDelegate : NSObject
@end
@implementation CameraWindowDelegate
- (BOOL)windowShouldClose:(id)sender {
[NSApp terminate:nil];
return YES;
}
@end
```
Ініціалізація вікна та вмісту
Ініціалізуйте вікно та вміст, і збережіть їх у nsWindow
та contentView
:
bool CameraWindow::Create() {
@autoreleasepool {
if (NSApp == nil) {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp finishLaunching];
}
NSRect contentRect = NSMakeRect(100, 100, width, height);
NSUInteger styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable;
NSWindow *window = [[NSWindow alloc] initWithContentRect:contentRect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
if (!window) {
return false;
}
[window setTitle:[NSString stringWithUTF8String:title.c_str()]];
[window makeKeyAndOrderFront:nil];
CameraContentView *cv = [[CameraContentView alloc] initWithFrame:contentRect];
[window setContentView:cv];
contentView = cv;
CameraWindowDelegate *delegate = [[CameraWindowDelegate alloc] init];
[window setDelegate:delegate];
nsWindow = (void *)window;
return true;
}
}
Відображення вікна
Перенесіть програму на передній план і зробіть її активною:
void CameraWindow::Show() {
@autoreleasepool {
[NSApp activateIgnoringOtherApps:YES];
}
}
Обробка клавіатурних подій
Перехоплення подій вводу з клавіатури:
bool CameraWindow::WaitKey(char key)
{
@autoreleasepool {
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (event) {
[NSApp sendEvent:event];
if (event.type == NSEventTypeKeyDown) {
NSString *characters = [event charactersIgnoringModifiers];
if ([characters length] > 0) {
char pressedKey = [characters characterAtIndex:0];
if (key == '\0' || pressedKey == key || pressedKey == std::toupper(key)) {
return false;
}
}
}
}
return true;
}
}
Малювання кадру камери, контурів та тексту
Оновлення кадру камери, контурів та тексту таким чином:
void CameraWindow::ShowFrame(const unsigned char *rgbData, int frameWidth, int frameHeight) {
if (contentView) {
[contentView updateFrame:rgbData width:frameWidth height:frameHeight];
}
}
void CameraWindow::DrawContour(const std::vector> &points) {
if (contentView) {
[contentView updateContour:points];
}
}
void CameraWindow::DrawText(const std::string &text, int x, int y, int fontSize, const CameraWindow::Color &color) {
if (contentView) {
[contentView updateText:text x:x y:y fontSize:fontSize color:color];
}
}
Оновлення CMakelists.txt для macOS
Щоб зібрати бібліотеку на macOS, оновіть файл CMakeLists.txt
:
...
if (WIN32)
...
elseif (UNIX AND NOT APPLE)
...
elseif (APPLE)
set(LIBRARY_SOURCES
src/CameraMacOS.mm
src/CameraPreviewMacOS.mm
)
set_source_files_properties(src/CameraMacOS.mm src/CameraPreviewMacOS.mm PROPERTIES COMPILE_FLAGS "-x objective-c++")
set_source_files_properties(src/main.cpp PROPERTIES COMPILE_FLAGS "-x objective-c++")
endif()
...
if (UNIX AND NOT APPLE)
...
elseif (APPLE)
find_library(COCOA_LIBRARY Cocoa REQUIRED)
find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED)
find_library(COREMEDIA_LIBRARY CoreMedia REQUIRED)
find_library(COREVIDEO_LIBRARY CoreVideo REQUIRED)
find_library(OBJC_LIBRARY objc REQUIRED) # Додати бібліотеку для середовища виконання Objective-C
target_link_libraries(litecam PRIVATE
${COCOA_LIBRARY}
${AVFOUNDATION_LIBRARY}
${COREMEDIA_LIBRARY}
${COREVIDEO_LIBRARY}
${OBJC_LIBRARY} # Підключити бібліотеку для середовища виконання Objective-C
)
elseif (WIN32)
...
endif()
...
if (APPLE)
target_link_libraries(camera_capture PRIVATE
${COCOA_LIBRARY}
${AVFOUNDATION_LIBRARY}
${COREMEDIA_LIBRARY}
${COREVIDEO_LIBRARY}
${OBJC_LIBRARY} # Підключити бібліотеку для середовища виконання Objective-C
)
endif()
Пояснення
-x objective-c++
забезпечує компіляцію вихідних файлів як Objective-C++.- На macOS необхідно підключити фреймворк Cocoa, AVFoundation та бібліотеку для середовища виконання Objective-C.
Створення програми для сканування штрих-кодів на macOS
Щоб створити додаток для сканування штрих-кодів, не потрібно вносити зміни в логіку сканування. Дотримуйтесь наступних кроків:
- Оновіть файл
CMakeLists.txt
, щоб додати специфічні налаштування для macOS.
...
if(WIN32)
...
elseif(APPLE)
set(CMAKE_CXX_FLAGS "-std=c++11 -O3 -Wl,-rpath,@executable_path")
set(CMAKE_INSTALL_RPATH "@executable_path")
link_directories(
${CMAKE_CURRENT_SOURCE_DIR}/../../dist/lib/macos
${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/platforms/macos
)
set(DBR_LIBS
"DynamsoftCore"
"DynamsoftLicense"
"DynamsoftCaptureVisionRouter"
"DynamsoftUtility"
"pthread"
)
elseif(UNIX)
...
endif()
...
if(WIN32)
...
elseif(APPLE)
add_custom_command(TARGET BarcodeScanner POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/../../../examples/10.x/sdk/platforms/macos
$
)
elseif(UNIX)
...
endif()
- Побудуйте додаток за допомогою CMake.
mkdir build
cd build
cmake ..
cmake --build .
Джерельний код
https://github.com/yushulx/cmake-cpp-barcode-qrcode-mrz/tree/main/litecam
Перекладено з: Mixing Objective-C and C++ in macOS to Build a Camera-Based Barcode Scanner