Swift에서 DALL-E API를 사용하는 방법 — 얼굴 생성

Nov 27 2022
OpenAI의 API로 기존 이미지에서 얼굴을 편집하는 SwiftUI 앱을 빌드합니다. 얼굴을 가리기 위해 Vision 얼굴 감지 요청을 사용합니다. OpenAI 팀은 최근 공개 베타에서 DALL-E API를 출시했습니다.

OpenAI의 API로 기존 이미지에서 얼굴을 편집하는 SwiftUI 앱을 빌드합니다. Vision 얼굴 감지 요청을 사용하여 얼굴을 마스킹합니다.

OpenAI 팀은 최근 공개 베타에서 DALL-E API를 출시했습니다. 작성 시점에 다음 세 가지 API 엔드포인트를 사용할 수 있습니다.

  • /generations — 텍스트 프롬프트에서 이미지 생성
  • /edits — 영역을 마스킹하여 텍스트 프롬프트를 기반으로 원본 이미지 편집
  • /variations — 이미지의 변형 생성

API에 대해 자세히 알아보고 Python 및 Node.js 구현에 대해 알아보려면 여기에서 OpenAI의 참조 가이드를 읽어보세요 . CURL 구현을 사용하여 Swift에서 URLRequest를 빌드합니다. 그 동안 콘솔 에서 OpenAI API 키를 생성하십시오 . Swift를 사용하여 애플리케이션을 실행하려면 이것이 필요합니다.

우리의 목표

다음은 OpenAI 기반 SwiftUI 애플리케이션을 구축하는 프로세스입니다.

  • generation서로 다른 API(즉 및 )를 실행하기 위해 두 개의 탭에 걸쳐 콘텐츠가 있는 SwiftUI TabView를 생성합니다 edits.
  • Vision 프레임워크를 활용하여 VNDetectFaceRectanglesRequest얼굴을 감지하고 잘라서 /edits끝점에 대한 마스크 이미지를 생성합니다.
  • async/await와 함께 Swift를 사용 URLSession하여 API 요청을 실행합니다. MultipartFormDataRequestmulti-part/form-data 요청을 통해 이미지 데이터를 업로드할 수 있도록 준비하겠습니다 .

enum OpenAIEndpoint: String{
    
    private var baseURL: String { return "https://api.openai.com/v1/images/" }
    
    case generations
    case edits
    
    var url: URL {
            guard let url = URL(string: baseURL) else {
                preconditionFailure("The url is not valid")
            }
            return url.appendingPathComponent(self.rawValue)
    }
}

Swift에서 Generations API 호출

ObservableObjectAPI에서 스토리를 가져와서 SwiftUI 인터페이스로 전달 하는 클래스를 설정해 보겠습니다 .

class OpenAIService: ObservableObject{
    
    let api_key_free = "<INSERT_API_KEY_HERE>"
    
    func generateImage(from prompt: String) async throws -> [Photo]{
        var request = URLRequest(url: OpenAIEndpoint.generations.url)
        request.setValue("Bearer \(api_key_free)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        
        let parameters: [String: Any] = [
            "prompt": prompt,
            "n": 1,
            "size": "256x256"
        ]
        
        let jsonData = try? JSONSerialization.data(withJSONObject: parameters)
        
        request.httpBody = jsonData

        let (data, response) = try await URLSession.shared.data(for: request)
        let dalleResponse = try? JSONDecoder().decode(DALLEResponse.self, from: data)
        
        return dalleResponse?.data ?? []

    }
}

  • URLSession(및 건너뛰기 완료 처리기)과 함께 새로운 async/await 구문을 사용하기 위해 generateImage함수를 로 표시했습니다 .async throws
  • 우리 URLRequest는 두 개의 헤더 필드와 JSON 본문으로 구성됩니다.
  • 우리는 데이터를 잡고 구조체 JSONDecoder()로 디코딩합니다. DALLEResponse의 데이터 필드에는 DALLEResponseURL 문자열 배열이 있습니다. 우리는 결국 SwiftUI에서 [Photo]하나 이상의 목록을 채우기 위해 이 배열을 반환할 것입니다.AsyncImage

struct DALLEResponse: Decodable {
    let created: Int
    let data: [Photo]
}

struct Photo: Decodable {
    let url: String
}

struct ContentView: View {
    
