Índice de contenidos
1. Introducción
CoreML es un framework disponible para las últimas versiones de la mayoría de los dispositivos de Apple y que permiten integrar modelos entrenados de machine learning en las Apps, apoyándose en primitivas de bajo nivel como Accelerate y BNNS y en la GPU por medio de metal.
A su vez, los frameworks Vision (análisis de imagenes), Foundation (para el procesado de lenguaje natural) y GameplayKit (para la evaluación de árboles de toma de decisiones) se apoyan en CoreML.
Además, los últimos SoC A11 Bionic que monta la última generación de iPhone (de momento) contiene hardware específico para el uso de redes neuronales que hace que CoreML sea más eficiente aún.
Podéis clonar el proyecto desde aquí https://github.com/DaniOtero/VisionCoreMLDemo
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 15’ (2.5 GHz Intel Core i7, 16 GB 1600 MHz DDR3)
- Sistema Operativo: Mac OS X Sierra 10.13.3
- Xcode 9.2
- iOS 11.2.6
3. Sobre los modelos
CoreML funciona mediante modelos entrenados de machine learning. Estos modelos tienen formato específico (mlmodel) para que puedan ser utilizados por CoreML. Apple dispone de una página con información disponible para los desarrolladores sobre cómo construir sus propios modelos bien sea desde cero o importándolos desde herramientas de terceros.
Así mismo también ofrece algunos modelos para poder empezar a trabajar con ellos directamente (aunque casi todos para el análisis de imágenes).
Toda esta información está disponible en https://developer.apple.com/machine-learning/
4. Análisis de la imagen
Bueno, vamos al lío. Para este tuto vamos a utilizar el framework Vision para analizar imágenes y detectar las caras, y dentro de ellas intentar detectar la posición de los ojos y la boca.
Para ello utilizaremos una vista muy simple, con un botón que tras pulsarlo nos permitirá bien capturar una foto, cargarla de las existentes o usar una de demo.
Una vez tenemos una UIImage, podemos iniciar el análisis. Creamos un request handler, en este caso de tipo VNImageRequestHandler al que se le especifica la información de la imagen e iniciamos el análisis invocando el método perform
func startProcessingImage(image: UIImage) {
self.selectedImage = image
self.loadingView.isHidden = false
let handler = VNImageRequestHandler(cgImage: image.cgImage!, options: [:])
DispatchQueue.global().async {
try! handler.perform([self.request])
}
}
Mediante la request le indicamos a Vision que queremos analizar en la imagen. Vision nos permite analizar varias cosas: caras, texto, códigos de barras, detección del horizonte, clasificación…
Para el caso concreto del reconocimiento de caras, no es necesario proporcionarle un modelo a la request ya que Vision internamente utiliza los propios desarrollados por Apple y que lleva usando desde varias versiones anteriores a iOS 11. Si quisiéramos detectar gatos por poner un ejemplo, habría que utilizar una request de tipo VNCoreMLRequest, y habría que proporcionarle un modelo entrenado que pueda reconocer gatos.
Para el caso que nos atañe, la request sería tan simple como:
let request = VNDetectFaceLandmarksRequest { [weak self] request, error in
guard let results = request.results as? [VNFaceObservation] else {
return
}
self?.processResults(results: results)
}
Y ya está, con tan solo esas pocas líneas de código podemos detectar caras ?
5. Obtención de resultados
En la lista de resultados VNFaceObservation, podemos obtener tanto el área donde se han detectado caras así como el contorno de ojos, labios, nariz, etc.
Mediante la propiedad boundingBox obtendremos el área de la cara. Hay que tener cierto cuidado, porque no utiliza el mismo sistema de coordenadas que UIKit. Mientras en UIKit el punto (0,0) se encuentra en la esquina superior izquierda, el punto (0,0) del boundingBox se encontraría en la esquina inferior izquierda.
Además, utiliza coordenadas relativas en vez de absolutas (rango [0,1], de forma que el punto central de la imagen aparecería referido como (0.5,0.5)).
Para ello puede ser útil crearse una extensión de CGRect que se encargue de aplicar las transformaciones.
extension CGRect {
static func *(left: CGRect, right: CGSize) -> CGRect {
let width = right.width
let height = right.height
return CGRect(x: left.origin.x * width, y: left.origin.y * height, width: left.size.width * width, height: left.size.height * height)
}
func toUIKitRect(totalHeight: CGFloat = 1) -> CGRect {
return CGRect(x: self.origin.x, y: totalHeight - self.origin.y - self.size.height, width: self.size.width, height: self.size.height)
}
Además del boundingBox también está la propiedad landmarks que contiene las detecciones de las distintas partes de la cara como una colección de puntos con el contorno de las mismas.
Para presentar los resultados, simplemente vamos a pintar recuadros a las distintas regiones detectadas sobre la imagen base. El código tendría una pinta tal que así:
func processResults(results: [VNFaceObservation]) {
self.drawNormalizedRectsOverImage(rects: results.map {$0.boundingBox})
let allLandmarks = results.flatMap {$0.landmarks}
self.drawLandMarkRegion(regions:self.getEyes(landmarks: allLandmarks), color: .yellow)
self.drawLandMarkRegion(regions:self.getMouths(landmarks: allLandmarks), color: .green)
}
func drawNormalizedRectsOverImage(rects: [CGRect], color: UIColor = UIColor.red) {
drawOverImage(rects: rects.map {$0.toUIKitRect() * selectedImage.size})
}
func drawOverImage(rects: [CGRect], color: UIColor = UIColor.red) {
let imageRect = CGRect(x: 0, y: 0, width: selectedImage.size.width, height: selectedImage.size.height)
UIGraphicsBeginImageContext(selectedImage.size)
self.selectedImage.draw(in: imageRect)
rects.forEach { rect in
let ctx = UIGraphicsGetCurrentContext()
ctx?.addRect(rect)
ctx?.setLineWidth(5)
ctx?.setStrokeColor(color.cgColor)
ctx?.drawPath(using: .stroke)
}
let imageResult = UIGraphicsGetImageFromCurrentImageContext()
self.selectedImage = imageResult!
DispatchQueue.main.async {
self.loadingView.isHidden = true
self.imageView.image = imageResult
}
}
func drawLandMarkRegion(regions: [VNFaceLandmarkRegion2D], color: UIColor) {
let rects = regions.flatMap {CGRect.boundingBox(points: $0.pointsInImage(imageSize: self.selectedImage.size))?.toUIKitRect(totalHeight: self.selectedImage.size.height)}
drawOverImage(rects: rects, color:color)
}
func getEyes(landmarks: [VNFaceLandmarks2D]) -> [VNFaceLandmarkRegion2D] {
var eyes = landmarks.flatMap {$0.leftEye}
eyes.append(contentsOf: landmarks.flatMap {$0.rightEye})
return eyes
}
func getMouths(landmarks: [VNFaceLandmarks2D]) -> [VNFaceLandmarkRegion2D] {
return landmarks.flatMap {$0.outerLips}
}
Y tendríamos como resultado algo como esto:
P.D: Es irónico, gracias a Vision hay que tirar más código para "pintar" los resultados que para todo el análisis de la imagen ?.
6. Conclusiones
Como se puede observar en la imagen con el resultado de la ejecución, obviamente no es perfecto, ni mucho menos. El contorno de la cara más o menos acierta en casi con todas las caras de la imagen de demo, pero la posición de los ojos y labios a veces no es capaz de determinarla correctamente, sin embargo el resultado global es bastante aceptable.
Con el auge del uso de IA y machine learning el poder disponer de una API sencilla que nos permita añadir una ligera capa de inteligencia a nuestras apps sin entrar en profundidad a la implementación de los modelos de redes neuronales creo que hará que cada vez sean capaces de hacer cosas que hasta ahora jamás habíamos imaginado.