はじめに
自然界のものには、それに対応する色があります。(バナナなら黄色、りんごなら赤、のような)
(光が当たって反射してくる〜のような、物理的な話はおいておきます)
また、デザインされたものに関しては複数の色が使われることが多いかと思います。
というのも、デザインの基本的な考え方として、こちらに記されるような配色の考え方が存在します。
一般的に、基本カラー3色を「70%:25%:5%」の比率にして配色すると、バランスの取れた美しい配色になるとされています。
最も大きな面積を占める色を「ベースカラー(70%)」、ブランドのイメージカラーなどデザインの中心になる色を「メインカラー(25%)」、画面にアクセントを持たせるための色を「アクセントカラー(5%)」と呼びます。
つまり、マンガアプリのサムネイルやキャラクターデザイン等では、色によって与えたい印象やイメージ、そのものの特性等を表すことはよくあります。
その色をUIに反映させたいな、と思ったのが今回のこの記事です。
成果物
実装
今回実装した
- 一番使われている色を抽出(スクショ左側)
- 使われている色の平均色を計算(スクショ右側)
それぞれについて見ていきます。
共通
色情報を取得する部分に関しては、一旦同じ処理としています。
(本当はそれぞれで適した処理をするのが速度的に良いと思う)
懸念点としては、イラスト等で背景色を敷いているもので、白や黒等の背景色が一番使われている色だった場合、ですね。
イラストと全然関係ない色が抽出されてしまいます。
その対応として今回は、除外する色を指定しておくことにしました。
プロジェクトごとに扱う色のフォーマットが決まっていれば、その色を除外することで有効色を抽出できそうです。
struct ColorFrequency {
let color: ColorFactor
var count: Int
}
struct ColorFactor: Equatable {
var red: Int
var green: Int
var blue: Int
var alpha: Int
static var zero: ColorFactor {
return .init(red: 0, green: 0, blue: 0, alpha: 0)
}
var uiColor: UIColor {
return .init(
red: CGFloat(self.red) / 255.0,
green: CGFloat(self.green) / 255.0,
blue: CGFloat(self.blue) / 255.0,
alpha: CGFloat(self.alpha) / 255.0
)
}
func calculateMixedColor(count: Int) -> UIColor {
return .init(
red: CGFloat(self.red) / CGFloat(count * 255),
green: CGFloat(self.green) / CGFloat(count * 255),
blue: CGFloat(self.blue) / CGFloat(count * 255),
alpha: CGFloat(self.alpha) / CGFloat(count * 255)
)
}
}
final class PickedImageColorView: BaseView {
@IBOutlet private weak var innerView: UIView!
@IBOutlet private weak var imageView: UIImageView!
private var colorFrequencies: [ColorFrequency] = []
// 無視したい色
private let excludeColors: [ColorFactor] = [
.init(red: 0, green: 0, blue: 0, alpha: 0),
.init(red: 1, green: 1, blue: 1, alpha: 1),
]
func setImage(_ imageUrl: String, pickType: PickType) {
self.imageView.setImage(with: imageUrl, placeholder: nil, completed: { [weak self] response in
guard let self = self else { return }
guard case .success(let imageResponse) = response else { return }
self.pickColor(image: imageResponse.image)
let pickedColor: UIColor = // ここで色を選定
self.innerView.backgroundColor = pickedColor
})
}
}
// MARK: - pick color
extension PickedImageColorView {
private func pickColor(image: UIImage) {
guard let provider = image.cgImage?.dataProvider,
let data = CFDataGetBytePtr(provider.data) else {
return
}
let numberOfComponents = 4
let maxWidth: Int = Int(image.size.width)
let maxHeight: Int = Int(image.size.height)
for x in 0..<maxWidth {
for y in 0..<maxHeight {
let targetPixel = (maxWidth * y + x) * numberOfComponents
let color: ColorFactor = .init(
red: Int(data[targetPixel]),
green: Int(data[targetPixel + 1]),
blue: Int(data[targetPixel + 2]),
alpha: Int(data[targetPixel + 3])
)
if !self.excludeColors.contains(color) {
self.arrangeColorFrequency(color: color)
}
}
}
}
private func arrangeColorFrequency(color: ColorFactor) {
if let index = self.colorFrequencies.firstIndex(where: { $0.color == color }) {
self.colorFrequencies[index].count += 1
}
else {
self.colorFrequencies.append(.init(color: color, count: 1))
}
}
}
一番使われている色を抽出
画像の中で一番使われている色、上の説明でいくとベースカラー。
一番使われている色、なので、各ピクセルの色を抽出し、その回数さえわかれば選定できそうです。
コード的にはこう。
// MARK: - pick color
extension PickedImageColorView {
private func getMajorColor(colorFrequencies: [ColorFrequency]) -> UIColor {
guard let majorColor: UIColor = colorFrequencies.sorted(by: { $0.count > $1.count }).first?.color.uiColor else {
return .init()
}
return majorColor
}
}
使われている色の平均色を計算
平均色なので、各ピクセルの色とそれが使われた回数がわかったら、対象ピクセル数で平均化できそうです。
こちらも先ほど同様、除外する色を指定して、それ以外を対象ピクセルとすることで有効色を抽出します。
コード的にはこう。
// MARK: - pick color
extension PickedImageColorView {
private func getMixedColor(colorFrequencies: [ColorFrequency]) -> UIColor {
var color: ColorFactor = .zero
let count: Int = colorFrequencies.reduce(0) { $0 + $1.count }
colorFrequencies.forEach { colorFrequency in
let singleCount: Int = colorFrequency.count
color.red += colorFrequency.color.red * singleCount
color.green += colorFrequency.color.green * singleCount
color.blue += colorFrequency.color.blue * singleCount
color.alpha += colorFrequency.color.alpha * singleCount
}
return color.calculateMixedColor(count: count)
}
}
それぞれの使い方
あとは、それぞれどう使うかってところなんですが、まぁenumで管理しておけばいいかなと。
enum PickType {
case major // 一番使われている色
case mixed // 平均色
}
こういうのを用意しておけば、共通のコードのところが以下のようになります。
final class PickedImageColorView: BaseView {
func setImage(_ imageUrl: String, pickType: PickType) {
self.imageView.setImage(with: imageUrl, placeholder: nil, completed: { [weak self] response in
guard let self = self else { return }
guard case .success(let imageResponse) = response else { return }
self.pickColor(image: imageResponse.image)
let pickedColor: UIColor = {
switch pickType {
case .major: return self.getMajorColor(colorFrequencies: self.colorFrequencies)
case .mixed: return self.getMixedColor(colorFrequencies: self.colorFrequencies)
}
}()
self.innerView.backgroundColor = pickedColor
})
}
}
画像URLとどの方法で色を選定するか、を指定して呼び出して、どうぞ。
最後に
今回はほんの誤差でもあれば「違う色」としましたが、実際使用するときは、ある程度の誤差を許容する方が良い場合もあるので、必要に応じてやってみてもいいかもです!
実装して、できたーと思ったらこんな記事を発見しまして。
https://qiita.com/shu223/items/805a179cfe83c47ec0f9
なるほど・・・という感じなんですが、想像した機能を自分で実現する術を知るというのは大事だと思うので、それをやってみました的な記事だと受け取ってもらえればいいかなと。
最終的にライブラリの方が良いな、とか、時間の都合で自分で試行錯誤してる場合じゃないというのは往々にしてあるとは思いますが、
こういう機能を作りたいとか、こういうことを実現したい、ってなったときに、ライブラリを探すのではなく、一旦自分でこうやったらいけるかな?と考えたりプロトタイプ実装してみたりしていくと自身の力になる説。
一旦が大事説。