LoginSignup
4
0

はじめに

こんにちは!
Swiftの学習を兼ねて、以前執筆した内容を、Swiftで実現してみようと思います。

今回は、ハミング距離による類似画像の検出にチャレンジです。

最初にお伝えしておくと、あまり実用性はありません!
こういうことができるんだ、という程度で見ていただけると幸いです!

要件

  •  画像を2枚アップロードできる
  •  比較ボタンをタップすると、画像のハミング距離と類似度を表示する

準備

プロジェクトの作成方法は、Swift & SwiftUIでタロットカードアプリを作る を参照してください。

User InterfaceはSwiftUIを選択しました。

今回のディレクトリ構成

ContentViewのみ修正してます。

実装

ContentView.swift
import SwiftUI
import PhotosUI

struct ContentView: View {
    @State private var image1: UIImage?
    @State private var image2: UIImage?
    @State private var hammingDistance: Int = 0
    @State private var similarity: Double = 0.0
    @State private var showImagePicker1 = false
    @State private var showImagePicker2 = false
    @State private var isLoading = false // ローディング状態
    @State private var showResult = false // 類似度表示状態

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                // タイトル
                Text("ImageDiff")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .padding()

                // 画像選択エリア
                HStack {
                    // 画像1を選択するボタン
                    Button(action: {
                        showImagePicker1 = true
                        // 写真を選択したら類似度の表示をオフにする
                        showResult = false
                    }) {
                        if let image1 = image1 {
                            Image(uiImage: image1)
                                .resizable()
                                .scaledToFit()
                                .frame(width: 200, height: 200)
                        } else {
                            Image(systemName: "photo")
                                .resizable()
                                .scaledToFit()
                                .frame(width: 200, height: 200)
                        }
                    }
                    .sheet(isPresented: $showImagePicker1) {
                        ImagePickerWrapper(selectedImage: $image1)
                    }

                    // 画像2を選択するボタン
                    Button(action: {
                        showImagePicker2 = true
                        // 写真を選択したら類似度の表示をオフにする
                        showResult = false
                    }) {
                        if let image2 = image2 {
                            Image(uiImage: image2)
                                .resizable()
                                .scaledToFit()
                                .frame(width: 200, height: 200)
                        } else {
                            Image(systemName: "photo")
                                .resizable()
                                .scaledToFit()
                                .frame(width: 200, height: 200)
                        }
                    }
                    .sheet(isPresented: $showImagePicker2) {
                        ImagePickerWrapper(selectedImage: $image2)
                    }
                }

                // 比較ボタン
                Button("比較") {
                    isLoading = true // ローディング開始
                    // 非同期で類似度計算を実行
                    Task {
                        await calculateDistanceAndSimilarity()
                    }
                }
                .padding()
                .font(.title2) // フォントサイズを大きく
                .frame(maxWidth: .infinity) // 横幅いっぱいに広げる
                .background(Color.blue) // 背景色を青に
                .foregroundColor(.white) // 文字色を白に
                .cornerRadius(10) // 角丸にする

                // ローディング表示
                if isLoading {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                        .scaleEffect(1.5)
                }

                // 類似度表示
                if showResult {
                    Text("ハミング距離: \(hammingDistance)")
                    Text("類似度: \(similarity * 100, specifier: "%.2f")%")
                }

