モノトーンの伝説日記

Apex Legends, Splatoon, Programming, and so on...

Apple platforms (tvOS, macOS, iPadOS, iOS, watchOS) で画像の雰囲気を読み取って,画像に追加処理を行う方法

 こんにちは,モノトーンです。

 今日は Apple platforms で画像を処理していくお話。

1. Core Graphics extensions

 Core Graphics extensions を公開しています。

 macOS (AppKit, Cocoa) と tvOS, iPadOS, iOS (UIKit, Cocoa Touch) の両方で扱えるように Core Graphics をベースに設計しています。

github.com

2. 利用例

2.1 利用例1: 画像の淵が背景色に近いとき,画像の淵に線を引く

 まずは,簡単な流れをご紹介

  1. avatar なら正方形アスペクト,それ以外なら実際に使うアスペクトを意識して,小さなピクセルにリサイズします。
  2. 実際に必要な淵部分を判定にかけ,どの程度の割合が背景色に近いのかを調べます。
enum BackgroundType {
  case light
  case dark
}

extension UIImage {
  func isNearestBackground(_ backgroundType: BackgroundType = .light) -> Bool? {
    guard
      let resizedImage = self.cgImage?.sRGB(to: (24, 24)), // sRGBで正規化します。8-bit RGBAになります。options: 引数でそれ以外にも変換できます
      let pixelProvider = resizedImage.pixelProvider.toHSV else { return nil }
    let conditionHandler: ([UInt8]) -> Bool
    if backgroundType == .dark {
      conditionHandler = { pixels in
        // 黒背景に近いケースなので,
        // S (彩度) 16 以下,B (明度) 16 以下, A (透過) 235以上
        pixels[1] < 16 && pixels[2] < 16 && pixels[3] > 235
        // 透過アイコンの場合,利用者は透過であることを想定しているので,淵を描画させない
      }
    } else {
      conditionHandler = { pixels in
        // 白背景に近いケースなので,
        // S (彩度) 16 以下,B (明度) 235 以上, A (透過) 235以上
        pixels[1] < 16 && pixels[2] > 235 && pixels[3] > 235
        // 透過アイコンの場合,利用者は透過であることを想定しているので,淵を描画させない
      }
    }
    // 淵のピクセルが 2/3 以上が背景に近い場合
    return pixelProvider.ratio(onEdge: .unitEdge, conditionHandler: conditionHandler) > 0.666
  }
}

2.2 利用例2: 画像の全体の中央値が,どちら寄りかで,暗めの画像なのか明るめの画像なのか判定する。

 まずは,簡単な流れをご紹介

  1. avatar なら正方形アスペクト,それ以外なら実際に使うアスペクトを意識して,小さなピクセルにリサイズします。
  2. ピクセル全体のヒストグラムを意識してその中央値となる部分をサーチします。

 Median は簡単に処理できるので,以下の関数を用います。この median は厳密 median じゃなくてもいいので,fast 版を call します。

gist.github.com

extension UIImage {
  func isDarkImage() -> Bool? {
    guard
      let resizedImage = self.cgImage?.grayscale(to: (24, 24)), // gray(to:) は RGBA 画像を G の単一要素に変換します
      let pixels = resizedImage.pixelProvider.pixels else { return nil }
    // grayscale 化した単一要素の明るさが 160 以下の場合,暗い画像とみなします
    return pixels.map { $0[0] }.medianFast() < 160
  }
}

まとめ

 白い淵が多い画像に枠線をつけたりとか用途は色々!

 自分の場合は,画像の更新を「RxSwift.Observable」で取れるようにしているので,その下段にリサイズから判定する処理仕込んで使ってます。

 ちなみに角丸アイコンで線引く処理,高速化したいって人は,MTKit っていうのを公開しているので,それを Swift 側で継承して以下のようにするといいです。テーマ自体のステートは保持していない(そもそも dark か light によって判定式が変わるので,仮に同じ画像を複数箇所で使うなら,viewModel に dark state 持たせて処理させたほうがいいですね)実装です。

import MTKit

enum MTBorderedRoundedRectProcessingType {
    case none
    case light
    case dark
}

final class MTBorderedRoundedRectProcessingImageView: MTRoundedRectProcessingImageView {
    var mode: MTBorderedRoundedRectProcessingType = .none {
        didSet {
            setNeedsProcessingImage()
        }
    }
    
    override func processImage(after context: CGContext, with rect: CGRect) {
        guard let path = maskPath else { return }

        switch mode {
        case .light:
            context.setStrokeColor(UIColor(white: 0.0, alpha: 0.3).cgColor)
            
        case .dark:
            context.setStrokeColor(UIColor(white: 1.0, alpha: 0.3).cgColor)
            
        default:
            return
        }
        context.setLineWidth(1.0)
        context.addPath(path)
        context.drawPath(using: .stroke)
    }
}

github.com

 色々投げやりですみません。

 iOS 13 でさらに触りたいテーマが増えたので,Core Graphics extensions は一旦終わりにして,SwiftUI 触ろうと思います(一週間後に記事上がってくると思います)。

 それでは〜