...

вторник, 15 октября 2013 г.

[Из песочницы] Как мы работаем с камерой iPhone в QCamplr

image

Приветствую все Хабра-сообщество!


Сегодня я бы хотел на примере нового продукта — QCamplr, рассказать вам о том, как работать с камерой iOS-девайса.

В этом посте я рассмотрю базовые аспекты настройки камеры и получения изображения для последующей работы с ним.



Шаг 1: Импортируем фреймворки



Я уже начал использовать новые синтаксические возможности Objective-C в Xcode 5, именно поэтому

#import

был заменен на

@import



Для работы с камерой iOS, нам однозначно понадобится AVFoundation.framework, также нам могут понадобится возможности из CoreMedia, CoreVideo и ImageIO. Советую импортировать все эти фреймворки прямо сейчас, что бы потом не возникало никаких ошибок.

@import AVFoundation;
@import CoreMedia;
@import CoreVideo;
@import ImageIO;




Шаг 2: Декларируем свойства и методы


@property (nonatomic, strong, readonly) AVCaptureSession *captureSession;
@property (nonatomic, strong, readonly) AVCaptureDevice *captureDevice;
@property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;
@property (nonatomic, strong, readonly) AVCaptureDeviceInput *captureDeviceInput;
@property (nonatomic, strong, readonly) AVCaptureStillImageOutput *captureStillImageOutput;

+ (QCCameraManager *)sharedManager;

- (void)setupCaptureSessionWithSessionPreset:(NSString *)sessionPreset captureDevice:(AVCaptureDevice *)captureDevice captureViewLayer:(CALayer *)captureViewLayer;

- (void)captureStillImageWithCompletionHandler:(void (^)(UIImage *capturedStillImage))completionHandler;

- (BOOL)toggleCaptureDevice;

- (AVCaptureDevice *)captureDeviceWithPosition:(AVCaptureDevicePosition)captureDevicePosition;

- (BOOL)configureFocusModeOnDevice:(AVCaptureDevice *)captureDevice withFocusMode:(AVCaptureFocusMode)focusMode focusPointOfInterest:(CGPoint)focusPointOfInterest;
- (BOOL)configureExposureModeOnDevice:(AVCaptureDevice *)captureDevice withExposureMode:(AVCaptureExposureMode)exposureMode exposurePointOfInterest:(CGPoint)exposurePointOfInterest;
- (BOOL)configureWhiteBalanceModeOnDevice:(AVCaptureDevice *)captureDevice withWhiteBalanceMode:(AVCaptureWhiteBalanceMode)whiteBalanceMode;
- (BOOL)configureFlashModeOnDevice:(AVCaptureDevice *)captureDevice withFlashMode:(AVCaptureFlashMode)flashMode;
- (BOOL)configureTorchModeOnDevice:(AVCaptureDevice *)captureDevice withTorchMode:(AVCaptureTorchMode)torchMode torchLevel:(CGFloat)torchLevel;
- (BOOL)configureLowLightBoostOnDevice:(AVCaptureDevice *)captureDevice withLowLightBoostEnabled:(BOOL)lowLightBoostEnabled;




Все наши свойства имеют readonly keyword. Так как мы не хотим, чтобы кто-то изменял их напрямую, но при этом бывают ситуации, когда нам нужно быстро получить доступ к активной сессии или любому другому свойству из основного AVFoundation stack, который нужен для осуществления фотосъемки с камеры iOS-девайса.

Далее мы объявили 11 методов, о которых я расскажу подробней в следующих шагах.


Теперь можно смело открывать .m файл и начинать писать реализацию нашей камеры.


Шаг 3: Singleton



Дело в том, что iOS-девайс поддерживает только одну активную AVCaptureSession. Если вы попробуете создать и запустить несколько сессий одновременно, то увидите ошибку в Console. Так же есть масса моментов, когда нам требуется получить доступ к свойствам камеры из любого класса нашего приложения, именно поэтому мы будем создавать singleton. Мы поддерживаем ARC, поэтому singeton создаем вот так:

