使用AVFoundation实现二维码识别的角点坐标和区域

实现的效果就是可以识别出来四个角点的位置,并绘制出来内容区域和角点,这个是使用swift+uikit原生开发的,为了实现这个目标,我们需要完成以下几个核心步骤:

1、配置相机权限:允许应用访问相机。

2、构建相机视图 (UIViewRepresentable):因为 SwiftUI 原生没有相机组件,我们需要封装 UIKit 的 AVCaptureSession。

3、处理元数据 (AVCaptureMetadataOutput):识别二维码并获取其角点(Corners)。

4、坐标转换:将相机感光元件的坐标(0.0 - 1.0)转换为屏幕的像素坐标。

5、绘制覆盖层:在 SwiftUI 中用不同颜色绘制四个点。

1、配置权限

在项目中要允许相机权限

2、定义数据模型

我们需要一个简单的方式来在相机逻辑和 SwiftUI 视图之间传递这四个点。

Swift 复制代码
// 定义存储四个顶点的坐标
struct QRCorners {
    var topLeft: CGPoint = .zero
    var topRight: CGPoint = .zero
    var bottomLeft: CGPoint = .zero
    var bottomRight: CGPoint = .zero

    // 只有当所有点都不是原点的时候才能被认为是有效的
    var isValid: Bool {
        return topLeft != .zero && topRight != .zero && bottomLeft != .zero && bottomRight != .zero
    }
}

3、构建相机扫描器 (核心逻辑)

这是最复杂的部分。我们需要创建一个 UIViewRepresentable,它负责管理相机流,并将识别到的坐标传回给 SwiftUI。其中要注册一个协调器,用于处理实现AVCaptureMetadataOutputObjectsDelegate,就是处理扫描的二维码结果,要实现metadataOutput方法, 当扫描到二维码的时候,就会被系统自动调用

构建相机扫描器,并显示在SwiftUI视图中,代码如下:

Swift 复制代码
// 构建相机扫描器,并显示在SwiftUI视图中
struct CameraScanner: UIViewRepresentable {
    // 使用binding将识别到的角点回传给父视图
    @Binding var corners: QRCorners

    func makeUIView(context: Context) -> some UIView {
        // 创建CameraView实例,自定义的UIView
        let cameraView = CameraPreView()
        // 设置代理
        cameraView.delegate = context.coordinator
        // 初始化相机配置,设置代理为Coordinator
        cameraView.setupCamera()
        // 让coordinator持有cameraView的弱引用,以便在代理方法中使用
        context.coordinator.cameraView = cameraView
        // 返回给SwiftUI使用
        return cameraView
    }

    func updateUIView(_ uiView: UIViewType, context: Context) {
        // 更新视图的逻辑
    }

    // 实现makeCoordinator方法,创建协调器实例
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    // 创建协调器,用于处理相机输出的代理方法
    class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
        // 持有父视图的引用
        var parent: CameraScanner
        // 持有CameraView的弱引用
        weak var cameraView: CameraPreView?

        init(parent: CameraScanner) {
            self.parent = parent
        }

        // 要实现这个函数,用于处理识别到的二维码,会被系统调用
        func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
            print("coordinator 识别到二维码")
            // 处理识别到的二维码逻辑,metadataObjects就是识别到的二维码数组
            if metadataObjects.isEmpty {
                // 没有检测到二维码,重置角点
                DispatchQueue.main.async {
                    self.parent.corners = QRCorners()
                }
                return
            }
            // 获取检测到的第一个二维码对象
            if let metadataObj = metadataObjects.first as? AVMetadataMachineReadableCodeObject {
                // 确保是二维码类型
                if metadataObj.type == .qr {
                    // 获取四个角点,这四个点坐标分别是左上、右上、右下、左下
                    let corners = metadataObj.corners
                    // 如果有四个角点,则打印它们的坐标
                    print("Coordinator QR Code Corners:")
                    // 打印四个角点坐标
                    for (index, corner) in corners.enumerated() {
                        print("Corner \(index + 1): \(corner)")
                    }
                    // 获取previewLayer进行坐标转换
                    guard let previewLayer = cameraView?.previewLayer,
                          let transformeObj = previewLayer.transformedMetadataObject(for: metadataObj) as? AVMetadataMachineReadableCodeObject
                    else {
                        return
                    }
                    // 获取转换后的角点
                    let transformCorners = transformeObj.corners
                    if transformCorners.count == 4 {
                        // 更新父视图的角点状态,注意顺序转换
                        parent.corners = QRCorners(
                            topLeft: transformCorners[0],
                            topRight: transformCorners[1],
                            bottomLeft: transformCorners[3],
                            bottomRight: transformCorners[2]
                        )
                    }
                }
            }
        }
    }
}

CameraPreView 用于处理二维码识别结果,并显示相机预览:

