4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZOZOAdvent Calendar 2024

Day 24

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

Last updated at Posted at 2024-12-23

はじめに

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

目次

  • 画像処理の概要・前準備
  • Swiftのみで画像処理
  • SwiftとObjective C++で画像処理
  • SwiftとC++で画像処理

画像処理の概要・前準備

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

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

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

グレースケール化 二値化 輪郭抽出
Simulator Screenshot - iPhone 15 Pro - 2024-03-18 at 19.51.25.png Simulator Screenshot - iPhone 15 Pro - 2024-03-18 at 22.13.52.png Simulator Screenshot - iPhone 15 Pro - 2024-03-18 at 22.14.52.png

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

Swiftのみで画像処理

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

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++でブリッジするという方法があります。そのため、ブリッジヘッダーを作成してから実装する必要があります。プログラムは以下の通りです。

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++の連携の方法について興味を持っていただけたら幸いです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?