djangoで物体検出

2021年6月15日

はじめに

前回はなんとかvscodeを使って、djangoをUbuntu上で動かしてみました。

今回はHello,Worldでないアプリを作ろうと考え、pythonらしく物体検出でもやってみることにしました。と、言っても検出ロジックは丸パクリです。

PythonでOpenCV DNNを利用して物体検知(Object Detection)する方法 | HaneCa

DNNはおろか、OpenCVすらよく分かってません。

仕様

  1. ユーザーがサイトに来る
  2. アプリがトップページを返す
  3. ユーザーが画像を指定してアップロードする
  4. アプリが画像から物体検出する
  5. アプリが検出済み画像をファイルに出力する
  6. アプリがファイルに出力した画像を挿し込んだトップページを返して3に戻る

リクエストが複数同時に来たときの仕様がありませんが、今回は固定ファイルに上書きします。結果は神のみぞ知るスタイルです。

設計

  • 画像あり/なしで表示を分けられるテンプレートトップページ
  • モデル空
  • 物体検出は固有のモデルでもビューでもフォームでもないものに実装
  • ビューで画像を受け取り、物体検出を呼び出し、結果を受けてテンプレートを呼び出す

実装

環境調整

djangoのような大きなモジュールをpipでインストールすると、vscodeのファイル監視が許容量オーバーしてしまいます。

Running Visual Studio Code on Linux

にあるように、システムの最大監視数を増やします(8,192→524,288)。

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

これでvscodeを起動し直して、警告が出なければ大丈夫です。

でもって、新しくアプリケーション作るため、vscodeのターミナルから以下を実行します。

python manage.py startapp detect_app

フェーズ1

まずは物体検出を行わず、受け取った画像そのものを返して実装。つまり物体検出はハリボテ(モック)。

ビューはこんな感じ。コントローラがないので、全てビューで記述してます。受け取ったファイルの検証はしてません(公開は出来ない)。

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .apps import DetectAppConfig
from .forms import UploadFileForm
from .upload import handle_uploaded_file
from .detect import detect


def upload_file(request):
    result = None
    if request.method == 'POST':
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            input_path = DetectAppConfig.image_filebase + DetectAppConfig.image_file_input
            output_path = DetectAppConfig.image_filebase + DetectAppConfig.image_file_output
            output_url = DetectAppConfig.image_urlbase + DetectAppConfig.image_file_output

            handle_uploaded_file(request.FILES['file'], input_path)
            detect(input_path, output_path)
            result = output_url
    else:
        form = UploadFileForm()
    return render(request, 'detect_app/upload.html', {'form': form, 'result': result})

中で使用されているフォームがこんな感じ。見事にファイルだけです。

from django import forms

class UploadFileForm(forms.Form):
    file = forms.FileField()

HTMLのテンプレートがこんな感じ。Bootstrap4を使ってますが、見た目はやはりアレです?。また画像ファイルのチェックをしていません(公開不可)。フォームデータと抽出結果のURLを渡されてビューから呼ばれます。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>物体検出</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>

<body class="bg-light">
    <div class="container">
        <div class="py-5 text-center">
            <h1>物体検出</h1>
        </div>
        <div class="row justify-content-center">
            <form method="post" enctype="multipart/form-data">
                {% csrf_token %}
                <div>
                    <label>画像ファイル</label>
                </div>
                <div>
                    {{form.file}}
                </div>
                {% if result %}
                <div>
                    <img src="{{ result }}" class="img-responsive">
                </div>
                {% endif %}
                <hr>
                {% for error in form.file.errors %}
                {{error}}
                {% endfor %}
                <button class="btn btn-primary btn-lg btn-block" type="submit">送信</button>
            </form>
        </div>
    </div>
</body>

</html>

そしてこれがハリボテ本体。

import shutil

def detect(input, output):
    shutil.copy2(input, output)

他にも設定ファイルなどはいじってますが、最後にgitを張るので省略します。フェーズ1はこんな感じです。

入力画像は下のサイトのをお借りしました(超綺麗な写真なので縮小したけど)。ありがとうございます。

Group of People Having Fun Together Under the Sun · Free Stock Photo

フェーズ2

いよいよ物体検出です。まずはpythonにパッケージの追加から。

pip install opencv-python
pip freeze > requirements.txt

このまま実装するとpylintがopencvを認めてくれないので、設定でpython.linting.pylintArgsを検索して、[項目追加]

「–extension-pkg-whitelist=cv2」を追加する。

