AVFoundation 高級捕捉功能

1. 視頻縮放

iOS 7.0 爲 AVCaptureDevice 提供了一個 videoZoomFactor 屬性用於對視頻輸出和捕捉提供縮放效果,這個屬性的最小值爲 1.0,最大值由下面的方法提供git

self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor;
複製代碼

於是判斷一個設備可否進行縮放也能夠經過判斷這一屬性來獲知數組

- (BOOL)cameraSupportsZoom
{
    return self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor > 1.0f;
}
複製代碼

設備執行縮放效果是經過居中裁剪由攝像頭傳感器捕捉到的圖片實現的,也能夠經過 videoZoomFactorUpscaleThreshold 來設置具體的放大中心。當 zoom factors 縮放因子比較小的時候,裁剪的圖片恰好等於或者大於輸出尺寸(考慮與抗邊緣畸變有關),則無需放大就能夠返回。可是當 zoom factors 比較大時,設備必須縮放裁剪圖片以符合輸出尺寸,從而致使圖片質量上的丟失。具體的臨界點由 videoZoomFactorUpscaleThreshold 值來肯定。bash

// 在 iphone6s 和 iphone8plus 上測試獲得此值爲 2.0左右
self.cameraHelper.activeVideoDevice.activeFormat.videoZoomFactorUpscaleThreshold;
複製代碼

能夠經過一個變化值從 0.0 到 1.0 的 UISlider 來實現對縮放值的控制。框架

{
    [self.slider addTarget:self action:@selector(sliderValueChange:) forControlEvents:UIControlEventValueChanged];
}

- (void)sliderValueChange:(id)sender
{
    UISlider *slider = (UISlider *)sender;
    [self setZoomValue:slider.value];
}

- (CGFloat)maxZoomFactor
{
    return MIN(self.cameraHelper.activeVideoDevice.activeFormat.videoMaxZoomFactor, 4.0f);
}

- (void)setZoomValue:(CGFloat)zoomValue
{
    if (!self.cameraHelper.activeVideoDevice.isRampingVideoZoom) {
        NSError *error;
        if ([self.cameraHelper.activeVideoDevice lockForConfiguration:&error]) {
            CGFloat zoomFactor = pow([self maxZoomFactor], zoomValue);
            self.cameraHelper.activeVideoDevice.videoZoomFactor = zoomFactor;
            [self.cameraHelper.activeVideoDevice unlockForConfiguration];
        }
    }
}    
複製代碼

首先注意在進行配置屬性前須要進行設備的鎖定,不然會引起異常。其次,插值縮放是一個指數形式的增加,傳入的 slider 值是線性的,須要進行一次 pow 運算獲得須要縮放的值。另外,videoMaxZoomFactor 的值可能會很是大,在 iphone8p 上這一個值是 16,縮放到這麼大的圖像是沒有太大意義的,所以須要人爲設置一個最大縮放值,這裏選擇 4.0。iphone

固然這裏進行的縮放是當即生效的,下面的方法能夠以一個速度平滑縮放到一個縮放因子上ide

- (void)rampZoomToValue:(CGFloat)zoomValue {
    CGFloat zoomFactor = pow([self maxZoomFactor], zoomValue);
	NSError *error;
	if ([self.activeCamera lockForConfiguration:&error]) {
		[self.activeCamera rampToVideoZoomFactor:zoomFactor
                                        withRate:THZoomRate];
		[self.activeCamera unlockForConfiguration];
	} else {
	}
}

- (void)cancelZoom {
	NSError *error;
	if ([self.activeCamera lockForConfiguration:&error]) {
		[self.activeCamera cancelVideoZoomRamp];
		[self.activeCamera unlockForConfiguration];
	} else {
	}
}
複製代碼

監聽設備的 videoZoomFactor 能夠獲知當前的縮放值函數

[RACObserve(self, activeVideoDevice.videoZoomFactor) subscribeNext:^(id x) {
        NSLog(@"videoZoomFactor: %f", self.activeVideoDevice.videoZoomFactor);
    }];
複製代碼

監聽設備的 rampingVideoZoom 能夠獲知設備是否正在平滑縮放測試

[RACObserve(self, activeVideoDevice.rampingVideoZoom) subscribeNext:^(id x) {
        NSLog(@"rampingVideoZoom : %@", (self.activeVideoDevice.rampingVideoZoom)?@"true":@"false");
    }];
