Detectar objetos con la cámara del iPhone sonaba hace unos años a proyecto de investigación. Hoy, con YOLOv8 y Core ML, lo tienes funcionando en tu app en una tarde, sin servidores y sin que las imágenes salgan del teléfono.
En este tutorial vamos paso a paso: exportar el modelo desde Python, añadirlo a Xcode, capturar la cámara, pasar cada fotograma por Vision y dibujar las cajas en pantalla. También veremos qué hacer para que el teléfono no se caliente ni se coma la batería.
¿Por qué combinar YOLOv8 con Core ML y Vision Framework?
Tradicionalmente, realizar inferencia de modelos pesados requería enviar fotogramas a un servidor mediante APIs REST o WebSockets. Este enfoque introduce latencia, costes de infraestructura elevados y riesgos para la privacidad del usuario.
La combinación del hardware de Apple con su software nativo resuelve estos problemas:
- Aprovechamiento del Apple Neural Engine (ANE): Core ML delega automáticamente las operaciones matriciales complejas al silicio dedicado de Apple, liberando la CPU y la GPU.
- Privacidad garantizada (Privacy-by-Design): Las imágenes nunca abandonan el dispositivo del usuario, cumpliendo estrictamente con normativas como el RGPD.
- Gestión inteligente de memoria y orientación con Vision: El framework Vision de Apple se encarga de redimensionar los búferes de píxeles, gestionar el espacio de color y ajustar las coordenadas espaciales sin código redundante.
Paso 1: Exportar YOLOv8 a formato Core ML (.mlpackage)
Para que iOS pueda interpretar los pesos entrenados de PyTorch, primero debemos exportar el modelo al formato nativo de Apple: .mlpackage.
Ultralytics simplifica este proceso enormemente a través de su librería oficial de Python. Es recomendable utilizar la variante YOLOv8n (nano) o YOLOv8s (small) para garantizar una latencia inferior a los 15 ms en iPhones recientes.
Abre tu terminal o un entorno Jupyter Notebook y ejecuta el siguiente script:
from ultralytics import YOLO
# 1. Cargar el modelo preentrenado (o tu modelo personalizado)
model = YOLO("yolov8n.pt")
# 2. Exportar a Core ML con Non-Maximum Suppression (NMS) integrado
model.export(
format="coreml",
nms=True, # Integra la supresión de no máximos directamente en el grafo
imgsz=640, # Tamaño de entrada cuadrado estándar
half=True # Precisión float16 para optimizar uso del Apple Neural Engine
)
Nota técnica clave: Incluir el argumento nms=True es muy recomendable. Esto evita tener que escribir algoritmos complejos de filtrado de cuadros delimitadores (bounding boxes) manualmente en Swift, haciendo que el modelo devuelva directamente las coordenadas finales y las etiquetas asociadas.
El archivo resultante (yolov8n.mlpackage) será el que importaremos en nuestro proyecto móvil.
Paso 2: Configuración del proyecto en Xcode
Una vez generado el archivo del modelo, prepara tu entorno de desarrollo en Xcode:
- Crea un nuevo proyecto en Xcode seleccionando la plantilla iOS App (usando SwiftUI o UIKit).
- Asegúrate de configurar la clave
NSCameraUsageDescriptionen el archivoInfo.plistpara solicitar los permisos de acceso a la cámara. - Arrastra y suelta el archivo
yolov8n.mlpackagedentro del navegador de proyectos de Xcode.
Al hacer clic sobre el modelo importado en Xcode, la interfaz mostrará sus metadatos: la arquitectura, el tamaño de entrada esperado (por ejemplo, Image (Color 640 x 640)) y las salidas generadas (confidence y coordinates). Xcode compilará automáticamente una clase Swift fuertemente tipada con el nombre del modelo.
Paso 3: Captura de vídeo en tiempo real con AVFoundation
Para alimentar el modelo, necesitamos una sesión de captura de vídeo continua. Utilizaremos AVCaptureSession configurada para emitir fotogramas en formato CVPixelBuffer.
Crea un controlador de cámara que implemente el delegado AVCaptureVideoDataOutputSampleBufferDelegate:
import AVFoundation
import Vision
class CameraManager: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
private let captureSession = AVCaptureSession()
var onFrameCaptured: ((CVPixelBuffer) -> Void)?
func setupCamera() {
captureSession.sessionPreset = .hd1280x720
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
let input = try? AVCaptureDeviceInput(device: device) else { return }
let output = AVCaptureVideoDataOutput()
output.alwaysDiscardsLateVideoFrames = true
output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera.frame.queue"))
if captureSession.canAddInput(input) { captureSession.addInput(input) }
if captureSession.canAddOutput(output) { captureSession.addOutput(output) }
// startRunning() bloquea el hilo: Apple recomienda llamarlo fuera del hilo principal
DispatchQueue.global(qos: .userInitiated).async { [captureSession] in
captureSession.startRunning()
}
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
onFrameCaptured?(pixelBuffer)
}
}
Paso 4: Pipeline de detección con Vision Framework
Esta es la parte importante. En lugar de interactuar directamente con la clase de Core ML, encapsulamos el modelo dentro de una petición de Vision (VNCoreMLRequest). Esto optimiza la conversión de formatos de imagen entre la cámara y la red neuronal.
import Vision
import CoreML
class ObjectDetector {
private var visionModel: VNCoreMLModel?
var onDetectionsUpdated: (([VNRecognizedObjectObservation]) -> Void)?
init() {
setupModel()
}
private func setupModel() {
do {
let config = MLModelConfiguration()
config.computeUnits = .all // Permite alternar entre CPU, GPU y Neural Engine
let coreMLModel = try yolov8n(configuration: config).model
self.visionModel = try VNCoreMLModel(for: coreMLModel)
} catch {
print("Error cargando el modelo Core ML: \(error)")
}
}
func detect(pixelBuffer: CVPixelBuffer) {
guard let visionModel = self.visionModel else { return }
let request = VNCoreMLRequest(model: visionModel) { (request, error) in
guard let results = request.results as? [VNRecognizedObjectObservation] else { return }
DispatchQueue.main.async {
self.onDetectionsUpdated?(results)
}
}
// .scaleFill estira el fotograma hasta el tamaño de entrada del modelo (640x640).
// Es rápido y las cajas siguen alineadas con la imagen original, aunque la deforma un poco;
// .scaleFit conserva la proporción añadiendo bandas.
request.imageCropAndScaleOption = .scaleFill
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: .right, options: [:])
try? handler.perform([request])
}
}
Para conectar las dos piezas basta con pasar cada fotograma de la cámara al detector:
let camara = CameraManager()
let detector = ObjectDetector()
// Cada fotograma de la cámara pasa al detector
camara.onFrameCaptured = { pixelBuffer in
detector.detect(pixelBuffer: pixelBuffer)
}
// Las detecciones llegan ya en el hilo principal
detector.onDetectionsUpdated = { observaciones in
for obs in observaciones {
guard let etiqueta = obs.labels.first else { continue }
print(etiqueta.identifier, "\(Int(etiqueta.confidence * 100))%")
}
}
camara.setupCamera()
Para mostrar la imagen de la cámara en pantalla, añade un AVCaptureVideoPreviewLayer con la misma captureSession (en SwiftUI, envuelto en un UIViewRepresentable).
Paso 5: Renderizar las detecciones en la interfaz
Vision Framework devuelve las coordenadas normalizadas entre 0.0 y 1.0, utilizando un sistema de coordenadas donde el origen (0,0) se sitúa en la esquina inferior izquierda (a diferencia del sistema clásico de iOS, que inicia en la superior izquierda).
Para dibujar los recuadros delimitadores (bounding boxes):
- Invertir el eje Y: Transforma la coordenada vertical para alinearla con UIKit o SwiftUI (
1.0 - y - height). - Escalar a la vista: Multiplica las coordenadas normalizadas por el ancho y alto del contenedor (
UIViewoGeometryReader). - Extraer etiquetas: Lee la primera observación de
observation.labelspara obtener la clase con mayor confianza (por ejemplo, «person: 92%»; el modelo preentrenado usa las 80 clases de COCO en inglés).
Los dos primeros pasos se resumen en una pequeña función:
import CoreGraphics
// Convierte el boundingBox de Vision (normalizado y con origen abajo) a coordenadas de la vista
func rectEnVista(_ boundingBox: CGRect, size: CGSize) -> CGRect {
CGRect(x: boundingBox.minX * size.width,
y: (1 - boundingBox.maxY) * size.height,
width: boundingBox.width * size.width,
height: boundingBox.height * size.height)
}
Ten en cuenta que, si la vista previa usa .resizeAspectFill, parte de la imagen queda recortada y tendrás que aplicar ese mismo recorte a las cajas para que coincidan exactamente.
Buenas prácticas para optimizar el rendimiento en producción
Implementar visión artificial en dispositivos móviles exige cuidar la autonomía del dispositivo y el control térmico. Aplica estas optimizaciones clave:
- Frame Throttling: No es necesario procesar todos los fotogramas a 60 FPS si tu caso de uso funciona a 30 o 15 FPS. Descartar fotogramas alternos reduce drásticamente el consumo de batería.
- Cuantización INT8: Si el modelo pesa demasiado, evalúa cuantizar los pesos a INT8 durante la exportación de Core ML. El tamaño del modelo se reducirá a la mitad con una pérdida mínima de mAP (mean Average Precision).
- Análisis de energía con Instruments: Utiliza la plantilla Core ML de Instruments (o el informe Performance del modelo en Xcode) para verificar que tu modelo se esté ejecutando en el Neural Engine y no sobrecargando la CPU.
Errores típicos que conviene evitar
- Orientación incorrecta. Si las detecciones no tienen sentido, revisa la orientación que pasas a
VNImageRequestHandler. - Procesar todos los fotogramas. Analizar 15 fotogramas por segundo suele ser suficiente y gasta mucha menos batería.
- Empezar con un modelo grande. Prueba primero con YOLOv8n; solo si la precisión no te llega pasa a una variante mayor.
- Probar en el simulador. No tiene cámara ni Neural Engine: el rendimiento real solo lo verás en un iPhone.
Conclusión
Lo más difícil de este tipo de proyectos no es el código, sino elegir bien el modelo y el caso de uso. Empieza con YOLOv8n y las 80 clases que trae de serie para validar la idea. Si funciona, entrena tu propio modelo con tus objetos (productos, piezas, plantas…) con la misma librería de Ultralytics y sustituye el archivo en Xcode.
Y prueba siempre en un iPhone real, a ser posible no el más nuevo: es donde verás si la app va fluida para la mayoría de tus usuarios.
Artículos relacionados: Generación de imágenes local en iPhone con Stable Diffusion y Core ML · Cómo ejecutar Whisper en iOS con Swift: Transcripción de audio offline y en tiempo real · Modelos locales en iOS: Cómo integrar LLMs en tu App usando Core ML y Swift