Swift 复制代码
// 自定义一个CameraView 来持有AVCaptureSession和PreviewLayer,并实现AVCaptureMetadataOutputObjectsDelegate协议,
// 用于处理二维码识别结果,并显示相机预览
class CameraPreView: UIView {
    // 相机会话和预览图层
    var session: AVCaptureSession!
    // 相机预览图层
    var previewLayer: AVCaptureVideoPreviewLayer!
    // 相机元数据输出代理
    weak var delegate: AVCaptureMetadataOutputObjectsDelegate?

    // 初始化方法
    override init(frame: CGRect) {
        super.init(frame: frame)
        // ❌ 问题1:这里 bounds 可能是错误的
        print("init 中的 bounds: \(bounds)")
        // 可能输出:init 中的 bounds: (0.0, 0.0, 0.0, 0.0)
        // ❌ 问题2:frame 可能还没设置
        print("init 中的 frame: \(frame)")
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // 一定要在 layoutSubviews 中设置 bounds
    override func layoutSubviews() {
        super.layoutSubviews()
        print("layoutSubviews 中的 bounds: \(bounds)")
        // 给预览层设置正确的帧大小
        previewLayer.frame = bounds // 这行必须要有!
    }

    // 设置相机
    func setupCamera() {
        // 配置相机会话和预览图层的逻辑
        let session = AVCaptureSession()
        // 获取默认的视频捕捉设备
        guard let device = AVCaptureDevice.default(for: .video) else {
            return
        }
        // 创建输入设备
        guard let input = try? AVCaptureDeviceInput(device: device) else { return }
        // 添加输入设备到会话
        if session.canAddInput(input) {
            session.addInput(input)
        }
        // 创建元数据输出
        let output = AVCaptureMetadataOutput()
        // 添加输出设备到会话
        if session.canAddOutput(output) {
            session.addOutput(output)
            // 设置元数据输出代理
            output.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
            // 设置识别类型为二维码,可以为多个
            output.metadataObjectTypes = [.qr]
        }
        // 创建预览图层
        let previewLayer = AVCaptureVideoPreviewLayer(session: session)
        // 设置预览图层的填充模式
        previewLayer.videoGravity = .resizeAspectFill
        // 设置预览图层的帧大小
        // previewLayer.frame = bounds
        print("setupCamera 中的 bounds: \(bounds)")
        // 将预览图层添加到视图的图层中
        layer.addSublayer(previewLayer)
        // 启动会话
        self.session = session
        self.previewLayer = previewLayer
        // 启动相机会话,这么做会不会阻塞UI?
        DispatchQueue.global(qos: .background).async {
            session.startRunning()
        }
    }
}
Swift 复制代码
// 辅助视图,画原点
struct CircleView: View {
    let color: Color
    let point: CGPoint
    // 视图主体
    var body: some View {
        Circle()
            .fill(color)
            .frame(width: 10, height: 10)
            .position(point)
            .shadow(radius: 2)
    }
}

主界面绘制层,就是将相机视图放在底层,然后在顶层绘制四个不同颜色的圆圈:

Swift 复制代码
struct ContentView: View {
    // 存储识别到的二维码角点
    @State var corners = QRCorners()

    var body: some View {
        ZStack {
            // 相机扫描器视图
            CameraScanner(corners: $corners)

            // 如果识别到有效的角点,则绘制边框
            if corners.isValid {
                ZStack {
                    CircleView(color: .red, point: corners.topLeft)
                    CircleView(color: .green, point: corners.topRight)
                    CircleView(color: .blue, point: corners.bottomLeft)
                    CircleView(color: .yellow, point: corners.bottomRight)
                    // 绘制连接四个角点的边框
                    Path { path in
                        path.move(to: corners.topLeft)
                        path.addLine(to: corners.topRight)
                        path.addLine(to: corners.bottomRight)
                        path.addLine(to: corners.bottomLeft)
                        path.closeSubpath()
                    }.stroke(Color.white, lineWidth: 2)
                }
            }
        }
    }
}
相关推荐
陌路201 小时前
C++ 单例模式
开发语言·c++
廋到被风吹走1 小时前
【JDK版本】JDK1.8相比JDK1.7 语言特性之函数式编程
java·开发语言·python
y***61311 小时前
PHP操作redis
开发语言·redis·php
fire-flyer1 小时前
Reactor Context 详解
java·开发语言
CoderYanger1 小时前
动态规划算法-简单多状态dp问题:14.粉刷房子
开发语言·算法·leetcode·动态规划·1024程序员节
momo小菜pa1 小时前
C#--BindingList
开发语言·c#
Sheffi661 小时前
iOS Crash 本质与捕获修复方案
macos·ios·cocoa
Rinai_R1 小时前
Golang 垃圾回收器执行链路分析
开发语言·后端·golang
代码不停1 小时前
Java字符串 和 队列 + 宽搜 题目练习
java·开发语言