Camera - ZhiJianShuSheng/Read-And-Learn GitHub Wiki

相机原理

感光度(ISO)

ISO也叫胶卷速度,衡量图像传感器对光的敏感程度。亮的时候需要的ISO就越小,暗的时候相反ISO就高噪点会多。

光圈

光圈用来衡量到达图像感应器的光所通过的通孔大小。iPhone6的光圈值是f/2.2,2.2就是表示镜头焦距和光圈的有效直径比例。

对焦

将离相机一定范围内物体渲染清晰,太近太远会模糊这种情况叫做失焦。

保存获取文件

关于Core Image处理RAW格式可以参考CoreImage/CIRAWFilter.h,以及WWDC上的讲座WWDC 2014 session 514 https://developer.apple.com/videos/wwdc/2014/#514

如何处理图像数据

处理位图的类是UIImage,CGImage(Core Graphics)和CIImage(Core Image)。从NSData得到UIImage使用imageWithContentsOfFiles:方法。

从相机捕捉图像

  • 相比较UIImagePickerController使用AVFoundation能够直接访问相机,提供完全的操作权,比如用编程方式更改硬件参数,或者操纵实时预览图。
  • 先创建AVCaptureStillImageOutput对象,使用captureStillImageAsynchronouslyFromConnection: completionHandler: 方法。
  • 使用AVCaptureStillImageOutput中的类方法jpegStillImageNSDataRepresentation:将其转换成NSData对象,接着用imageWithData:得到一个UIImage
  • 过程中可以调节很多参数例如曝光,聚焦,光补偿,闪光灯,ISO等。所有这些设置都会被应用到一个AVCaptureDevice里。

AVFoundation相关类

可以通过以下类访问来自相机设备原始数据并控制他们的组件。

  • AVCaptureDevice:控制硬件特性,比如镜头位置,曝光,闪光灯
  • AVCaptureDeviceInput:设备数据
  • AVCaptureOutput:抽象类包括了三种静态图片捕捉类
    • AVCaptureStillImageOutput:用于捕捉静态图片
    • AVCaptureMetadataOutput:启用检测人脸和二维码
    • AVCaptureVideoOutput:实时预览图提供原始帧
  • AVCaptureSession:管理输入输出之间数据流,以及在出现问题时生成运行时错误。
  • AVCaptureVideoPreviewLayer:CALayer的子类,用于自动显示相机产生的实时图像,

捕捉设置

//先需要一个AVCaptureSession对象
let session = AVCaptureSession()

//接着遍历所能提供视频数据设备并检查position属性。
let availableCameraDevices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo)
for device in availableCameraDevices as [AVCaptureDevice] {
     if device.position == .Back {
          backCameraDevice = device
     }
     else if device.position == .Front {
          frontCameraDevice = device
     }
}

//发现合适的相机设备,就可以获得相关的AVCaptureDeviceInput对象。将它设置为session输入:
var error:NSError?

let possibleCameraInput: AnyObject? = AVCaptureDeviceInput.deviceInputWithDevice(backCameraDevice, error: &error)
if let backCameraInput = possibleCameraInput as? AVCaptureDeviceInput {
     if self.session.canAddInput(backCameraInput) {
          self.session.addInput(backCameraInput)
     }
}
//第一次调用AVCaptureDeviceInput.deviceInputWithDevice()会出发系统提示,向用户请求访问相机。
let authorizationStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
switch authorizationStatus {
case .NotDetermined:
     // 许可对话没有出现,发起授权许可
     AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo,
          completionHandler: { (granted:Bool) -> Void in
          if granted {
               // 继续
          }
          else {
               // 用户拒绝,无法继续
          }
     })
case .Authorized:
     // 继续
case .Denied, .Restricted:
     // 用户明确地拒绝授权,或者相机设备无法访问
}

//允许访问后的处理,有两种方法显示来自相机的图像流。
//第一种,生成一个带有AVCaptureVideoPreviewLayer的view。用capture session做初始参数。AVCaptureVideoPreviewLayer会自动的显示来自相机输出
previewLayer = AVCaptureVideoPreviewLayer.layerWithSession(session) as AVCaptureVideoPreviewLayer
previewLayer.frame = view.bounds
view.layer.addSublayer(previewLayer)