でもdnnはこれでもダメなようです。うまく回避する方法は見つかりませんでした。ドキュメントコメントがないと怒られてるワーニングも大量にあるので、これ以上は諦めます。

では、気を取り直して実装します。中身を入れたdetect.pyです。

import cv2

def detect(input, output):
    classes = {0: 'background',
                1: 'person', 2: 'bicycle', 3: 'car', 4: 'motorcycle', 5: 'airplane', 6: 'bus',
                7: 'train', 8: 'truck', 9: 'boat', 10: 'traffic light', 11: 'fire hydrant',
                13: 'stop sign', 14: 'parking meter', 15: 'bench', 16: 'bird', 17: 'cat',
                18: 'dog', 19: 'horse', 20: 'sheep', 21: 'cow', 22: 'elephant', 23: 'bear',
                24: 'zebra', 25: 'giraffe', 27: 'backpack', 28: 'umbrella', 31: 'handbag',
                32: 'tie', 33: 'suitcase', 34: 'frisbee', 35: 'skis', 36: 'snowboard',
                37: 'sports ball', 38: 'kite', 39: 'baseball bat', 40: 'baseball glove',
                41: 'skateboard', 42: 'surfboard', 43: 'tennis racket', 44: 'bottle',
                46: 'wine glass', 47: 'cup', 48: 'fork', 49: 'knife', 50: 'spoon',
                51: 'bowl', 52: 'banana', 53: 'apple', 54: 'sandwich', 55: 'orange',
                56: 'broccoli', 57: 'carrot', 58: 'hot dog', 59: 'pizza', 60: 'donut',
                61: 'cake', 62: 'chair', 63: 'couch', 64: 'potted plant', 65: 'bed',
                67: 'dining table', 70: 'toilet', 72: 'tv', 73: 'laptop', 74: 'mouse',
                75: 'remote', 76: 'keyboard', 77: 'cell phone', 78: 'microwave', 79: 'oven',
                80: 'toaster', 81: 'sink', 82: 'refrigerator', 84: 'book', 85: 'clock',
                86: 'vase', 87: 'scissors', 88: 'teddy bear', 89: 'hair drier', 90: 'toothbrush'}

    # Load a model imported from Tensorflow
    tensorflowNet = cv2.dnn.readNetFromTensorflow('./model/frozen_inference_graph.pb', './model/graph.pbtxt')

    # Input image
    img = cv2.imread(input)
    rows, cols, channels = img.shape

    # Use the given image as input, which needs to be blob(s).
    tensorflowNet.setInput(cv2.dnn.blobFromImage(img, size=(300, 300), swapRB=True, crop=False))

    # Runs a forward pass to compute the net output
    networkOutput = tensorflowNet.forward()

    # Loop on the outputs
    for detection in networkOutput[0,0]:
        score = float(detection[2])
        if score > 0.2:
            left = detection[3] * cols
            top = detection[4] * rows
            right = detection[5] * cols
            bottom = detection[6] * rows
    
            #draw a red rectangle around detected objects
            cv2.rectangle(img, (int(left), int(top)), (int(right), int(bottom)), (0, 0, 255), thickness=2)
            #draw category name in top left of rectangle
            cv2.putText(img, classes[int(detection[1])], (int(left), int(top-4)), cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 2, 8)

    cv2.imwrite(output, img)

動かすにはトレーニング済みモデルデータと設定ファイルがいるのですが、これをダウンロードするスクリプトが↓です。

#!/bin/sh
mkdir -p model
wget -O - 'http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v2_coco_2018_03_29.tar.gz' | tar xvfzO - ssd_mobilenet_v2_coco_2018_03_29/frozen_inference_graph.pb > model/frozen_inference_graph.pb
wget -O - 'https://raw.githubusercontent.com/opencv/opencv_extra/master/testdata/dnn/ssd_mobilenet_v2_coco_2018_03_29.pbtxt' > model/graph.pbtxt

サーバー起動して、POSTしてみると…

一人多いですね。

前回と同じですが、このサーバーのGitList Giteaです。

デバッグ

djangoのデバッグも特別なことはありません。デバッグメニューからデバッグの開始を選びます。

Pythonを選びます。

Djangoを選びます。

ブレークポイントを張ります。

アップロードしてみます。

こんな感じで止まりました。変数も普通に見えてます。他と変わりません。

まとめ

今回はdjangoで物体検出するアプリを作って、デバッグまでしてみました。

次回はこのアプリを進化させ、モデルも使って、PostgreSQLにデータを入れてみたいと思います。