ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • swift 에서 C++ 사용하기 - opencv 연동
    iOS Swift 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

     

    댓글

Designed by Tistory.