iOS人臉識別、自定義相機、圖像掃描、系統自帶二維碼識別

前段時間遇到一個需求,需要實時掃描圖像,并且設定攝像頭的尺寸為1080x1920,然后從中間截取出512x512的圖片傳給第三方SDK做進一步業務處理,直到SDK返回正確的處理結果。

一頓Google,發現圖像預覽、人臉識別、二維碼識別這些蘋果都幫我們做好了,而且它們都是基于AVFoundation框架實現的。

話不多說,上代碼~!

用到的類,主要有這些:

//硬件設備
@property (nonatomic, strong) AVCaptureDevice *device;
//輸入流
@property (nonatomic, strong) AVCaptureDeviceInput *input;
//協調輸入輸出流的數據
@property (nonatomic, strong) AVCaptureSession *session;
//預覽層
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;

//輸出流
@property (nonatomic, strong) AVCaptureStillImageOutput *stillImageOutput;  //用于捕捉靜態圖片
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;    //原始視頻幀,用于獲取實時圖像以及視頻錄制
@property (nonatomic, strong) AVCaptureMetadataOutput *metadataOutput;      //用于二維碼識別以及人臉識別
  1. 首先我們需要在手機上把圖像顯示出來

1.1 獲取硬件設備

-(AVCaptureDevice *)device{
    if (_device == nil) {
        _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
        if ([_device lockForConfiguration:nil]) {
            //自動閃光燈
            if ([_device isFlashModeSupported:AVCaptureFlashModeAuto]) {
                [_device setFlashMode:AVCaptureFlashModeAuto];
            }
            //自動白平衡
            if ([_device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance]) {
                [_device setWhiteBalanceMode:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance];
            }
            //自動對焦
            if ([_device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
                [_device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
            }
            //自動曝光
            if ([_device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
                [_device setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
            }
            [_device unlockForConfiguration];
        }
    }
    return _device;
}

device有很多屬性可以調整(注意調整device屬性的時候需要上鎖, 調整完再解鎖):
閃光燈

typedef NS_ENUM(NSInteger, AVCaptureFlashMode) {
 AVCaptureFlashModeOff  = 0,
 AVCaptureFlashModeOn   = 1,
 AVCaptureFlashModeAuto = 2
} NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;

前后置攝像頭

typedef NS_ENUM(NSInteger, AVCaptureDevicePosition) {
 AVCaptureDevicePositionUnspecified         = 0,
 AVCaptureDevicePositionBack                = 1,
 AVCaptureDevicePositionFront               = 2
} NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;

手電筒

typedef NS_ENUM(NSInteger, AVCaptureTorchMode) {
 AVCaptureTorchModeOff  = 0,
 AVCaptureTorchModeOn   = 1,
 AVCaptureTorchModeAuto = 2,
} NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;

對焦

typedef NS_ENUM(NSInteger, AVCaptureFocusMode) {
 AVCaptureFocusModeLocked              = 0,
 AVCaptureFocusModeAutoFocus           = 1,
 AVCaptureFocusModeContinuousAutoFocus = 2,
} NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;

曝光

typedef NS_ENUM(NSInteger, AVCaptureExposureMode) {
 AVCaptureExposureModeLocked                            = 0,
 AVCaptureExposureModeAutoExpose                        = 1,
 AVCaptureExposureModeContinuousAutoExposure            = 2,
 AVCaptureExposureModeCustom NS_ENUM_AVAILABLE_IOS(8_0) = 3,
} NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;

白平衡

typedef NS_ENUM(NSInteger, AVCaptureWhiteBalanceMode) {
 AVCaptureWhiteBalanceModeLocked                      = 0,
 AVCaptureWhiteBalanceModeAutoWhiteBalance            = 1,
 AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance  = 2,
} NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;

1.2 獲取硬件的輸入流

-(AVCaptureDeviceInput *)input{
    if (_input == nil) {
        _input = [[AVCaptureDeviceInput alloc] initWithDevice:self.device error:nil];
    }
    return _input;
}

創建輸入流的時候,會彈出alert向用戶獲取相機權限
1.3 需要一個用來協調輸入和輸出數據的會話,然后把input添加到會話中

-(AVCaptureSession *)session{
    if (_session == nil) {
        _session = [[AVCaptureSession alloc] init];
        if ([_session canAddInput:self.input]) {
            [_session addInput:self.input];
        }
    }
    return _session;
}

1.4 然后我們需要一個預覽圖像的層

-(AVCaptureVideoPreviewLayer *)previewLayer{
    if (_previewLayer == nil) {
        _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
        _previewLayer.frame = self.view.layer.bounds;
    }
    return _previewLayer;
}

1.5 最后把previewLayer添加到self.view.layer上

[self.view.layer addSublayer:self.previewLayer];

1.6 找個合適的位置,讓session運行起來,比如viewWillAppear

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    [self.session startRunning];
}
  1. 搞一個按鈕用來控制手電筒
#pragma mark - 手電筒
-(void)openTorch:(UIButton*)button{
    button.selected = !button.selected;
    Class captureDeviceClass = NSClassFromString(@"AVCaptureDevice");
    if (captureDeviceClass != nil) {
        if ([self.device hasTorch] && [self.device hasFlash]){
            [self.device lockForConfiguration:nil];
            if (button.selected) {
                [self.device setTorchMode:AVCaptureTorchModeOn];
            } else {
                [self.device setTorchMode:AVCaptureTorchModeOff];
            }
            [self.device unlockForConfiguration];
        }
    }
}
  1. 再搞一個按鈕來切換前后置攝像頭
#pragma mark - 切換前后攝像頭
-(void)switchCamera{
    NSUInteger cameraCount = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
    if (cameraCount > 1) {
        AVCaptureDevice *newCamera = nil;
        AVCaptureDeviceInput *newInput = nil;
        AVCaptureDevicePosition position = [[self.input device] position];
        if (position == AVCaptureDevicePositionFront){
            newCamera = [self cameraWithPosition:AVCaptureDevicePositionBack];
        }else {
            newCamera = [self cameraWithPosition:AVCaptureDevicePositionFront];
        }
        newInput = [AVCaptureDeviceInput deviceInputWithDevice:newCamera error:nil];
        if (newInput != nil) {
            [self.session beginConfiguration];
            [self.session removeInput:self.input];
            if ([self.session canAddInput:newInput]) {
                [self.session addInput:newInput];
                self.input = newInput;
            }else {
                [self.session addInput:self.input];
            }
            [self.session commitConfiguration];
        }
    }
}
-(AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position{
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for ( AVCaptureDevice *device in devices )
        if ( device.position == position ) return device;
    return nil;
}
  1. 使用AVCaptureStillImageOutput捕獲靜態圖片

4.1 創建一個AVCaptureStillImageOutput對象

-(AVCaptureStillImageOutput *)stillImageOutput{
    if (_stillImageOutput == nil) {
        _stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
    }
    return _stillImageOutput;
}

4.2 將stillImageOutput添加到session中

if ([_session canAddOutput:self.stillImageOutput]) {
       [_session addOutput:self.stillImageOutput];
 }

4.3 搞個拍照按鈕,截取靜態圖片

//AVCaptureStillImageOutput截取靜態圖片,會有快門聲
-(void)screenshot{
    AVCaptureConnection * videoConnection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
    if (!videoConnection) {
        NSLog(@"take photo failed!");
        return;
    }
    
    [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        if (imageDataSampleBuffer == NULL) {
            return;
        }
        NSData * imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
        UIImage *image = [UIImage imageWithData:imageData];
        [self saveImageToPhotoAlbum:image];
    }];
}
  1. 使用AVCaptureVideoOutput實時獲取預覽圖像,這也是樓主的項目需求所在

5.1 創建AVCaptureVideoOutput對象

-(AVCaptureVideoDataOutput *)videoDataOutput{
    if (_videoDataOutput == nil) {
        _videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
        [_videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
    }
    return _videoDataOutput;
}

5.2 將videoDataOutput添加session中

if ([_session canAddOutput:self.videoDataOutput]) {
       [_session addOutput:self.videoDataOutput];
}

5.3 遵守AVCaptureVideoDataOutputSampleBufferDelegate協議,并實現它的代理方法

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
//AVCaptureVideoDataOutput獲取實時圖像,這個代理方法的回調頻率很快,幾乎與手機屏幕的刷新頻率一樣快
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
        largeImage = [self imageFromSampleBuffer:sampleBuffer];
}

5.4 實現imageFromSampleBuffer:方法,將CMSampleBufferRef轉為NSImage

//CMSampleBufferRef轉NSImage
-(UIImage *)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    // 為媒體數據設置一個CMSampleBuffer的Core Video圖像緩存對象
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    // 鎖定pixel buffer的基地址
    CVPixelBufferLockBaseAddress(imageBuffer, 0);
    // 得到pixel buffer的基地址
    void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
    // 得到pixel buffer的行字節數
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
    // 得到pixel buffer的寬和高
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);
    // 創建一個依賴于設備的RGB顏色空間
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    // 用抽樣緩存的數據創建一個位圖格式的圖形上下文(graphics context)對象
    CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    // 根據這個位圖context中的像素數據創建一個Quartz image對象
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);
    // 解鎖pixel buffer
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);
    // 釋放context和顏色空間
    CGContextRelease(context); CGColorSpaceRelease(colorSpace);
    // 用Quartz image創建一個UIImage對象image
    UIImage *image = [UIImage imageWithCGImage:quartzImage];
    // 釋放Quartz image對象
    CGImageRelease(quartzImage);
    return (image);
}

