Hands-on · Paso 4 de 10
Vision: reconocimiento de texto y objetos
Crea una app que lee texto en tiempo real desde la cámara y detecta objetos comunes.
En esta demo construirás una app SwiftUI que usa la cámara para reconocer texto en tiempo real (OCR) y detectar objetos comunes. Todo ocurre en el dispositivo, sin enviar imágenes a ningún servidor.
Arquitectura de la demo
La app tiene dos modos que puedes alternar con un Picker:
- Texto — usa
VNRecognizeTextRequestpara leer palabras de la cámara. - Objetos — usa
VNDetectRectanglesRequest+VNRecognizeTextRequestpara encontrar documentos y leer su contenido.
Ambos requests se ejecutan sobre un CVPixelBuffer que proviene de AVCaptureVideoDataOutput.
Flujo de datos: desde la cámara hasta la interfaz de usuario, pasando por el procesamiento de Vision.
Paso 1: Configurar la sesión de cámara
Creamos un CameraManager observable que gestiona AVCaptureSession. Esto mantiene la lógica fuera de la vista y facilita los previews en SwiftUI.
import AVFoundation
import SwiftUI
import Combine
class CameraManager: NSObject, ObservableObject {
@Published var session = AVCaptureSession()
@Published var previewLayer: AVCaptureVideoPreviewLayer?
private var videoOutput = AVCaptureVideoDataOutput()
private let sessionQueue = DispatchQueue(label: "camera-session")
var onFrame: ((CMSampleBuffer) -> Void)?
override init() {
super.init()
configureSession()
}
func configureSession() {
sessionQueue.async { [weak self] in
guard let self = self else { return }
self.session.beginConfiguration()
self.session.sessionPreset = .high
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
let input = try? AVCaptureDeviceInput(device: device),
self.session.canAddInput(input) else {
self.session.commitConfiguration()
return
}
self.session.addInput(input)
self.videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "video-output"))
self.videoOutput.alwaysDiscardsLateVideoFrames = true
if self.session.canAddOutput(self.videoOutput) {
self.session.addOutput(self.videoOutput)
}
self.session.commitConfiguration()
self.session.startRunning()
}
}
}
extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
onFrame?(sampleBuffer)
}
}
CameraPreviewView.swift
Paso 2: Crear la vista de preview
Usamos UIViewRepresentable para envolver AVCaptureVideoPreviewLayer dentro de SwiftUI.
import SwiftUI
import AVFoundation
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
CameraPreviewView.swift
Paso 3: Reconocimiento de texto con Vision
Implementamos recognizeText(from:) como una función async que ejecuta el request de Vision en un Task.
import Vision
func recognizeText(from sampleBuffer: CMSampleBuffer) async -> [String] {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return [] }
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate // .fast para tiempo real
request.usesLanguageCorrection = true
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: .right, options: [:])
do {
try handler.perform([request])
guard let observations = request.results as? [VNRecognizedTextObservation] else { return [] }
return observations.compactMap { $0.topCandidates(1).first?.string }
} catch {
print("Error en OCR: \(error)")
return []
}
}
TextRecognizer.swift
Paso 4: Vista principal en SwiftUI
Conectamos todo en una vista que muestra la cámara, un selector de modo y las palabras detectadas en una lista superpuesta.
import SwiftUI
struct VisionDemoView: View {
@StateObject private var camera = CameraManager()
@State private var detectedTexts: [String] = []
@State private var mode: Mode = .text
enum Mode: String, CaseIterable {
case text = "Texto"
case objects = "Objetos"
}
var body: some View {
ZStack {
CameraPreviewView(session: camera.session)
.ignoresSafeArea()
VStack {
Picker("Modo", selection: $mode) {
ForEach(Mode.allCases, id: \.self) { mode in
Text(mode.rawValue).tag(mode)
}
}
.pickerStyle(.segmented)
.padding()
.background(.ultraThinMaterial)
.cornerRadius(12)
.padding(.horizontal)
Spacer()
ScrollView {
VStack(alignment: .leading, spacing: 8) {
ForEach(detectedTexts, id: \.self) { text in
Text(text)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(.black.opacity(0.6))
.foregroundColor(.white)
.cornerRadius(8)
}
}
.padding()
}
.frame(maxHeight: 200)
}
}
.onAppear {
camera.onFrame = { sampleBuffer in
Task {
let texts = await recognizeText(from: sampleBuffer)
await MainActor.run {
self.detectedTexts = texts
}
}
}
}
}
}
VisionDemoView.swift
Variante: detección de objetos con rectángulos
Si eliges el modo Objetos, cambia el request por VNDetectRectanglesRequest para resaltar documentos, tarjetas o carteles.
func detectRectangles(from sampleBuffer: CMSampleBuffer) async -> [VNRectangleObservation] {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return [] }
let request = VNDetectRectanglesRequest()
request.minimumAspectRatio = 0.3
request.maximumAspectRatio = 1.0
request.minimumSize = 0.2
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
do {
try handler.perform([request])
return request.results as? [VNRectangleObservation] ?? []
} catch {
return []
}
}
RectangleDetector.swift
Puntos clave para el hackathon
- No olvides el Info.plist: añade
NSCameraUsageDescriptiono la app se cerrará al acceder a la cámara. - Rendimiento: en tiempo real usa
.fastenrecognitionLevel. Si el usuario toma una foto fija, cambia a.accurate. - Combínalo: después de detectar un rectángulo, recorta esa región y pásala a
VNRecognizeTextRequestpara leer solo el documento enfocado.
Recursos relacionados
Cuando hayas leído el texto, marca la lección para seguir el progreso.