🔍

Core Imageフィルタを活用したUIImageの画像一致判定メソッドの実装

2024/10/10に公開

はじめに

この記事では、Swiftで書かれた UIImage の拡張メソッド matchesImage(_:tolerance:) について、画像の一致判定を行う方法とその原理について説明します。このコードは、iOSアプリで画像比較のニーズがある場合に役立つユーティリティです。具体的には、2つの画像がどれだけ似ているかをピクセル単位で判定する機能を提供しています。

背景

最初に、この画像一致判定を実装する際には、Core Graphics を使って1ピクセルずつ比較する方法を試みました。しかし、この方法では非常に時間がかかり、特に画像サイズが大きい場合には実用的ではありませんでした。そこで、Core Image のフィルタを活用した現在の実装に変更した結果、処理時間は約1/10に短縮されました。この変更により、より効率的で実用的な画像比較が可能になっています。

コード全文

import CoreImage
import UIKit

extension UIImage {
    /// 画像の一致判定
    /// - Parameters:
    ///   - image: 比較対象のUIImage
    ///   - tolerance: 許容するピクセルの誤差(0.0から1.0まで、デフォルトは0.1)
    /// - Returns: 画像が許容範囲内で一致する場合は`true`、それ以外は`false`
    func matchesImage(_ image: UIImage, tolerance: CGFloat = 0.1) -> Bool {
        guard size == image.size else { return false }

        let ciImage1 = CIImage(image: self)!
        let ciImage2 = CIImage(image: image)!

        // CIDifferenceBlendModeフィルタを使用して差分を計算
        let differenceFilter = CIFilter(name: "CIDifferenceBlendMode")!
        differenceFilter.setValue(ciImage1, forKey: kCIInputImageKey)
        differenceFilter.setValue(ciImage2, forKey: kCIInputBackgroundImageKey)
      
        // フィルタを適用した結果を取得
        let outputImage = differenceFilter.outputImage!

        // 平均色を計算するフィルタを使用
        let extentVector = CIVector(x: outputImage.extent.origin.x, y: outputImage.extent.origin.y, z: outputImage.extent.size.width, w: outputImage.extent.size.height)
        let avgFilter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: outputImage, kCIInputExtentKey: extentVector])!
        let avgOutputImage = avgFilter.outputImage! // avgOutputImage は 1 ピクセルに圧縮された平均色の画像です

        // CIContextを使って、平均色を取得
        let context = CIContext(options: nil)
        var bitmap = [UInt8](repeating: 0, count: 4)
        context.render(avgOutputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)

        // 平均色がゼロ(差分なし)に近ければ画像は一致している
        let diffR = CGFloat(bitmap[0]) / 255.0
        let diffG = CGFloat(bitmap[1]) / 255.0
        let diffB = CGFloat(bitmap[2]) / 255.0

        // RGBの平均値が許容誤差以下であれば、一致とみなす
        let totalDifference = (diffR + diffG + diffB) / 3.0
        return totalDifference <= tolerance
    }
}

画像一致判定メソッドの概要

このメソッドは、UIImage クラスに対して以下のような画像比較機能を提供します。

  • パラメータ: 比較対象となるもう一つの UIImage と、許容する誤差 (tolerance) を設定できます。
  • 返り値: 画像が指定した誤差の範囲で一致する場合は true、一致しない場合は false を返します。

tolerance パラメータを使うことで、完全に一致していなくてもわずかな違いを許容する柔軟な一致判定が可能です。

実装の詳細

メソッドの実装は以下のように進行します:

1. サイズの一致確認

guard size == image.size else { return false }

まず、画像のサイズが異なる場合、即座に false を返します。サイズが違う画像は異なるとみなされ、これ以降の比較処理は無駄になるためです。

2. CIImageの生成

let ciImage1 = CIImage(image: self)!
let ciImage2 = CIImage(image: image)!

UIImage インスタンスから CIImage を生成します。CIImage は Core Image で画像処理を行うためのクラスです。

3. CIDifferenceBlendModeフィルタによる差分計算