眼看大功告成,結果一運行,創建core graphic上下文的時候報錯:

CGBitmapContextCreate: invalid data bytes/row: should be at least 7680 for 8 integer bits/component, 3 components, kCGImageAlphaPremultipliedFirst.**
CGBitmapContextCreateImage: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.**

又是一通Google,發現stackoverflow上很多這種問答,樓主英語不好,又是一通翻譯,發現大家都是在說像素組件位數什么的,摸索半天找到解決辦法,設置videoDataOutput的像素格式:

[_videoDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];

再運行,還有問題,獲取到的圖片是顛倒的,尼瑪,真是多災多難,不過這個簡單,很快找到解決方法,設置一下視頻的方向:

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
//AVCaptureVideoDataOutput獲取實時圖像,這個代理方法的回調頻率很快,幾乎與手機屏幕的刷新頻率一樣快
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
    [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
    largeImage = [self imageFromSampleBuffer:sampleBuffer];
}

5.5 還記得樓主一開始的需求嗎,設定攝像頭的尺寸為1080x1920,然后從中間截取出512x512的圖片傳給第三方SDK做進一步業務處理:

[_session setSessionPreset:AVCaptureSessionPreset1920x1080];
smallImage = [largeImage imageCompressTargetSize:CGSizeMake(512.0f, 512.0f)];