                Spacer() // コンテンツを上部に寄せる
            }
            .onAppear {
                showResult = false
            }
            .padding() // VStack 全体のパディング
        }
    }

    // MARK: - 画像処理とハミング距離・類似度の計算

    func calculateDistanceAndSimilarity() async {
        guard let image1 = image1, let image2 = image2 else { return }

        // UIImageをCGImageに変換
        guard let cgImage1 = image1.cgImage, let cgImage2 = image2.cgImage else { return }

        // CGImageからCIImageを作成
        let ciImage1 = CIImage(cgImage: cgImage1)
        let ciImage2 = CIImage(cgImage: cgImage2)

        // CIFilterを使ってグレースケールに変換
        guard let grayFilter = CIFilter(name: "CIPhotoEffectMono"),
              let grayImage1 = applyFilter(image: ciImage1, filter: grayFilter),
              let grayImage2 = applyFilter(image: ciImage2, filter: grayFilter) else { return }

        // CIImageをUIImageに変換
        let context = CIContext()
        if let cgImage1 = context.createCGImage(grayImage1, from: grayImage1.extent),
           let cgImage2 = context.createCGImage(grayImage2, from: grayImage2.extent) {

            // UIImage を生成する処理を if 文の中に入れる
            let grayscaleImage1 = UIImage(cgImage: cgImage1)
            let grayscaleImage2 = UIImage(cgImage: cgImage2)

            // ここで画像を縮小
            let thumbnailSize = CGSize(width: 100, height: 100) // 縮小後のサイズ
            guard let thumbnail1 = grayscaleImage1.resized(to: thumbnailSize),
                  let thumbnail2 = grayscaleImage2.resized(to: thumbnailSize) else { return }

            // バイナリ化
            let binaryImage1 = thumbnail1.binarize(threshold: 0.5)
            let binaryImage2 = thumbnail2.binarize(threshold: 0.5)

            guard let binaryImage1 = binaryImage1, let binaryImage2 = binaryImage2 else { return }

            let data1 = binaryImage1.pixelData() // リサイズ後の画像からpixelDataを取得
            let data2 = binaryImage2.pixelData() // リサイズ後の画像からpixelDataを取得

            hammingDistance = calculateHammingDistance(data1, data2)
            similarity = 1.0 - Double(hammingDistance) / Double(data1.count)

            // 処理が終わってからメインスレッドでUIを更新
            await MainActor.run {
                isLoading = false // ローディング終了
                showResult = true // 類似度表示をオンにする
            }

        } else {
            // CIImage から CGImage への変換に失敗した場合の処理
            print("CIImage から CGImage への変換に失敗しました")
            // 処理が終わってからメインスレッドでUIを更新
            await MainActor.run {
                isLoading = false // ローディング終了
            }
            return
        }
    }
    
    // MARK: - CIImageにフィルターを適用する関数
    private func applyFilter(image: CIImage, filter: CIFilter) -> CIImage? {
        filter.setValue(image, forKey: kCIInputImageKey)
        return filter.outputImage
    }

    // MARK: - ハミング距離の計算

    func calculateHammingDistance(_ data1: [UInt8], _ data2: [UInt8]) -> Int {
        guard data1.count == data2.count else { return 0 }
        var distance = 0
        for i in 0..<data1.count {
            if data1[i] != data2[i] {
                distance += 1
            }
        }
        return distance
    }
}

// MARK: - UIImageの拡張

extension UIImage {
    // バイナリ化
    func binarize(threshold: CGFloat) -> UIImage? {
        guard let cgImage = self.cgImage else { return nil }
        let width = cgImage.width
        let height = cgImage.height
        let colorSpace = CGColorSpaceCreateDeviceGray()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
        guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { return nil }

        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
        guard let pixelData = context.data else { return nil }

        let bytesPerPixel = 1
        for y in 0..<height {
            for x in 0..<width {
                let offset = (y * width + x) * bytesPerPixel
                let pixelValue = pixelData.load(fromByteOffset: offset, as: UInt8.self)
                let newPixelValue = pixelValue > UInt8(threshold * 255) ? UInt8(255) : UInt8(0)
                pixelData.storeBytes(of: newPixelValue, toByteOffset: offset, as: UInt8.self)
            }
        }

        guard let newCGImage = context.makeImage() else { return nil }
        return UIImage(cgImage: newCGImage)
    }

    // UIImageのピクセルデータを取得
    func pixelData() -> [UInt8] {
        guard let cgImage = self.cgImage,
              let dataProvider = cgImage.dataProvider,
              let pixelData = dataProvider.data else { return [] }

        let dataPointer = CFDataGetBytePtr(pixelData)
        let width = cgImage.width
        let height = cgImage.height
        let bytesPerPixel = cgImage.bitsPerPixel / 8

        var pixelArray: [UInt8] = []
        for y in 0..<height {
            for x in 0..<width {
                let offset = (y * width + x) * bytesPerPixel
                for i in 0..<bytesPerPixel {
                    pixelArray.append(dataPointer![offset + i])
                }
            }
        }

        return pixelArray
    }

