iOS Swift

swift 에서 C++ 사용하기 - opencv 연동

basker 2020. 9. 9. 23:44

swift에서는 직접적으로 C++ 을 사용할 수 없다. C++을 사용하기 위해서는 Object-C 파일에서 C++ 를 사용하고, Object-C 소스 파일과 헤더파일을 생성한 후 Header 파일을 브릿지해서 swift에서 object-c 함수를 호출하도록 해야한다. 

구조는 아래와 같다. 

실제 C++ 코드는 OpenCVWapper.mm 에서 사용되고 해당 인터페이스의 정의 부분인 OpenCVWrapper.h 파일을 EdgeDector-Bridging-Header.h를 통해서 브릿지 하면 swfit 소스 파일에서 이를 호출해 사용할 수 있는 구조로 이루어 진다. 

 

여기서는 opencv의 프레임워크를 연동해 이미지의 에지를 그리는 예제를 통해서 실제 어떻게 C++ 라이브러리를 swift를 통해서 사용할 수 있는지를 알아보자. 

 

iOS를 위한 opencv2.framework는 cocopad을 통해서 설치하거나, 소스를 컴파일해서 사용할 수 있다. 

소스를 컴파일하는 경우 소스 코드에서 아래와 같이 build_framwork.py 파일을 이용해 컴파일 할 수 있다. 

python opencv/platforms/ios/build_framework.py ios

cocopad은 아래의 이름으로 PodFile에 아래 와 같이 추가해서 opencv2.framework를 프로젝트에 추가할 수 있다. 

pod 'OpenCV2'

 

여기서는 pod을 사용하지 않고 컴파일된 opencv2.framework를 사용해 개발한다. 

 

우선 프로젝트를 생성하고 프로젝트의 하위에 framework 그룹을 생성한 후 opencv2.framework 파일을 추가한다. 

 

이제 OpenCV 라이브러리를 사용하기 위해서 OpenCVWrapper Object-C 파일을 아래와 같이 생성한다. 여기서 중요한 점은 

Object-C bridging header 생성 여부를 묻는 창에서 이를 허용해야 한다. 또 생성된 OpenCVWrapper.m 파일의 확장을 .mm으로 변경해야 한다. 이는  .m 파일의 경우 컴파일러가 표준 C++ 헤더 파일을 추가시키지 않기 때문이다. C++을 사용할 경우에는 .mm을 C를 사용할 경우에는 .m 확장자를 사용해야 한다고 기억해 두면 된다.

 

이제 OpenCVWrapper 클래스에 클래스 함수 detectEdge:(UIImage*) image 함수를 추가해 보자 .

OpenCVWrapper.h

//
//  OpenCVWrapper.h
//  edgeDetector
//
//  Created by HYEONJUN PARK on 2020/09/09.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@class UIImage;
@interface OpenCVWrapper : NSObject
+(UIImage*) detectEdge:(UIImage*) image;
@end

NS_ASSUME_NONNULL_END

OpenCVWrapper.mm

//
//  OpenCVWrapper.m
//  edgeDetector
//
//  Created by HYEONJUN PARK on 2020/09/09.
//

#import <opencv2/opencv.hpp>
#import <opencv2/imgcodecs/ios.h>
#import "OpenCVWrapper.h"

@implementation OpenCVWrapper
+(UIImage*) detectEdge:(UIImage*) image {
    cv::Mat imageMat;
     UIImageToMat(image, imageMat);
     if(imageMat.channels() == 1) {
         return image;
     }
     cv::Mat grayMat;
     cv::cvtColor(imageMat, grayMat, cv::COLOR_BGR2GRAY);
    cv::Mat cannyMat;
    cv::Canny(grayMat, cannyMat, 50, 150, 3);
    return MatToUIImage(cannyMat);
}
@end

edgeDetector-Bridging-Header.h

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#include "OpenCVWrapper.h"

이제 CollectionView를 이용해 포토라이브러리에서 사진을 읽어와 해당 사진의 edgeDection을 할 수 있도록 아래와 같이 스트리보드를 디자인한다. 

포토 선택화면은 UICollectionViewController를 생성해 구현하고 해당 선택 화면의 컨트롤러의 소스를 아래와 같이 작성한다. 

//
//  GalleryCollectionViewController.swift
//  edgeDetector
//
//  Created by HYEONJUN PARK on 2020/09/09.
//

import UIKit
import Photos
private let reuseIdentifier = "PhotoCell"
protocol GalleryViewDelegate {
  func didSelectedImage(image : UIImage);
}


class GalleryViewController: UICollectionViewController {
  
    var selectedPhoto: UIImage!
  var delegate: GalleryViewDelegate?
    private var images = [PHAsset]()
    override func viewDidLoad() {
        super.viewDidLoad()
        fetchPhotos()
    }
    
    override func viewWillDisappear(_ animated: Bool) {

    }
    private func fetchPhotos() {
        //1. 포토라이브러리 인증 요청
        PHPhotoLibrary.requestAuthorization { [weak self] status in
            if status == .authorized {
                let assets = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: nil)
                assets.enumerateObjects {(asset, count, stop) in
                    self?.images.append(asset)
                }
                self?.images.reverse()
                //이미지를 모두 가져오면 콜렉션뷰 리로드
                DispatchQueue.main.async {
                    self?.collectionView.reloadData()
                }
            }
        }
    }
}

 
extension GalleryViewController {
    