let differenceFilter = CIFilter(name: "CIDifferenceBlendMode")!
differenceFilter.setValue(ciImage1, forKey: kCIInputImageKey)
differenceFilter.setValue(ciImage2, forKey: kCIInputBackgroundImageKey)

CIDifferenceBlendMode フィルタを使用して、2つの画像の差分を計算します。このフィルタは各ピクセルで差分をとり、どれだけ異なるかを明らかにします。

4. 平均色の計算

let outputImage = differenceFilter.outputImage!
// 画像全体を表す範囲を指定するためのベクトルを生成
let extentVector = CIVector(x: outputImage.extent.origin.x, y: outputImage.extent.origin.y, z: outputImage.extent.size.width, w: outputImage.extent.size.height)
let avgFilter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: outputImage, kCIInputExtentKey: extentVector])!
let avgOutputImage = avgFilter.outputImage! // avgOutputImage は 1 ピクセルに圧縮された平均色の画像です

差分画像から、CIAreaAverage フィルタを使って全体の平均色を計算します。差分がない場合、平均色はほぼゼロになります。これにより、画像がどれほど一致しているかを定量的に評価します。

5. 平均色の取得と一致判定

let context = CIContext(options: nil)
var bitmap = [UInt8](repeating: 0, count: 4)
context.render(avgOutputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)

ここでは、CIContext を使って avgOutputImage の平均色を取得し、その結果を bitmap 配列に格納しています。ここで得られる色成分 (R, G, B) をもとに差分を評価します。

次に、RGB成分を正規化して比較します。

let diffR = CGFloat(bitmap[0]) / 255.0
let diffG = CGFloat(bitmap[1]) / 255.0
let diffB = CGFloat(bitmap[2]) / 255.0
let totalDifference = (diffR + diffG + diffB) / 3.0
return totalDifference <= tolerance

平均色の RGB 成分を0から1の範囲に変換し、それらの平均値が指定された許容誤差 (tolerance) 以下であれば画像は一致しているとみなします。

このコードの目的と許容事項

このコードは、主にテストコード向けに設計されています。そのため、以下のような問題点が指摘される場合がありますが、テスト用途での利便性を重視しているため、これらの点については許容しています。

  • 強制アンラップによるクラッシュリスク: 強制アンラップ (!) を使用していますが、これはテスト環境で確実に有効な入力を想定しているためです。
  • メモリ使用量の増加: Core Image の利用に伴うメモリ消費が増加する可能性がありますが、テスト用途では許容範囲としています。
  • 処理の重さ: リアルタイムでの使用は想定していないため、処理の重さについては問題視していません。
  • カラースペースの扱い: カラースペースの違いによる影響がある場合もありますが、テストの目的上、それほど厳密な一致を求めていないため、許容しています。

利用例と注意点

利用例

このメソッドは、以下のようなユースケースに適しています。

  • 画像のテスト: UIテストや、処理の結果として期待する画像と一致しているかを確認したい場合。
  • 重複画像の検出: 画像ライブラリ内で重複している画像を検出したい場合。

注意点

  • パフォーマンス: 画像のサイズが大きい場合、処理に時間がかかることがあります。そのため、アプリのリアルタイム処理には向いていないかもしれません。
  • 正確性: 許容誤差 (tolerance) によって結果が大きく変わります。微妙な差異を検出したい場合、tolerance の値を調整する必要があります。

まとめ

この拡張メソッド matchesImage(_:tolerance:) は、Core Image フィルタを活用して2つの画像の一致度を高速に評価します。CIDifferenceBlendMode フィルタを使って差分を計算し、CIAreaAverage フィルタで全体の違いを確認することで、どの程度画像が一致しているかを評価しています。このアプローチにより、ピクセル単位の厳密な一致判定を行うだけでなく、許容範囲を指定して柔軟な一致判定も可能となります。

画像比較を効率的に実装したい場合や、アプリ内で画像の正確性を担保する必要があるシーンで活用してみてください。

GitHubで編集を提案

Discussion