본문 바로가기
ML/Machine Learning

ML) CoreML로 이미지 분류하기 (Feat: SwiftUI Dynamic Size View , SwiftUI ImagePickerView

by 후르륵짭짭 2023. 3. 25.
728x90
반응형

갑자기 만난 무서운 곰돌이

안녕하세요. 후르륵짭짭입니다.
"매번 앱을 만들고 싶다~~" 라고 생각을 했지만 만드는데 시간을 쓰고 싶을 정도의 앱이 없었습니다.
하지만 이번에는 결심을 했죠 ㅎㅎㅎㅎ.
이번에는 그 앱의 시작점인 간단한 CoreML에 대해서 작성해보려고 합니다.
 

** CreateML를 사용해서 모델만들기 **

우리가 어떤 데이터를 분류할 때, 경험을 통해서 분류를 하죠.
아무리 비슷하더라도 코드를 많이 보면 이게 C 코드인지, Java인지, Swift인지, JavaScript 인지 구분하는 거 처럼
컴퓨터도 학습된 모델로 대상을 분류할 수 있습니다.
 
iOS에서는 기본적인 모델들을 많이 제공합니다.
인지, 분류, 음성 등등
이번에는 저는 분류를 사용할 겁니다.
Xcode를 실행 시킨뒤, 왼쪽 상단 상태바에서 Xcode에서 Open Development tool에서 CoreML를 눌러 줍니다.

그리고 원하는 Template를 눌러 줍니다.
저는 여기서 Image Classification을 눌러 줍니다.

그리고 위와 같이 모델에 대한 기본적인 내용을 담아주고 Next를 눌러주고 분류하고 싶은 사진들을 준비합니다!

여기서 부터 중요합니다!
Data에 Training Data와 TestingData가 있습니다!
준비한 사진들을 최소 두개 이상의 데이터로 분류합니다!
예를 들어 고양이와 강아지 / 파충류와 조류 / 남자와 여자와 외계인 이렇게 최소 두개 이상으로 분류 될 수 있게 
훈련용 사진과 검증용 사진을 준비합니다!
그리고 위에 TrainData에서 "+" 버튼을 눌러서 두개 이상으로 분류된 폴도를 담고 있는 하나의 폴더를 넣어줍니다.
Testing도 마찮가지 입니다!
 
Parameters 라는 것이 있습니다!
이것은 우리가 데이터가 많이 없을 수 있습니다.
이때 데이터를 확대하기 위해 Train 데이터를 더 생성 해주는 겁니다.

Add Noice - 노이즈가 들어간 사진 추가

Blur - 사진을 흐리게 해서 추가하기

Crop - 붕괴된 사진 추가하기

,,,,

 
이제 모든게 준비 됐다면 이제 Train 버튼을 눌러주면 현재 훈련시킨 결과에 대한 성적표가 나옵니다.

이제 해당 결과의 대한 아웃풋을 뽑습니다.

 
Preview를 통해서 새로운 이미지들을 넣고 결과를 직접 확인할 수 있습니다.

좋은 결과를 올리기 위해서는 좋은 데이터가 많이 들어가면 됩니다.

 
 

** 실제로 적용하기 ** 

 - 필요한 View 생성하기 - 

일단 필요한 View를 생성해줍니다. (SwiftUI는 잘 못하지만 ㅠㅠ )
1. 메인 부모View 

struct ContentView: View {

    @ObservedObject var viewModel = ContentViewManager()

    var body: some View {
        VStack {

            Spacer()

            ImageSelectView(showingDialog: $viewModel.showingDialog,
                            showingImagePicker: $viewModel.showingImagePicker,
                            viewModel: viewModel)

            Spacer()

            ImageResultView(imageClassficationDone: $viewModel.imageClassficationDone, viewModel: viewModel)

        }
        .padding()
    }
}

2. 이미지를 보여주는 View ( SwiftUI ImagePickerView )

더보기
struct ImageSelectView : View {

    @State var pickedImage : Image = Image(systemName: "star.fill")
    
    //부모의 상태가 변경 될 때 자녀의 View를 변경하고 싶다면 @Binding으로 값을 받아야한다.
    @Binding var showingDialog : Bool
    @Binding var showingImagePicker : Bool

    let viewModel : ContentViewManager

    var body: some View {
        VStack(spacing: 20, content: {
            pickedImage
                .resizable()
                .scaledToFit()
                .frame(height: 200)
                .foregroundColor(.accentColor)

            Button(action: {
                self.viewModel.changeDialog()
            }, label: {
                Text("get Picture")
            })
            //confirmationDialog를 통해서 Alert를 생성할 수 있다.
            .confirmationDialog("사진 선택하기", isPresented: $showingDialog, actions: {
                Button(action: {
                    self.viewModel
                        .changeImagePicker(sourceType: .camera)
                }, label: {
                    Text("카메라")
                })
                Button(action: {
                    self.viewModel
                        .changeImagePicker(sourceType: .photoLibrary)
                }, label: {
                    Text("앨범")
                })
            })
            //화면 전체를 Present를 하는 방법
            .fullScreenCover(isPresented: $showingImagePicker,
                             content: {
                ImagePickerViewController(sourceType: self.viewModel.sourceType, callBack: { image in
                    self.pickedImage = Image(uiImage: image)
                    self.viewModel.imageAnalizeSetup(image: image)
                })
            })
        })
    }
}

 

- 해당 내용에서 Binding을 통해서 부모의 상태가 변경 될 때 자식도 해당 상태값 변경을 모니터링 하기 위해서 

@Binding을 사용했다.

- UIViewController에서 Present와 같은 방식을 사용하기 위해서는 FullScreenCover라는 메소드를 사용한다.

3. 이미지 Picker View

더보기
//UIKit을 사용하는 View를 SwiftUI와 접목 시키기 위해서는 아래와 같이 UIViewContollerRepresentable을 사용해야한다.
struct ImagePickerViewController : UIViewControllerRepresentable {
    typealias UIViewControllerType = UIImagePickerController

	//View의 Dissmiss를 사용할 때 사용한다.
    @Environment(\.presentationMode)
    private var presentationMode

    private let sourceType :  UIImagePickerController.SourceType
    private let imageCallback : (UIImage) ->()
    
    init(sourceType : UIImagePickerController.SourceType, callBack : @escaping (UIImage) -> ()){
        self.sourceType = sourceType
        self.imageCallback = callBack
    }
	
    //이는 반환하는 SwiftUI View에 Delegate를 넣어주거나 기타 기능을 만들어 줄 필요가 있을 때 
    //makeUIVIewController 이전에 호출 된다.
    func makeCoordinator() -> ImageCoordinator {
        ImageCoordinator(parent: self)
    }

	//원하는 UIKit의 ViewController를 반환를 반환한다.
    func makeUIViewController(context: Context) -> UIImagePickerController {
        let pickerViewController = UIImagePickerController()
        pickerViewController.delegate = context.coordinator
        pickerViewController.sourceType = self.sourceType
        return pickerViewController
    }

	// 뷰 컨트롤러에 영향을 미치는 변화가 발생했을 때 호출
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
        print("Call Change But Nothing to do")
    }

	//기본적인 UI
    class ImageCoordinator : NSObject , UIImagePickerControllerDelegate , UINavigationControllerDelegate {

        var parent : ImagePickerViewController

        init(parent : ImagePickerViewController) {
            self.parent = parent
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[.originalImage] as? UIImage {
                self.parent.imageCallback(image)
                self.parent.presentationMode.wrappedValue.dismiss()
            }
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            self.parent.presentationMode.wrappedValue.dismiss()
        }

    }

}

 

