💎

災害時安否確認用の顔検索システムを作ってみた

2021/11/09に公開

開発背景

日本は東日本大震災、阪神・淡路大震災など数多くの自然災害が発生する災害大国です。

もし家族と離れ離れになった時、被災した家族に連絡手段がなかったら。「子供がいなくなった」とか「スマホ持ってないおばあちゃんと離れ離れになった」とか、大事な人がどこへいってしまったか分からなくなったら不安でたまらないですよね。

そんな時に顔認証技術でどこにいるのか探せたらいいと思いませんか?

調べてみたのですがOSSでその様なシステムが存在しなさそうでしたので今回作ってみようと思いました。

ソースコード

https://github.com/yKesamaru/disaster

出来上がったもの

Disaster(ディザスター)と命名しました。MITライセンスです。
demo
screenshot
demo

HEROKUにデプロイしたもの(追記)

Disaster
Webアプリケーションの起動に30秒以上かかることがあります

使用技術

手っ取り早く形にするためにWebアプリケーションフレームワークを使っています。顔データ作成アプリケーションには機械学習ライブラリとして有名なdlibなどの他に東海顔認証で開発したコードを流用することで開発日数を短縮しました。

  • Webアプリケーション
    • flask
    • bulma
  • 顔データ作成アプリケーション
    • dlib, face-recognition
    • PySimpleGUI, opencv等

特徴

  • シェルターに設置されているカメラの映像から元の顔画像に復元不可能な数値データに変換します。プライバシーを最重要視するため被災者の顔画像は破棄されますし表示もされません。
  • 家族の写真をDisaster Webアプリケーションにアップロードすると、似ている人を自動的に探し、いつどのシェルターに被災者がいたかという情報を表示します
  • 自治体や組織は自由にシステムを利用する事が可能です

システム要件

  • Unix-like OS
  • NVIDIA GeForce GTX 1660 Ti +
  • Python 3.7 +
  • ネットワークカメラやWebカメラ

使用方法

$ git clone https://github.com/yKesamaru/disaster.git

こちらを参照してPython実行環境等を構築して下さい。システムのPythonを汚さないようにPython仮想環境の構築方法も記載してあります。

作成手順

Webアプリケーション

Flask


Pythonで使えるWebアプリケーションフレームワークではDjangoが有名ですが、手っ取り早く開発するためにFlaskを採用しました。Flaskは必要最小限の機能を持っておりササッと作る場合には最適かと思います。例えばDisasterでは家族の顔が写っている写真をアップロードしてもらう時、不正なファイル名であった場合や大きすぎるファイルサイズであると困ってしまいます。そういう対策は以下の様に簡単に対策することが出来ます。

不正なファイル名対策
@app.route('/uploads', methods=['get', 'post'])
def send():
    img_file = request.files['img_file']
    uploaded_file_path = os.path.join(
        UPLOAD_FOLDER, secure_filename(img_file.filename))
    img_file.save(uploaded_file_path)

    check_images_file_npData = cv2.imread(
        os.path.join(UPLOAD_FOLDER, secure_filename(img_file.filename)))

    # convert BGR to RGB
    check_images_file_npData = check_images_file_npData[:, :, ::-1]

この様にすると「123日本語abc.jpg」だったら「123abc.jpg」に自動的に変更されます。

大きすぎるファイルをはねる
# limit upload file size : 16MB
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
@app.errorhandler(werkzeug.exceptions.RequestEntityTooLarge)
def handle_over_max_file_size(error):
    return render_template(
        'too_large_file.html'
    )


またFlaskではリンクが一度に複数作成される場合大変便利な構文が存在します。

複数リンクのルーティング
@app.route('/static/faces/<name>.html')
def name_path(name):
    name_path = 'static/faces/' + name

上記3行だけで下図のような複数リンク(複数の顔がそれぞれリンクになる)を実現しています。

Flaskを使用する時にVSCodeのlaunch.jsonに以下の様に記載するとコードを書いた側から実行できます。

VSCodeのlaunch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "flask",
      "type": "python",
      "request": "launch",
      "python": "python3へのpath",  ←書き換えて下さい
      "cwd": "/home/user/disaster/web_app",  ←書き換えて下さい
      "module": "flask",
      "env": {
        "FLASK_APP": "main.py",  ←書き換えて下さい
        "FLASK_ENV": "development"
      },
      "args": [
        "run",
        "--no-debugger"
      ],
      "jinja": true
    }
  ]
}

更に便利なことにベースとなるhtmlファイルを作っておくとその他のhtmlファイルは記述量をかなり減らせます。

base.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.2/css/bulma.min.css">
    <title>Disaster</title>
</head>
<body style="background-color:#ffe8f6">
(中略)
    <section class="section">
        <div class="content has-text-centered">
            <div class="container is-mobile">
                {% block content %}  ←この部分と
                {% endblock %}  ←この部分
            </div>
        </div>
    </section>
(後略)
その他のファイル
{% extends "base.html" %}
{% block content %}  ←この行と
(中略)
{% endblock %}  ←この行に挟むだけ

このお陰でコード量が短くなり見通しも良くなります。
この他にも標準で開発用サーバがついてくるなど便利機能があるのでぜひ使ってみてほしいフレームワークです。
https://flask.palletsprojects.com/en/2.0.x/

Bulma

CSSフレームワークにはBulmaを採用しました。Flaskと同様に小規模サイトに向きます。最も簡単な使い方は<head>タグに<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.2/css/bulma.min.css">を挟み込むだけです。
例えばナビゲーションバーは以下の様に簡単に記述することが出来ます。

