くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

iOSのカメラについて

今回はiOSのカメラ機能についてまとめようと思います

カメラ権限設定

最初にカメラと写真のライブラリーを使用するため、アクセスの許可をする必要があります。 プロジェクトのinfo.plistを開いて、アプリのアクセス許可を表示させるように設定を追加します

info.plistのInformationPropertyListの右側の+を押して Privacy - Camera Usage Description(カメラを使用することの許可を求める)を設定します。 右側のvalueに許可を求めるダイアログに表示する文言を設定します。 設定した文言が権限チェックのダイアログに表示されます。 権限を設定していない場合には下記のような権限確認のダイアログを表示させる処理を実装しましょう

AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in           
})

AVCaptureSessionの設定

次にAVCaptureSessionの設定を行います。 AVCaptureSessionはキャプチャ動作を管理し、入力デバイスからキャプチャ出力へのデータの流れを調整するオブジェクトです。 AVCaptureDeviceInputAVCaptureOutputの仲介役を行なっているのがAVCaptureSessionクラスになります。

AVCaptureDeviceInputは、AVFoundationフレームワークで使用されるクラスの1つで、 ビデオやオーディオなどのキャプチャデバイスからの入力をキャプチャセッションに提供するために使用されます

AVCaptureOutputも同様に、AVFoundationフレームワークのクラスの1つで、キャプチャーセッションからの出力データを処理するために使用されます。 キャプチャーセッションからのビデオフレームやオーディオサンプルなど、セッションからのデータを取得、加工、保存するために使用されます。

AVCaptureSessionの実装の流れは次の手順で行います

  1. AVCaptureDeviceインスタンスを生成
  2. AVCaptureDeviceインスタンスからAVCaptureDeviceInputを構築
  3. AVCaptureSessionにAVCaptureDeviceInputを登録
  4. AVCaptureSessionにAVCaptureOutputを登録
  5. AVCaptureVideoPreviewLayerで画面を構築

AVCaptureDeviceInputを構築

AVCaptureDeviceInputインスタンスを構築するためにはAVCaptureDeviceクラスを用いて、まず使用するデバイスを設定する必要があります

使用するAVCaptureDeviceインスタンスを生成するには以下の2つのどちらかを使用します

  • AVCaptureDevice.defaultメソッド
  • AVCaptureDevice.DiscoverySessionクラス

defaultメソッドは引数に指定されたタイプのデフォルトデバイスを返します。引数にはAVMediaTypeの任意の値を渡します

// .audioなど指定できる
guard let videoDevice = AVCaptureDevice.default(for: .video) else { return }

DiscoverySessionは特定の条件にマッチするAVCaptureDeviceを検索するクラスです。 引数にはデバイスタイプ(カメラの種類)とメディアタイプ(videoやaudioなど)、ポジション(カメラの位置)を渡します。 こちらの方がカスタマイズできます

let cameraDevice = AVCaptureDevice.default(
            AVCaptureDevice.DeviceType.builtInWideAngleCamera,
            for: AVMediaType.video,
            position: .back
        )

AVCaptureDeviceインスタンスからVCaptureDeviceInputを構築

設定したデバイスを元にAVCaptureDeviceInputインスタンスを生成します。 またAVCaptureSessionインスタンスもここで生成しておきます。そして、AVCaptureSessionインスタンスに対してInputとOutputを登録します

let cameraDevice = AVCaptureDevice.default(
            AVCaptureDevice.DeviceType.builtInWideAngleCamera,
            for: AVMediaType.video,
            position: .back
        )

let captureSession = AVCaptureSession()
        
let videoInput: AVCaptureDeviceInput
        
do {
    videoInput = try AVCaptureDeviceInput(device: cameraDevice)
} catch {
    captureSession.commitConfiguration()
    return
}

AVCaptureSessionにAVCaptureDeviceInputを登録

AVCaptureDeviceInputは、デバイスがカメラ、マイク、またはその他の種類であるかどうかに応じて、AVCaptureDeviceInputを使用してビデオデータまたはオーディオデータをキャプチャできます。 カメラの映像を処理するアプリケーションでは、AVCaptureDeviceInputを使用して、カメラからの映像を取得し、処理することができます

AVCaptureDeviceInputを登録する際はcanAddInputメソッドを使用して追加が可能かを識別し、問題なければ登録します

if captureSession.canAddInput(videoInput) {
    captureSession.addInput(videoInput)
} else {
    captureSession.commitConfiguration()
    return
}

AVCaptureSessionにAVCaptureOutputを登録

AVCaptureOutputは、AVCaptureSessionに接続され、キャプチャされたデータを受け取り、アプリケーションが処理できるフォーマットに変換することができます。 例えば、AVCaptureVideoDataOutputを使用して、キャプチャーセッションからのビデオデータをアプリケーションに提供することができます。 同様に、AVCaptureAudioDataOutputを使用して、キャプチャーセッションからのオーディオデータをアプリケーションに提供することができます

AVCaptureOutputもcanAddOutputメソッドを使用して追加が可能かを識別し、問題なければ登録します

let photoOutput = AVCapturePhotoOutput()
if captureSession.canAddOutput(photoOutput!) {
    captureSession.addOutput(photoOutput!)
}

AVCaptureVideoPreviewLayerで画面を構築

カメラデバイスからビューを表示するためのレイヤーを構築します。 この手順を行わないとカメラアプリを起動したときのように背景が画面が映らないので注意してください

