Swift × C++ × OpenCVで画像処理

# はじめに
こんにちは！株式会社ZOZOでiOSエンジニアをしている加藤です。本記事では、SwiftとOpenCVを用いて画像処理する方法を解説します。さらに、C++で画像処理を行い、Swiftで結果を表示する方法についても紹介します。

# 目次
- 画像処理の概要・前準備
- Swiftのみで画像処理
- SwiftとObjective C++で画像処理
- SwiftとC++で画像処理

# 画像処理の概要・前準備
まず画像処理とは、デジタル画像に対して計算処理を行い、画像の特徴を強調したり、必要な情報を抽出したりする技術や手法の総称です。今回は、画像の波線部分の色を黄色から緑色に変換する処理を行います。具体的な処理の流れは、以下の通りです。

1. 画像を白〜黒にする（グレースケール化）
1. 画像を白黒にする（二値化）
1. 画像から輪郭を抽出する
1. 輪郭を塗りつぶす

以下は、画像処理の各ステップを示したスクリーンショットです。

|グレースケール化|二値化|輪郭抽出|
|---|---|---|
|![Simulator Screenshot - iPhone 15 Pro - 2024-03-18 at 19.51.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588901/f3c5f235-e6d3-d368-7eed-26072fbe6f4f.png)|![Simulator Screenshot - iPhone 15 Pro - 2024-03-18 at 22.13.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588901/6e2185c4-b77a-2a51-5f87-c0d52ead132e.png)|![Simulator Screenshot - iPhone 15 Pro - 2024-03-18 at 22.14.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588901/400885b0-d48a-efc0-45ca-d875430363fd.png)|

今回の記事ではSwiftとOpenCVの説明が主なので、上記の画像処理フローの詳細な説明は割愛します。

# Swiftのみで画像処理
SwiftでOpenCVを利用する場合には、`opencv2.framework`などのフレームワークを追加する必要があります。今回はXcodeで開発しており、Xcodeにおける`opencv2.framework`の追加方法は以下の記事を参考にしました。

https://dev.classmethod.jp/articles/build-opencv2-xcframework-manually/

Swiftのみで行う画像処理は以下の通りです。
```ImagePainter.swift
import UIKit
import opencv2

enum ImagePainter {
    static func imagePainting(assetName: String) -> UIImage? {
        guard let image = UIImage(named: assetName) else {
            return nil
        }
        let mat = Mat(uiImage: image)
        let matGray = mat.clone()

        // グレースケール化
        Imgproc.cvtColor(
            src: mat,
            dst: matGray,
            code: ColorConversionCodes.COLOR_RGBA2GRAY
        )

        // 二値化
        let matBinary = mat.clone()
        Imgproc.threshold(
            src: matGray,
            dst: matBinary,
            thresh: 100,
            maxval: 255,
            type: .THRESH_BINARY
        )

        let contoursNSMutableArray: NSMutableArray = []
        let hierarchy = Mat()
        // 領域抽出
        Imgproc.findContours(
            image: matBinary,
            contours: contoursNSMutableArray,
            hierarchy: hierarchy,
            mode: RetrievalModes.RETR_TREE,
            method: ContourApproximationModes.CHAIN_APPROX_SIMPLE
        )
        var contours: [[Point2i]] = []
        for contourObj in contoursNSMutableArray {
            if let contour = contourObj as? [Point2i] {
                if contour.count < 100 { // 面積閾値を設定して、小さい領域を無視
                    continue
                }
                contours.append(contour)
            }
        }

        // 塗りつぶしの色を指定
        let green: Scalar = Scalar(0, 255, 0, 255)
        let coloringImage = mat.clone()

        // 指定した色で塗りつぶし
        Imgproc.drawContours(image: coloringImage, contours: contours, contourIdx: -1, color: green, thickness: -1)

        return coloringImage.toUIImage()
    }
}
```
imagePainting関数からreturnされたUIImageが画像処理された画像になります。