ナビゲーションバー
    <nav class="navbar is-light" role="navigation" aria-label="main navigation">
        <div class="navbar-brand">
            <a class="navbar-item" href="{{ url_for('index') }}">
                <img src="/static/images/logo.png" width="112">
            </a>

            <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false"
                data-target="navbarBasicExample">
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
            </a>
        </div>

        <div id="navbarBasicExample" class="navbar-menu">
            <div class="navbar-start">
                <a class="navbar-item" href="{{ url_for('index') }}">Home</a>
                <a class="navbar-item">Disaster Information</a>
                <div class="navbar-item has-dropdown is-hoverable">
                    <a class="navbar-link">Contribution</a>
                    <div class="navbar-dropdown">
                        <a class="navbar-item">Individual</a>
                        <a class="navbar-item">Enterprise</a>
                    </div>
                    <a class="navbar-item" href="https://github.com/yKesamaru/disaster#disaster" target="blank">Documentation</a>
                </div>
            </div>

            <div class="navbar-end">
                <div class="navbar-item">
                    <div class="buttons">
                        <a class="button is-primary">
                            <strong>Sign up</strong>
                        </a>
                        <a class="button is-light">Log in</a>
                    </div>
                </div>
            </div>
        </div>
    </nav>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
            if ($navbarBurgers.length > 0) {
                $navbarBurgers.forEach(el => {
                    el.addEventListener('click', () => {
                        const target = el.dataset.target;
                        const $target = document.getElementById(target);
                        el.classList.toggle('is-active');
                        $target.classList.toggle('is-active');
                    });
                });
            }
        });
    </script>

簡単にそれっぽい形に出来るのでこちらも大変オススメします。公式サイトを覗いてみて下さい。
https://bulma.io/
Bulmaを推していますがどんなサイトにも適用可能なわけではないです。メリットとデメリット両方に当てはまることですが、例えばBulmaにはJavascriptがついてきません。使い慣れたJavascriptフレームワークを使えば良いですし、そもそもDisasterの様にマイクロ規模であれば手書きのほうが早いです。でも最初からJavascriptが入ってたほうが良い人もいます。Bulmaのよさは「とっつきやすさ」だと思うのでそこら辺は好みの問題かなと思います。
あとは大人気CSSフレームワーク「Bulma」を採用した理由とやっぱり使うのをやめた理由で解説されていますがクラス名のスコープがグローバルです。これはサイトが大きくなればなるほど問題が生じやすいということです。工夫は出来ますが、ある程度大きく成長しそうなら他のCSSフレームワークを選ぶほうが良いと思います。

顔データ作成アプリケーション

dlib, face-recognition

face-recognitionはdlibの顔認証機能を扱いやすくするラッパーライブラリです。特に複雑なことはしていないのでdlibをPythonで呼び出しても良いと思います。またface-recognition-modelsには商用利用できないdlib由来のmodelデータが含まれており、商用利用する場合はそのまま使うことは出来ません。注意して下さい。
といってもサクッと使いたい場合は重宝しますので以下の様な場所で使っています。

face_location_list = face_recognition.face_locations(
        small_frame, upsampling, mode)

face_encodings = face_recognition.face_encodings(
        small_frame, face_location_list, jitters, model)

PySimpleGUI

GUIアプリケーションを簡単に作りたい時にかかせません。
ウィンドウは以下の様にとても分かりやすく記述することが出来ます。

PySimpleGUIによるウィンドウ描画
import PySimpleGUI as sg
sg.theme('Reddit')

layout = [
    [sg.Text('Disaster sample window')],
    [sg.Image(key='display')],
    [sg.Button('terminate', key='terminate', button_color='red')]
]

window = sg.Window('Disaster sample window', layout, location=(50,50))

windowのウィジェットをPythonのリスト形式でコーディングできてしまいます。これに慣れるとtkinterを生で扱いたくなくなります。
PySimpleGUIで動画を扱う際はpng形式にコンバートしてあげる必要があります。

windowのupdate
while True:
    imgbytes = cv2.imencode(".png", small_frame)[1].tobytes()
    window["display"].update(data=imgbytes)
    if event == 'terminate':
        break

opencv-pythonのimshow()はpyenvと相性が悪いのかPython3.6, Python3.9などPython仮想環境中で試しましたが機能しませんでした。Macではバージョンの組み合わせがどうのこうの、UbuntuではPyQtがどうのこうのと議論があるようですが要はimshow()を使わなければいいわけで、そういう時にこそPySimpleGUIを使えば良いと思います。

作ってみた感想

冒頭にも書いた通り日本は災害大国です。日本以外だと自然災害に加えて民族紛争などもあったりして難民キャンプの映像を度々見ます。Disasterの顔検索結果には避難所の電話番号とGoogleマップを表示させました。電話できれば名前から安否確認できるからです。反面、顔検索はプライバシー侵害の恐れがあります。ですのでカメラ映像を検索結果に表示させませんし、サーバ側に顔画像ファイルを残すことがないように心がけました。

それらしいものは一応形になりましたが、実際に現場で使ってみないとどこをどう直してどう機能拡張すればよいのか分からないものです。それそのものを作ってみるより、どこかの現場の方が使ってみたいと思って頂けるようにする方が何百倍も難しいものだなぁと思いました。

最後までお読み頂きありがとうございました。

Discussion