ダウンロード画像と顔画像の簡易管理アプリ

# はじめに
- 機械学習用の画像を収集し、目視で仕分けする作業が発生する場合があります。
- Mac では、Finder で画像を見ながら削除を繰り返したりしますね。これは、かなり骨の折れる作業です。
- 今回は、Webブラウザベースの簡易管理アプリを作成しました。
- ソース一式は [ここ](https://github.com/maedamikio/public/tree/master/abe_or_ishihara/learning) です。


# 概要
- `トップページ`では、`config.py` の `CLASSES` を元に各メニューを表示しています。
- `ダウンロード画像、顔画像`では、両者を比較することができます。
- また、顔画像をクリックし、まとめて削除を実施できます。
- `学習画像の予測結果`と`テスト画像の予測結果`は、別途提供する予定です。

## トップページ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68632/4958d4d0-431c-5ef8-ee39-eb19c6962857.png)

## ダウンロード画像と顔画像

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68632/cd1a34ea-3dcb-93f6-9165-2a8b8b13d530.png)

## ライブラリ
- `Flask` を用いています。
- `Pillow` は、画像の拡大縮小に用いています。
- `Bootstrap` を用いています。
- `Font Awesome` で特殊なアイコンを表示しています。今回は、顔画像の右上に表示するゴミ箱のアイコンで利用しています。


# トップページ
- トップページは、`index.html` の内容をほぼそのまま表示しています。
- `config.py` の `CLASSES` は、`index.html` の `items` として渡しています。
- 各メニューの識別のために、`Bootstrap` で色を変えています。

```image_viewer.py
@app.route('/')
def index():
    """Top Page."""
    return render_template('index.html', items=CLASSES)
```

```templates/index.html
    <div class="container-fluid">
      {% for item in items %}
      <div class="row">
        <div class="col">
          {{ loop.index }} {{ item }}
        </div>
        <div class="col-11">
          <a class="btn btn-primary" href="/download_and_face/{{ item }}" role="button">ダウンロード画像、顔画像</a>
          <a class="btn btn-secondary" href="/predict/train/{{ item }}" role="button">学習画像の予測結果</a>
          <a class="btn btn-success" href="/predict/test/{{ item }}" role="button">テスト画像の予測結果</a>
        </div>
      </div>
      <br />
      {% endfor %}
    </div>
```


# ダウンロード画像と顔画像
- `DOWNLOAD_PATH` 配下には、Google カスタム検索等でダウンロードした画像が保存されています。
- 例: data/download/安倍乙/0001.jpeg
- `FACE_PATH` 配下には、`OpenCV` の `Haar Cascade` を用いて顔認識した画像が保存されています。
- また、ダウンロード画像のファイル名を元にファイル名を生成しています。
- 例: data/face/安倍乙/0001-0001.jpeg

## GET の処理
- `安倍乙` などを `item` で受け取ります。
- `DOWNLOAD_PATH` `item` `*.jpeg` をキーとして、ダウンロード画像の一覧を作成します。
- `FACE_PATH` `item` `*.jpeg` をキーとして、顔画像の一覧を作成します。

```image_viewer.py
@app.route('/download_and_face/<item>', methods=['GET', 'POST'])
def download_and_face(item):
    """ダウンロード画像、顔画像."""

    download_list = glob.glob(os.path.join(DOWNLOAD_PATH, item, '*.jpeg'))
    download_list = sorted([os.path.basename(filename) for filename in download_list])
    face_list = glob.glob(os.path.join(FACE_PATH, item, '*.jpeg'))
    face_list = sorted([os.path.basename(filename) for filename in face_list])
```

- ダウンロード画像から顔画像の検索キーを作成し、配列 `row` を作成しています。
- 各ダウンロード画像と顔画像の組み合わせは、`rows` に再格納されます。
- `item` と `rows` をテンプレートエンジンへ引き渡します。

```image_viewer.py
    rows = []
    for download in download_list:
        row = [download]
        key = download.split('.')[0] + '-'
        for face in face_list:
            if face.startswith(key):
                row.append(face)
        rows.append(row)

    return render_template('download_and_face.html', item=item, rows=rows)
```

