DALL·E 2 y SwiftUI para editar imágenes con Inteligencia Artificial
DALL·E 2 y SwiftUI para editar imágenes con Inteligencia Artificial

DALL·E 2 en SWIFTUI y SWIFT 🤖 Creamos una app para EDITAR imágenes con Inteligencia Artificial (Alamofire)

Añadimos DALL·E 2 a nuestra app para editar imágenes. Las editamos especificando una máscara y un texto descriptivo. Creamos una View en SwiftUI para añadir la imagen desde la cámara del iPhone o la galería de imágenes. Usamos Alamofire para realizar las peticiones HTTP

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
🤩 ¡Sígueme en Twitter!
▶️ ¡Suscríbete al canal!
🎁 ¡Apoya el canal!

Aprende a editar imágenes con Inteligencia Artificial

Hoy en SwiftBeta vamos a continuar con nuestra app en SWIFTUI integrando la API de DALL·E 2. Aprenderemos a cómo editar una imagen que le proporcionaremos nosotros con un texto. Vas a ver que va a ser muy chulo, vamos a poder hacer una foto desde nuestro iPhone o seleccionar una foto de nuestra galería para poder aplicar una máscara, la máscara básicamente es para que la API de DALL·E 2 sepa qué parte de la imagen queremos modificar.

A parte de especificar la máscara, debemos indicar qué queremos que aparezca. Es decir, imagina que tienes la siguiente foto, nosotros podemos dibujar una máscara e indicar un texto descriptivo para que DALL·E 2 nos genere la imagen. La imagen solo se modificará en la zona de la máscara que hayamos especificado. Esto es muy potente y puedes modificar imágenes de una manera que ni te imaginas.

También vamos a aprender a usar Alamofire para subir imágenes y parámetros en una misma petición. Pero vamos por partes, vamos a ver muchos temas interesantes en el video de hoy.

Esta es la segunda parte, y vamos a partir del código que creamos en el anterior video, si no lo has visto te lo dejo por aquí arriba.

Pero antes, si quieres apoyar el contenido que subo cada semana, suscríbete. De esta manera seguiré publicando contenido completamente grautito en Youtube.

Creamos la vista EditView en SwiftUI

Lo primero de todo que vamos hacer es crear una vista en SwiftUI, a esta nueva vista la llamamos EditView. Y ya que estamos, vamos a organizar un poco las carpetas de nuestra app.

Ahora volvemos a nuestra vista y vamos a añadir el siguiente código:

struct EditView: View {
    @StateObject var viewModel = ViewModel()
    @State var text = ""
    
    var body: some View {
        Text("Hello")
    }
}
Creamos la View en SwiftUI

Hemos creado una instancia de nuestro ViewModel (el que creamos en el anterior video) y hemos creado una propiedad @State para recoger el texto descriptivo de la imagen que queremos generar dentro de nuestra máscara (esta propiedad la usaremos desde dentro de un TextField, exactamente igual a como vimos en el anterior video).

A continuación, vamos a crear dos propiedades, esta propiedad nos servirá para mostrar un placeholder o la imagen que haya escogido el user desde la cámara o galería de fotos.

struct EditView: View {
    @StateObject var viewModel = ViewModel()
    @State var text = ""
    @State var selectedImage: Image?
    @State var emptyImage: Image = Image(systemName: "photo.on.rectangle.angled")
    
    var currentImage: some View {
        if let selectedImage {
            return selectedImage
                .resizable()
                .scaledToFill()
                .frame(width: 300, height: 300)
        } else {
            return emptyImage
                .resizable()
                .scaledToFill()
                .frame(width: 40, height: 40)
        }
    }
    
    var body: some View {
        Text("Hello")
    }
}
Añadimos una propiedad llamada currentImage de tipo Image

Perfecto, vamos a continuar, dentro de nuestro body vamos a crear un Form, y dentro de este Form vamos a añadir un Text y un AsyncImage. Al crear el AsyncImage podemos copiar y pegar el que vimos en el anterior video. Ya que solo vamos a modificar dos lineas:

struct EditView: View {
    @StateObject var viewModel = ViewModel()
    @State var text = ""
    @State var selectedImage: Image?
    @State var emptyImage: Image = Image(systemName: "photo.on.rectangle.angled")
    