- SwiftUI에서 ImagePickerView를 사용하는 방법 

https://www.appcoda.com/swiftui-camera-photo-library/

https://hryang.tistory.com/32

 

- SwiftUI에서 UIKit을 접목시키는 방법을 알아봤습니다.

자세한 내용 : https://mildwhale.github.io/2020-03-12-Interfacing-with-UIKit-1/

- self.parent.presentationMode.wrappedValue.dismiss() 를 통해서 SwiftUI에서 Dismiss를 감지하여 View를 내려줄 수 있습니다.

https://kka7.tistory.com/276 ($Binding을 이용한 방법)

https://stackoverflow.com/questions/57190511/dismiss-a-swiftui-view-that-is-contained-in-a-uihostingcontroller

4. 결과를 보여주는 View ( SwiftUI Dynamic Size View 내용 포함 )

더보기
struct ImageResultView : View {

    @State var textHeight : CGFloat = 0
    @Binding var imageClassficationDone : Bool
    let viewModel : ContentViewManager

    var body: some View {
        HStack( content: {

            ZStack(content: {
                Rectangle()
	                //TextHeight가 변경 될 때 Rectangle의 사이즈도 변하게 만들었다.
                    .frame(width: textHeight + 20,  height: 100)
                    .foregroundColor(.brown)

                VStack(alignment: .leading, content: {
                    Text("Name : \(viewModel.classifiedName)")
                    Text("percentage : \(viewModel.resultPercentage)%")
                })
                .overlay(content: {
                    GeometryReader(content: { proxy in
	                    //PreferenceKey를 준수하는 Instance 통해서 Text에 따라 너비가 달라지는 
                        //VStack의 가로 사이즈를 감지할 수 있게 됩니다.
                        Color.clear.preference(key: ContentLengthPreference.self, value: proxy.size.width)
                    })
                })
            })

            Spacer()
        })
        //PreferenceKey로 감지된 것은 해당 부분에서 Notificatoin이 오게 되고 
        //TextHeight를 변경할 수 있습니다.
        .onPreferenceChange(ContentLengthPreference.self, perform: { value in
            DispatchQueue.main.async {
                self.textHeight = value
            }
        })

    }

}