let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
// photoViewはカメラを表示させるview(適時置き換えてください)
previewLayer.frame = photoView.bounds
// カメラから得られた映像の表示形式を設定
previewLayer.videoGravity = .resizeAspectFill
captureSession.sessionPreset = AVCaptureSession.Preset.photo
self.view.layer.addSublayer(previewLayer)

// 最後にstartRunningメソッドを実行して、セッションの入力から出力へのデータフローを開始します
captureSession.startRunning()

startRunning()を行うことでカメラアプリを起動したときのように端末を移動させると背景も移動するようになります。 逆にセッションを終了させる場合にはstopRunning()を行いましょう。 ここまでがカメラ撮影機能を実装する基本の流れです

撮影する

先ほどまでの状態実行するとでiPhone標準のカメラアプリを起動したときと同じになりはずです。 ここからはシャッターボタンを押したときの処理を追加する必要があります。 なお今回はシャッターボタンの作成は省きます。 シャッターボタンをタップして撮影するところからの実装です

AVCapturePhotoSettingsクラスで、撮影する際のフラッシュや手ぶれ補正などをおこなうかなどの設定をします。 そして、AVCapturePhotoOutputクラスのcapturePhotoメソッドで指定した設定で写真の撮影を開始します

let settings = AVCapturePhotoSettings()
// フラッシュの設定
settings.flashMode = .auto
// 撮影された画像をdelegateメソッドで処理
photoOutput?.capturePhoto(with: settings, delegate: self)

しかし、この処理だけだと不完全です。 撮影された写真データををdelegateメソッドで受け取る必要があります

撮影後の処理

写真を撮影した後、画像データを保存したりする場合にどうすればいいでしょうか?

AVCapturePhotoOutputクラスのcapturePhotoメソッドで撮影した画像データを受け取るために、AVCapturePhotoCaptureDelegatephotoOutputメソッドで撮影した画像データを取得します。 撮影された画像データは、パラメータのphotoにピクセルデータとメタデータなどの関連データと共に格納されています。 fileDataRepresentationメソッドでデータを生成し、UIImageオブジェクトに変換することが多い気がします

 extension ViewController: AVCapturePhotoCaptureDelegate {
    // 撮影した画像データが生成されたときに呼び出されるデリゲートメソッド
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            // Data型をUIImageオブジェクトに変換
            let uiImage = UIImage(data: imageData)
        }
    }
}

補足

ここからは今までの説明の補足をいくつか追加しようと思います

向きを変える

iPhoneだとあまりないと思いますが、iPadだと横向きに変更して撮影を行う場合があるかもしれません。 ここまでの実装だけだとカメラの向きが変わらないので、少し実装を追加する必要があります

// UIInterfaceOrientationをAVCaptureVideoOrientationにConvert(全方位カメラの向きを許容する)
func convertOrientation(orientation: () -> UIInterfaceOrientation) -> AVCaptureVideoOrientation? {
    let orientation = orientation()
    switch orientation {
    case UIInterfaceOrientation.unknown: return nil
    default:
        return ([
            .portrait: .portrait,
            .portraitUpsideDown: .portraitUpsideDown,
            .landscapeLeft: .landscapeLeft,
            .landscapeRight: .landscapeRight
        ])[orientation]
    }
}

// UIInterfaceOrientationをAVCaptureVideoOrientationにConvert
func appOrientation() -> UIInterfaceOrientation {
    let scenes = UIApplication.shared.connectedScenes
    let windowScenes = scenes.first as? UIWindowScene
    guard let orientation = windowScenes?.interfaceOrientation else {
        retrun .landscape
    }

    return orientation
}

プレビューに関しては AVCaptureVideoPreviewLayer を利用しており、AVCaptureVideoPreviewLayer.connection.videoOrientation、 撮影後の写真に関しては AVCaptureConnection.videoOrientationをそれぞれ画面の回転に合わせて更新する必要があります

画面の向きについてはUIApplication.shared.connectedScenesから取得しています

上記の処理をプレビューのフレームを設定するときや写真を撮る前、画面が回転したときに行うと正しい向きで処理が行われます。 次のように使います

// 画面を回転したとき、プレビューの向き設定
override func viewWillTransition(
        to size: CGSize,
        with coordinator: UIViewControllerTransitionCoordinator
    ) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(
            alongsideTransition: nil,
            completion: {(UIViewControllerTransitionCoordinatorContext) in
                if let orientation = self.convertOrientation(orientation: {return self.appOrientation()}) {
                    previewLayer.connection?.videoOrientation = orientation
                }
        })
}


// 撮影後の写真の向き設定
if let connection = photOutPut?.connection(with: .video), 
    if let orientation = self.convertOrientation(orientation: {return self.appOrientation()}) {
    connection.videoOrientation = orientation
}

lockForConfiguration()について

focusModeexposureModeなどデバイスのプロパティを設定・更新を行う場合には更新前にlockForConfiguration()で端末をロックして設定を更新します。 更新が終わったらunlockForConfiguration()をロックを解除します

イメージとしてはDBを更新する際に他からの更新がないようにロックするようなイメージです

下記はライトを点灯させる処理です。 ちなみにライトが搭載されていない端末で下記の処理を行おうとするとクラッシュするので、try~catchで囲みましょう

let avCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video)

if avCaptureDevice!.hasTorch, avCaptureDevice!.isTorchAvailable {
    do {
        try avCaptureDevice!.lockForConfiguration()
        try avCaptureDevice!.setTorchModeOn(level: 1.0)
    } catch let error {
        print(error)
    }
    avCaptureDevice!.unlockForConfiguration()
}

参照

developer.apple.com

superhahnah.com

qiita.com

note.com

qiita.com

shiba1014.medium.com

developer.apple.com

developer.apple.com