複製代碼

2. 人臉識別

人臉識別須要用到 AVCaptureMetadataOutput 做爲輸出,首先將其加入到捕捉會話中ui

self.metaDataOutput = [[AVCaptureMetadataOutput alloc] init];
    if ([self.captureSession canAddOutput:self.metaDataOutput]) {
        [self.captureSession addOutput:self.metaDataOutput];
        NSArray *metaDataObjectType = @[AVMetadataObjectTypeFace];
        self.metaDataOutput.metadataObjectTypes = metaDataObjectType;
        [self.metaDataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    }
複製代碼

能夠看到這裏須要指定 AVCaptureMetadataOutput 的 metadataObjectTypes 屬性,將其設置爲 AVMetadataObjectTypeFace 的數組,它表明着人臉元數據對象。而後設置其遵循 AVCaptureMetadataOutputObjectsDelegate 協議的委託對象及回調線程,當檢測到人臉時就會調用下面的方法編碼

- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    if (self.detectFaces) {
        self.detectFaces(metadataObjects);
    }
}
複製代碼

其中 metadataObjects 是一個包含了許多 AVMetadataObject 對象的數組,這裏則能夠認爲都是 AVMetadataObject 的子類 AVMetadataFaceObject。對於 AVMetadataFaceObject 對象,有四個重要的屬性

  • faceID,用於標識檢測到的每個 face
  • rollAngle,用於標識人臉斜傾角,即人的頭部向肩膀方便的側傾角度
  • yawAngle,偏轉角,即人臉繞 y 軸旋轉的角度
  • bounds,標識檢測到的人臉區域

這裏咱們將其回調給 ViewController,用於進行 UI 展現。

@weakify(self)
        self.cameraHelper.detectFaces = ^(NSArray *faces) {
            @strongify(self)
            NSMutableArray *transformedFaces = [NSMutableArray array];
            for (AVMetadataFaceObject *face in faces) {
                AVMetadataObject *transformedFace = [self.previewLayer transformedMetadataObjectForMetadataObject:face];
                [transformedFaces addObject:transformedFace];
            }
            NSMutableArray *lostFaces = [self.faceLayers.allKeys mutableCopy];
            for (AVMetadataFaceObject *face in transformedFaces) {
                NSNumber *faceId = @(face.faceID);
                [lostFaces removeObject:faceId];
                
                CALayer *layer = self.faceLayers[faceId];
                if (!layer) {
                    layer = [CALayer layer];
                    layer.borderWidth = 5.0f;
                    layer.borderColor = [UIColor colorWithRed:0.188 green:0.517 blue:0.877 alpha:1.000].CGColor;
                    [self.previewLayer addSublayer:layer];
                    self.faceLayers[faceId] = layer;
                }
                layer.transform = CATransform3DIdentity;
                layer.frame = face.bounds;
                
                if (face.hasRollAngle) {
                    layer.transform = CATransform3DConcat(layer.transform, [self transformForRollAngle:face.rollAngle]);
                }
                
                if (face.hasYawAngle) {
                    NSLog(@"%f", face.yawAngle);
                    layer.transform = CATransform3DConcat(layer.transform, [self transformForYawAngle:face.yawAngle]);
                }
            }
            
            for (NSNumber *faceID in lostFaces) {
                CALayer *layer = self.faceLayers[faceID];
                [layer removeFromSuperlayer];
                [self.faceLayers removeObjectForKey:faceID];
            }
        };
        
// Rotate around Z-axis
- (CATransform3D)transformForRollAngle:(CGFloat)rollAngleInDegrees {        // 3
    CGFloat rollAngleInRadians = THDegreesToRadians(rollAngleInDegrees);
    return CATransform3DMakeRotation(rollAngleInRadians, 0.0f, 0.0f, 1.0f);
}

// Rotate around Y-axis
- (CATransform3D)transformForYawAngle:(CGFloat)yawAngleInDegrees {          // 5
    CGFloat yawAngleInRadians = THDegreesToRadians(yawAngleInDegrees);
    
    CATransform3D yawTransform = CATransform3DMakeRotation(yawAngleInRadians, 0.0f, -1.0f, 0.0f);
    
    return CATransform3DConcat(yawTransform, [self orientationTransform]);
}