//해당 Instance는 PrefercenceKey를 준수하고
//reduce를 통해서 최종 값을 반환하게 됩니다.
struct ContentLengthPreference: PreferenceKey {
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
       value = nextValue()
    }

   static var defaultValue: CGFloat { 0 }

}

 

Dynamic하게 View의 사이즈가 변할 수 있게 하는 방법은 PreferenceKey를 이용했습니다.

1.

https://stackoverflow.com/questions/69946104/make-a-view-the-same-size-as-another-view-which-has-a-dynamic-size-in-swiftui

2.

https://stackoverflow.com/questions/66423613/dynamic-height-of-rectangle-as-from-text-content-in-swiftui

이렇게 4개를 만들었습니다. 
 

 - CoreML과 Vision 사용해서 이미지 분류하기 - 

이렇게 프로젝트에 생성한 CoreML 모델을 넣어줍니다.

class ContentViewManager : ObservableObject {

    var sourceType : UIImagePickerController.SourceType = .photoLibrary
    var classifiedName : String = "EMPTY"
    var resultPercentage : Float = 0.00
	
    <,,, 생략 ,,, >

	//1. Vision Framework가 인식 할 수 있도록 UIImage를 CIImage로 변경
    func imageAnalizeSetup(image : UIImage) {
        guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") }
        let orientation = CGImagePropertyOrientation(image.imageOrientation)

        Task(operation: {
            let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
            try handler.perform([imageAnalizeRequest(processClassifications)]
            )
        })
    }

	//2. CreateML 모델을 VisionML에 적용
    private func imageAnalizeRequest(_ callBack : @escaping (_ request : VNRequest, _ error : Error?) -> () ) throws -> VNCoreMLRequest {
        let config = MLModelConfiguration()
        let classifier =  try NailArt17(configuration: config)

        let model = try VNCoreMLModel(for: classifier.model)

        let request = VNCoreMLRequest(model: model, completionHandler: { request, error in
            callBack(request, error)
        })
        request.imageCropAndScaleOption = .centerCrop
        return request
    }