    var currentImage: some View {
        if let selectedImage {
            return selectedImage
                .resizable()
                .scaledToFill()
                .frame(width: 300, height: 300)
        } else {
            return emptyImage
                .resizable()
                .scaledToFill()
                .frame(width: 40, height: 40)
        }
    }
    
    var body: some View {
        Form {
            Text("Create a mask")
                .font(.headline)
                .padding(.vertical, 12)
            
            AsyncImage(url: viewModel.imageURL) { image in
                image
                    .resizable()
                    .scaledToFit()
                    .overlay(alignment: .bottomTrailing, content: {
                        Button {
                            self.viewModel.saveImageInGallery()
                        } label: {
                            HStack {
                                Image(systemName: "arrow.down.circle.fill")
                                    .resizable()
                                    .frame(width: 30, height: 30)
                                    .shadow(color: .black, radius: 0.2)
                            }
                            .padding(8)
                            .foregroundColor(.green)
                        }
                    })
            } placeholder: {
                VStack {
                    if !viewModel.isLoading {
                        ZStack {
                            currentImage
                        }
                    } else {
                        ProgressView()
                            .padding(.bottom, 12)
                        Text("¡Tu imagen se está generando, espera 2 segundos! 🚀")
                            .multilineTextAlignment(.center)
                    }
                }
                .frame(width: 300, height: 300)
            }
        }
    }
}
Añadimos AsyncImage a la View en SwiftUI

Fíjate que mientras no estamos haciendo una petición HTTP, estamos mostrando el placeholder o la imagen que haya seleccionado un user ya sea tomando una foto o desde la galería. Vamos a continuar con la vista.

Añadimos dependencias con SwiftPackageManager

Para ahorrarte crear la lógica de cómo abrir la cámara, escoger una foto de la galería de imágenes o dibujar en un canvas, que todo esto ya lo vimos cuando expliqué cómo crear tu primera app desde un iPad o cuando usamos filtros en fotos tomadas directamente desde el iPad. He creado un repositorio en Github que puedes importar en 3 clicks.

Tan solo debes ir a la sección de Swift Package Manager de Xcode y añadir la URL del repositorio https://github.com/SwiftBeta/HelpersTutorialDALLE2

GitHub - SwiftBeta/HelpersTutorialDALLE2
Contribute to SwiftBeta/HelpersTutorialDALLE2 development by creating an account on GitHub.
Añadimos dependencia de un repositorio de SwiftBeta
Añadimos dependencia de un repositorio de SwiftBeta

Una vez añadido el paquete. Vamos a importarlo en nuestra vista EditView y vamos a continuar.

Vamos a crear dos propiedades nuevas en nuestra vista:

    @State var showCamera: Bool = false
    @State var showGallery: Bool = false
Creamos dos propiedades para mostrar la cámara o la galería

Y vamos a crear un HStack justo a continuación del AsyncImage, en este HStack vamos a añadir 2 Buttons, uno para mostrar la cámara de fotos y el segundo para seleccionar una foto desde la galería:

HStack {
    Button {
        showCamera.toggle()
    } label: {
        Text("📷 Take a photo!")
    }
    .tint(.orange)
    .buttonStyle(.borderedProminent)
    .fullScreenCover(isPresented: $showCamera) {
        CameraView(selectedImage: $selectedImage)
    }
    .padding(.vertical, 12)
    
    Spacer()
    
    Button {
        showGallery.toggle()
    } label: {
        Text("Open Gallery")
    }
    .tint(.purple)
    .buttonStyle(.borderedProminent)
    .fullScreenCover(isPresented: $showGallery) {
        GalleryView(selectedImage: $selectedImage)
    }
    .padding(.vertical, 12)
}
Creamos Buttons para mostrar la cámara o la galería

Si observamos el canvas vemos como aparecen los dos Buttons. Podemos interactuar con ellos y ver qué ocurre. El primer abre una cámara, que en el Canvas no funciona, si queremos tomar una foto deberíamos compilar en un device real, y el segundo Button abre la galería de imágenes. Si seleccionamos una imagen nos aparece en la vista de EditView.