- (CATransform3D)orientationTransform {                                     // 6
    CGFloat angle = 0.0;
    switch ([UIDevice currentDevice].orientation) {
        case UIDeviceOrientationPortraitUpsideDown:
            angle = M_PI;
            break;
        case UIDeviceOrientationLandscapeRight:
            angle = -M_PI / 2.0f;
            break;
        case UIDeviceOrientationLandscapeLeft:
            angle = M_PI / 2.0f;
            break;
        default: // as UIDeviceOrientationPortrait
            angle = 0.0;
            break;
    }
    return CATransform3DMakeRotation(angle, 0.0f, 0.0f, 1.0f);
}

static CGFloat THDegreesToRadians(CGFloat degrees) {
    return degrees * M_PI / 180;
}
複製代碼

咱們用一個字典來管理每個展現一個 face 對象的 layer,它的 key 值即 faceID,回調時更新當前已存在的 faceLayer,移除不須要的 faceLayer。其次對每個 face,根據其 rollAngle 和 yawAngle 要經過 transfor 來變換展現的矩陣。

還要注意一點,transformedMetadataObjectForMetadataObject 方法能夠將設備座標系上的數據轉換到視圖座標系上,設備座標系的範圍是 (0, 0) 到 (1,1)。

3. 機器可讀代碼識別

機器可讀代碼包括一維條碼和二維碼等,AVFoundation 支持多種一維碼和三種二維碼,其中最多見的是 QR 碼,也即二維碼。

掃碼仍然須要用到 AVMetadataObject 對象,首先加入到捕捉會話中。

self.metaDataOutput = [[AVCaptureMetadataOutput alloc] init];
    if ([self.captureSession canAddOutput:self.metaDataOutput]) {
        [self.captureSession addOutput:self.metaDataOutput];
        [self.metaDataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
        NSArray *types = @[AVMetadataObjectTypeQRCode];
        self.metaDataOutput.metadataObjectTypes = types;
    }
複製代碼

而後實現委託方法

- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    [metadataObjects enumerateObjectsUsingBlock:^(__kindof AVMetadataObject * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) {
            NSLog(@"%@", ((AVMetadataMachineReadableCodeObject*)obj).stringValue);
        }
    }];
}
複製代碼

對於一個 AVMetadataMachineReadableCodeObject,有如下三個重要屬性

  • stringValue,用於表示二維碼編碼信息
  • bounds,用於表示二維碼的矩形邊界
  • corners,一個角點字典表示的數組,比 bounds 表示的二維碼區域更精確

因此能夠經過以上屬性,在 UI 界面上對二維碼區域進行高亮展現

首先須要注意,一個從 captureSession 得到的 AVMetadataMachineReadableCodeObject,其座標是設備座標系下的座標,須要進行座標轉換

- (NSArray *)transformedCodesFromCodes:(NSArray *)codes {
    NSMutableArray *transformedCodes = [NSMutableArray array];
    [codes enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        AVMetadataObject *transformedCode = [self.previewLayer transformedMetadataObjectForMetadataObject:obj];
        [transformedCodes addObject:transformedCode];
    }];
    return [transformedCodes copy];
}
複製代碼

其次,對於每個 AVMetadataMachineReadableCodeObject 對象,其 bounds 屬性因爲是 CGRect,因此能夠直接繪製出一個 UIBezierPath 對象

- (UIBezierPath *)bezierPathForBounds:(CGRect)bounds {
    return [UIBezierPath bezierPathWithRect:bounds];
}
複製代碼

而 corners 屬性是一個字典,須要手動生成 CGPoint,而後進行連線操做,生成 UIBezierPath 對象

- (UIBezierPath *)bezierPathForCorners:(NSArray *)corners {
    UIBezierPath *path = [UIBezierPath bezierPath];
    for (int i = 0; i < corners.count; i++) {
        CGPoint point = [self pointForCorner:corners[i]];
        if (i == 0) {
            [path moveToPoint:point];
        } else {
            [path addLineToPoint:point];
        }
    }
    [path closePath];
    return path;
}

- (CGPoint)pointForCorner:(NSDictionary *)corner {
    CGPoint point;
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)corner, &point);
    return point;
}
複製代碼

corners 字典的形式大體以下所示,能夠調用 CGPointMakeWithDictionaryRepresentation 便捷函數將其轉換爲 CGPoint 形式。