+ (QCCameraManager *)sharedManager {
static dispatch_once_t dispatchOncePredicate;
__strong static QCCameraManager *cameraManager = nil;
dispatch_once(&dispatchOncePredicate, ^{
cameraManager = [[QCCameraManager alloc] init];
});
return cameraManager;
}


Шаг 4: Создаем и настраиваем наш AVFoundation Stack


- (void)setupCaptureSessionWithSessionPreset:(NSString *)sessionPreset captureDevice:(AVCaptureDevice *)captureDevice captureViewLayer:(CALayer *)captureViewLayer {
[self setCaptureSession:[[AVCaptureSession alloc] init]];

if([[self captureSession] canSetSessionPreset:sessionPreset]) {
[[self captureSession] setSessionPreset:sessionPreset];
} else {
[[self captureSession] setSessionPreset:AVCaptureSessionPresetHigh];
}

[self setCaptureDevice:captureDevice];

[self setCaptureDeviceInput:[[AVCaptureDeviceInput alloc] initWithDevice:[self captureDevice] error:nil]];

if(![[[self captureSession] inputs] count]) {
if([[self captureSession] canAddInput:[self captureDeviceInput]]) {
[[self captureSession] addInput:[self captureDeviceInput]];
}
}

[self setCaptureStillImageOutput:[[AVCaptureStillImageOutput alloc] init]];
[[self captureStillImageOutput] setOutputSettings:[[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG, AVVideoCodecKey, nil]];

if ([[self captureSession] canAddOutput:[self captureStillImageOutput]]) {
[[self captureSession] addOutput:[self captureStillImageOutput]];
}

[self configureWhiteBalanceModeOnDevice:[self captureDevice] withWhiteBalanceMode:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance];
[self configureLowLightBoostOnDevice:[self captureDevice] withLowLightBoostEnabled:YES];

[self setCaptureVideoPreviewLayer:[[AVCaptureVideoPreviewLayer alloc] initWithSession:[self captureSession]]];
[[self captureVideoPreviewLayer] setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[[self captureVideoPreviewLayer] setBounds:[captureViewLayer bounds]];
[[self captureVideoPreviewLayer] setFrame:[captureViewLayer bounds]];

[captureViewLayer setMasksToBounds:YES];
[captureViewLayer insertSublayer:[self captureVideoPreviewLayer] atIndex:0];
}


Тут все довольно просто:



  • Мы создаем сессию, и задаем ей preset, он влияет на качество и разрешение картинки, которое вы будете получать во время вывода картинки на экран и непосредственной съемки.

  • Выбираем устройство которое будет использовано для съемки, в нашем случае по умолчанию это всегда задняя камера

  • Далее мы должны присоединить input к нашей сессии. Он нам понадобится для того, чтобы выводить изображение с камеры на экран в реальном времени. Сессия поддерживает только один input, при этом количество output не ограничивается одним. Проверки типа «могу ли я присоединить этот input к сессии?» нужно делать всегда!

  • Делаем примерно тоже самое, только теперь присоединяем output, он нам понадобится для того, чтобы осуществлять съемку и получать реальное изображение с камеры. Проверки типа «могу ли я присоединить этот output к сессии?» нужно делать, всегда!

  • Далее, мы проводим небольшую конфигурацию тех опций камеры, к которым мы не даем доступ пользователю. Подробнее о них я расскажу чуть позже.

  • Наконец мы создаем слой, на который и будет выводится картинка в реальном времени с нашего input девайса. Слой добавляем как sublayer слоя, который мы передали как аргумент к этому методу.


Сессия создана! Чтобы ее запустить, нужно вызвать метод startRunning у свойства captureSession, для того чтобы прервать сессию, нужно вызвать метод stopRunning.


Вот так может выглядеть вызов данного метода в вашем контролере:



[[QCCameraManager sharedManager] setupCaptureSessionWithSessionPreset:AVCaptureSessionPresetPhoto captureDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo] captureViewLayer:[[self view] layer]];
[[[[QCCameraManager sharedManager] captureSession] startRunning];


Шаг 5: Настраиваем камеру


- (BOOL)configureTorchModeOnDevice:(AVCaptureDevice *)captureDevice withTorchMode:(AVCaptureTorchMode)torchMode torchLevel:(CGFloat)torchLevel {
if([captureDevice hasTorch] && [captureDevice isTorchAvailable]) {
if([captureDevice torchMode] != torchMode) {
if([captureDevice isTorchModeSupported:torchMode]) {
if(!(([captureDevice isTorchActive]) && (torchMode == AVCaptureTorchModeOn))) {
if([captureDevice lockForConfiguration:nil]) {
if((torchMode == AVCaptureTorchModeOn) && (torchLevel >= 0.0f)) {
[captureDevice setTorchModeOnWithLevel:torchLevel error:nil];
} else {
[captureDevice setTorchMode:torchMode];
}
[captureDevice unlockForConfiguration];
} else {
return NO;
}
}
} else {
return NO;
}
}
return YES;
} else {
return NO;
}
}

- (BOOL)configureFlashModeOnDevice:(AVCaptureDevice *)captureDevice withFlashMode:(AVCaptureFlashMode)flashMode {
if([captureDevice isFlashAvailable] && [captureDevice isFlashModeSupported:flashMode]) {
if([captureDevice flashMode] != flashMode) {
if([captureDevice lockForConfiguration:nil]) {
[captureDevice setFlashMode:flashMode];
[captureDevice unlockForConfiguration];
} else {
return NO;
}
}
return YES;
} else {
return NO;
}
}

- (BOOL)configureWhiteBalanceModeOnDevice:(AVCaptureDevice *)captureDevice withWhiteBalanceMode:(AVCaptureWhiteBalanceMode)whiteBalanceMode {
if([captureDevice isWhiteBalanceModeSupported:whiteBalanceMode]) {
if([captureDevice whiteBalanceMode] != whiteBalanceMode) {
if([captureDevice lockForConfiguration:nil]) {
[captureDevice setWhiteBalanceMode:whiteBalanceMode];
[captureDevice unlockForConfiguration];
} else {
return NO;
}
}
return YES;
} else {
return NO;
}
}

- (BOOL)configureFocusModeOnDevice:(AVCaptureDevice *)captureDevice withFocusMode:(AVCaptureFocusMode)focusMode focusPointOfInterest:(CGPoint)focusPointOfInterest {
if([captureDevice isFocusModeSupported:focusMode] && [captureDevice isFocusPointOfInterestSupported]) {
if([captureDevice focusMode] == focusMode) {
if([captureDevice lockForConfiguration:nil]) {
[captureDevice setFocusPointOfInterest:focusPointOfInterest];
[captureDevice setFocusMode:focusMode];
[captureDevice unlockForConfiguration];
} else {
return NO;
}
}
return YES;
} else {
return NO;
}
}

- (BOOL)configureExposureModeOnDevice:(AVCaptureDevice *)captureDevice withExposureMode:(AVCaptureExposureMode)exposureMode exposurePointOfInterest:(CGPoint)exposurePointOfInterest {
if ([captureDevice isExposureModeSupported:exposureMode] && [captureDevice isExposurePointOfInterestSupported]) {
if([captureDevice exposureMode] == exposureMode) {
if([captureDevice lockForConfiguration:nil]) {
[captureDevice setExposurePointOfInterest:exposurePointOfInterest];
[captureDevice setExposureMode:exposureMode];
[captureDevice unlockForConfiguration];
} else {
return NO;
}
}
return YES;
} else {
return NO;
}
}

- (BOOL)configureLowLightBoostOnDevice:(AVCaptureDevice *)captureDevice withLowLightBoostEnabled:(BOOL)lowLightBoostEnabled {
if([captureDevice isLowLightBoostSupported]) {
if([captureDevice isLowLightBoostEnabled] != lowLightBoostEnabled) {
if([captureDevice lockForConfiguration:nil]) {
[captureDevice setAutomaticallyEnablesLowLightBoostWhenAvailable:lowLightBoostEnabled];
[captureDevice unlockForConfiguration];
} else {
return NO;
}
}
return YES;
} else {
return NO;
}
}




Все возможные настройки камеры осуществляются нашими собственными методами, все они работают по одному и тому же принципу. Все что нужно знать, это то, что перед изменениям параметров нам нужно вызвать метод lockForConfiguration:, после того как мы завершили процесс конфигурации нужно вызвать метод unlockForConfiguration.

В QCamplr мы даем возможность пользователю настраивать 4 опции: вспышка, фонарь(ночная съемка), фокус и экспозиция.


image


Шаг 5: Фотографируем


- (void)captureStillImageWithCompletionHandler:(void (^)(UIImage *capturedStillImage))completionHandler {
if(![[self captureStillImageOutput] isCapturingStillImage]) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"QCCameraManagedWillCaptureStillImageNotification" object:nil userInfo:nil];
[[self captureStillImageOutput] captureStillImageAsynchronouslyFromConnection:[[self captureStillImageOutput] connectionWithMediaType:AVMediaTypeVideo] completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
if (imageDataSampleBuffer) {
UIImage *capturedStillImage = [[UIImage alloc] initWithData:[AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer]];
completionHandler([capturedStillImage croppedImageFromCaptureDevice:[self captureDevice]]);
}
}];
}
}


