본문 바로가기
개발하기/SwiftUI

[2탄]The Oxford-IIIT Pet Dataset를 이용해 닮은 동물 ios 앱 만들기

by lovedeveloping 2024. 9. 30.
반응형

SwiftUI로 나를 닮은 앱 만들기

이번 시간엔 1탄에 이어서 2탄을 작성해보겠습니다. 저번 시간에 카메라는 잘 나오셨나요? 댓글 달아드리면 어떤 게 문제인지 봐드리겠습니다. 그럼 바로 시작하겠습니다.

준비물

1. Xcode
2. 실제 실행 해볼 iphone
3. The Oxford-IIIT Pet Dataset
4. 엄청나게 소중한 구글

데이터 파일 만들기

데이터 셋 다운 받기

캐글에  동물 얼굴에 대한 데이터셋이 있는 데, 이걸 다운 받아 보겠습니다.

https://www.kaggle.com/datasets/julinmaloof/the-oxfordiiit-pet-dataset?resource=download

 

The Oxford-IIIT Pet Dataset With Annotations

Classify and segment images of pets

www.kaggle.com

원본은 토렌토? 로 다운 받아야 해서 혹시나 하는 마음에 찾아 봤더니 다행히 다른 캐글에도 존재하네요.

Python으로 동물 분류하기

다운 받아 열어 보시면 images 폴더 안에 모든 동물 명들의 사진이 한 번에 존재합니다. 저희는 이걸 Python을 이용해서 개별 동물 파일을 만들 것이고 해당 이미지를 넣겠습니다. 또한 영어 동물 이름은 저희가 알아볼 수 없기에 한글 이름으로도 변환해 주는 작업도 할 것입니다.

 

먼저 Animals 폴더에 각 동물의 폴더를 만들고 해당 이미지로 분리하는 코드입니다.

import os
import shutil

# 원본 이미지가 있는 디렉토리
source_dir = "여기에 이미지 폴더가 있는 경로 입력"

# 분류된 이미지를 저장할 디렉토리 ( 마지막에 /Animals 라고 꼭 붙여주세요 )
target_dir = "경로 입력하기"

# 원본 디렉토리의 모든 파일을 순회
for filename in os.listdir(source_dir):
    # 파일 이름에서 확장자 제거
    name_without_ext = os.path.splitext(filename)[0]

    # 마지막 언더스코어 이전의 모든 부분을 동물 이름으로 사용
    animal_name = "_".join(name_without_ext.split("_")[:-1])

    # 동물 이름에 해당하는 디렉토리 생성 (없는 경우)
    animal_dir = os.path.join(target_dir, animal_name)
    os.makedirs(animal_dir, exist_ok=True)

    # 파일을 해당 동물 디렉토리로 복사
    source_file = os.path.join(source_dir, filename)
    target_file = os.path.join(animal_dir, filename)
    shutil.copy2(source_file, target_file)

print("이미지 분류가 완료되었습니다.")

다음으로는 영어로 된 동물 명을 한글로 바꿔주겠습니다. 이 작업은 추후에 카메라에 어떤 동물이 닮았는지 띄워주는데, 그때 한글 명으로

닮은 동물을 알려주기 위함입니다. 

import os

