未分類

はじめに

前回はなんとか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にデータを入れてみたいと思います。

未分類

はじめに

前回はvscodeのメニュー表示バグの回避+αを記事にしました。

今回はようやくdjangoです。

git導入

まずは開発の基本gitです。バージョン管理すると失敗してもいつでも元に戻せます。というわけでgitをインストールします。

sudo apt install git

vscode上で環境構築

フォルダ作成

まずはvscodeを起動します。[フォルダを開く]から、~/python/を開き、右上のボタンからdjango_appsフォルダを作成して開きます。

gitリポジトリの作成

フォルダが開いたら次はメニューからターミナルを開きます。

開いたターミナルでgitリポジトリをその場に作ります。

git init

Python環境の作成とgit無視ファイルの設定

出来たら次にpythonの環境をその場に作ります。

python3 -m venv env

すると何やら左側に怪しい数字が出てきます。数字の出てるボタンをクリックします。

見てみると、さっきのpython環境を作成したときに出来たファイルのようです。つまり環境作成したときに346個のファイルが変更(追加)されたよと言っています。これはvscodeがデフォルトで持っているgitの機能で、さきほど作ったリポジトリを認識していて、その変更を検出したために変更ファイル数を教えてくれてたのです。ただ、自動生成されるファイルや外部から取得するファイルはgit管理対象にしないのがお約束なので、ここでは無視ファイルに指定します。いきなり346個も指定するのは辛いので、右上の階段状のボタンを押します。

これでフォルダ階層表示になるので、変更部分が分かりやすくなります。envを右クリックして、

[.gitignoreに追加]を選択します。これは、選択したフォルダ以下全てをgit管理対象にしない=無視するという意味です。

無視にすると、変更として検出されなくなり、無視設定ファイルである.gitignoreが作成されました。このファイルを開くと

346個のファイルが全て一覧されてしまっているので、

最上位フォルダのみに修正しておきます。 (以降このフォルダに追加された場合も自動で無視したいので)

これで心置きなくpython環境を設定できます。

vscode用python環境選択

次はvscodeにpython環境を認識させます。F1キーを押して、python: Select Interpreterと打ち込んでEnterしてください。

さっき作成したpython環境が./env/bin/pythonにあるので、それを選びます。

これでvscodeがこのフォルダで使用するpython環境が設定されました。

pipを使ったインストール

後はvscodeのメニューのターミナルから新しいターミナルを開きます。env環境で立ち上がるので、ここでpipでパッケージを更新しておきます。

pip install -U pip
pip list -o
pip install -U setuptools

これで更新が必要なpythonモジュールがなくなるので、djangoをインストールします。

pip install django

djangoのプロジェクト作成と確認

まずはsample_siteプロジェクトをカレントディレクトリに作成します。

django-admin startproject sample_site .

これでサービスが稼働可能な状態になるので、djangoの開発サーバーを起動してみます。

python manage.py runserver

プロンプトが戻らずに出力が止まり、http://127.0.0.1:8000/などと出てきていたら起動成功です。firefoxを立ち上げて、http://127.0.0.1:8000/を開いてみましょう。

まだ何もプログラミングもしてないのに、画面が出ましたね。これがフレームワークの力です。骨組みはすでにあるので肉付けしてくれという状態なのです。でもこれは確認用の画面なので、この画面を編集することはありません。

アプリケーション作成

まずはCtrl+Cで開発用サーバーを止めて、sample_appを作成します。

python manage.py startapp sample_app

ここまでで骨組みが出来ています。

pipのrequirements.txt作成

今、pythonの環境は全てgitからは無視ファイルとして扱っているため、保存されません。そのため、pythonの環境はいつでも復元できるようにインストールしたパッケージのリストをバージョン付きで保存します。それがrequirements.txtです。以下のコマンドで作成します。

pip freeze > requirements.txt

sqlite3データの無視ファイル追加

データベースのデータはgitには入れないので、このファイルも無視します。db.sqlite3を右クリックして[.gitignoreに追加]してください。あとは、pythonバイトコードのキャッシュ(.pyc)も無視ファイルに追加します。最終的に.gitignoreは↓な感じになります。

env/
db.sqlite3
*.pyc

gitリポジトリにコミット

切りが良いので、リポジトリにコミットしておきます。まずは数字の出ているところアイコンを押して、変更と書いてあるバーの右側の+を押します。

すると、[変更]の上に[ステージング済みの変更]という項目が出来上がり、元々検出されていた変更ファイル群が[ステージング済みの変更]に移動しました。後はコミットするだけなのですが、初回はその前にやることがあります。確認の意味でメッセージに「初回コミット」と入れて、一度チェックボタンを押して見ると…

こんな感じで怒られます。gitは原則コミットした人の名前とメールアドレスをちゃんと記述する慣習で、その設定がされていないとエラーになります。

初回だけなので、vscode内のターミナルからサクっと登録しておきます。

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

※メールアドレスと名前は自分の物に変えてください

これでコミットできるようになったので、チェックボタンを押してコミットしてみてください。成功すれば何も出ずに16個の変更ファイルがなくなります。ターミナルからgit logとすると、履歴を見ることが出来ます。

(env) user@ubuntu1804:~/python/django_apps$ git log
commit de2a5a426e41514b9281ee3250d19560f92e632a (HEAD -> master)
Author: first_user <first_user@elephantcat.work>
Date:   Sat Feb 8 01:28:39 2020 +0900

    初回コミット
(env) user@ubuntu1804:~/python/django_apps$ 

Hello,Worldの作成