//第二种,从输出数据流捕捉单一图像帧,使用OpenGL手动显示在view上。这种方法适用于想实时预览滤镜效果的情况。
glContext = EAGLContext(API: .OpenGLES2) //
glView = GLKView(frame: viewFrame, context: glContext)
ciContext = CIContext(EAGLContext: glContext)
//AVCaptureVideoOutput
videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: dispatch_queue_create("sample buffer delegate", DISPATCH_QUEUE_SERIAL))
if session.canAddOutput(self.videoOutput) {
session.addOutput(self.videoOutput)
}

//创建完AVCaptureVideoDataOutput后需要通过代理captureOutput(_:didOutputSampleBuffer:fromConnection:)获得所有图像帧,将他们绘制在GLKView中。
func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {
     let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
     let image = CIImage(CVPixelBuffer: pixelBuffer)
     if glContext != EAGLContext.currentContext() {
          EAGLContext.setCurrentContext(glContext)
     }
     glView.bindDrawable()
     ciContext.drawImage(image, inRect:image.extent(), fromRect: image.extent())
     glView.display()
}
//注意的问题, AVCaptureVideoPreviewLayer会自动处理样本旋转90度的情况。

//使用AVCaptureStillImageOutput捕捉静态图片。
stillCameraOutput = AVCaptureStillImageOutput()
if self.session.canAddOutput(self.stillCameraOutput) {
     self.session.addOutput(self.stillCameraOutput)
}

//配置,为对象找最合适的配置。有两个方法,简单的方法是使用session preset
session.sessionPreset = AVCaptureSessionPresetPhoto //AVCaptureSessionPresetPhoto会为照片捕捉选择最合适的配置,允许使用最高的感光度(ISO)和曝光时间。基于相位检测(phase detection)的自动对焦,以及输出全分辨率的JPEG格式压缩的静态图片。使用AVCaptureDeviceFormat这个类可以有更多的操控,比如静态分辨率,视频预览分辨率,自动对焦类型,感光度和曝光时间限制,支持格式都在AVCaptureDevice.formats中,可以赋值给AVCaptureDevice的activeFormat。

操作相机

到了iOS8后可以对所有参数进行手动调整了。包括镜头光圈。

//先启动相机
sessionQueue = dispatch_queue_create("com.example.camera.capture_session", DISPATCH_QUEUE_SERIAL)
dispatch_async(sessionQueue) { () -> Void in
     self.session.startRunning()
}

//建议将所有block调用操作和配置分配到后台串行队列中。在相机设备改变一些参数前先锁定,改完再解锁
var error:NSError?
if currentDevice.lockForConfiguration(&error) {
     // 锁定成功,继续配置
     // currentDevice.unlockForConfiguration()
}
else {
     // 出错,相机可能已经被锁
}

对焦

AVCaptureFocusMode枚举描述可用对焦模式

  • locked:镜片固定位置
  • AutoFocus:先自动对焦一次就处于Locked模式
  • ContinuousAutoFocus:场景改变自动重新对焦到画面中心点

对焦可以使用UISlider设置,类似单反对焦环。手动对焦有个辅助标识指向清晰区域,可以通过对焦峰值(focus peaking)将对焦区域高亮显示的方式,方法是使用阈值边缘(threshold edge)滤镜,自定义CIFilter或GPUImageThresholdEdgeDetectionFilter,并调用AVCaptureAudioDataOutputSampleBufferDelegate下的captureOutput(_:didOutputSampleBuffer:fromConnection:)方法将它覆盖到实时预览图上。

//设置对焦模式需要在锁定后
let focusMode:AVCaptureFocusMode = ...
if currentCameraDevice.isFocusModeSupported(focusMode) {
     ... // 锁定以进行配置
     currentCameraDevice.focusMode = focusMode
     ... // 解锁
}

//AutoFocus会让屏幕中心为清晰区,也可以通过预览图上点击手势改变这个区域。
var pointInPreview = focusTapGR.locationInView(focusTapGR.view)
var pointInCamera = previewLayer.captureDevicePointOfInterestForPoint(pointInPreview)
...// 锁定,配置
// 设置感兴趣的点
currentCameraDevice.focusPointOfInterest = pointInCamera
// 在设置的点上切换成自动对焦
currentCameraDevice.focusMode = .AutoFocus
...// 解锁

