iOS AI相机App开发教程,01 实现基本相机功能

logo

SwiftUI 基础

首先,如果你是刚刚开始学习SwiftUI的,那么你可以先学习一下基础的SwiftUI。
这方面的教程网上有很多,这里就不介绍了,我们直接从实现相机功能开始入手。

相机拍照

拍照protocol和Events

定义相机拍照协议(protocol),所有相机相关的View需要实现该协议来处理结果。
定义一个ObservableObject类型,用来控制相机拍照和翻转。


// 拍照协议
public protocol CameraFxImage {
    // 拍照完成方法
    // image: 照片 UIImage 对象
    mutating func didImageOk(_ image: UIImage)

    // 执行特效方法
    // fx:特效名称
    mutating func doFx(_ fx: String)
}

// 拍照Events
public class UserEvents: ObservableObject {
    // 触发相机拍照
    @Published public var didAskToCapturePhoto = false
    // 触发相机翻转
    @Published public var didAskToRotateCamera = false
    
    public init() {
        
    }
}

BBMetalImage

需要先使用 CocosPods 安装 BBMetalImage 具体方法加不在这里赘述了。
通过 BBMetalImage 库的 BBMetalCamera 来实现相机和基础滤镜功能。

相机预览 CameraPhotoFilterView

通过实现 UIViewController 和 UIViewControllerRepresentable 协议,创建相机预览SwiftUI View
详细代码可以参考: MagicCamera/SwiftUICam/CameraPhotoFilterVC.swift

CameraPhotoFilterVC 实现 UIViewController

// 引入依赖的库
import AVFoundation
import BBMetalImage

class CameraPhotoFilterVC: UIViewController {
    // 相机
    private var camera: BBMetalCamera!
    // 预览View
    private var metalView: BBMetalView!
    // 相机拍照协议
    public var delegateFx: CameraFxImage?
    // 相机方向
    public var position: AVCaptureDevice.Position = .front
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // view.backgroundColor = .white
        
        var smooth = Float(0.5)
        var brightness = Float(0.01)
        var preset = AVCaptureSession.Preset.photo
        ...
        
        // 创建相机
        camera = BBMetalCamera(sessionPreset: preset, position: position)
        if camera == nil {
            camera = BBMetalCamera(sessionPreset: .high, position: position)
        }
        
        let x: CGFloat = 0
        let width: CGFloat = view.bounds.width
        let height: CGFloat = min(width*4/3, view.bounds.height*2/3)
        
        // 创建预览
        metalView = BBMetalView(frame: CGRect(x: x, y: 0, width: width, height: height))
        metalView.setContentHuggingPriority(.defaultLow, for: .horizontal)
        metalView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        view.addSubview(metalView)
        
        camera.canTakePhoto = true
        camera.photoDelegate = self
        
        // 将滤镜添加到相机预览
        camera
            .add(consumer: BBMetalBeautyFilter(distanceNormalizationFactor: 4, stepOffset: 4, edgeStrength: 1, smoothDegree: smooth))
            .add(consumer: BBMetalBrightnessFilter(brightness: brightness))
            .add(consumer: metalView)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // 启动相机预览
        camera.start()
        metalView.isHidden = false
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // 停止相机预览
        camera.stop()
    }
    
    public func switchCameraPosition()  {
        // 翻转相机
        camera.switchCameraPosition()
    }
    
    public func takePhoto()  {
        // 拍照
        metalView.isHidden = true
        camera.takePhoto()
    }
}

// 实现 BBMetalCameraPhotoDelegate,处理拍照结果
extension CameraPhotoFilterVC: BBMetalCameraPhotoDelegate {
    func camera(_ camera: BBMetalCamera, didOutput texture: MTLTexture) {
        // In main thread
        ...

        // 添加滤镜
        let brightness = BBMetalBrightnessFilter(brightness: bright)
        let imageSource = BBMetalStaticImageSource(image: texture.bb_image!)
        // 设置滤镜链,并设置滤镜同步执行
        imageSource.add(consumer: BBMetalBeautyFilter(distanceNormalizationFactor: 4, stepOffset: 4, edgeStrength: 1, smoothDegree: smooth))
            .add(consumer: brightness)
            .runSynchronously = true

        // 开始处理
        imageSource.transmitTexture()
        // 返回处理后结果
        guard let filteredImage = brightness.outputTexture?.bb_image else { return  }
        
        metalView.isHidden = false
        DispatchQueue.main.async {
            // 完成后调用代理方法,返回照片
            self.delegateFx?.didImageOk(filteredImage)
        }
    }
}

CameraPhotoFilterView 实现 UIViewControllerRepresentable


struct CameraPhotoFilterView: UIViewControllerRepresentable {
    @ObservedObject var events: UserEvents
    private var level: Int
    private var position: AVCaptureDevice.Position = .front
    private var delegateFx: CameraFxImage?
    
    typealias UIViewControllerType = CameraPhotoFilterVC
    func makeUIViewController(context: Context) -> CameraPhotoFilterVC {
        // 创建 CameraPhotoFilterVC 实例
        let vc = CameraPhotoFilterVC()
        vc.delegateFx = self.delegateFx
        vc.level = self.level
        vc.position = self.position
        return vc
    }
    
    // 更新时调用
    func updateUIViewController(_ cameraController: CameraPhotoFilterVC, context: Context) {
        if events.didAskToCapturePhoto {
            // 开始拍照
            events.didAskToCapturePhoto = false
            cameraController.takePhoto()
        }
        
        if events.didAskToRotateCamera {
            // 开始翻转相机
            events.didAskToRotateCamera = false
            cameraController.switchCameraPosition()
        }
    }
    
    public init(events: UserEvents, delegateFx: CameraFxImage?, level: Int = 1, position: AVCaptureDevice.Position = .front) {
        ...
    }
}

拍照界面

拍照界面代码如下:需要定义拍照Events,拍照协议,相机方向等参数
使用 ZStack 把 CameraPhotoFilterView 放在底层,在预览界面上方放置拍照按钮等
详细代码可以参考: MagicCamera/SwiftUICam/LiveCameraFilterView.swift

struct LiveCameraFilterView: View {
    // 拍照Events
    @ObservedObject var events: UserEvents
    // 拍照协议
    var delegateFx: CameraFxImage?
    // 相机方向
    var position: AVCaptureDevice.Position = .front
    
    // 初始化参数
    public init(events: UserEvents, delegateFx: CameraFxImage?, position: AVCaptureDevice.Position = .front) {
        ...
    }

    var body: some View {
        ZStack {
            // 相机预览 CameraPhotoFilterView
            CameraPhotoFilterView(events:events, delegateFx: delegateFx, level: level, position: position)
            // 在预览上方显示其他界面,如拍照按钮等
            VStack {
                HStack {
                    Spacer()
                    // 拍照按钮
                    Button(action: {
                        weakself.events.didAskToCapturePhoto = true
                    }, label: {
                        ...
                    }).disabled(takeing)
                    Spacer()
                    // 翻转按钮
                    Button(action: {
                        weakself.events.didAskToRotateCamera = true
                    }, label: {
                        ...
                    }).disabled(takeing)
                    Spacer()
                }
                ...
            }

        }
    }
}

使用拍照

在需要拍照的页面上使用 LiveCameraFilterView, 并实现 CameraFxImage 协议
详细代码可以参考: MagicCamera/Cameras/BeautyCameraView.swift

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容