まずはViewから作成します。見たまんまなHello,Worldを返すビューです。

from django.http import HttpResponse

def index(request):
    return HttpResponse("<html><body>こんにちは!世界</body></html>")

このアプリのURLとビューの対応表です。

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

このサイトのURLとアプリの対応表です。

"""sample_site URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('sample_app/', include('sample_app.urls')),
    path('admin/', admin.site.urls),
]

では、開発サーバー起動

python manage.py runserver

firefoxで確認

OK!なので、ステージングしてコミットします。

一応GitList Giteaに置いておきました。

まとめ

djangoでHello,Worldまでをgitにコミットしながらローカルvscode環境で実行させるところまでできた。

次回(↓)はアプリケーションとしての何かの機能を実装して、デバッグまでしてみたい。

未分類

前回前々回で作成したHello, World!なアプリを外からGetして時間を測定してみました。

for url in django.cgi/polls/ hello.cgi/;do
        time bash -c "for i in {0..49};do wget -q -O - https://elephantcat.work/cgi-bin/$url >/dev/null;done"
done

結果は

0.33user 0.31system 0:31.69elapsed 2%CPU (0avgtext+0avgdata 7176maxresident)k
0inputs+256outputs (0major+21808minor)pagefaults 0swaps
0.30user 0.34system 0:10.98elapsed 5%CPU (0avgtext+0avgdata 7180maxresident)k
0inputs+88outputs (0major+21767minor)pagefaults 0swaps
フレームワーク 50回取得にかかった時間[秒]
django 31.7
bottle 11.0

django遅いのは分かるけど、bottleでも十分遅いと思う。

CGI Python恐るべし。

未分類

前回はbottleだったので今回はdjangoを同じようにやってみようと思います。やることはdjango tutorialです。

venvの仮想環境構築

$ cd ~/public_html/elephantcat.work/cgi-bin/
$ python3 -m venv fordjango
$ . ./fordjango/bin/activate
(fordjango) $ 

djangoのインストール

(fordjango) $ pip install --upgrade pip
(fordjango) $ pip install django
(fordjango) $ python -m django --version
3.0.2

djangoプロジェクト作成

(fordjango) $ django-admin startproject djangoroot
(fordjango) $ cd djangoroot

このフォルダにはパスワードなども入るので、今のうちに.htaccessでwebからアクセスできないようにしておきます。

deny from all

django用のmysql設定

djangoはデフォルトだとdatabaseにsqlite3を使います。VALUE-SERVERでも sqlite3は使えるのですが、バージョンに問題があって、エラーになります。なので今回はmysqlを使います。私のようにエコプランの方はdatabase1つしかないのでテーブル名に衝突がないように注意してください。

さて、WordPressとか使うために事前にmysql自体の設定はしてますよね?そこは端折って、djangoからmysqlを使う設定をしていきます。

(fordjango) $ vi djangoroot/settings.py

↓な辺りをこんなふうに修正します。

DATABASES = {
    'default': {
        # 'ENGINE': 'django.db.backends.sqlite3',
        'ENGINE': 'django.db.backends.mysql',
        # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        'NAME': 'databasename', # VALUE-SERVERではユーザーIDと同じ
        'USER': 'username', # ユーザーIDを入れてください
        'PASSWORD': 'password', # パスワードを入れてください
        'HOST': 'localhost',
        'PORT': '3306',
    }
}

ここでオススメされてるpython用のmysql接続モジュールをインストール

(fordjango) $ pip install mysqlclient

Pollsアプリの作成

(fordjango) $ python manage.py startapp polls

チュートリアルのとおりにviewを用意します(プロジェクト名だけdjangorootなので注意)。

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
"""djangoroot URL Configuration
ドキュメントコメントなので省略
"""
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

チュートリアルはまだ続くわけですが、この辺で一回画面が見たい…でもrunserverはレンタルサーバでは出来ないので、重い腰を上げてCGIで繋ぎます。

CGIでdjangoを呼ぶ

今さら断っておくと、djangoは正式にCGIをサポートしていません。サポートしているのはFastCGI/SCGI/WSGIとかです。理由はとにかく遅いから!

遅くてもいいからとにかく繋ぎたいんじゃ~という人のためにチョロっとCGIスクリプトを書いてくれたものがコレ

これをcgi-binに置くのですが、そのままでは使えないので、まずはこちらの環境に合わせてシバンをvenv用に直します。

#!(ホーム)/public_html/elephantcat.work/cgi-bin/fordjango/bin/python
...

次に最後の部分をdjangoプロジェクトの設定に合わせれば使えます。

...
# Change this to the directory above your site code.
sys.path.append("(ホーム)/public_html/elephantcat.work/cgi-bin/djangoroot")
# Change mysite to the name of your site package
os.environ['DJANGO_SETTINGS_MODULE'] = 'djangoroot.settings'
run_with_cgi(get_wsgi_application())

ここまでで実行可能にすればdjangoフレーム自体は動作します。が、最後にサイトの設定settings.pyを修正します。

許可ドメイン追加とデバッグ設定OFF。 デバッグ設定OFFにするとエラー発生時のデバッグ情報からパスワードなどの漏洩が防げます。

...
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = [
    'elephantcat.work'
]
...

あとはcgiスクリプトを実行可能にするだけ!

(fordjango) $ chmod u+x django.cgi

上記で動作するようになったpollsアプリがこちら

実際のチュートリアルはまだ続くのですが、今回はここまで。

一応再度書いておくと、djangoはCGIで使っちゃダメです。