Этот метод вызывается тогда, когда вы нажимаете на большой красный «туглер» в QCamplr. В completionHandler приходит картинка в формате jpeg, завернутая в объект класса UIImage.


image


Теперь немного вспомогательных методов, которые помогут вам в работе с камерой.


Метод #1



Получить доступ к фронтальной или задней камере можно при помощи данного метода:

- (AVCaptureDevice *)captureDeviceWithPosition:(AVCaptureDevicePosition)captureDevicePosition {
NSArray *captureDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for(AVCaptureDevice *captureDevice in captureDevices) {
if([captureDevice position] == captureDevicePosition) {
return captureDevice;
}
}
return nil;
}


Метод #2



Переключиться с фронтальной камеры на заднюю и наоборот можно при помощи данного метода:

- (BOOL)toggleCaptureDevice {
if ([[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count] > 1) {
AVCaptureDeviceInput *captureDeviceInput = [self captureDeviceInput];

if([[[self captureDeviceInput] device] position] == AVCaptureDevicePositionBack) {
[self setCaptureDeviceInput:[[AVCaptureDeviceInput alloc] initWithDevice:[self captureDeviceWithPosition:AVCaptureDevicePositionFront] error:nil]];
} else if([[[self captureDeviceInput] device] position] == AVCaptureDevicePositionFront) {
[self setCaptureDeviceInput:[[AVCaptureDeviceInput alloc] initWithDevice:[self captureDeviceWithPosition:AVCaptureDevicePositionBack] error:nil]];
} else if([[[self captureDeviceInput] device] position] == AVCaptureDevicePositionUnspecified) {
return NO;
}

[self setCaptureDevice:[[self captureDeviceInput] device]];

[[self captureSession] beginConfiguration];

[[self captureSession] removeInput:captureDeviceInput];

if([[self captureSession] canAddInput:[self captureDeviceInput]]) {
[[self captureSession] addInput:[self captureDeviceInput]];
} else {
[[self captureSession] addInput:captureDeviceInput];
}

[[self captureSession] commitConfiguration];

return YES;
} else {
return NO;
}
}


Полезные Ссылки



Документация по AVFoundation

Официальный сайт QCamplr

P.S. Надеюсь, эта статья была полезна всем тем, кто хотел получить больше свободы и гибкости в работе с камерами устройства, но не знал, с чего начать. Желаю всем побольше качественного кода, и удачи в нашем нелегком деле!


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий