[iOS] 画像の平均色を抽出する

画像（`UIImage`）の全ピクセルの平均の色（`UIColor`）を抽出するメソッドが、[Chameleon](https://github.com/viccalexander/Chameleon)という有名OSSにあります。

```swift:swift
public func AverageColorFromImage(_ image: UIImage) -> UIColor
```

```objc:objc
+ (UIColor *)colorWithAverageColorFromImage:(UIImage *)image
```

この平均色を抽出するメソッドの実装が「なるほどそうやるのか」と参考になったのでここに書いておきます。

https://github.com/viccalexander/Chameleon/blob/master/Pod/Classes/Objective-C/UIColor%2BChameleon.m

```objc
+ (UIColor *)colorWithAverageColorFromImage:(UIImage *)image withAlpha:(CGFloat)alpha {
    
    //Work within the RGB colorspoace
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    unsigned char rgba[4];
    CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    
    //Draw our image down to 1x1 pixels
    CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image.CGImage);
    CGColorSpaceRelease(colorSpace);
    CGContextRelease(context);
    
    //Check if image alpha is 0
    if (rgba[3] == 0) {        
        CGFloat imageAlpha = ((CGFloat)rgba[3])/255.0;
        CGFloat multiplier = imageAlpha/255.0;
        
        UIColor *averageColor = [UIColor colorWithRed:((CGFloat)rgba[0])*multiplier green:((CGFloat)rgba[1])*multiplier blue:((CGFloat)rgba[2])*multiplier alpha:imageAlpha];
        
        //Improve color
        averageColor = [averageColor colorWithMinimumSaturation:0.15];
        
        //Return average color
        return averageColor;
    }    
    else {        
        //Get average
        UIColor *averageColor = [UIColor colorWithRed:((CGFloat)rgba[0])/255.0 green:((CGFloat)rgba[1])/255.0 blue:((CGFloat)rgba[2])/255.0 alpha:alpha];
        
        //Improve color
        averageColor = [averageColor colorWithMinimumSaturation:0.15];
        
        //Return average color
        return averageColor;
    }
}
```


## 実装のポイント

ポイントは、画像の全ピクセルを走査して各ピクセル値を合算して平均色を出す、みたいなことをやるのではなく、`CGContextDrawImage`でいったん**1x1ピクセルの画像に書き出す**という処理。

```objc
CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image.CGImage);
```

`CGContextDrawImage`でダウンサンプルする際に平均化されることを利用しています。Core Graphicsが中でどれだけ効率よくやってくれてるのかはわかりませんが、画像の全ピクセルをメモリに展開して走査するよりは良さそうです。

もうひとつのポイントは、"Improve color"というコメントつきで書かれている次の1行。

```objc
averageColor = [averageColor colorWithMinimumSaturation:0.15];
```

`colorWithMinimumSaturation:`は、`UIColor+ChameleonPrivate.m`に実装されている、HSB色空間で彩度の最低値を補正するメソッドです。

```objc
- (UIColor *)colorWithMinimumSaturation:(CGFloat)saturation {
    if (!self)
        return nil;
    
    CGFloat h, s, b, a;
    [self getHue:&h saturation:&s brightness:&b alpha:&a];
    
    if (s < saturation)
        return [UIColor colorWithHue:h saturation:saturation brightness:b alpha:a];
    
    return self;
}
```

ここで指定している`0.15`が、色処理の世界でこういう処理をやる際によく使われる値なのか、Chameleon独自の調整値なのかはわかりません。ガンマを考慮してないのでえいやで彩度を上げてるのかもしれないし、平均取った結果の彩度が高いほうがライブラリとしての顧客（ユーザー）満足度が上がるという判断なのかもしれません。

## その他

- Swiftで実装しなおす機会があれば追記します
- GPUImage2にも[AverageColorExtractor](https://github.com/BradLarson/GPUImage2/blob/master/framework/Source/Operations/AverageColorExtractor.swift)というクラスがあるようです
  - が、`UIColor`（CPU側で取り扱う）ものを抽出するためにGPU側で処理をするのは直感的には筋が良くないかなと思いました