pythonでスキャン画像のカラーページ判定



# 概要
※ プログラムは https://github.com/rennone/is_color_page に上がっています。
本の自炊をやっておりまして、かれこれ1000冊以上の本(主に漫画)を電子化しました。
PDFのサイズとかタブレットで読みやすくする関係上、白黒ページはグレースケール化してコントラストかけたりしています。

そこを自動化するために、スキャンした画像がカラーページなのか白黒ページなのか判定する処理を実装しました。
(まあスキャナに自動判別はあるんですが、原本データはあくまでカラーでとっておきたいのです)

結論から言うと, python + SVMで簡単で割と精度の高い結果になりました。
実装の際に https://qiita.com/kazuki_hayakawa/items/18b7017da9a6f73eba77 を参考にさせていただきました。ありがとうございます。

```python
import sklearn
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
import cv2
import numpy as np

class IsColorPageSVM:
    def __init__(self, kernel='linear', random_state=None):
        self.svm = SVC(kernel=kernel, random_state=random_state)
        self.scaler =  StandardScaler() 

    def learn(self, vec_array, label_array, test_size=0.3, random_state=None):
        # 訓練データとテストデータに分離する
        vec_train, vec_test, label_train, label_test = sklearn.model_selection.train_test_split(vec_array, label_array, test_size=test_size, random_state=random_state )

        # データの標準化処理
        self.scaler.fit(vec_train)
        # モデルの学習
        self.svm.fit(self.scaler.transform(vec_train), label_train)

        # 訓練データとテストデータの正解率を返す
        return self.test(vec_train, label_train), self.test(vec_test, label_test)

    # テストデータと答えを渡すと正解率を返す
    def test(self, vec_array, label_array):
        vec_array_std   = self.scaler.transform(vec_array)
        pred_train        = self.svm.predict(vec_array_std)
        return sklearn.metrics.accuracy_score(label_array, pred_train) 
```

# なぜSVMなのか
2クラス分類に特化していて精度が高いらしいからです。
あとはpythonですぐできそうだったからぐらいの理由です。流行りのディープラーニングとか使えばまた違う結果になるかも