# 영어 이름과 한글 이름 매핑
name_mapping = {
    "Abyssinian": "아비시니안",
    "american_bulldog": "아메리칸 불독",
    "american_pit_bull_terrier": "아메리칸 핏불 테리어",
    "basset_hound": "바셋 하운드",
    "beagle": "비글",
    "Bengal": "벵갈",
    "Birman": "버만",
    "Bombay": "봄베이",
    "boxer": "복서",
    "British_Shorthair": "브리티시 쇼트헤어",
    "chihuahua": "치와와",
    "Egyptian_Mau": "이집션 마우",
    "english_cocker_spaniel": "잉글리시 코커 스패니얼",
    "english_setter": "잉글리시 세터",
    "german_shorthaired": "저먼 쇼트헤어드 포인터",
    "great_pyrenees": "그레이트 피레니즈",
    "havanese": "하바네즈",
    "japanese_chin": "제페니스 친",
    "keeshond": "키스혼드",
    "leonberger": "레온베르거",
    "Maine_Coon": "메인 쿤",
    "miniature_pinscher": "미니어처 핀셔",
    "newfoundland": "뉴펀들랜드",
    "Persian": "페르시안",
    "pomeranian": "포메라니안",
    "pug": "퍼그",
    "Ragdoll": "래그돌",
    "Russian_Blue": "러시안 블루",
    "saint_bernard": "세인트 버나드",
    "samoyed": "사모예드",
    "scottish_terrier": "스코티시 테리어",
    "shiba_inu": "시바 이누",
    "Siamese": "시암",
    "Sphynx": "스핑크스",
    "staffordshire_bull_terrier": "스태퍼드셔 불 테리어",
    "wheaten_terrier": "휘튼 테리어",
    "yorkshire_terrier": "요크셔 테리어"
}

# 폴더가 있는 기본 디렉토리 경로
base_directory = "Animals"  # 이 경로를 실제 폴더 경로로 변경하세요

# 모든 하위 폴더에 대해 반복
for folder_name in os.listdir(base_directory):
    old_path = os.path.join(base_directory, folder_name)
    if os.path.isdir(old_path):
        # 매핑된 한글 이름 찾기
        new_name = name_mapping.get(folder_name, folder_name)  # 매핑이 없으면 원래 이름 유지
        new_path = os.path.join(base_directory, new_name)

        # 폴더 이름 변경
        os.rename(old_path, new_path)
        print(f"'{folder_name}'을(를) '{new_name}'(으)로 변경했습니다.")

print("모든 폴더 이름 변경이 완료되었습니다.")

 이제 가공은 모두 끝났습니다. 다음으로는 Xcode에서 무료로 제공하는 Create ML을 이용해 Animals.ml 파일을 만들어 보겠습니다.

Create ML 파일 만들기

이미지 분류 선택
이미지 분류 선택

 

Image Classfication을 선택하여 모델을 만들어 줍니다. 저는 파일명을 Animals 라고 만들었습니다.

분류해 둔 Animals 폴더를 Training Data에 넣어주시길 바랍니다. 아래 선택한 옵션들에 대한 정보입니다.

 

  • Crop (자르기): 동물의 특정 부분에 초점을 맞추거나 다양한 구도를 생성하여 모델의 일반화 능력을 향상시킵니다.
  • Rotate (회전): 동물이 다양한 각도에서 찍힌 사진에 대응할 수 있도록 합니다.
  • Flip (뒤집기): 좌우 대칭성을 학습하여 모델의 유연성을 높입니다.
  • Expose (노출 조정): 다양한 조명 조건에서 찍힌 동물 사진에 대한 대응력을 키웁니다.
  • Blur (흐림 효과): 약간의 흐림 효과를 추가하여 모델이 선명하지 않은 이미지에도 대응할 수 있게 합니다.

이제 Train을 시켜주고 Output에서 내보내면 됩니다. 그리고 프로젝트에 넣어주세요.

파일 추가 및 코드 추가

1탄에서 생성했던 코드 파일 중에 추가할 것과 좀 전에 생성한 ml 파일에 대해 필요한 코드들을 추가해보겠습니다.

CameraManager.swift 파일 코드 추가

import VideoToolbox  // 이 줄을 추가합니다.

class CameraManager: NSObject, ObservableObject {
    @Published var capturedImage: UIImage?
    @Published var matchedAnimal: (String, Double)?
    
    private var photoOutput = AVCapturePhotoOutput()
    private let animalClassifier = AnimalClassifier()
    
    private var classificationResults: [(String, Double)] = []
    private let classificationThreshold = 10
    
    // setupCaptureSession() 메서드 내부에 추가
    if captureSession.canAddOutput(photoOutput) {
        captureSession.addOutput(photoOutput)
    }
    