//iOS8中可以通过选项移动镜片的位置
... // 锁定,配置
var lensPosition:Float = ... // 0.0 到 1.0的float
currentCameraDevice.setFocusModeLockedWithLensPosition(lensPosition) {
     (timestamp:CMTime) -> Void in
     // timestamp 对应于应用了镜片位置的第一张图像缓存区
}
... // 解锁

曝光

曝光档数范围在minExposureTargetBias和maxExposureTargetBias之间。0为默认没有补偿

var exposureBias:Float = ... // 在 minExposureTargetBias 和 maxExposureTargetBias 之间的值
... // 锁定,配置
currentDevice.setExposureTargetBias(exposureBias) { (time:CMTime) -> Void in
}
... // 解锁

//设置ISO和曝光时间
var activeFormat = currentDevice.activeFormat
var duration:CTime = ... //在activeFormat.minExposureDuration 和 activeFormat.maxExposureDuration 之间的值,或用 AVCaptureExposureDurationCurrent 表示不变
var iso:Float = ... // 在 activeFormat.minISO 和 activeFormat.maxISO 之间的值,或用 AVCaptureISOCurrent 表示不变
... // 锁定,配置
currentDevice.setExposureModeCustomWithDuration(duration, ISO: iso) { (time:CMTime) -> Void in
}
... // 解锁

//检验曝光是否准确可以通过KVO观察AVCaptureDevice的exposureTargetOffset属性。

白平衡

iOS8可以手动控制白平衡。可以通过开尔文所表示的温度来调节色温和色彩。典型色温值在2000-3000K(类似蜡烛或灯泡的暖光源)到8000K(纯净的蓝色天空)之间。色彩范围从最小的-150(偏绿)到150(偏品红)。

var incandescentLightCompensation = 3_000
var tint = 0 // 不调节
let temperatureAndTintValues = AVCaptureWhiteBalanceTemperatureAndTintValues(temperature: incandescentLightCompensation, tint: tint)
var deviceGains = currentCameraDevice.deviceWhiteBalanceGainsForTemperatureAndTintValues(temperatureAndTintValues)
... // 锁定,配置
currentCameraDevice.setWhiteBalanceModeLockedWithDeviceWhiteBalanceGains(deviceGains) {
     (timestamp:CMTime) -> Void in
     }
}
... // 解锁

实时人脸检测

使用AVCaptureMetadataOutput可以检测人脸和二维码。

var metadataOutput = AVCaptureMetadataOutput()
metadataOutput.setMetadataObjectsDelegate(self, queue: self.sessionQueue)
if session.canAddOutput(metadataOutput) {
     session.addOutput(metadataOutput)
}
metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeFace]

func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
     for metadataObject in metadataObjects as [AVMetadataObject] {
          if metadataObject.type == AVMetadataObjectTypeFace {
               var transformedMetadataObject = previewLayer.transformedMetadataObjectForMetadataObject(metadataObject)
          }
     }
}

捕捉静态图片

捕捉高分辨率图像调用captureStillImageAsynchronouslyFromConnection(connection, completionHandler)。在视觉上反应图片捕捉何时开始以及何时结束可以使用KVO来观察AVCaptureStillImageOutput的isCapturingStillImage属性。

dispatch_async(sessionQueue) { () -> Void in

     let connection = self.stillCameraOutput.connectionWithMediaType(AVMediaTypeVideo)

     // 将视频的旋转与设备同步
     connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIDevice.currentDevice().orientation.rawValue)!

     self.stillCameraOutput.captureStillImageAsynchronouslyFromConnection(connection) {
          (imageDataSampleBuffer, error) -> Void in

          if error == nil {

               // 如果使用 session .Photo 预设,或者在设备输出设置中明确进行了设置
               // 我们就能获得已经压缩为JPEG的数据

               let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataSampleBuffer)

               // 样本缓冲区也包含元数据,我们甚至可以按需修改它

               let metadata:NSDictionary = CMCopyDictionaryOfAttachments(nil, imageDataSampleBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate)).takeUnretainedValue()

               if let image = UIImage(data: imageData) {
                    // 保存图片,或者做些其他想做的事情
                    ...
               }
          }
          else {
               NSLog("error while capturing still image: \(error)")
          }
     }
}

