swift 에서 C++ 사용하기 - opencv 연동
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 을 이용해 추가하거나 소스를 다운받아서 컴파일 해서 사용해야 한다.