- テンプレートでは、ダウンロード画像と顔画像の組みを 1行 で表示します。
- ダウンロード画像と顔画像のリンクを作成します。
- `size=200` で画像のサイズを指定しています。このサイズは、画像の縦のサイズとなります。
- サイズを揃える事で、ダウンロード画像と顔画像の比較がしやすくなります。
- 顔画像では、クリックで指定しやすい様に `CSS` と `JS` を利用しています。
- こちらは、下記を参考にしました。
    - [Bootstrap image checkbox](https://codepen.io/kskhr/pen/pRwKjg)


```templates/download_and_face.html
          <tbody>
            {% for row in rows %}
            <tr>
              <td>
                {{ loop.index }}
              </td>
              <td>
                <figure class="figure">
                  <img src="/data/download/{{ item }}/{{ row[0] }}?size=200" />
                  <figcaption class="figure-caption">{{ row[0] }}</figcaption>
                </figure>
              </td>
              <td>
                {% for filename in row[1:] %}
                <figure class="figure">
                  <label class="image-checkbox">
                  <img src="/data/face/{{ item }}/{{ filename }}?size=200" />
                  <input type="checkbox" name="filename" value="{{ filename }}" />
                  <i class="fa fa-trash-o d-none"></i>
                  </label>
                  <figcaption class="figure-caption">{{ filename }}</figcaption>
                </figure>
                {% endfor %}
              </td>
            </tr>
            {% endfor %}
          </tbody>
```

- 顔画像は、チェックボックスを指定した場合、指定しない場合の表示を調整しています。

```static/download_and_face.css
.image-checkbox {
  cursor: pointer;
  border: 2px solid transparent;
  box-sizing: border-box;
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  position: relative;
}

.image-checkbox input[type="checkbox"] {
  display: none;
}

.image-checkbox-checked {
  border-color: #d9534f;
}

.image-checkbox .fa {
  color: #ffffff;
  background-color: #d9534f;
  font-size: 20px;
  padding: 4px;
  position: absolute;
  right: 0;
  top: 0;
}

.image-checkbox-checked .fa {
  display: block !important;
}
```

- 初回表示の時、チェックボックスの状態をクラスに設定しています。
- また、クリック時にクラスを変更しています。

```static/download_and_face.js
// image gallery
// init the state from the input
$(".image-checkbox").each(function () {
  if ($(this).find('input[type="checkbox"]').first().attr("checked")) {
    $(this).addClass('image-checkbox-checked');
  }
  else {
    $(this).removeClass('image-checkbox-checked');
  }
});

// sync the state to the input
$(".image-checkbox").on("click", function (e) {
  $(this).toggleClass('image-checkbox-checked');
  var $checkbox = $(this).find('input[type="checkbox"]');
  $checkbox.prop("checked",!$checkbox.prop("checked"))

  e.preventDefault();
});
```

## 画像のサイズの変更
- ダウンロード画像、顔画像のパスを生成します。

```image_viewer.py
@app.route('/data/<folder>/<item>/<filename>')
def get_image(folder, item, filename):
    """画像のレスポンス size で拡大縮小."""

    if folder not in ['download', 'face']:
        abort(404)

    filename = os.path.join(DATA_PATH, folder, item, filename)
```

- `Pillow` を利用して画像を読み込みます。

```image_viewer.py
    try:
        image = Image.open(filename)
    except Exception as err:
        pprint.pprint(err)
        abort(404)
```

- URL のオプションで `size` がある場合は、画像のサイズを修正します。
- 今回の場合は、縦のサイズを元に画像が拡大縮小します。
- 縦のサイズを、ダウンロード画像と顔画像で揃えると、目視で比較しやすくなりました。
- また、`Pillow` には、`thumbnail` でサイズを変更することもできます。
- ただ、こちらは縦横の比率が変わってしまいます。

```image_viewer.py
    if 'size' in request.args:
        height = int(request.args.get('size'))
        width = int(image.size[0] * height / image.size[1])
        image = image.resize((width, height), Image.LANCZOS)
```

- 最後に、`Pillow` のデータをバイトデータに変換し、`image/jpeg` でレスポンスを作成します。

```image_viewer.py
    data = io.BytesIO()
    image.save(data, 'jpeg', optimize=True, quality=95)
    response = make_response()
    response.data = data.getvalue()
    response.mimetype = 'image/jpeg'

    return response
```

## POST の処理
- フォームで削除する顔画像のファイル名が POST されます。
- 対象の顔画像を確認し、`os.remove` で削除しています。
- ここは、もう少し慎重な仕組みが必要だと思いますが、個人で利用する事を前提に、簡易な形にしています。

```image_viewer.py
@app.route('/download_and_face/<item>', methods=['GET', 'POST'])
def download_and_face(item):
    """ダウンロード画像、顔画像."""

    if request.method == 'POST' and request.form.get('action') == 'delete':
        for filename in request.form.getlist('filename'):
            filename = os.path.join(FACE_PATH, item, filename)
            if os.path.isfile(filename):
                os.remove(filename)
                print('delete face image: {}'.format(filename))
```


# おわりに
- ダウンロード画像と顔画像を比較しながら、不要な顔画像を削除するWebアプリケーションを作成しました。
- Mac の Finder を用いて顔画像を削除するよりは、格段に便利になったと思います。
- Webアプリケーションの作成には、それなりに時間や慣れが必要ですね。
- 次回は、顔画像を水増しを実施する予定です。