iPhoneの自作カメラアプリで接写がピンボケする場合の対応方法

## はじめに
iPhone11シリーズ辺りからQRコードなどの小さい対象をある程度近づけて撮影したい場合、[builtInWideAngleCamera](https://developer.apple.com/documentation/avfoundation/avcapturedevice/devicetype/2361449-builtinwideanglecamera)を使うだけではピントがボケてしまう問題があります。

これは最近のiPhoneは焦点距離が伸びており、それより近づけて撮影しようとしてもボケてしまうことが原因です。

## 対策

調べたところいくつか方法があります。

1. [What’s new in camera capture](https://developer.apple.com/videos/play/wwdc2021/10047/) (Apple公式)
2. [iPhone 15 Pro/Pro Max で発生する、近距離でカメラのフォーカスが合わない問題への対応](https://zenn.dev/luup_developers/articles/ios-tsuboyan-20231213)
3. [タップした場所にフォーカスを合わせる](https://qiita.com/john-rocky/items/80084e6ff0ac05c96c21)

それぞれ違うアプローチになっていますが、結論としては1が様々な焦点距離のカメラ対応ができる方法になります。

### 1. 焦点距離に応じて予めズームを行い接写でもボケを防止する

```swift
    /// 40 x 40mmの被写体に合わせる
    private let initialCloseUpTargetSize = Float(40)

    private var rectOfInterestWidth = Double()
    private var rectOfInterestHeight = Double()

    private func setRectOfInterest() {
        let formatDimensions = CMVideoFormatDescriptionGetDimensions(self.captureDeviceInput.device.activeFormat.formatDescription)
        self.rectOfInterestWidth = Double(formatDimensions.height) / Double(formatDimensions.width)
        self.rectOfInterestHeight = 1.0
    }

    private func setRecommendedZoomFactor() {
        let deviceMinimumFocusDistance = Float(self.captureDeviceInput.device.minimumFocusDistance)
        guard deviceMinimumFocusDistance != -1 else { return }

        let deviceFieldOfView = self.captureDeviceInput.device.activeFormat.videoFieldOfView
        let minimumSubjectDistanceForCode = minimumSubjectDistanceForCode(fieldOfView: deviceFieldOfView,
                                                                          minimumCodeSize: self.initialCloseUpTargetSize,
                                                                          previewFillPercentage: Float(self.rectOfInterestWidth))
        if minimumSubjectDistanceForCode < deviceMinimumFocusDistance {
            let zoomFactor = deviceMinimumFocusDistance / minimumSubjectDistanceForCode
            do {
                try captureDeviceInput.device.lockForConfiguration()
                // ズーム倍率を設定する
                captureDeviceInput.device.videoZoomFactor = CGFloat(zoomFactor)
                captureDeviceInput.device.unlockForConfiguration()
            } catch {
                print("設定のロック失敗: \(error)")
            }
        }
    }

    private func minimumSubjectDistanceForCode(fieldOfView: Float, minimumCodeSize: Float, previewFillPercentage: Float) -> Float {
        let radians = degreesToRadians(fieldOfView / 2)
        let filledCodeSize = minimumCodeSize / previewFillPercentage
        return filledCodeSize / tan(radians)
    }

    private func degreesToRadians(_ degrees: Float) -> Float {
        return degrees * Float.pi / 180
    }
```

これで接写してもボケない状態になります。
その代わり端末を被写体から離して撮影する必要がでますので、体験としては落ちます。
またズームするってことは画質が劣化する懸念もありますが、光学ズームなのでよほどのことがない限り気にする必要は無いと思います。

### 2. 被写体との距離によってカメラを切り替える

```swift
    func setSwitchZoomFactor() {
        guard let zoomFactor = captureDeviceInput.device.virtualDeviceSwitchOverVideoZoomFactors.first else {
            return
        }
        do {
            try captureDeviceInput.device.lockForConfiguration()
            captureDeviceInput.device.videoZoomFactor = CGFloat(truncating: zoomFactor)
            captureDeviceInput.device.unlockForConfiguration()
        } catch {
            print(error)
        }
    }
```

申し訳ないのですが、iPhone12miniで見る限りあまり変わりません。
Pro系端末でないと恩恵がないかもしれません。その場合はPro系以外ではボケてしまうことになります。また将来切り替わるせいでさらにボケてしまうなどのリスクも考えられます。

### 3. タップした場所にフォーカスを合わせる

```swift
    DispatchQueue.main.async {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTapFocus(_:)))
        self.previewView.addGestureRecognizer(tapGesture)
    }

    func changeFocusPoint(_ focusPoint: CGPoint) {
        do {
            try captureDeviceInput.device.lockForConfiguration()
            captureDeviceInput.device.focusPointOfInterest = focusPoint
            captureDeviceInput.device.focusMode = .autoFocus
            captureDeviceInput.device.unlockForConfiguration()
        } catch let error {
            print(error)
        }
    }

    @objc func handleTapFocus(_ recognizer: UITapGestureRecognizer) {
        let pointInView = recognizer.location(in: recognizer.view)
        print(pointInView)
        changeFocusPoint(pointInView)
    }
```

こちらは確かにタップした箇所にピントが合いますが、焦点距離はかわらないので接写の時にボケる事象は解決しません。