如何在SwiftUI中实现相机测箱体三维尺寸及Vision矩形检测
在SwiftUI中实现箱体尺寸测量功能
一、SwiftUI中封装相机预览界面
SwiftUI没有原生相机组件,我们可以通过UIViewRepresentable封装AVFoundation的相机预览,实现实时取景和拍照功能:
import SwiftUI import AVFoundation struct CameraView: UIViewRepresentable { @Binding var capturedImage: UIImage? @Binding var isCameraAuthorized: Bool class Coordinator: NSObject, AVCapturePhotoCaptureDelegate { let parent: CameraView init(parent: CameraView) { self.parent = parent } func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { guard let imageData = photo.fileDataRepresentation(), let image = UIImage(data: imageData) else { return } parent.capturedImage = image } } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } func makeUIView(context: Context) -> UIView { let view = UIView(frame: UIScreen.main.bounds) let captureSession = AVCaptureSession() captureSession.sessionPreset = .photo guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back), let input = try? AVCaptureDeviceInput(device: backCamera) else { isCameraAuthorized = false return view } if captureSession.canAddInput(input) { captureSession.addInput(input) } let photoOutput = AVCapturePhotoOutput() if captureSession.canAddOutput(photoOutput) { captureSession.addOutput(photoOutput) photoOutput.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])], completionHandler: nil) } let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) captureSession.startRunning() isCameraAuthorized = true photoOutput.setDelegate(context.coordinator, queue: DispatchQueue.main) return view } func updateUIView(_ uiView: UIView, context: Context) {} // 暴露拍照触发方法 func takePhoto(in session: AVCaptureSession) { guard let photoOutput = session.outputs.first as? AVCapturePhotoOutput else { return } let settings = AVCapturePhotoSettings() photoOutput.capturePhoto(with: settings, delegate: context.coordinator) } }
二、修正Vision框架的矩形检测代码
你提供的代码存在两个核心问题:一是用boundingBox只能得到矩形的包围盒,无法反映箱体表面的实际尺寸;二是坐标转换未考虑图像方向。以下是修正后的代码,会提取矩形的四个顶点并计算真实边长:
import UIKit import Vision func detectBoxDimensions(in image: UIImage, completion: @escaping (CGFloat?, CGFloat?) -> Void) { guard let cgImage = image.cgImage else { completion(nil, nil) return } let request = VNDetectRectanglesRequest { request, error in guard let results = request.results as? [VNRectangleObservation], let box = results.first else { completion(nil, nil) return } // 将Vision的归一化坐标转换为图像实际坐标 let imageSize = CGSize(width: CGFloat(cgImage.width), height: CGFloat(cgImage.height)) let transform = CGAffineTransform(scaleX: imageSize.width, y: imageSize.height) .scaledBy(x: 1, y: -1) .translatedBy(x: 0, y: -imageSize.height) // 获取矩形四个顶点的实际坐标 let topLeft = box.topLeft.applying(transform) let topRight = box.topRight.applying(transform) let bottomLeft = box.bottomLeft.applying(transform) // 计算宽和高的像素长度 let width = hypot(topRight.x - topLeft.x, topRight.y - topLeft.y) let height = hypot(bottomLeft.x - topLeft.x, bottomLeft.y - topLeft.y) completion(width, height) } // 设置矩形检测参数,适配箱体的长宽比范围 request.minimumAspectRatio = VNAspectRatio(0.3) request.maximumAspectRatio = VNAspectRatio(3.0) request.minimumConfidence = 0.8 request.maximumObservations = 1 let handler = VNImageRequestHandler(cgImage: cgImage, orientation: .init(image.imageOrientation), options: [:]) DispatchQueue.global().async { do { try handler.perform([request]) } catch { print("矩形检测失败: \(error)") completion(nil, nil) } } }
三、实现长宽高三个维度的测量
单张照片只能获取平面的两个维度,要得到箱体的完整三维尺寸,推荐两种方案:
方案1:多面拍照测量
- 引导用户依次拍摄箱体的正面、侧面、顶面三个相邻面
- 对每张照片用Vision检测矩形,获取对应的两组边长
- 通过匹配公共边(比如正面的高度=侧面的高度),整合得到长、宽、高三个维度
方案2:ARKit 3D测量(推荐,支持LiDAR的设备精度更高)
如果用户设备支持LiDAR,可以用ARKit直接在3D空间中测量箱体的三个维度,无需多步拍照:
import ARKit class ARMeasurementController: UIViewController, ARSessionDelegate { let session = ARSession() let sceneView = ARSCNView() override func viewDidLoad() { super.viewDidLoad() sceneView.session = session sceneView.delegate = self let config = ARWorldTrackingConfiguration() config.planeDetection = .horizontal session.run(config) } func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { guard let planeAnchor = anchors.first as? ARPlaneAnchor else { return } // 提取平面的宽度和高度(单位:米) let width = planeAnchor.extent.x let height = planeAnchor.extent.z // 结合其他平面数据计算第三个维度 } }
四、SwiftUI主视图整合示例
将相机预览、拍照、尺寸检测整合到SwiftUI界面:
struct MeasurementView: View { @State private var capturedImage: UIImage? @State private var isCameraAuthorized = false @State private var currentDimensions: (width: CGFloat?, height: CGFloat?) = (nil, nil) @State private var measuredValues: [CGFloat] = [] var body: some View { VStack { if isCameraAuthorized { CameraView(capturedImage: $capturedImage, isCameraAuthorized: $isCameraAuthorized) .frame(maxWidth: .infinity, maxHeight: .infinity) Button("拍摄当前面") { guard let image = capturedImage else { return } detectBoxDimensions(in: image) { width, height in DispatchQueue.main.async { self.currentDimensions = (width, height) if let w = width, let h = height { measuredValues.append(contentsOf: [w, h]) } } } } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } else { Text("请授予相机权限") .onAppear { AVCaptureDevice.requestAccess(for: .video) { granted in DispatchQueue.main.async { self.isCameraAuthorized = granted } } } } if let width = currentDimensions.width, let height = currentDimensions.height { Text("当前面尺寸:宽\(String(format: "%.2f", width))px,高\(String(format: "%.2f", height))px") .padding() } if measuredValues.count >= 3 { Text("箱体三维尺寸:长\(String(format: "%.2f", measuredValues[0]))px,宽\(String(format: "%.2f", measuredValues[1]))px,高\(String(format: "%.2f", measuredValues[2]))px") .padding() } } } }
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