# SwiftとObjective C++で画像処理
C++で画像処理を行い、Swiftで表示する方法の一つとして、Objective C++でブリッジするという方法があります。そのため、ブリッジヘッダーを作成してから実装する必要があります。プログラムは以下の通りです。

```swift
let objcPaintedImage = ObjcImagePainter.imagePainting(UIImage(named: assetName))
```
```ObjcImagePainter.mm
#import <opencv2/opencv.hpp>
#import <opencv2/imgcodecs/ios.h>
#include "ObjcImagePainter.h"

@implementation ObjcImagePainter

+(UIImage *)imagePainting: (UIImage *)image {
    cv::Mat source_img;
    UIImageToMat(image, source_img);

    cv::Mat painted_img, gray_img, bin_img;
    painted_img = source_img.clone();
    cv::cvtColor(source_img, gray_img, cv::COLOR_RGBA2GRAY); // グレースケール化
    cv::threshold(gray_img, bin_img, 1, 255, cv::THRESH_BINARY); // 二値化

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(bin_img, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // 輪郭抽出
    for( size_t i = 0; i< contours.size(); i++ ) {
        // 塗りつぶし
        cv::drawContours(painted_img, contours, i, cv::Scalar(0, 255, 0, 255), -1);
    }
    UIImage *dst_img = MatToUIImage(painted_img);
    return dst_img;
}

@end 
```
このプログラムでは、UIImageをObjective C++内でcv::Mat型（OpenCV C++の画像型）に変換して、画像処理を施した後、再度UIImageに戻してreturnしています。

