안녕하세요. 후르륵짭짭입니다.
"매번 앱을 만들고 싶다~~" 라고 생각을 했지만 만드는데 시간을 쓰고 싶을 정도의 앱이 없었습니다.
하지만 이번에는 결심을 했죠 ㅎㅎㅎㅎ.
이번에는 그 앱의 시작점인 간단한 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/
- 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을 이용한 방법)
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.
2.
이렇게 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
PHPPicker :
https://sweetdev.tistory.com/727
SwiftUI ActionSheet 띄우기 :
https://www.hohyeonmoon.com/blog/swiftui-tutorial-action-sheet/
MainActor 뿌수기 :
https://sujinnaljin.medium.com/swift-actor-%EB%BF%8C%EC%8B%9C%EA%B8%B0-249aee2b732d
Info.plist에서 NO UIScene configuration 오류:
https://ios-daniel-yang.tistory.com/38
- CoreML -
CoreML을 통해서 백그라운드 지우기
https://img.ly/blog/how-to-remove-backgrounds-using-coreml/
CoreML을 사용해서 이미지 분류와 CoreML 훈련 시키기
https://www.kodeco.com/7960296-core-ml-and-vision-tutorial-on-device-training-on-ios
CoreML을 사용해서 이미지 인식하기
https://ajith.blog/image-recognition-with-core-ml-and-vision/
CoreML로 OnDevice 훈련하기
https://machinethink.net/blog/coreml-training-part1/
CoreML Tool
https://coremltools.readme.io/docs
이미지 창고:
https://images.cv/
'ML > Machine Learning' 카테고리의 다른 글
ML) KNN 이웃 알고리즘 (지도학습) (1) | 2020.08.08 |
---|---|
ML) Python으로 크롤링한 것을 엑셀로 만들기 (0) | 2020.07.13 |
ML) Python으로 HTML 파싱하기! (1) | 2020.07.01 |
댓글