 // MARK: UICollectionViewDataSource
    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return images.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? PhotoCollectionViewCell else {
            fatalError("PhotoCell Not Found")
        }
        
        let asset = self.images[indexPath.row]
        //PHAsset 을 UIImage 로 변환
        let manager = PHImageManager.default()
        manager.requestImage(for: asset, targetSize: CGSize(width: 120, height: 120), contentMode: .aspectFit, options: nil) { (image, _) in
            DispatchQueue.main.async {
                cell.imageView.image = image
            }
        }
        return cell
    }
    
    // MARK: UICollectionViewDelegate
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let asset = images[indexPath.row]
        PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 300, height: 300), contentMode: .aspectFit, options: nil) { [weak self](image, info) in
            guard let info = info else { return }
            let isDegradedImage = info["PHImageResultIsDegradedKey"] as! Bool
            if !isDegradedImage {
                if let image = image {
                  self?.delegate?.didSelectedImage(image: image)
                  self?.dismiss(animated: true, completion: nil)
                }
            }
        }
        
    }
}


//Mark - UICollectionViewDelegateFlowLayout 프로토콜 구현
//CollectionViewCell의 크기 설정
extension GalleryViewController: UICollectionViewDelegateFlowLayout {
    
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let cellRatio: CGFloat = 1.0
        if (UIDevice.current.orientation.isLandscape) {
            return getCellSize(numberOFItemsRowAt: 5, cellRatio: cellRatio)
            //return CGSize(width: 120, height: 120)
        } else {
            return getCellSize(numberOFItemsRowAt: 3, cellRatio: cellRatio)
            //return CGSize(width: 120, height: 120)
        }
    }
    
   
    
    
  private func getCellSize(numberOFItemsRowAt: Int, cellRatio: CGFloat) -> CGSize {
    var screenWidth = UIScreen.main.bounds.width
    
    if #available(iOS 11.0, *) {
      let window = UIApplication.shared.windows.first{ $0.isKeyWindow }
      let leftPadding = window?.safeAreaInsets.left ?? 0
      let rightPadding = window?.safeAreaInsets.right ?? 0
      screenWidth -= (leftPadding + rightPadding)
    }
    screenWidth -= 10
    let cellWidth =  screenWidth / CGFloat(numberOFItemsRowAt)
    let cellHeight = cellWidth * cellRatio
    return CGSize(width: cellWidth, height: cellHeight)
  }
}

위에서 fetchPhotos()는 PHPhotoLibrary 에 접근하기 위해 info.plist 에 "Privacy - Photo Library Usage Description"을 추가해 포토라이브러리 사용 허가 요청을 할 수 있게 해야한다. 

 UICollectionViewDelegateFlowLayout을 추가해 해당 Collection Cell의 크기를 조절하고, collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) 에서는 PHAsset을 images 배열에 저장하고 

선택 시 collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)가 호출되는 데 이때 PHAsset을 PHImageMager를 통해 UIImage로 변환한다. 

 

선택된 이미지는 self?.delegate?.didSelectedImage(image: image)를 통해 ViewController로 전달된다. 

 

//
//  ViewController.swift
//  edgeDetector
//
//  Created by HYEONJUN PARK on 2020/09/09.
//

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var applyButton: UIButton!
    @IBOutlet weak var photoView: UIImageView!
    override func viewDidLoad() {
        super.viewDidLoad()
        //applyButton.isHidden = true
    }
    
    private func updateUI(with image: UIImage) {
        photoView.image = image
        //applyButton.isHidden = false
    }
}

//MARK: sugue 관련 연산
extension ViewController {
    
    @IBAction func goToGallery(_ sender: UIButton) {
        performSegue(withIdentifier: "goToGalleryView", sender: self)
    }
  
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "goToGalleryView" {
            guard let galleryVC = segue.destination as? GalleryViewController else {
                fatalError("GalleryViewControlller error")
            }
          galleryVC.delegate = self 
        }
    }
  @IBAction func  apply() {
    if let image = photoView.image {
      let edgeImage = OpenCVWrapper.detectEdge(image)
      photoView.image = edgeImage
    }
  }
}

extension ViewController: GalleryViewDelegate {
  func didSelectedImage(image : UIImage) {
    photoView.image = image;
  }
}

ViewController의 apply() 함수는 Object-C 클래스 OpenCVWrapper.detectEdge() 함수를 호출하게 되는 데 이는 내부적으로 opencv 라이브러리의 함수를 호출해 선택된 이미지에 대한 edgeImage를 반환해주고 이를 photoView.image에 출력한다. 

 

아래 소스에는 opencv2.framework 의 크기 상 이를 삭제한 소스이다. 테스트를 위해서는 opencv2.framework를 cocopod 을 이용해 추가하거나 소스를 다운받아서 컴파일 해서 사용해야 한다. 

edgeDetector.tar.gz
0.04MB