    @ObservedObject var fetcher = OpenAIService()
    @State var photos : [Photo] = []
    @State private var textPrompt: String = ""

    var body: some View {
        VStack {

            List {
               ForEach(photos, id: \.url) { photo in

                   AsyncImage(url: URL(string: photo.url)) { image in
                               image
                                   .resizable()
                                   .aspectRatio(contentMode: .fit)
                           } placeholder: {
                               Image(systemName: "photo.fill")
                           }.frame(maxWidth: .infinity, maxHeight: 500)
                       .listRowInsets(.init(.zero))
               }
            }

            TextField("Enter the prompt", text: $textPrompt)
                        .textFieldStyle(RoundedTextFieldStyle())
                        .padding()
            
            Button(action: runOpenAIGeneration, label: {Text("Generate Text From Prompt")})
        }
        .padding()
    }
    
    func runOpenAIGeneration(){
        Task{
            do{
                self.photos = try await fetcher.generateImage(from: textPrompt)
            }catch(let error){
                print(error)
            }
        }
    }
}

Edit Image API 및 Vision Framework를 사용하여 얼굴 생성

원본 이미지를 편집하고 외부 그림을 만드는 기능은 사용하기에 더 흥미로운 API입니다(제 생각에는!). 얼굴을 바꾸는 것부터 다양한 패션 복장을 생성하는 것까지 이미지의 영역을 마스킹하고 프롬프트를 사용하여 새 이미지를 설명함으로써 할 수 있는 일이 너무 많습니다.

이 섹션에서는 Vision프레임워크를 사용하여 얼굴 감지 요청을 실행하고, 감지된 얼굴에 경계 상자를 그리고, Core Graphics를 사용하여 마스크된 이미지를 생성하기 위해 해당 영역을 지우고, 새 이미지를 생성하기 위해 멀티파트 요청을 실행합니다.

Vision 요청을 설정하여 시작하겠습니다.

비전 얼굴 감지기 요청 설정

다음은 a를 시작하고 수행하는 코드입니다 VNDetectFaceRectanglesRequest.

class VisionRequester: ObservableObject{
    
    @Published var maskedImage: UIImage?
    //1 - @Published
    var faceRect: VNFaceObservation?
    
    func maskFaceUsingVisionRequest(inputImage: UIImage){
        
        let request = VNDetectFaceRectanglesRequest()
        let handler = VNImageRequestHandler(cgImage: inputImage.cgImage!, options: [:])
        Task{
          do{
              try handler.perform([request])
              
              guard let results = request.results else {return}
              faceRect = results.first //2
              guard let faceRect else {return}

              //3 - convert bounding boxes to UIKit coordinate system and erase face.
  
          }catch{
              print(error)
          }
        }
    }
}

@Published에 대한 속성 래퍼를 주석 처리했습니다 faceRect. 그러나 SwiftUI 보기의 변경 사항을 관찰하고 경계 상자를 그리는 데 이상적으로 사용할 수 있습니다(Vision 좌표를 SwiftUI 시스템으로 변환한 후 overlay) Rectangle.

.overlay{
     Rectangle()
     .stroke(Color.green,lineWidth: 3.0)
     .frame(width: rect.width, height: rect.height)
     .position(x: rect.midX, y: rect.midY)                                          
 }

      
                

VNFaceObservation따라서 Vision 요청에서 입력 이미지와 검색된 결과가 있습니다. 자르기 및 마스크된 이미지 만들기로 이동하기 전에 Vision 좌표를 UIKit 시스템으로 변환해 보겠습니다.

Vision 좌표는 정규화되고(왼쪽 하단 모서리가 원점임을 의미) 화면 해상도와 무관하므로 다음 코드를 사용하여 상자를 변환하고 크기를 조정합니다.

func getBoundingBoxes(rect : VNFaceObservation, on imageSize: CGSize) -> CGRect {
        
        let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -imageSize.height)
        let scale = CGAffineTransform.identity.scaledBy(x: imageSize.width, y: imageSize.height)

        let bounds = rect.boundingBox.applying(scale).applying(transform)
        
        return bounds
        
}

다음은 이미지에서 경계 상자를 잘라내고 투명 영역으로 바꾸는 코드입니다.