{
    X = "336.9957633633747";
    Y = "265.7881843381643";
}
複製代碼

通常來講一個 corners 裏會包含 4 個 corner 字典。

獲取到每個 code 對應的兩個 UIBezierPath 對象後,就能夠在視圖上添加相應的 CALayer 來顯示高亮區域了。

4. 使用高幀率捕捉

高幀率捕獲視頻是在 iOS 7 之後加入的,具備更逼真的效果和更好的清晰度,對於細節的增強和動做流暢度的提高很是明顯,尤爲是錄製快速移動的內容時更爲明顯,也能夠實現高質量的慢動做視頻效果。

實現高幀率捕捉的基本思路是,經過設備的 formats 屬性獲取全部支持的格式,也就是 AVCaptureDeviceFormat 對象;而後根據對象的 videoSupportedFrameRateRanges 屬性,能夠獲知其所支持的最小幀率、最大幀率及時長信息;而後手動設置設備的格式和幀時長。

首先寫一個 AVCaptureDevice 的 category,獲取支持格式的最大幀率的方法以下

AVCaptureDeviceFormat *maxFormat = nil;
    AVFrameRateRange *maxFrameRateRange = nil;
    for (AVCaptureDeviceFormat *format in self.formats) {
        FourCharCode codecType = CMVideoFormatDescriptionGetCodecType(format.formatDescription);
        if (codecType == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
            NSArray *frameRateRanges = format.videoSupportedFrameRateRanges;
            for (AVFrameRateRange *range in frameRateRanges) {
                if (range.maxFrameRate > maxFrameRateRange.maxFrameRate) {
                    maxFormat = format;
                    maxFrameRateRange = range;
                }
            }
        } else {
        }
    }
複製代碼

codecType 是一個無符號32位的數據類型,可是是由四個字符對應的四個字節組成,通常可能值爲 "420v" 或 "420f",這裏選取 420v 格式來配置。

能夠經過判斷最大幀率是否大於 30,來判斷設備是否支持高幀率

- (BOOL)isHighFrameRate {
    return self.frameRateRange.maxFrameRate > 30.0f;
}
複製代碼

而後就能夠進行配置了

if ([self hasMediaType:AVMediaTypeVideo] && [self lockForConfiguration:error] && [self.activeCamera supportsHighFrameRateCapture]) {
        CMTime minFrameDuration = self.frameRateRange.minFrameDuration;
        self.activeFormat = self.format;
        self.activeVideoMinFrameDuration = minFrameDuration;
        self.activeVideoMaxFrameDuration = minFrameDuration;
        [self unlockForConfiguration];
    }
複製代碼

這裏首先鎖定了設備,而後將最小幀時長和最大幀時長都設置成 minFrameDuration,幀時長與幀率是倒數關係,因此最大幀率對應最小幀時長。

播放時能夠針對 AVPlayer 設置不一樣的 rate 實現變速播放,在 iphone8plus 上實測,若是 rate 在 0 到 0.5 之間, 則實際播放速率仍爲 0.5。

另外要注意設置 AVPlayerItem 的 audioTimePitchAlgorithm 屬性,這個屬性容許你指定當視頻正在各類幀率下播放的時候如何播放音頻

  • AVAudioTimePitchAlgorithmLowQualityZeroLatency 質量低,適合快進,快退或低質量語音
  • AVAudioTimePitchAlgoruthmTimeDomain 質量適中,計算成本較低,適合語音
  • AVAudioTimePitchAlgorithmSpectral 最高質量,最昂貴的計算,保留了原來的項目間距
  • AVAudioTimePitchAlgorithmVarispeed 高品質的播放沒有音高校訂

一般選擇 AVAudioTimePitchAlgorithmSpectral 或 AVAudioTimePitchAlgoruthmTimeDomain 便可。

5. 視頻處理

AVCaptureMovieFileOutput 能夠簡單地捕捉視頻,可是不能進行視頻數據交互,所以須要使用 AVCaptureVideoDataOutput 類。AVCaptureVideoDataOutput 是一個 AVCaptureOutput 的子類,能夠直接訪問攝像頭傳感器捕捉到的視頻幀。與之對應的是處理音頻輸入的 AVCaptureAudioDataOutput 類。