    func capturePhoto() {
        let settings = AVCapturePhotoSettings()
        photoOutput.capturePhoto(with: settings, delegate: self)
    }
    
    func detectFaces(in image: CVPixelBuffer) {
        guard let request = detectionRequest else { return }
        do {
            try detectionSequenceHandler.perform([request], on: image, orientation: .right)
            
            if let results = request.results as? [VNFaceObservation], !results.isEmpty {
                if let cgImage = CGImage.create(from: image) {
                    // 얼굴 영역만 추출
                    if let faceImage = cropFaceImage(cgImage, observation: results[0]) {
                        classifyAnimal(for: faceImage)
                    }
                }
            } else {
                DispatchQueue.main.async {
                    self.matchedAnimal = nil
                    self.classificationResults.removeAll()
                }
            }
        } catch {
            print("얼굴 감지 실패: \(error.localizedDescription)")
        }
    }
    
    private func cropFaceImage(_ image: CGImage, observation: VNFaceObservation) -> CGImage? {
        let faceRect = VNImageRectForNormalizedRect(observation.boundingBox, image.width, image.height)
        return image.cropping(to: faceRect)
    }
    
    private func classifyAnimal(for image: CGImage) {
        animalClassifier.classifyAnimal(for: image) { [weak self] animal, confidence in
            guard let self = self else { return }
            self.classificationResults.append((animal, confidence))
            
            if self.classificationResults.count >= self.classificationThreshold {
                let mostFrequentAnimal = self.getMostFrequentAnimal()
                DispatchQueue.main.async {
                    self.matchedAnimal = mostFrequentAnimal
                    self.classificationResults.removeAll()
                }
            }
        }
    }
    
    private func getMostFrequentAnimal() -> (String, Double) {
        let groupedResults = Dictionary(grouping: classificationResults, by: { $0.0 })
        let sortedResults = groupedResults.sorted { $0.value.count > $1.value.count }
        
        if let topResult = sortedResults.first {
            let averageConfidence = topResult.value.reduce(0.0) { $0 + $1.1 } / Double(topResult.value.count)
            return (topResult.key, averageConfidence)
        }
        
        return ("알 수 없음", 0.0)
    }
}

extension CameraManager: AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        guard let imageData = photo.fileDataRepresentation(),
              let image = UIImage(data: imageData) else {
            print("이미지 처리 중 오류 발생")
            return
        }
        
        DispatchQueue.main.async {
            self.capturedImage = image
        }
    }
}

extension CGImage {
    static func create(from cvPixelBuffer: CVPixelBuffer) -> CGImage? {
        var cgImage: CGImage?
        VTCreateCGImageFromCVPixelBuffer(cvPixelBuffer, options: nil, imageOut: &cgImage)
        return cgImage
    }
}

 

추가된 코드는 다음과 같습니다.

 

  1. 이미지 캡처 및 동물 분류 기능:
    • @Published var capturedImage: UIImage?: 캡처된 이미지를 저장하고 UI에 반영합니다.
    • @Published var matchedAnimal: (String, Double)?: 매칭된 동물과 신뢰도를 저장합니다.
    • private var photoOutput = AVCapturePhotoOutput(): 사진 캡처를 위한 출력을 추가했습니다.
    • private let animalClassifier = AnimalClassifier(): 동물 분류를 위한 클래스를 사용합니다.
  2. 분류 결과 처리:
    • private var classificationResults: [(String, Double)] = []: 분류 결과를 저장합니다.
    • private let classificationThreshold = 10: 분류 결과를 확정짓기 위한 임계값입니다.
  3. 사진 캡처 메서드:
    • func capturePhoto(): 사진을 캡처하는 메서드를 추가했습니다.
  4. 얼굴 감지 및 동물 분류 로직:
    • func detectFaces(in image: CVPixelBuffer): 얼굴 감지 후 동물 분류를 수행합니다.
    • private func cropFaceImage(_ image: CGImage, observation: VNFaceObservation) -> CGImage?: 감지된 얼굴 영역만 추출합니다.
    • private func classifyAnimal(for image: CGImage): 추출된 얼굴 이미지로 동물을 분류합니다.
    • private func getMostFrequentAnimal() -> (String, Double): 가장 빈번하게 분류된 동물을 결정합니다.
  5. AVCapturePhotoCaptureDelegate 구현:
    • 사진 캡처 완료 시 이미지를 처리하고 capturedImage에 저장합니다.
  6. CGImage 확장:
    • CVPixelBuffer에서 CGImage를 생성하는 메서드를 추가했습니다.