func erase(region: CGRect, from image: UIImage) -> UIImage? {
        UIGraphicsBeginImageContext(image.size)
        image.draw(at: CGPoint.zero)
        let context = UIGraphicsGetCurrentContext()!
        let bez = UIBezierPath(rect: region)
        context.addPath(bez.cgPath)
        context.clip()
        context.clear(CGRect(x:0,y:0,width: image.size.width,height: image.size.height))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return newImage
}

업데이트된 maskFaceUsingVisionRequest기능은 다음과 같습니다.

func maskFaceUsingVisionRequest(inputImage: UIImage){
        
        let request = VNDetectFaceRectanglesRequest()
        let handler = VNImageRequestHandler(cgImage: inputImage.cgImage!, options: [:])
        
        Task{
            
            do{
                try handler.perform([request])
                
                guard let results = request.results else {return}
                faceRect = results.first
                guard let faceRect else {return}
                
                let box = getBoundingBoxes(rect: faceRect, on: inputImage.size)
                
                await MainActor.run{
                    self.maskedImage = erase(region: box, from: inputImage)
                }
                
            }catch{
                print(error)
            }
        }
}

모든 설정! 마스킹된 이미지를 생성하는 데 성공했습니다. DALL-E API를 실행할 시간입니다!

URLSession을 사용하여 DALL-E API에 이미지 업로드

API는 이미지가 알파 채널이 있는 마스킹된 이미지와 함께 <4MB 미만의 사각형 모양이어야 합니다.

다음은 async/await를 사용하여 네트워크 요청을 실행하는 코드입니다.

func generateEditedImage(from image: UIImage, with mask: UIImage) async throws -> [Photo]  {
    
        guard let imageData = image.pngData() else{return []}
        guard let maskData = mask.pngData() else{return []}
        
        let formFields: [String: String] = [
            "prompt": "A woman wearing a red dress with an angry face",
            "size": "256x256"
        ]
        
        let multipart = MultipartFormDataRequest(url: OpenAIEndpoint.edits.url)
        multipart.addDataField(fieldName:  "image", fileName: "image.png", data: imageData, mimeType: "image/png")
        multipart.addDataField(fieldName:  "mask", fileName: "mask.png", data: maskData, mimeType: "image/png")
        
        for (key, value) in formFields {
            multipart.addTextField(named: key, value: value)
        }

        var request = multipart.asURLRequest()
        request.setValue("Bearer \(api_key_free)", forHTTPHeaderField: "Authorization")
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        
        let dalleResponse = try? JSONDecoder().decode(DALLEResponse.self, from: data)
        
        return dalleResponse?.data ?? []
        
}

구조체는 , 및 기타 멋진 항목 MultiPartFormDataRequest으로 다중 요청을 준비하는 데 사용됩니다 . Donny Wals의 가이드 를 사용하여 서버에 이미지 및 양식 업로드를 확인 하여 여러 부분으로 구성된 양식 데이터 요청 작성의 복잡성을 알아보세요.boundaryContent-DisposationURLSession

이미지 업로드 와 URLRequest양식 데이터가 준비되면 generateEditedImage다음과 같은 방법으로 SwiftUI 보기에서 함수를 호출합니다.


struct EditImageView: View {
    
    @StateObject var visionProcessing  = VisionRequester()
    @StateObject var fetcher = OpenAIService()
    @State var inputImage : UIImage = UIImage(named: "sample")!

    var body: some View {
 
      //....
     
      if let outputImage = self.visionProcessing.maskedImage{
        Image(uiImage: outputImage)
            .resizable()
            .aspectRatio(contentMode:.fit)
            .onReceive(self.visionProcessing.$maskedImage,
                       perform: { updated in
                if let _ = updated?.size{
                    Task{
                        do{
                            self.photos = try await fetcher.generateEditedImage(from: inputImage, with: outputImage)
                        }catch(let error){
                            print(error)
                        }
                    }
                }
            })
      }
      
      //....

    }

}

이것으로 이 튜토리얼을 마칩니다. 프로젝트는 내 GitHub 저장소 에서 사용할 수 있습니다 . 그룹 사진에서 여러 얼굴을 가지고 놀거나 반복적으로 마스킹하거나 variations끝점을 사용하여 다른 이미지를 생성해 보십시오.

읽어 주셔서 감사합니다.