    // UIImage を指定サイズにリサイズする
    func resized(to size: CGSize) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        draw(in: CGRect(origin: .zero, size: size))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return resizedImage
    }
}

// MARK: - ImagePicker構造体

struct ImagePickerWrapper: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?

    func makeUIViewController(context: Context) -> PHPickerViewController {
        var config = PHPickerConfiguration()
        config.filter = .images
        let picker = PHPickerViewController(configuration: config)
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        // 更新処理は不要
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        let parent: ImagePickerWrapper

        init(_ parent: ImagePickerWrapper) {
            self.parent = parent
        }

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)

            guard let provider = results.first?.itemProvider else { return }

            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { image, error in
                    if let image = image as? UIImage {
                        self.parent.selectedImage = image
                    }
                }
            }
        }
    }
}

// MARK: - プレビュー用構造体

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

比較ボタンを押した後のロジック解説

比較ボタンを押すと、calculateDistanceAndSimilarity() 関数が非同期で実行されます。
この関数は、2つの画像のハミング距離と類似度を計算する処理を行います。
処理の流れは以下の通りです。

  1. 画像データの取得:

    • image1image2 に格納されている UIImage データを取得します。
    • 画像データがない場合は、処理を終了します。
  2. CGImageへの変換:

    • UIImage から CGImage を取得します。
    • CGImage は、Core Graphics で画像を扱うためのデータ形式です。
  3. CIImageへの変換:

    • CGImage から CIImage を作成します。
    • CIImage は、Core Image で画像を扱うためのデータ形式です。
  4. グレースケール変換:

    • CIFilter を使って、CIImage をグレースケールに変換します。
    • CIPhotoEffectMono というフィルターを使って、モノクロ画像を作成します。
  5. UIImageへの変換:

    • CIContext を使って、グレースケールに変換した CIImage から CGImage を作成し、それを UIImage に変換します。
  6. 画像の縮小

    • グレースケール変換後の画像を指定サイズに縮小します。
    • resized(to:) 関数を使って、画像をリサイズします。
  7. バイナリ化:

    • binarize(threshold:) 関数を使って、縮小後のグレースケール画像をバイナリ画像に変換します。
    • しきい値 threshold を使って、各ピクセルを白か黒に変換します。
  8. ピクセルデータの取得:

    • pixelData() 関数を使って、バイナリ画像のピクセルデータを取得します。
    • ピクセルデータは、UInt8 の配列として取得されます。
  9. ハミング距離の計算:

    • calculateHammingDistance(_:_:) 関数を使って、2つのバイナリ画像のピクセルデータのハミング距離を計算します。
    • ハミング距離とは、2つのバイナリデータ列で異なるビットの数を表します。
  10. 類似度の計算:

    • ハミング距離と画像のサイズから、類似度を計算します。
    • 類似度は、0 から 1 の間の値で表され、1 に近いほど類似度が高いことを意味します。
  11. UIの更新:

    • 計算が完了したら、isLoadingfalse に設定してローディング表示を非表示にし、showResulttrue に設定して類似度を表示します。
    • UIの更新は、メインスレッドで行う必要があるため、await MainActor.run を使ってメインスレッドで実行するようにしています。

このように、比較ボタンを押すと、画像の読み込み、変換、縮小、計算、UIの更新といった一連の処理が非同期で行われます。
非同期処理を行うことで、UIがフリーズすることなく、スムーズな動作を実現しています。

補足

  • Task は、非同期処理を実行するためのSwiftの機能です。
  • await は、非同期処理の完了を待つためのキーワードです。
  • MainActor.run は、メインスレッドでクロージャを実行するための関数です。

動作確認

起動直後の画面

まずは同じ画像をアップして、比較ボタンをタップ。

ハミング距離が0、類似度は100%でした!

続いて、違う画像で比較してみます。

ハミング距離が32,445、類似度は63.95%でした!

類似度の精度がどのくらい正確なのか分かりませんが、違う画像として認識したようです。

まとめ

今回は、ハミング距離を使った類似度の計算をSwiftで実装しました。
最初にお伝えしたように、あまり実用性がありませんが、みなさんも一緒にがんばりましょう🙂

4
0
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
0