	//3. Vision을 통해서 받은 결과를 최종 처리
    private func processClassifications(for request: VNRequest, error: Error?) {
            guard let results = request.results else {
                print("\(#function) \(error?.localizedDescription ?? "NO Error" )")
                self.classifiedName = "Fail"
                return
            }

            let classifications = results as! [VNClassificationObservation]

            if classifications.isEmpty {
                self.classifiedName = "EMPTY Result"
            } else {
                let topClassifications = classifications.prefix(2)
                guard let descriptions = topClassifications.map({ classification in
                    return (percent : classification.confidence , name : classification.identifier)
                }).first else {return}
                DispatchQueue.main.async {
                    self.classifiedName = descriptions.name
                    self.resultPercentage = descriptions.percent
                    self.imageClassficationDone.toggle()
                }
            }

    }

}

모든 처리는 1 -> 2 -> 3 순으로 작동하게 됩니다.

func imageAnalizeSetup(image : UIImage) {
        guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") }
        let orientation = CGImagePropertyOrientation(image.imageOrientation)

        Task(operation: {
            let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
            try handler.perform([imageAnalizeRequest(processClassifications)]
            )
        })
    }

에서 VNImageRequestHandler를 통해서 이미지 분석에 대한 요청을 Vision Framework에 의뢰합니다.
이때 ciImage와 orientation이 필요하게 되는데, 

extension CGImagePropertyOrientation {
    /**
     Converts a `UIImageOrientation` to a corresponding
     `CGImagePropertyOrientation`. The cases for each
     orientation are represented by different raw values.

     - Tag: ConvertOrientation
     */
    init(_ orientation: UIImage.Orientation) {
        switch orientation {
        case .up: self = .up
        case .upMirrored: self = .upMirrored
        case .down: self = .down
        case .downMirrored: self = .downMirrored
        case .left: self = .left
        case .leftMirrored: self = .leftMirrored
        case .right: self = .right
        case .rightMirrored: self = .rightMirrored
        default : self = .up
        }
    }
}

이렇게 UIImage의 Orientation을 CGImagePropertyOrientation Enum으로 변경해줘야합니다.

private func imageAnalizeRequest(_ callBack : @escaping (_ request : VNRequest, _ error : Error?) -> () ) throws -> VNCoreMLRequest {
        let config = MLModelConfiguration()
        let classifier =  try NailArt17(configuration: config)

        let model = try VNCoreMLModel(for: classifier.model)

        let request = VNCoreMLRequest(model: model, completionHandler: { request, error in
            callBack(request, error)
        })
        request.imageCropAndScaleOption = .centerCrop
        return request
    }

그리고 우리가 생성한 CreateML을 통해서 Vision에서 분석을 하게 됩니다 .
해당 결과는 VNCoreRequest의 클로저에 request로 결과가 나오게 됩니다.

private func processClassifications(for request: VNRequest, error: Error?) {
            guard let results = request.results else {
                print("\(#function) \(error?.localizedDescription ?? "NO Error" )")
                self.classifiedName = "Fail"
                return
            }

            let classifications = results as! [VNClassificationObservation]

            if classifications.isEmpty {
                self.classifiedName = "EMPTY Result"
            } else {
                let topClassifications = classifications.prefix(2)
                guard let descriptions = topClassifications.map({ classification in
                    return (percent : classification.confidence , name : classification.identifier)
                }).first else {return}
                DispatchQueue.main.async {
                    self.classifiedName = descriptions.name
                    self.resultPercentage = descriptions.percent
                    self.imageClassficationDone.toggle()
                }
            }

    }

이제 모든 결과가 VNClassificationObservation에
결과에 대한 Percentage를 담은 confidence와 분류 결과의 이름의 identifier가 반환하게 됩니다.
 

** 결과 **

성공적으로 원하는 이미지를 분류 했을 때 