分级捕捉

通过设定-1,0,-1三个不同曝光档数用HDR算法合并成一张。

dispatch_async(sessionQueue) { () -> Void in
     let connection = self.stillCameraOutput.connectionWithMediaType(AVMediaTypeVideo)
     connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIDevice.currentDevice().orientation.rawValue)!

     var settings = [-1.0, 0.0, 1.0].map {
(bias:Float) -> AVCaptureAutoExposureBracketedStillImageSettings in

          AVCaptureAutoExposureBracketedStillImageSettings.autoExposureSettingsWithExposureTargetBias(bias)
     }

     var counter = settings.count

     self.stillCameraOutput.captureStillImageBracketAsynchronouslyFromConnection(connection, withSettingsArray: settings) {
(sampleBuffer, settings, error) -> Void in

          ...
          // 保存 sampleBuffer(s)

          // 当计数为0,捕捉完成
          counter--
     }
}

操作图像

//将两个图片拼接,并在图片上加个区域强调
-(UIImage*)composeStereogramLeft:(UIImage *)leftImage right:(UIImage *)rightImage
{
     float w = leftImage.size.width;
     float h = leftImage.size.height;
     UIGraphicsBeginImageContext(CGSizeMake(w * 2.0, h + 32.0));
     [leftImage drawAtPoint:CGPointMake(0.0, 32.0)];
     [rightImage drawAtPoint:CGPointMake(w, 32.0)];
     float leftCircleX = (w / 2.0) - 8.0;
     float rightCircleX = leftCircleX + w;
     float circleY = 8.0;
     [[UIColor blackColor] setFill];
     UIRectFill(CGRectMake(0.0, 0.0, w * 2.0, 32.0));

     [[UIColor whiteColor] setFill];
     CGRect leftRect = CGRectMake(leftCircleX, circleY, 16.0, 16.0);
     CGRect rightRect = CGRectMake(rightCircleX, circleY, 16.0, 16.0);
     UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:leftRect];
     [path appendPath:[UIBezierPath bezierPathWithOvalInRect:rightRect]];
     [path fill];
     UIImage *savedImg = UIGraphicsGetImageFromCurrentImageContext();
     UIGraphicsEndImageContext();
     return savedImg;
}

//使用CGBitmapContextCreate创建位图绘制环境,然后可以遍历位图获得各个颜色通道值,维持一张图片中的绿色和蓝色的原值,然后将蓝色和绿色的值按一定方式计算后赋值给另一张图片的红色值。
//可以访问实际像素的所有信息,做任何处理,不过Core Image也会提供一些滤镜方便使用。
UInt8 *rightPtr = rightBitmap;
UInt8 *leftPtr = leftBitmap;
UInt8 r1, g1, b1;
UInt8 r2, g2, b2;
UInt8 ra, ga, ba;

for (NSUInteger idx = 0; idx < bitmapByteCount; idx += 4) {
     r1 = rightPtr[0]; g1 = rightPtr[1]; b1 = rightPtr[2];
     r2 = leftPtr[0]; g2 = leftPtr[1]; b2 = leftPtr[2];

     // r1/g1/b1 右侧图像,用于计算合并值
     // r2/g2/b2 左侧图像,用于被合并值赋值
     // ra/ga/ba 合并后的像素

     ra = 0.7 * g1 + 0.3 * b1;
     ga = b2;
     ba = b2;

     leftPtr[0] = ra;
     leftPtr[1] = ga;
     leftPtr[2] = ba;
     rightPtr += 4; // 指向下一个像素 (4字节, 包括透明度 alpha 值)
     leftPtr += 4;
}

CGImageRef composedImage = CGBitmapContextCreateImage(_leftContext);
UIImage *retval = [UIImage imageWithCGImage:composedImage];
CGImageRelease(composedImage);
return retval;

元数据

存储图像信息的标准格式是Exif(可交换图像文件格式)。通常会保存照相时间日期,快门速度和光圈,设备支持还会包括GPS坐标。可以使用CGImageSourceCopyPropertiesAtIndex方法访问Exif信息。