ImagePicker.swift 파일 추가

import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?
    @Environment(\.presentationMode) private var presentationMode
    
    func makeUIViewController(context: Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        picker.sourceType = .photoLibrary
        return picker
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        let parent: ImagePicker
        
        init(_ parent: ImagePicker) {
            self.parent = parent
        }
        
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[.originalImage] as? UIImage {
                parent.selectedImage = image
            }
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
}

 

이미지를 처리하는 기능입니다.

AnimalClassifier.swift 파일 추가

import Vision
import CoreImage
import CoreML

class AnimalClassifier {
    private var model: VNCoreMLModel
    
    init() {
        do {
            let configuration = MLModelConfiguration()
            let animals = try Animals(configuration: configuration)
            model = try VNCoreMLModel(for: animals.model)
        } catch {
            fatalError("ML 모델 로드 실패: \(error)")
        }
    }
    
    func classifyAnimal(for image: CGImage, completion: @escaping (String, Double) -> Void) {
        guard let resizedImage = resizeImage(image, to: CGSize(width: 224, height: 224)),
              let pixelBuffer = resizedImage.toPixelBuffer(width: 224, height: 224) else {
            completion("알 수 없음", 0.0)
            return
        }

        let request = VNCoreMLRequest(model: model) { request, error in
            guard let results = request.results as? [VNClassificationObservation],
                  let topResult = results.first else {
                completion("알 수 없음", 0.0)
                return
            }
            let confidence = Double(topResult.confidence) * 100
            completion(topResult.identifier, confidence)
        }
        
        request.imageCropAndScaleOption = .scaleFit
        
        let handler = VNImageRequestHandler(ciImage: CIImage(cgImage: resizedImage), options: [:])
        do {
            try handler.perform([request])
        } catch {
            print("이미지 분류 실패: \(error)")
            completion("알 수 없음", 0.0)
        }
    }
    
    private func resizeImage(_ image: CGImage, to size: CGSize) -> CGImage? {
        let context = CGContext(data: nil,
                                width: Int(size.width),
                                height: Int(size.height),
                                bitsPerComponent: 8,
                                bytesPerRow: 0,
                                space: CGColorSpaceCreateDeviceRGB(),
                                bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
        
        context?.interpolationQuality = .high
        context?.draw(image, in: CGRect(origin: .zero, size: size))
        
        return context?.makeImage()
    }
}

extension CGImage {
    func toPixelBuffer(width: Int, height: Int) -> CVPixelBuffer? {
        var pixelBuffer: CVPixelBuffer?
        let attributes = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
                          kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
        let status = CVPixelBufferCreate(kCFAllocatorDefault,
                                         width,
                                         height,
                                         kCVPixelFormatType_32ARGB,
                                         attributes,
                                         &pixelBuffer)
        
        guard let buffer = pixelBuffer, status == kCVReturnSuccess else {
            return nil
        }
        
        CVPixelBufferLockBaseAddress(buffer, CVPixelBufferLockFlags(rawValue: 0))
        let pixelData = CVPixelBufferGetBaseAddress(buffer)
        
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: pixelData,
                                width: width,
                                height: height,
                                bitsPerComponent: 8,
                                bytesPerRow: CVPixelBufferGetBytesPerRow(buffer),
                                space: rgbColorSpace,
                                bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
        
        context?.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))
        CVPixelBufferUnlockBaseAddress(buffer, CVPixelBufferLockFlags(rawValue: 0))
        
