0%

对焦和曝光功能的实现

iOS设备上大多数后置摄像头都支持基于给定兴趣点设置对焦和曝光数据.在界面上的直接体现就是点击视频预览界面的某个位置,就会在这个点自动对焦和曝光.本文将实现点击对焦和点击曝光功能.

Kcamera的代码可以在https://github.com/changjianfeishui/Kcamera上找到.

点击对焦功能的实现

坐标空间转换

在iOS设备中,4寸屏的屏幕坐标左上角为(0,0),垂直模式下右下角坐标为(320,568),水平模式下右下角坐标为(568,320).而捕捉设备坐标系则不同,设备左上角为(0,0),右下角为(1,1),并且不支持水平方向旋转.

在PreView.swift中增加如下方法:

//将屏幕坐标系上的触控点转换为摄像头设备坐标系上的点
func captureDevicePointForPoint(point:CGPoint) -> CGPoint{
    let layer = self.layer as! AVCaptureVideoPreviewLayer
    return layer.captureDevicePointOfInterestForPoint(point)
}

添加点击事件

由于对焦和曝光功能涉及到屏幕坐标系和摄像头设备坐标系之间的转换,而转换功能是在PreView的AVCaptureVideoPreviewLayer上进行的.所以我们最好将对焦和曝光的触发事件定义在PreView中.

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.setupView()
}

func setupView() -> Void {
    //1. 添加对焦点击事件
    let singleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleSingleTap(_:)))
    self.addGestureRecognizer(singleTapRecognizer)
}

//对焦功能的触发
func handleSingleTap(tap:UITapGestureRecognizer) -> Void {
    print("对焦")
}    

如果允许程序的话会发现,这时是无法触发点击手势的.因为PreView上层还有一层ControlView.所以还要修改ControlView中UI事件的传递.在ControlView中增加下面的方法:

//屏蔽除了statusView和modeView区域外的点击事件
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    if self.statusView.pointInside(self.convertPoint(point, toView: self.statusView), withEvent: event) || self.modeView.pointInside(self.convertPoint(point, toView: self.modeView), withEvent: event){
        return true
    }
    return false
}

重新编译运行程序,PreView可以响应手势了.

实现对焦框UI

在PreView中添加一个属性:

//对焦框UI
lazy var focusBox:UIView = {
    let view = UIView(frame: BOX_BOUNDS)
    view.backgroundColor = UIColor.clearColor()
    view.layer.borderColor = UIColor(colorLiteralRed: 0.102, green: 0.636, blue: 1.000, alpha: 1.000).CGColor
    view.layer.borderWidth = 5.0
    view.hidden = true
    return view
}()

在setupView()方法最后:

//2. 添加对焦框矩形UI
self.addSubview(self.focusBox)    

完善之前的handleSingleTap()方法:

//对焦功能的实现
func handleSingleTap(tap:UITapGestureRecognizer) -> Void {
    //1. 点击坐标
    let point = tap.locationInView(self)
    //2. 动画
    self.runBoxAnimationOnView(self.focusBox, point: point)
}

其中runBoxAnimationOnView()实现如下:

//矩形框缩放动画
func runBoxAnimationOnView(view:UIView,point:CGPoint) -> Void {
    view.center = point
    view.hidden = false
    UIView.animateWithDuration(0.15, delay: 0, options: .CurveEaseInOut, animations: { 
        view.layer.transform = CATransform3DMakeScale(0.5, 0.5, 1)
        }) { (_) in
            let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC)))
            dispatch_after(time, dispatch_get_main_queue(), { 
                view.hidden = true
                view.layer.transform = CATransform3DIdentity
            })
            
            
    }
}

实现对焦功能

目前只是实现了对焦功能的UI部分,并没有实现具体的对焦功能.由于所有关于摄像头的操作都是在CaptureModel中完成的,所以事件传递需要按照View->ViewController->Model的顺序来进行传递.

在PreView.swift中类外部增加如下协议:

protocol PreViewDelegate {
    //兴趣点对焦
    func focuxAtPoint(point:CGPoint)->Void
}            

在PreView类中增加一个属性:

var delegate:PreViewDelegate?

并在handleSingleTap()方法中增加一行:

    //3. 通知代理
    self.delegate?.focuxAtPoint(self.captureDevicePointForPoint(point))    

修改ViewController的viewDidLoad()方法,增加设置代理的代码:

override func viewDidLoad() {
    super.viewDidLoad()
    //初始化捕捉会话
    self.captureModel = CaptureModel()
    if self.captureModel.setupSession() {
        self.preView.session = self.captureModel.captureSession
        self.preView.delegate = self    //new add
        self.captureModel.startSession()
    }
}	    

这时ViewController必须遵循PreViewDelegate协议且实现协议中的focuxAtPoint()方法:

//MARK: - PreViewDelegate
func focuxAtPoint(point:CGPoint)->Void{
    self.captureModel.focusAtPoint(point)
} 

最后是在CaptureModel中添加focusAtPoint()方法:

//MARK: - 对焦
//传入是摄像头设备坐标系下的point
func focusAtPoint(point:CGPoint) -> Void {
    //1. 获取当前使用的摄像头
    let device = self.activeVideoInput.device
    //2. 判断当前设备是否支持兴趣点对焦和自动对焦模式
    /*比如前置摄像头就不支持对焦操作,因为设备和目标的距离不会太长*/
    if device.focusPointOfInterestSupported && device.isFocusModeSupported(.AutoFocus) {
        //3. 修改配置前需要先锁定设备(设备是多个应用程序通用的)
        if ((try? device.lockForConfiguration()) != nil) {
            //4. 设置对焦点,修改对焦模式
            device.focusPointOfInterest = point
            device.focusMode = .AutoFocus
            device.unlockForConfiguration()
        }
    }else{
        //暂时忽略了对设备不支持情况的处理,比如前置摄像头
    }
    
}    

测试对焦功能

编译运行程序,注意使用后置摄像头,点击屏幕,观察对焦过程.

点击曝光功能的实现

添加点击事件

在PreView的setupView()方法中继续添加一个双击手势:

//3. 添加曝光点击事件
let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
doubleTapRecognizer.numberOfTapsRequired = 2
singleTapRecognizer.requireGestureRecognizerToFail(doubleTapRecognizer)
self.addGestureRecognizer(doubleTapRecognizer)
    

其中handleDoubleTap()方法先定义如下:

//曝光功能的触发
func handleDoubleTap(tap:UITapGestureRecognizer) -> Void {
    print("曝光")
}        

实现曝光框UI

与之前对焦UI实现类似,在PreView增加属性:

//曝光框UI
lazy var exposureBox:UIView = {
    let view = UIView(frame: BOX_BOUNDS)
    view.backgroundColor = UIColor.clearColor()
    view.layer.borderColor = UIColor(colorLiteralRed: 1.000, green: 0.421, blue: 0.054, alpha: 1.000).CGColor
    view.layer.borderWidth = 5.0
    view.hidden = true
    return view
}()

在setupView()方法中将其添加到视图:

//4. 添加曝光矩形框UI
self.addSubview(self.exposureBox)    

完善之前的handleDoubleTap()方法:

//曝光功能的触发
func handleDoubleTap(tap:UITapGestureRecognizer) -> Void {
    //1. 点击坐标
    let point = tap.locationInView(self)
    //2. 动画
    self.runBoxAnimationOnView(self.exposureBox, point: point)
}    

实现曝光功能

在协议PreViewDelegate中添加一个方法:

//曝光
func exposeAtPoint(point:CGPoint)->Void

在handleDoubleTap()方法最后通知代理:

//3. 通知代理
self.delegate?.exposeAtPoint(self.captureDevicePointForPoint(point))

在ViewController中实现PreViewDelegate协议的exposeAtPoint()方法如下:

func exposeAtPoint(point: CGPoint) {
    self.captureModel.exposeAtPoint(point)
}

CaptureModel中exposeAtPoint()方法实现如下:

//MARK: - 曝光
func exposeAtPoint(point:CGPoint) -> Void {
    let device = self.activeVideoInput.device
    if device.exposurePointOfInterestSupported && device.isExposureModeSupported(.ContinuousAutoExposure) {
        if ((try? device.lockForConfiguration()) != nil) {
            device.exposurePointOfInterest = point
            device.exposureMode = .AutoExpose
            device.unlockForConfiguration()
        }
    }
}    
    

测试曝光功能

运行程序,切换环境敏感度,用一个手指双击屏幕.

切换回连续对焦和曝光模式

在协议PreViewDelegate中增加一个方法:

//重置连续对焦和曝光模式
func resetFocusAndExposureModes()->Void

在PreView的setupView()方法中增加一个两个手指双击的手势:

//5. 添加重置回连续对焦和曝光模式的手势
let doubleDoubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handDoubledoubleTap(_:)))
doubleDoubleTapRecognizer.numberOfTapsRequired = 2
doubleDoubleTapRecognizer.numberOfTouchesRequired = 2
self.addGestureRecognizer(doubleDoubleTapRecognizer)

其中handDoubledoubleTap()方法实现如下:

func handDoubledoubleTap(tap:UITapGestureRecognizer) -> Void {
    //1. 动画
    let center = (self.layer as! AVCaptureVideoPreviewLayer).pointForCaptureDevicePointOfInterest(CGPoint(x: 0.5, y: 0.5))
    self.focusBox.center = center
    self.exposureBox.center = center
    self.exposureBox.transform = CGAffineTransformMakeScale(1.2, 1.2)
    self.focusBox.hidden = false
    self.exposureBox.hidden = false
    UIView.animateWithDuration(0.15, delay: 0, options:.CurveEaseInOut, animations: { 
        self.focusBox.layer.transform = CATransform3DMakeScale(0.5, 0.5, 1.0)
        self.exposureBox.layer.transform = CATransform3DMakeScale(0.7, 0.7, 1.0)
        }) { (_) in
            let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC)))
            dispatch_after(time, dispatch_get_main_queue(), {
                self.focusBox.hidden = true
                self.exposureBox.hidden = true
                self.focusBox.transform = CGAffineTransformIdentity
                self.exposureBox.transform = CGAffineTransformIdentity
            })
    }
    //2. 通知代理
    self.delegate?.resetFocusAndExposureModes()
}    

在ViewController中实现PreViewDelegate的resetFocusAndExposureModes()方实现:

func resetFocusAndExposureModes() {
    self.captureModel.resetFocusAndExposureModes()
}

运行程序,多次点击切换对焦和曝光,用两根手指双重置回连读对焦和曝光模式.