# データ形式
画像データをそのまま突っ込むと画像によって次元数がバラバラなので変換処理をかけます。
128*128にReshapeとかでもいいかもですが、カラー判定だともっと少なくていいだろうという事調べたところ。
[教えてgoo](https://oshiete.goo.ne.jp/qa/6788162.html) に以下のような投稿がありました。

> cvAvgSdvを用いてHSV各要素の標準偏差を算出してみました。
そうしたところ、手持ちのイメージで試した限りですが、
彩度(S)が大体以下のようになりました。
　　カラー：単色の多いもので約40～50。通常そうに見えるもので80以上、100オーバー。
　　白黒　：20前後。
　　黄ばみ：20～30前後。※あまりサンプルなかったのでそれなりですが
これから、30～40あたりを閾値にすることで判定できそうです。

じゃあ機械学習とかしなくても、これで判断すればいいじゃんって話になるかと思います。
自分もそう思って手持ちの画像で試してみたところ、大体あっているのですが、あまりに焼けすぎているページがカラー判定されて、かといって閾値を上げると逆に微妙なカラーページが白黒判定されるといった問題がありました。
そうなると、色相とかも含めて判断する必要があるし、閾値も目分量で判断するのは無理！ってなったので機械学習させようってなった背景です。

というわけで、画像をHSV形式に変換し,HSV全部の標準偏差出して, ついでに平均値/最大値/最小値も加えてみて試してみます。

最終的に以下の12次元のベクトルをデータ形式としてテストしました。
> (Hの標準偏差, Sの標準偏差, Vの標準偏差, Hの平均値, Sの平均値, Vの平均値, Hの最大値, Sの最大値, Vの最大値, Hの最小値, Sの最小値, Vの最小値)

# テスト方法


以下のようにカラーページ/白黒ページのデータを読み込み, 100回学習させた平均値を出しました。
その際, 12次元のベクトルのうち、どこまでが学習に寄与しているのかを計る意味も込めて, 一部の次元の抽出したうえでのテストも行いました

| 項目 | 内容 |
| ---- | ---- | 
| 環境 | python 3.8 |
| カラーページの学習データ数 | 267 |
| 白黒ページの学習データ数 | 267 |
| 試行回数 | 100 |


```python

def main():
    # 2次元配列のcsvファイル読み込み
    color_vec_array = np.loadtxt('color_page.csv', delimiter=',')
    gray_vec_array = np.loadtxt('gray_page.csv', delimiter=',')
    # 白黒ページの学習データはカラー画像に対して多いので同じサイズにする
    gray_vec_array = gray_vec_array[: len(color_vec_array),:]

    # 1つの2次元配列にマージ    
    vec_array = np.append(color_vec_array, gray_vec_array, axis=0)

    # カラー画像のラベルは0, 白黒画像のラベルを1にする
    label_array = [0] * len(color_vec_array) +[1] * len(gray_vec_array)
    
    # i, j は抜き出すベクトルの次元
    for i in range(0,vec_array.shape[1]):
        for j in range(i+1,vec_array.shape[1] + 1):
            train_score, test_score = 0.0, 0.0
            # 100回訓練した結果の平均をとる
            test_num = 100
            print('{0:2}:{1:2}要素のデータ'.format(i, j), end='  ')
            for n in range(test_num):
                data_list = vec_array[:,i:j]
                model = IsColorPageSVM()                    
                train, test = model.learn(data_list, label_array)
                train_score += train
                test_score += test
            print(u'正解率(訓練/テスト):{0:0.3} - {1:0.3}'.format(train_score/test_num, test_score/test_num))

if __name__ == '__main__':
    main()
```

# 結果
以下が抽出した次元ごとの正解率になります。次元数の項目は12次元のうちどこからどこまでの範囲を使ったかを表します(3:6番だと3番目の要素から6番の一つ前の要素までの3次元を使っているという意味)。
これを見るとSの標準偏差だけでかなりの正解率にはなっています. (その辺は↑のデータ形式で話した内容と合致)

テストデータの正解率1位なのは, 0:4データ(HSV要素の標準偏差 + Hの平均値)と3:9(HSVの平均値 + HSVの最大値)の99%でした。
意外なのは標準偏差を使っていない3:9番のスコアも1位タイの結果を出していることです(訓練データのスコアも1位なのでこれが最も良い結果)
これに関しては、データ形式の章でも少し書きましたが単色のカラーページ(青一色のページとか)などもカラー判定させるように画像にラベリングしていたのでそれが原因かもしれません。

当然ですが, 最大値, 最小値のみや明度のみの情報はほぼ認識できてないです(2分類なので50%前後は実質ランダムと同じ)。最小値は黒いピクセルがあれば0になるだろうし,明度はカラーも白黒ページも関係ないので。


| 次元数 | 正解率(訓練データ) | 正解率(テストデータ)| 備考 |
| ---- | ---- | ----|----|
  0: 1 | 0.774 | 0.777 | Hの標準偏差のみ
 0: 2 | 0.958 | 0.96 | H, Sの標準偏差
 0: 3 | 0.97 | 0.968 | H, S, Vの標準偏差
 0: 4 | 0.99 | 0.99 (1位タイ) | H, S, Vの標準偏差, Hの平均値
 0: 5 | 0.99 | 0.987 | H, S, Vの標準偏差, H, Sの平均値
 0: 6 | 0.991 | 0.984 | H, S, Vの標準偏差, H, S, Vの平均値
 0: 7 | 0.991 | 0.985 | H, S, Vの標準偏差, H, S, Vの平均値, Hの最大値
 0: 8 | 0.993 | 0.986 | H, S, Vの標準偏差, H, S, Vの平均値, H, Sの最大値
 0: 9 | 0.996 | 0.989 | H, S, Vの標準偏差, H, S, Vの平均値, H, S, Vの最大値
 0:10 | 0.995 | 0.988 | H, S, Vの標準偏差, H, S, Vの平均値, H, S, Vの最大値, Hの最小値
 0:11 | 0.995 | 0.986 | H, S, Vの標準偏差, H, S, Vの平均値, H, S, Vの最大値, H, Sの最小値
 0:12 | 0.995 | 0.987 | 全要素 
 1: 2 | 0.953 | 0.952 | Sの標準偏差のみ
 1: 3 | 0.968 | 0.964
 1: 4 | 0.99 | 0.987
 1: 5 | 0.989 | 0.989
 1: 6 | 0.991 | 0.986
 1: 7 | 0.992 | 0.983
 1: 8 | 0.992 | 0.988
 1: 9 | 0.995 | 0.989
 1:10 | 0.996 | 0.987
 1:11 | 0.994 | 0.987
 1:12 | 0.995 | 0.985 
 2: 3 | 0.602 | 0.588 | Vの標準偏差のみ
 2: 4 | 0.832 | 0.831
 2: 5 | 0.928 | 0.92
 2: 6 | 0.941 | 0.937
 2: 7 | 0.943 | 0.935
 2: 8 | 0.991 | 0.986
 2: 9 | 0.994 | 0.988
 2:10 | 0.994 | 0.989
 2:11 | 0.994 | 0.987
 2:12 | 0.994 | 0.986 
 3: 4 | 0.814 | 0.816 | Hの平均値のみ
 3: 5 | 0.925 | 0.925
 3: 6 | 0.937 | 0.935
 3: 7 | 0.938 | 0.939
 3: 8 | 0.99 | 0.988
 3: 9 | 0.994 | 0.99 (1位タイ) | H, S, Vの平均値, H, S, Vの最大値 
 3:10 | 0.994 | 0.989
 3:11 | 0.993 | 0.986
 3:12 | 0.993 | 0.986 
 4: 5 | 0.772 | 0.775 | Sの平均値のみ
 4: 6 | 0.761 | 0.75
 4: 7 | 0.762 | 0.754
 4: 8 | 0.961 | 0.96
 4: 9 | 0.97 | 0.964
 4:10 | 0.97 | 0.961
 4:11 | 0.975 | 0.964
 4:12 | 0.981 | 0.97 
 5: 6 | 0.525 | 0.491 | Vの平均値のみ
 5: 7 | 0.529 | 0.492
 5: 8 | 0.961 | 0.954
 5: 9 | 0.969 | 0.962
 5:10 | 0.969 | 0.964
 5:11 | 0.974 | 0.966
 5:12 | 0.982 | 0.971 
 6: 7 | 0.516 | 0.477 | HVの最大値
 6: 8 | 0.957 | 0.961
 6: 9 | 0.97 | 0.963
 6:10 | 0.969 | 0.964
 6:11 | 0.973 | 0.966
 6:12 | 0.98 | 0.969
 7: 8 | 0.961 | 0.957 | Sの最大値のみ
 7: 9 | 0.97 | 0.965
 7:10 | 0.968 | 0.969
 7:11 | 0.973 | 0.97
 7:12 | 0.978 | 0.968 
 8: 9 | 0.521 | 0.487 | Vの最大値のみ
 8:10 | 0.522 | 0.484
 8:11 | 0.533 | 0.493
 8:12 | 0.903 | 0.897 
 9:10 | 0.512 | 0.477 | Hの最小値のみ　
 9:11 | 0.518 | 0.477
 9:12 | 0.896 | 0.891 
10:11 | 0.518 | 0.481 | Sの最小値のみ
10:12 | 0.895 | 0.891
11:12 | 0.894 | 0.894 | Vの最小値のみ

# 今後の予定

カラー判定に関しては今後やるとしたら, 画像データをreshapeしてそのまま使う形式と他のディープラーニングとかの他の手法で試してみるぐらいかな。
あとは、小説の挿絵か文章ページかを判断したいとも考えているのでそれもやるかも（てか同じような感じでそこそこ結果出たので書くかもしれません）
以上になります。最後まで読んでいただきありがとうございました。