# SwiftとC++で画像処理
SwiftとC++で画像処理では、C++ interoperabilityという機能を使用してみました。プログラムは以下の通りです。（こちらのプログラムは、高解像度画像の場合、メモリ消費が大きくなるかもしれません。必要に応じて画像サイズを調整してください。）
```ImageConverterView.swift
// 3.1 画像ステータスの取得
let pixelValues = originalImage.getPixelValueFromUIImage()
let channels = originalImage.getImageChannel()
let imageWidth = originalImage.cgImage!.width
let imageHeight = originalImage.cgImage!.height

// 3.2 Array<UInt8> (swift) -> std::vector<uint8_t> (c++)
var cxxVector = CxxImageProcessor.Vector()
for value in pixelValues {
    cxxVector.push_back(value)
}

// 3.3 Image Processing in C++
let convertedPixelValues = Array<UInt8>(
    cxxImageProcessor.process(cxxVector, Int32(channels), Int32(imageWidth), Int32(imageHeight))
)

// 3.4 Create UIImage from Array<UInt8>
let cxxPaintedImage = UIImage.arrayToImage(pixelValues: convertedPixelValues, width: imageWidth, height: imageHeight)
```
```UIImage+.swift
import UIKit

extension UIImage {
    func getImageChannel() -> Int {
        guard let cgImage = self.cgImage else {
            return 4
        }

        guard let colorSpace = cgImage.colorSpace else {
            return 4
        }

        let colorSpaceModel = colorSpace.model

        var channelCount: Int = 0
        switch colorSpaceModel {
        case .monochrome:
            channelCount = 1
        case .rgb:
            channelCount = 3
        default:
            break
        }

        if cgImage.alphaInfo != .none && cgImage.alphaInfo != .noneSkipLast && cgImage.alphaInfo != .noneSkipFirst {
            channelCount += 1
        }

        return channelCount
    }

    func getPixelValueFromUIImage() -> [UInt8] {
        guard let cgImage = self.cgImage else { return [] }
        let width = cgImage.width
        let height = cgImage.height
        let colorSpaceModel = cgImage.colorSpace?.model

        var bytesPerPixel = 4
        var bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue
        var colorSpace = CGColorSpaceCreateDeviceRGB()

        switch colorSpaceModel {
        case .monochrome:
            bytesPerPixel = 1
            bitmapInfo = CGImageAlphaInfo.none.rawValue
            colorSpace = CGColorSpaceCreateDeviceGray()
        case .rgb:
            if cgImage.alphaInfo == .none {
                bytesPerPixel = 3
                bitmapInfo = CGImageAlphaInfo.noneSkipLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
            }
        default:
            break
        }
        let bytesPerRow = bytesPerPixel * width
        let bitsPerComponent = 8
        var pixelData = [UInt8](repeating: 0, count: width * height * bytesPerPixel)

        // ビットマップコンテキストを作成
        guard let context = CGContext(data: &pixelData,
                                      width: width,
                                      height: height,
                                      bitsPerComponent: bitsPerComponent,
                                      bytesPerRow: bytesPerRow,
                                      space: colorSpace,
                                      bitmapInfo: bitmapInfo) else { return [] }

        // CGContextに画像を描画してピクセルデータを取得
        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))

        return pixelData
    }

    static func arrayToImage(pixelValues: [UInt8], width: Int, height: Int) -> UIImage? {
        let bytesPerPixel = 4
        let bytesPerRow = bytesPerPixel * width
        let bitsPerComponent = 8

        // ピクセルデータを使ってCGImageを生成
        guard let providerRef = CGDataProvider(data: Data(pixelValues) as CFData) else { return nil }
        guard let cgim = CGImage(
            width: width,
            height: height,
            bitsPerComponent: bitsPerComponent,
            bitsPerPixel: bytesPerPixel * bitsPerComponent,
            bytesPerRow: bytesPerRow,
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
            provider: providerRef,
            decode: nil,
            shouldInterpolate: true,
            intent: .defaultIntent
        ) else { return nil }

        return UIImage(cgImage: cgim)
    }
}
```
```CxxImageProcessor.cpp
#include "CxxImageProcessor.hpp"
#import "IconPainter-Swift.h"

#import <opencv2/opencv.hpp>

std::vector<uint8_t> CxxImageProcessor::process(std::vector<uint8_t> pixel_values, int channel, int image_width, int image_height) {

    cv::Mat src_img = array_to_Mat(pixel_values, image_width, image_height, channel);
    cv::Mat origin_img, gray_img, bin_img, bin_inv_img;
    origin_img = src_img.clone();
    cv::cvtColor(src_img, gray_img, cv::COLOR_RGBA2GRAY);
    cv::threshold(gray_img, bin_img, 1, 255, cv::THRESH_BINARY);

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(bin_img, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
    for( size_t i = 0; i< contours.size(); i++ ) {
        cv::drawContours(origin_img, contours, i, cv::Scalar(0, 255, 0, 255), -1);
    }

    std::vector<uint8_t> outputPixelValues = Mat_to_array(origin_img);

    return outputPixelValues;
}

cv::Mat CxxImageProcessor::array_to_Mat(std::vector<uint8_t> array, int width, int height, int channel) {
    cv::Mat image;
    if (channel == 4) {
        image = cv::Mat(height, width, CV_8UC4, array.data());
        cv::cvtColor(image, image, cv::COLOR_BGRA2RGBA); // RGBAの並びに変更
    }
    return image;
}

std::vector<uint8_t> CxxImageProcessor::Mat_to_array(cv::Mat image) {
    std::vector<uint8_t> array;
    array.assign(image.data, image.data + image.total() * image.elemSize());
    return array;
}

CxxImageProcessor::CxxImageProcessor() {}
```
上記のプログラムでは、UIImageをC++にそのまま渡せないことを考慮して、UIImageの画像における色を1ピクセルずつ抽出して配列に格納しています。この配列をC++側に渡して、配列からcv::Mat型で画像を復元して、画像処理を施します。その後、画像処理を適用した画像における色を1ピクセルごとに抽出して再度配列に戻して、Swift側に渡します。そして、Swift側で配列からUIImageを復元することで画像処理を施したUIImageを取得できます。

# まとめ
いかがだったでしょうか？本記事では、SwiftとC++を用いて画像処理を行う方法について紹介しました。記事を通して、Swiftを用いた画像処理の方法や、SwiftとC++の連携の方法について興味を持っていただけたら幸いです。