到這里為止,樓主的需求就大功告成啦

  1. 使用AVCaptureMetadataOutput識別二維碼

6.1 創建AVCaptureMetadataOutput對象

-(AVCaptureMetadataOutput *)metadataOutput{
    if (_metadataOutput == nil) {
        _metadataOutput = [[AVCaptureMetadataOutput alloc]init];
        [_metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
        //設置掃描區域
        _metadataOutput.rectOfInterest = self.view.bounds;
    }
    return _metadataOutput;
}

6.2 將metadataOutput添加到session中,并且設置掃描類型

if ([_session canAddOutput:self.metadataOutput]) {
       [_session addOutput:self.metadataOutput];
       //設置掃碼格式
       self.metadataOutput.metadataObjectTypes = @[
                                                   AVMetadataObjectTypeQRCode,
                                                   AVMetadataObjectTypeEAN13Code,
                                                   AVMetadataObjectTypeEAN8Code,
                                                   AVMetadataObjectTypeCode128Code
                                                   ];
}

6.3 遵守AVCaptureMetadataOutputObjectsDelegate協議,并實現其代理方法

#pragma mark - AVCaptureMetadataOutputObjectsDelegate
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects.count>0) {
        [self.session stopRunning];
        AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex :0];
        NSLog(@"二維碼內容 : %@",metadataObject.stringValue);
    }
}
  1. 關于人臉識別

人臉識別也是基于AVCaptureMetadataOutput實現的,跟二維碼識別的區別在于,掃描類型:

self.metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeFace];
#pragma mark - AVCaptureMetadataOutputObjectsDelegate
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects.count>0) {
        [self.session stopRunning];
        AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex :0];
        if (metadataObject.type == AVMetadataObjectTypeFace) {
            AVMetadataObject *objec = [self.previewLayer transformedMetadataObjectForMetadataObject:metadataObject];
            NSLog(@"%@",objec);
        }
    }
}

至于怎么利用它來實現具體的功能需求,樓主也很方哈,這里有個鏈接可以參考一下:基于 OpenCV 的人臉識別


好啦,就這么多了,代碼在這里,水平有限,有不對的地方還請多多指教
參考資料:iOS 上的相機捕捉

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容