원하는 이미지가 아닌 다른 것으로 분류 했을 때 

 

참고 사이트 : 

https://sumniya.tistory.com/26

 

분류성능평가지표 - Precision(정밀도), Recall(재현율) and Accuracy(정확도)

기계학습에서 모델이나 패턴의 분류 성능 평가에 사용되는 지표들을 다루겠습니다. 어느 모델이든 간에 발전을 위한 feedback은 현재 모델의 performance를 올바르게 평가하는 것에서부터 시작합니

sumniya.tistory.com

 
PHPPicker :
https://sweetdev.tistory.com/727

 

[SwiftUI] Image picker만들기 - PHPicker

그동안 Multiselect 구현하느라 힘들었는데, 새로나온 PHPicker는 검색도 되고, zoom in - zoom out도 가능하다...! 앨범에서도 선택 가능하고, 기존의 UIImagePickerController의 상위 호환정도로 생각하면 된다. s

sweetdev.tistory.com

 
SwiftUI ActionSheet 띄우기 :
https://www.hohyeonmoon.com/blog/swiftui-tutorial-action-sheet/

 

SwiftUI ActionSheet 띄우기 | Hohyeon Moon

www.hohyeonmoon.com

 
MainActor 뿌수기 :
https://sujinnaljin.medium.com/swift-actor-%EB%BF%8C%EC%8B%9C%EA%B8%B0-249aee2b732d

 

[Swift] Actor 뿌시기

근데 이제 async, Task 를 곁들인..

sujinnaljin.medium.com

 

Info.plist에서 NO UIScene configuration 오류:

https://ios-daniel-yang.tistory.com/38

 

[iOS/Swift] Info.plist contained no UIScene configuration dictionary 에러발생 시 해결 방법

Info.plist 에러 [SceneConfiguration] Info.plist contained no UIScene configuration dictionary (looking for configuration named "(no name)") 이와 같은 에러 발생 시 info.plist의 Application Scene Manifest에서 Scene Configuration을 추가시켜

ios-daniel-yang.tistory.com

 

- CoreML - 

CoreML을 통해서 백그라운드 지우기 
https://img.ly/blog/how-to-remove-backgrounds-using-coreml/

 

How to Remove Backgrounds Using Core ML

Bring background removal and replacement to your iOS application.

img.ly

 
CoreML을 사용해서 이미지 분류와 CoreML 훈련 시키기 
https://www.kodeco.com/7960296-core-ml-and-vision-tutorial-on-device-training-on-ios

 

Core ML and Vision Tutorial: On-device training on iOS

This tutorial introduces you to Core ML and Vision, two cutting-edge iOS frameworks, and how to fine-tune a model on the device.

www.kodeco.com

 
CoreML을 사용해서 이미지 인식하기 
https://ajith.blog/image-recognition-with-core-ml-and-vision/

 

Image recognition with Core ML and Vision

Our goal is to build a simple implementation of image recognition. To achieve this, we will use the Vision Framework, which was introduced in iOS 11 to apply computer vision algorithms to perform a variety of tasks on input images and video.

ajith.blog

 
CoreML로 OnDevice 훈련하기 
https://machinethink.net/blog/coreml-training-part1/

 

On-device training with Core ML – part 1

Machine learning on mobile gets more popular every year! WWDC 2019 gave us lots of new goodies for adding ML into our apps. One of the biggest announcements was that Core ML 3 now supports training of models on the iPhone and iPad. Who would have thought a

machinethink.net

 
CoreML Tool
https://coremltools.readme.io/docs

 

Core ML Tools Overview

Use Core ML Tools to convert models from third-party libraries to Core ML.

coremltools.readme.io

 
이미지 창고: 
https://images.cv/

 

Search and downlod labeled computer vision image datasets

Big selection of labeled image datasets | Use our built-in tools for dataset pre-processing: image color, data split, image size, and data augmentation

images.cv

 

728x90
반응형

댓글