Si quieres saber cómo funciona el poder abrir la cámara de fotos, te invito a que veas este video del canal donde lo explico detalladamente.

Video explicando como funciona el Canvas

Una vez hemos visto que podemos obtener una imagen, ya sea desde la cámara o desde la galería de imágenes. Vamos con el siguiente paso. A continuación del HStack, vamos a crear el TextField con el que enviaremos la descripción de la imagen que queremos generar:

TextField("Añade un texto la IA generará una imagen",
                      text: $text,
                      axis: .vertical)
            .lineLimit(10)
            .lineSpacing(5)
Creamos TextField para poder añadir el texto descriptivo (prompt)

Muy sencillo, vamos a crear nuestro último HStack con un Button para que lance la acción al ViewModel y se envíe la petición HTTP. A continuación del TextField vamos a crear el HStack

HStack {
    Spacer()
    Button("🪄 Generate Image") {
        // TODO
    }
    .buttonStyle(.borderedProminent)
    .disabled(viewModel.isLoading)
}
.padding(.vertical, 12)
Creamos Button para generar la imagen

La lógica que debe implementarse cuando este Button sea pulsado es muy sencilla, debe:

  • Obtener la imagen original
  • Obtener el texto descriptivo para generar la imagen
  • Obtener la máscara

Los dos primeros valores ya los podríamos obtener, pero necesitamos el último. Necesitamos dibujar la máscara y obtenerla al pulsar el Button. Para hacerlo, usamos una vista llamada SwiftBetaCanvas que está dentro del repositorio que has añadido al proyecto con Swift Package Manager.

Vamos a añadir esta vista dentro del placeholder de la vista AsynImage:

if !viewModel.isLoading {
        ZStack {
            currentImage
            SwiftBetaCanvas(lines: $lines, currentLineWidth: 30)
        }
}
Añadimos la vista SwiftBetaCanvas

Al añadir este código tendrás un error, no te preocupes que lo vamos a solucionar ahora, vamos a la sección de las propiedades y añadimos:

@State var lines: [Line] = []
Propiedad para poder inicializar la vista SwiftBetaCanvas

Ahora si vamos al Canvas, vemos que podemos dibujar encima de la foto original. Ahora ya tenemos todos lo que necesitamos, vamos al Button donde hemos añadido el TODO y aquí dentro vamos a usar ImageRender para poder crear imágenes de nuestras vistas en SwiftUI. Vamos a crear una imagen de la imagen original, y una imagen aplicando una máscara a la imagen original:

