[SwiftUI]iOSでOpenCVのサンプルプロジェクトを作成する。Objective-Cは使わずSwiftだけで書く。

この記事では、iOSでOpenCVを使う方法を説明していきます。
今までは、Objective-Cでブリッジを作る必要があったようです。
今回紹介する方法は、ヘッダファイルを自分で用意するのではなく、既にOpenCVをSwiftで使えるようにラッピングしてくれているモジュールを使っていきます。

リリースにビルド済みモジュールがあったのでそちらを使ってプロジェクトを作成していきました。ただ、私はM1のMac Book Airを使っており、そのモジュールだと警告が出ます。(おそらくM1が原因と思っているという程度ですが)シュミレーターではビルドエラーとなります。実際のデバッグは、実機デバッグかMy Mac(Designed for iPad)で実行する必要がありました。

プロジェクトの準備

プロジェクトを作るところから話をしていきます。
こんなプロジェクトを作りました。

以下リンクにあるAssetsのopencv-4.5.5-ios-framework.zipをダウンロードする。

https://github.com/opencv/opencv/releases

opencv2を使うための設定をしていきます。
※opencv2という名称ですが、バージョンはちゃんと4.5.5となってます。なぜopencv2となっているかについては、こちらに理由が書いてありました。

左側にあるMyOpencvSampleという部分をクリックします。
真ん中がプロジェクトの設定画面になるので、Generalをクリックします。
Frameworks,Libraries,and Embedded Contentに解凍したopencv-4.5.5-ios-frameworkを入れます。

解凍したファイルをこんな感じにドラッグ&ドロップします。
あとEmbedをDo Not Embedにします。
デスクトップにopencv2.frameworkを置いてましたが、これだとモジュールが見つからないとエラーで怒られました。なので、Frameworksフォルダーを作ってモジュールを移動させてそこを参照するように修正してます。(そこからドラッグ&ドロップしたらいけました)

プラスボタンを押して

検索欄にlibc++と入力する。
出てきたlibc++.tbdを選択してAddします。

追加して以下のようになればOKです。

次に、以下のようにOther Linker Flagsで-all_loadと入力します。
Build Settingsをクリックして、検索欄にother linkとでも入れます。
Other Linker Flagsが空欄となっています。

空欄でダブルクリックすると小さい窓が出てくるのでプラスボタンを押します。

手動で-all_loadと入力します。

これでプロジェクトの準備はOKなはずです。

二値化のサンプルコード

途中ミスってるので最終的なソースコード先に載せておきます。

まずは、OpenCVを使わずに画像を読み込みます。

Assetsに画像を追加して画像を読み込んでいます。

import SwiftUI
import opencv2

struct ContentView: View {
    @State var img:UIImage? = UIImage(named: "IMG")
    var body: some View {
        if let img = img {
            Image(uiImage: img)
                .resizable()
                .scaledToFit()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Mac上で実行したので以下のような出力になりました。

この画像をOpenCVを使って二値化したいと思います。

二値化するにあたり、使用するメソッドはImgproc.thresholdです。
c#のopencvsharpとかpythonでopencv使ってた方はどのメソッドが対応しているのか?という疑問が出てくると思います。
その場合は、まずはc++でどう表現するかを確認し、それが分かればc++のメソッドをそのままDocumentsで検索するのが早いかと思います。

ドキュメントはこちら。

http://xtravision.stars.ne.jp/opencv-objc-doc-test/docs/index.html

今回はまず、
①グレースケールする。
②その後に二値化する。

②については、thresholdを使うなと当たりがついていたのその検索ワードで調べました。一番上には出ませんでしたが、これかな?という勘があればすぐにわかると思います。

というわけで、一度ソースを修正しています。

import SwiftUI
import opencv2

struct ContentView: View {
    @State var img:UIImage? = nil
    var body: some View {
        if let img = img {
            Image(uiImage: img)
                .resizable()
                .scaledToFit()
        }
        EmptyView().onAppear{
            let readImg = UIImage(named: "IMG")
            let src = Mat(uiImage: readImg!)
            let dst = Mat()
            Imgproc.threshold(src: src, dst: dst, thresh: 0, maxval: 255, type: ThresholdTypes.THRESH_OTSU)
            img = dst.toUIImage()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

結果は以下のような感じ。

なんか、思ってたんと違う。

で、EmptyViewのonAppearでやってるのがダメっぽい。
なので、ボタンを押したタイミングで画像を加工するように修正してみた。

        Button(action: {
            let readImg = UIImage(named: "IMG")
            let src = Mat(uiImage: readImg!)
            let dst = Mat()
            // 二値化
            Imgproc.threshold(src: src, dst: dst, thresh: 0, maxval: 255, type: ThresholdTypes.THRESH_OTSU)
            img = dst.toUIImage()
        }){
            Text("change img")
        }

しかし、以下のようなエラーが。
あらかじめグレースケールしておくのがセオリーみたいだ。

terminating with uncaught exception of type cv::Exception: OpenCV(4.5.5) /Volumes/build-storage/build/master_iOS-mac/opencv/modules/imgproc/src/thresh.cpp:1555: error: (-2:Unspecified error) in function 'double cv::threshold(cv::InputArray, cv::OutputArray, double, double, int)'
> THRESH_OTSU mode:
>     'src_type == CV_8UC1 || src_type == CV_16UC1'
> where
>     'src_type' is 24 (CV_8UC4)

ちゃんとドキュメント見ないとダメですね。。
ドキュメントを日本語訳したものがこちら。

配列の各要素に固定レベルの閾値を適用します.

この関数は,マルチチャンネル配列に対して固定レベルの閾値を適用します.この関数は通常,グレースケール画像から2値(バイナリ)画像を得るために利用されます( #compare もこの目的に使えます).また,ノイズの除去,つまり,値が小さすぎたり大きすぎたりする画素をフィルタリングするために利用されます.この関数がサポートする閾値処理には,いくつかの種類があります.これらは,typeパラメータによって決定されます.

また、特別な値#THRESH_OTSUや#THRESH_TRIANGLEは、上記の値の1つと組み合わされることもある。これらの場合、関数は、大津または三角のアルゴリズムを用いて最適な閾値を決定し、指定された閾値の代わりにそれを使用します。

http://xtravision.stars.ne.jp/opencv-objc-doc-test/docs/Classes/Imgproc.html#/c:objc(cs)Imgproc(cm)threshold:dst:thresh:maxval:type:

最終的なソースコード

グレースケールするようにして正常に動くようになったソースはこちら。

import SwiftUI
import opencv2

struct ContentView: View {
    @State var img:UIImage? = nil
    var body: some View {
        if let img = img {
            Image(uiImage: img)
                .resizable()
                .scaledToFit()
        }
        Button(action: {
            let readImg = UIImage(named: "IMG")
            let src = Mat(uiImage: readImg!)
            // グレースケール
            //Imgproc.cvtColor(src: src, dst: src, code: ColorConversionCodes.COLOR_RGB2GRAY)
            let dst = Mat()
            // 二値化
            Imgproc.threshold(src: src, dst: dst, thresh: 0, maxval: 255, type: ThresholdTypes.THRESH_OTSU)
            img = dst.toUIImage()
        }){
            Text("change img")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

グレースケールしたらこうなって

二値化するとこうなる。

参考

https://qiita.com/treastrain/items/0090d1103033b20de054