AVCaptureVideoDataOutput 有一個遵循 AVCaptureVideoDataOutputSampleBufferDelegate 協議的委託對象,它有下面兩個主要方法

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; // 有新的視頻幀寫入時調用,數據會基於 output 的 videoSetting 進行解碼或從新編碼
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; // 有遲到的視頻幀被丟棄時調用,一般是由於在上面一個方法裏進行了比較耗時的操做
複製代碼

5.1 CMSampleBufferRef

CMSampleBufferRef 是一個由 Core Media 框架提供的 Core Foundation 風格的對象,用於在媒體管道中傳輸數字樣本。

5.1.1 樣本數據

能夠對 CMSampleBufferRef 的每個 Core Video 視頻幀進行處理

int BYTES_PER_PIXEL = 4;
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); //CVPixelBufferRef 在主內存中保存像素數據
    CVPixelBufferLockBaseAddress(pixelBuffer, 0); // 獲取相應內存塊的鎖
    size_t bufferWidth = CVPixelBufferGetWidth(pixelBuffer);
    size_t bufferHeight = CVPixelBufferGetHeight(pixelBuffer);// 獲取像素寬高
    unsigned char *pixel = (unsigned char *)CVPixelBufferGetBaseAddress(pixelBuffer); // 獲取像素 buffer 的起始位置
    unsigned char grayPixel;
    for (int row = 0; row < bufferHeight; row++) {
        for (int column = 0; column < bufferWidth; column ++) { // 遍歷每個像素點
            grayPixel = (pixel[0] + pixel[1] + pixel[2])/3.0;
            pixel[0] = pixel[1] = pixel[2] = grayPixel;
            pixel += BYTES_PER_PIXEL;
        }
    }
    CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer]; // 經過 buffer 生成對應的 CIImage
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); // 解除鎖
複製代碼

5.1.2 格式描述

CMSampleBufferRef 還提供了每一幀數據的格式信息,CMFormatDescription.h 頭文件定義了大量函數來獲取各類信息。

CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer);
    CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDescription);
複製代碼

5.1.3 時間信息

CMTime presentation = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); // 獲取幀樣本的原始時間戳
    CMTime decode = CMSampleBufferGetDecodeTimeStamp(sampleBuffer); // 獲取幀樣本的解碼時間戳
複製代碼

5.1.4 附加元數據

CFDictionaryRef exif = (CFDictionaryRef)CMGetAttachment(sampleBuffer, kCGImagePropertyExifDictionary, NULL);
複製代碼

CMAttachment.h 定義了 CMAttachment 形式的元數據協議,能夠獲取每一幀的底層元數據,如上述獲取到圖片 Exif 格式的元數據以下

{
    ApertureValue = "1.6959938131099";
    BrightnessValue = "-8.636618904801434";
    ColorSpace = 1;
    DateTimeDigitized = "2018:04:24 14:17:33";
    DateTimeOriginal = "2018:04:24 14:17:33";
    ExposureBiasValue = 0;
    ExposureTime = "0.05882352941176471";
    FNumber = "1.8";
    Flash = 0;
    FocalLenIn35mmFilm = 28;
    FocalLength = "3.99";
    ISOSpeedRatings =     (
        2000
    );
    LensMake = Apple;
    LensModel = "iPhone 8 Plus back camera 3.99mm f/1.8";
    LensSpecification =     (
        "3.99",
        "3.99",
        "1.8",
        "1.8"
    );
    MeteringMode = 5;
    PixelXDimension = 1440;
    PixelYDimension = 1080;
    SceneType = 1;
    SensingMethod = 2;
    ShutterSpeedValue = "4.0608667208218";
    SubsecTimeDigitized = 067;
    SubsecTimeOriginal = 067;
    WhiteBalance = 0;
}
複製代碼

5.2 AVCaptureVideoDataOutput

AVCaptureVideoDataOutput 的配置與 AVCaptureMovieFileOutput 大體相同,但要指明它的委託對象和回調隊列。

self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    self.videoDataOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}; // 攝像頭的初始格式爲雙平面 420v,這是一個 YUV 格式,而 OpenGL ES 經常使用 BGRA 格式
    if ([self.captureSession canAddOutput:self.videoDataOutput]) {
        [self.captureSession addOutput:self.videoDataOutput];
        [self.videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
    }
複製代碼

爲了確保視頻幀按順序傳遞,因此這裏的隊列要求必須是串行隊列。

相關文章
相關標籤/搜索