Button("🪄 Generate Image") {
    isFocused = false
    let selectedImageRenderer = ImageRenderer(content: currentImage)
    let maskRenderer = ImageRenderer(content: currentImage.reverseMask { SwiftBetaCanvas(lines: $lines, currentLineWidth: 30) })
    
 	// TODO
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
Usamos ImageRender para poder crear imágenes de nuestras vistas en SwiftUI

Una vez tenemos las imágenes necesarias, vamos a obtener los datos de la imagen en PNG

Button("🪄 Generate Image") {
    isFocused = false
    let selectedImageRenderer = ImageRenderer(content: currentImage)
    let maskRenderer = ImageRenderer(content: currentImage.reverseMask { SwiftBetaCanvas(lines: $lines, currentLineWidth: 30) })
    
    Task {
        guard let selecteduiImage = selectedImageRenderer.uiImage,
              let selectedPNGData = selecteduiImage.pngData(),
              let maskuiImage = maskRenderer.uiImage,
              let maskPNGData = maskuiImage.pngData() else {
            return
        }
        // TODO llamar al viewModel
    }
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
Creamos los datos necesarios para hacer la petición HTTP

Ahora, nos vamos al ViewModel y vamos a crear un método que tenga como parámetros la foto original, las máscara y el prompt (el texto del TextField)

Dentro de nuestro ViewModel, añadimos la siguiente firma de método:

func generateEdit(withText text: String, imageData: Data, maskData: Data)
Firma del método que realizará la petición HTTP a openAI

Para entender todo lo que tenemos que enviar, vamos a revisar el endpoint que vamos a usar, vamos a visitar esta URL https://beta.openai.com/docs/guides/images/usage

Una vez entendemos qué es lo que tenemos que enviar en nuestra nueva peticieon HTTP, vamos a continuar.

Pero esta vez, como te decía al principio del video, en lugar de usar URLSession como en el anterior video. En este caso vamos a usar Alamofire y te voy a enseñar a cómo subir dos imágenes y varios parámetros.

Lo siguiente que vamos hacer, para poder usar Alamofire, vamos a tener que usar Swift Package Manager. Pero antes debemos saber la URL del repositorio de Github, puedes ir a google y poner github alamofire y saldrá el primer resultado.

GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift
Elegant HTTP Networking in Swift. Contribute to Alamofire/Alamofire development by creating an account on GitHub.
Repositorio Alamofire

Para añadir esta dependencia, repetimos el mismo proceso. Nos vamos a la sección de Xcode de Swift Package Manager. Y una vez añadida, importas el nuevo Framework en la class ViewModel.

OpenAI API
An API for accessing new AI models developed by OpenAI
Documentación de la API de openAI

Una vez sabemos la URL voy a añadir código y voy a comentarlo a continuación.

func generateEdit(withText text: String, imageData: Data, maskData: Data) {
        let url = URL(string: "https://api.openai.com/v1/images/edits")! // 1
        let headers = HTTPHeaders(["Authorization": "Bearer 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"]) // 2
        
        let dictionary: [String : String] = [ // 3
            "n": "1",
            "size": "1024x1024",
            "prompt": text
        ]
Añadimos los headers y parámetros necesarios para realizar la petición HTTP
  1. Creamos la URL con el endpoint al que vamos a llamar
  2. Creams el header con nuestro API_TOKEN (¿no sabes como se crea? Eso es porque no has visto la primera parte. Echa un vistazo que lo explico paso por paso)
  3. Parámetros que necesitamos enviar. Son los mismos parámetros que enviábamos en el anterior video. n es el número de resultados que queremos obtener (solo queremos recibir una foto), size es el tamaño de la imagen que recibiremos, y prompt es el texto descriptivo que obtenemos de nuestro TextField de la vista.

Vamos a continuar añadiendo lógica:

DispatchQueue.main.async {
    self.isLoading = true
}
Cambiamos el valor de la propiedad isLoading para mostrar un ProgressView

El siguiente código que añado es para mostrar un ProgressView mientras realizamos una petición HTTP a la API de DALL·E 2.

Y ahora sí vamos a crear la petición HTTP usando Alamofire, vamos a ver la primera parte:

AF.upload(multipartFormData: { multipartFormData in
    for (key, value) in dictionary {
        if let data = value.data(using: .utf8) {
            multipartFormData.append(data, withName: key)
        }
    }
    multipartFormData.append(imageData, withName: "image", fileName: "image.png", mimeType: "image/png")
    multipartFormData.append(maskData, withName: "mask", fileName: "mask.png", mimeType: "image/png")
}, to: url,
          headers:  headers)
Usamos Alamofire para enviar los parámetros y las imágenes

Por cada parámetro del dictionar que hemos visto antes, lo añadimos como data. Y también añadimos la data de la imagen original y la data de la máscara. Fíjate que también le doy el nombre de image y mask respectivamente.

A continuación añado la url y los headers. Ahora solo falta controlar el resultado de esta petición HTTP.

AF.upload(multipartFormData: { multipartFormData in
    for (key, value) in dictionary {
        if let data = value.data(using: .utf8) {
            multipartFormData.append(data, withName: key)
        }
    }
    multipartFormData.append(imageData, withName: "image", fileName: "image.png", mimeType: "image/png")
    multipartFormData.append(maskData, withName: "mask", fileName: "mask.png", mimeType: "image/png")
}, to: url,
          headers:  headers)
.responseDecodable(of: ModelResponse.self, completionHandler: { dataResponse in
    let model = try! JSONDecoder().decode(ModelResponse.self, from: dataResponse.data!)
    DispatchQueue.main.async {
        self.isLoading = false
        guard let firstModel = model.data.first else {
            return
        }
        self.imageURL = URL(string: firstModel.url)
    }
})
Al obtener la respuesta de la petición HTTP parseamos los datos a un modelo de nuestro dominio

En la respuesta mapeo el JSON a un modelo llamado ModelResponse que creamos en el primer video. Y cambiamos al hilo principal para quitar el ProgressView de la vista y asignamos a la propiedad imageURL la url recibida en el JSON. De esta manera cuando recibamos la respuesta de la petición HTTP, automáticamente se verá reflejado en la vista.

Ahora, para poder usar este método, vamos a llamarlo cuando pulsemos el Button de nuestra vista.

Button("🪄 Generate Image") {
    isFocused = false
    let selectedImageRenderer = ImageRenderer(content: currentImage)
    let maskRenderer = ImageRenderer(content: currentImage.reverseMask { SwiftBetaCanvas(lines: $lines, currentLineWidth: 30) })
    
    Task {
        guard let selecteduiImage = selectedImageRenderer.uiImage,
              let selectedPNGData = selecteduiImage.pngData(),
              let maskuiImage = maskRenderer.uiImage,
              let maskPNGData = maskuiImage.pngData() else {
            return
        }
        self.viewModel.generateEdit(withText: text,
                                    imageData: selectedPNGData,
                                    maskData: maskPNGData)
    }
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
Conectamos la vista en SwiftUI con nuestro ViewModel

Ahora sí, ya podemos probar nuestra app:

  • Vamos al Canvas
  • Seleccionamos una imagen de la galería del simulador
  • Aplicamos una máscara
  • Escríbimos un prompt en el TextField indicando qué queremos que aparezca en la máscara
  • Pulsamos el Button de enviar

Y al darle al Button y esperar unos segundos, obtenemos una respuesta con la imagen generada por la IA de DALL·E 2 🚀

Ahora, para mejorar un poco nuestra app vamos hacer que sea posible escoger otra foto. Para hacerlo vamos a crear un Button con title Reset.

Y lo vamos a añadir a continuación del de generar la imagen:

Button("Reset") {
    viewModel.imageURL = nil
    selectedImage = nil
    lines.removeAll()
}
.tint(.red)
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
Creamos un Button para resetear 

Y para los que estéis probando la app en un dispositivo real, vamos a añadir una propiedad @FocusState para dismissear el teclado una vez le demos al Button de Generar.

Vamos arriba a la sección de propiedades y añadimos"

@FocusState var isFocused: Bool
Creamos propiedad @FocusState para dismissear el teclado

Añadimos el modificador al TextField"

TextField("Añade un texto la IA generará una imagen",
          text: $text,
          axis: .vertical)
.lineLimit(10)
.lineSpacing(5)
.focused($isFocused)
Asignamos la propiedad al modificador focused

Y setteamos su valor una vez se pulse el Button de generar:

HStack {
  Spacer()
  Button("🪄 Generate Image") {
      isFocused = false
      let selectedImageRenderer = ImageRenderer(content: currentImage)
      ...
}
Dismisseamos el teclado al pulsar el Button que realiza la petición HTTP

Y ahora sí, nuestra app está lista para probarse tanto en devices físicos como el simulador.

Para finalizar, vamos a nuetra vista ContentView y añadimos un nuevo Tab

struct ContentView: View {
    var body: some View {
        TabView {
            GenerateView()
                .tabItem {
                    Image(systemName: "wand.and.stars.inverse")
                    Text("Generate")
                }
            EditView()
                .tabItem {
                    Image(systemName: "scribble.variable")
                    Text("Edit")
                }
        }
    }
}
Creamos una segunda vista en nuestro TabView

Ya podemos compilar nuestra app y probar tanto el video anterior, como el que acabamos de crear hoy.

Vamos a probar de usar estas dos frases en dos imágenes del simulador diferentes.

"Add a green lemon"
"Add Tesla with doors open"

y hasta aquí el video de hoy!

Conclusión

Hoy hemos aprendido a cómo editar una imagen con Inteligencia Artificial usando DALL·E 2. Para hacerlo, hemos creado una app para poder selecciona una imagen desde la cámara del iPhone o desde la galería de imágenes. Una vez seleccionada la imagen hemos podido añadir una máscara. Dentro de esta máscara es donde la Inteligencia Artificial modificará nuestra imagen y para hacerlo hay que enviar también un texto descriptivo.

Para realizar peticiones HTTP hemos usado Alamofire.