        return pixelBuffer
    }
}

이 코드에서 추가된 내용은

 

  1. AnimalClassifier 클래스 추가:
    • CoreML 모델을 로드하고 초기화합니다.
    • 이미지를 분류하는 classifyAnimal(for:completion:) 메서드를 구현합니다.
    • 이미지 크기를 조정하는 resizeImage(_:to:) 메서드를 포함합니다.
  2. CoreML 모델 사용:
    • Animals 모델을 사용하여 동물을 분류합니다.
  3. 이미지 전처리:
    • 입력 이미지를 224x224 크기로 조정합니다.
    • CGImage를 CVPixelBuffer로 변환합니다.
  4. Vision 프레임워크 활용:
    • VNCoreMLRequest를 사용하여 이미지 분류를 수행합니다.
  5. 분류 결과 처리:
    • 최상위 결과와 신뢰도를 반환합니다.
  6. CGImage 확장 추가:
    • toPixelBuffer(width:height:) 메서드를 추가하여 CGImage를 CVPixelBuffer로 변환합니다.

ContentView.swiftUI 파일 코드 추가

UI 파일은 전체 코드로 올리겠습니다!

import SwiftUI

struct ContentView: View {
    @StateObject private var cameraManager = CameraManager()
    
    var body: some View {
        ZStack {
            CameraView(cameraManager: cameraManager)
                .edgesIgnoringSafeArea(.all)
            
            VStack {
                Spacer()
                
                if cameraManager.isRecognizing, let (animal, confidence) = cameraManager.matchedAnimal {
                    Text("당신과 닮은 동물: \(animal)")
                        .padding()
                        .background(Color.blue.opacity(0.7))
                        .foregroundColor(.white)
                        .cornerRadius(10)
                    
                    Text("닮은 정도: \(String(format: "%.1f", confidence))%")
                        .padding()
                        .background(Color.blue.opacity(0.7))
                        .foregroundColor(.white)
                        .cornerRadius(10)
                        .padding(.bottom, 10)
                }
                
                Text(cameraManager.isRecognizing ? "얼굴 인식 중: \(cameraManager.detectedFacesCount)명" : "얼굴을 찾는 중...")
                    .padding()
                    .background(cameraManager.isRecognizing ? Color.green.opacity(0.7) : Color.red.opacity(0.7))
                    .foregroundColor(.white)
                    .cornerRadius(10)
                    .padding(.bottom, 30)
            }
        }
        .onAppear {
            cameraManager.startSession()
        }
        .onDisappear {
            cameraManager.stopSession()
        }
    }
}

 

추가 된 내용은

 

  1. 동물 매칭 결과 표시:
    • cameraManager.matchedAnimal 프로퍼티를 사용하여 매칭된 동물과 신뢰도를 확인합니다.
    • 매칭된 동물 정보를 조건부로 표시합니다.
  2. "당신과 닮은 동물" 텍스트 추가:
    • 매칭된 동물 이름을 표시합니다.
    • 파란색 반투명 배경에 흰색 텍스트로 스타일링합니다.
  3. "닮은 정도" 텍스트 추가:
    • 매칭 신뢰도를 백분율로 표시합니다.
    • 마찬가지로 파란색 반투명 배경에 흰색 텍스트로 스타일링합니다.
  4. 얼굴 인식 상태 텍스트 업데이트:
    • 기존 텍스트를 조건부 텍스트로 변경 (얼굴 인식 중일 때와 아닐 때 다른 메시지 표시)
    • 배경색을 동적으로 변경 (인식 중일 때 녹색, 아닐 때 빨간색)
  5. 레이아웃 조정:
    • 새로 추가된 요소들의 패딩과 간격을 조정하여 전체적인 레이아웃을 개선했습니다.

최종 결과 확인

최종 결과물
최종 결과물

 

아주 잘 작성 됐군요!  뿌듯합니다

반응형