【Webアプリ開発】顔検出、DB保存、加工結果映像リアルタイム表示 Python + OpenCV + SQLAlchemy + FastAPI
概要
タイトルの通り。
前にも記事を書いているようにお気に入りの笑い男GIFで遊ぶ。
と今回はFastAPIを使ってOpenCVの加工結果をDBに保存しつつ、Webブラウザ上に表示するアプリを作ってみた。
普段は画像認識系のエンジニア 兼 販促担当をやっているのだけど、プロト開発がサクッと出来るともっと売れそうなのでね。スキル向上を目指してね。
コードについて
公開先
GitHubに公開中。
基本的にはgit clone
して使ってもらえば動くと思う。
著作権的に画像とかは同梱してないので注意。各自ダウンロードのほど。
説明
構成要素
大きく分けると下記の4要素から成る。
- 画面
- 処理フローとStream API
- DBアクセス
- 顔検出&顔加工
画面
見えやすいところから噛み砕いていった方が分かりやすいと思うんでまずは画面について。
といってもtemplates/index.html
でシンプルに記述してるだけ。
Jinja2でパースできるようなフォーマット。
キモなのは
<img src="{{ url_for('video_feed') }}">
のところで、後に説明するFastAPIのスクリプト内で/video_feed
でroutingされるところにアクセスすると映像がStreamingで表示される。
<html> <head> <title>Video Streaming Demonstration</title> </head> <body> <h1>Video Streaming Demonstration</h1> <img src="{{ url_for('video_feed') }}"> </body> </html>
処理フローとStream API
ほいでここがメインな訳だけど、スクリプト名でいうとapp.py
で、DBアクセス、顔検出&顔加工とHTMLを繋ぐ役割を果たしてる。
importとか
まずはモジュールのimport
やら便利ツールのインスタンス化やらをしていきましょう。
ちなみにpyscripts
というのは自作モジュール名。Camera操作とか顔検出&加工とかDB操作とか。
import uvicorn import cv2 from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse from fastapi.templating import Jinja2Templates from pyscripts.camera import Camera from pyscripts.laughing_man import LaughingManMaskStream, detect_faces, overlay_lms from pyscripts.db import insert_faces app = FastAPI() templates = Jinja2Templates(directory="templates")
htmlのパース
index
関数はまぁ、ページ表示のためのテンプレ関数。ここでさっきのhtmlファイルをJinja2フォーマットでパースしてるのが分かる。
@app.getというのはFastAPIのデコレータ。第一引数でURLのパス指定をして、第二引数でレスポンス種別を指定。asyncはようわからん。
@app.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse('index.html', {"request": request})
コア
ほいでここがキモ。
gen
関数の中では、下記の処理を行うというフローを記述してる。
- WebCameraからフレームを読み込んで、
- 描画するgifフレームを読み込んで、
- 顔を検出して、
- gifフレームを描画して、
- DBに検出結果を保存して、
- 描画結果をバイト形式にエンコードして、
- HTMLレスポンスを生成
そんでもってvideo_feed
関数でストリーミングレスポンスとしてデータを返すということになってる。
さっきindex.html
で記述してた
<img src="{{ url_for('video_feed') }}">
で引っ張ってくるデータがこれなわけだ。
def gen(camera): lm_mask_stream = LaughingManMaskStream() while True: frame = camera.get_frame() lm_mask = lm_mask_stream.next() faces = detect_faces(frame) frame = overlay_lms(frame, faces, lm_mask) insert_faces(faces) _, jpeg = cv2.imencode('.jpg', frame) byte_frame = jpeg.tobytes() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + byte_frame + b'\r\n') @app.get('/video_feed', response_class=HTMLResponse) async def video_feed(): return StreamingResponse(gen(Camera()), media_type='multipart/x-mixed-replace; boundary=frame')
サーバ起動
で最後↓でサーバを起動する。
if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)
DBアクセス
ということで、全体の流れは上記のような感じで記述できたので、以降はモジュールの詳しい説明になるわけだけども。
pyscripts/db.py
の中でsqlalchemyを使ったDB定義、初期化、挿入関数定義を行っていく。
from sqlalchemy import Column, Integer, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker SQLALCHEMY_DATABASE_URL = "sqlite:///./data/face_location.db" engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, echo=True ) Base = declarative_base() class Face(Base): __tablename__ = "face" id = Column(Integer, primary_key=True) st_x = Column(Integer) st_y = Column(Integer) width = Column(Integer) height = Column(Integer) Base.metadata.create_all(bind=engine) def insert_faces(faces): SessionClass = sessionmaker(autocommit=False, autoflush=False, bind=engine) session = SessionClass() for face in faces: face_instance = Face() face_instance.st_x = int(face[0]) face_instance.st_y = int(face[1]) face_instance.width = int(face[2]) face_instance.height = int(face[3]) session.add(face_instance) session.commit() session.close()
顔検出&顔加工
ここの説明はこちらのページに譲ろうかな。
構成要素だけ書くと、下記のような感じ。
LaughingManMaskStream
クラスでgifから1フレームずつ引っ張ってきて描画用に加工detect_faces
関数で顔検出overlay_lms
関数で加工
import os import cv2 import numpy as np def _extract_fg(img): fg_region = np.zeros(img.shape) for v, line in enumerate(img): non_white_pxs = np.array(np.where(line < 250)) if non_white_pxs.size > 0: min_h, max_h = np.min(non_white_pxs, axis=1)[0], np.max(non_white_pxs, axis=1)[0] fg_region[v, min_h:max_h, :] = 1 return fg_region class LaughingManMaskStream: def __init__(self, gif_path=os.path.join(os.path.abspath(__file__), "laughing_man.gif")): self.cap = cv2.VideoCapture(gif_path) self.fg_region = _extract_fg(self.cap.read()[1]) def next(self): ret, frame = self.cap.read() if not ret: self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) ret, frame = self.cap.read() mask = frame * self.fg_region return mask def release(self): self.cap.release() def detect_faces(img): PATH_TO_HAARCASCADE = '/haarcascade_frontalface_default.xml' # To Edit cascade = cv2.CascadeClassifier(PATH_TO_HAARCASCADE) small_img = cv2.resize(img, (int(img.shape[1]/4), int(img.shape[0]/4))) faces = np.array(cascade.detectMultiScale(small_img)) * 4 return faces def _overlay_lm(frame, face, lm_mask): offset = 100 x = int(max(face[0] - offset / 2, 0)) y = int(max(face[1] - offset / 2, 0)) w = int(min(face[2] + offset, frame.shape[1] - x)) h = int(min(face[3] + offset, frame.shape[0] - y)) lm_mask = cv2.resize(lm_mask, (w,h)) mask_idxs = np.where(lm_mask>0) overlay_idxs = (mask_idxs[0]+y, mask_idxs[1]+x, mask_idxs[2]) frame[overlay_idxs] = lm_mask[mask_idxs] return frame def overlay_lms(frame, faces, lm_mask): if faces.shape[0] > 0: for face in faces: frame = _overlay_lm(frame, face, lm_mask) return frame
コードの走らせ方
GitHubのHow To Start
を参照のこと。
まとめ
いかがだったでしょうか。
なんていう、時代錯誤な、ボケなのか何なのかわからない、まとめ方が、真っ先に思いついた俺は、もうダメなのかもしれない。
笑い男GIFで顔隠蔽(OpenCV3+Python3)
動機
pythonで画像処理サービスとか作ってみたいけど、とりあえず面白そうなことやってみようということで、割りと定番の笑い男合成。
以下のサイトがかなり参考になりました。 というかほとんどパクリに近い… pythonのOpenCVでリアルタイムに笑い男 - BlankTar
一応、gifを対象としましたっていうのがこの記事の違い。
事前準備
使うのはPython3とOpenCV3とNumpy。 Homebrew使ってれば基本的には下で環境は整うはず。
brew install python3 pip3 install numpy brew tap homebrew/science brew install opencv3 --with-python3 --without-python --with-contrib
コード
GIFの扱い
ここが今回のキモ。 といってもgifを扱うのはimreadでなくて、VideoCaptureです。ってだけ。
対応するのは下の2箇所。
gif_cap = cv2.VideoCapture("laughing_man.gif")
_, lm = gif_cap.read()
segmented_lm, mask = segment_lm(lm)
def stream_lm_gif(gif_cap): gif_cap.grab() gif_cap.grab() gif_ret, lm = gif_cap.read() if not gif_ret: gif_cap = cv2.VideoCapture("laughing_man.gif") gif_ret, lm = gif_cap.read() return lm, gif_cap
ちなみに、gifの透過画像化ってのはできないみたいだったので、以下関数を入れてる。
# Segment Laughing Man (to make gif transparent) def segment_lm(lm): lm_region = np.zeros(lm.shape) for v, line in enumerate(lm): non_white_pxs = np.array(np.where(line < 250)) if non_white_pxs.size > 0: min_h, max_h = np.min(non_white_pxs, axis=1)[0], np.max(non_white_pxs, axis=1)[0] lm_region[v, min_h:max_h, :] = 1 segmented_lm = lm_region * lm return segmented_lm, lm_region
言い訳
今回はなぜかVideoCaptureのsetメソッドによるgifのリプレイが出来なかったため、最後のフレームまでいったらもう一回VideoCaptureインスタンスを作るってことをやってる。
Mac+PythonでKinect v2 for Windows
WindowsだとKinect SDKで比較的簡単にKinect for windowsが使えるが, Macだとちょっとややこしいので書いておく. 最初にライブラリのインストール方法を書いて,次にPythonでの使用法を書く.
libfreenect2のインストール
Macでも無料で使えるライブラリlibfreenect2を使う. 元ページはここ. GitHub - OpenKinect/libfreenect2: Open source drivers for the Kinect for Windows v2 device
先に依存ライブラリをインストール.今回はhomebrewを使うことを前提とする. それぞれ読み替えて下さい.
brew update brew install libusb brew tap homebrew/versions brew install glfw3
TurboJPEG,CUDA,OPENNI2などを使いたい場合には先にそれらをインストールしておく. 詳しくは元ページ参照.
で,libfreenect2のインストールとテスト. Kinect v2を繋いで(USB接続→ACアダプタ接続という順序じゃないとデバイス認識をしてくれないので注意),
git clone https://github.com.OpenKinect/libfreenect2.git cd libfreenect2 mkdir build && cd build cmake .. make make install ./bin/Protonect
これでKinect v2の取得映像がストリーミング表示されるようになれば成功.
pythonでの使用
pythonからKinect v2を利用したい場合には,pylibfreenect2が使える. 元ページはここ. GitHub - r9y9/pylibfreenect2: A python interface for libfreenect2 for python 2.7, 3,4 and 3.5
このライブラリはpipからインストールできる.
pip install pylibfreenect2
テストには元ページのexamples/selective_streams.pyを使う. Kinect v2を繋いで(USB接続→ACアダプタ接続の順序),下コードを動かせればOK.
wget https://raw.githubusercontent.com/r9y9/pylibfreenect2/master/examples/selective_streams.py python selective_streams.py
基本的な画像・映像処理が分かってれば,selective_streams.pyを参考にすれば結構簡単に使い方は習得出来そう.
推薦システム概要資料とPythonでの協調フィルタリング参考記事
推薦システム概要
「推薦はとりあえず協調フィルタリングやればいいんでしょ?」という適当な認識を改めるために,手法サーベイ.
基本的には朱鷺の杜Wikiでも有名な神嶌先生のこの資料を参考にする.
わかりやすい上にかなり網羅的.
取り敢えず協調フィルタリングが重要なのは間違ってなかったようなので,練習をしてみよう.
協調フィルタリングが実装済みのPythonライブラリ
ということでpythonで協調フィルタリングが実装済みのライブラリは無いかなと探してみると,
このサイトでいくつか紹介してくださっている.
基本的には後学の為にSparkをいじってみたいので,Spark + MLlibを使う予定.
おまけ
サーベイしててNMFって何だっけ?となったので,調べてみたところ,このサイトの説明が分かりやすかった.
abicky.net
一応画像認識をちょっとかじっている身としては覚えとかないと駄目だったかな...
デュアルブートしていたLinuxのパーティションをWindows7側から削除したら動かなくなった.→ MBRをWindows8.1の回復ドライブの作成で修復
以前から使っていたLet's NoteにデュアルブートしていたArchLinuxのパーティションをWindows7側からフォーマットしたところ,起動しなくなった.(GRUB2 rescue画面でbootディレクトリが見つからない...)
GRUB2でデュアルブート処理を行っていたが,そのブートローダがArchLinuxパーティション内に保存されていたらしく,それもまとめて消してしまったらしい.
色々復元方法を試みたが,最終的に参考にして成功したのは以下のサイトの手順.
コマンドプロンプトからWindowsを復旧する4つの方法 (Vista/7/8/8.1/10) - ぼくんちのTV 別館
MBRの回復は修復ディスクが手元になかったので諦めていたが,別PC内のWindows8.1の「回復ドライブの作成」を試しに使ってみようとやってみたらうまく行った.
回復ドライブの作成に関しては以下のサイトが役に立った.
Windows8.1 回復ドライブの作成と復元(リカバリ)手順 : 下手の横好き!
初めは「GRUB2のブートローダをWindows7のパーティション内にインストールすればいいんでしょ?イケるっしょ.」という感じだったが,割りと泥沼化.
その後ArchLinuxのインストールUSBを使ったり,UbuntuのインストールUSBを使ったり.完全に迷子.本質的な問題を把握できてないからこんなことになるんです.
FreeDOSにも手を出したが最後,GRUB2 rescue画面ですら遠い彼方へ.
で結果,今回紹介した方法になんとかたどり着いて助かったけど,もうちょっと早く気付けたかな.(別バージョンの回復ドライブでうまくいくとは.)
演劇「みんなしねばいいのに」- うさぎストライプ 感想
友人に誘われて京都のアトリエ劇研で鑑賞した「みんなしねばいいのに」- うさぎストライプの簡単な感想を.
演劇を観に行ったのは初めてで,雰囲気が楽しめればいいかなぁという軽い気持ちで行った.
演劇に慣れてなくて内容に集中しきれたわけではないので,解釈はかなり間違っていると思いますが,予めご了承を.
あらすじ
霊が出ると曰く付きの,病院の女子寮が舞台.
主人公がふと呟く「みんなしねばいいのに」という言葉に共鳴するかのように,世界が狂気に満ちていく.
女子寮に住む3人の看護婦たちの,その世界への三者三様の反応.
本作では世界が狂気に満ちていくことの象徴として,終わらないハロウィンを舞台にしており,その中では仮装して常識から解放された人々が軽々しく他人を傷つけている.
世紀末的な世界を生きながら,主人公はそれまでの息苦しい生活から解き放たれ,それまで自身の心の奥底に押し込めてきた感情を大衆に向かって吐露する.
その一方で,そんな世界の中でも男と結ばれ,幸せを見つける女性と,世界とともに狂っていく彼氏(多分,医者)を持ち,気持ちが荒んでいく女性が描かれている.
ラストでは,主人公自身も命の危機に晒され,死と向き合う.
主人公は部屋に住まう霊との対話の中で,生きているのと死んでいるのはほとんど同じだということを聞く.
感想
世界系みたいな話は映画評論でよく聞くけど,本作はまさにそれで,世界が主人公の希望するものに変容する.
最終的には彼女は地獄のような生活に向き合うことができたのだろうか.
正直つかめていない.
ただ個人的な感想としては,メインのストーリーというよりは,この作品がコメディ的に演出されていることが非常に面白く感じた.
特に,主人公が「みんなしねばいいのに」と呟くシーンとかは,強調しなくてはならないはずなのに,一方で同じ舞台の上で非常に笑えるやりとりがなされている.
このシーンには鳥肌が立った.
映画とかだと,セリフは脳内で分割しづらくて,並列的に出来事を並べるというのが出来ないけど,一方で演劇だと舞台が空間的な広がりを持っているせいか,上手く脳内で並列に処理してくれる.
演劇一般に使われる手法なのかもしれないけど,僕には新鮮で,感動した.
word2vecをwikipediaコーパスで学習
作業動機
今更ながら練習として映画推薦サービスを作ってみようかなと思った.
とりあえず
をいじってみようと思ってダウンロード.
内容はこんな感じ.
- genome-scores.csv: タグと映画の関連性
- genome-tags.csv: タグID
- links.csv: 別データ・セットとの映画ID対応表
- movies.csv: 映画ID・タイトル・ジャンル(複数)
- ratings.csv: ユーザの,映画に対する評価値(悪0.5~5良)と評価した時刻.
- tags.csv: ユーザが映画に対してつけたtagとその時刻.
ぱっと思い浮かんだのは,ユーザに幾つかの映画に対する評価をしてもらい,ratingから似た評価をしているユーザを取ってきて,そのユーザの評価が高い映画を薦めるという能動学習的な方法.
その方法だときっちりユーザ登録型のWebサイト運営出来てないとキツいと思うので,出来ればその他のTwitterのツイートとか見せてもらうとかしてユーザの趣向を取ってきて,推薦したい(出来なさそうだけどとりあえず).
ということで自然言語処理のノウハウが必要になってくるけど,とりあえずはWord2Vecでお茶を濁す.日本語wikipediaのデータを学習してみる.
参考にしたWebサイトは以下の通り.
作業手順
WikipediaのデータをWord2Vecで使える形式に変換
まずwikipediaのデータを取ってくる.
$ curl https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2 -o jawiki-latest-pages-articles.xml.bz2
このままだとテキストデータではないので,変換する必要がある.
python環境でやっていく予定なので,WikiExtractor.pyが使える.
$ git clone https://github.com/attardi/wikiextractor $ python3 wikiextractor/WikiExtractor.py jawiki-latest-pages-articles.xml.bz2
ここまでやるとtextディレクトリが作成されており,それらの下部には各ページのテキストが保存されている.
それらを一つにまとめるために,以下の処理を行う.
$ cat text/*/* > jawiki.txt
でword2vecに読み込ませる前にWord単位のセグメンテーションを行っておく必要がある.そこで,MeCabをhomebrewで入れてそれを使う.
$ brew install mecab $ mecab -Owakati jawiki.txt -o data.txt
Word2Vecのインストールと実行
$ pip3 install gensim
でipyhtonを使って学習.
from gensim.models import word2vec data = word2vec.Text8Corpus("data.txt") model = word2vec.Word2Vec(data, size=200)
modelが学習されるのに結構時間が掛かりそうなのでとりあえず今はここまで書いておく.
出力
後日,出力の確認をしてみた.
よく知られている例で,イチロー - 野球 + 本田 → サッカーっていう挙動をするってのがあるけど,今回のWikipediaのデータセットのみではそんな出力は出なかったので一応報告.
米googleの研究者が開発したWord2Vecで自然言語処理(独自データ) - Qiita
もちろん上サイトではfacebookデータセットを独自に作ってるみたいなので,挙動が異るのは当たり前.
実際にTwitterとかから映画のレビューを取ってくるときにはそれなりのデータセットを用意する必要がある.
out = model.most_similar(positive=["イチロー", "本田"], negative=["野球"]) for x in out: print(x[0], x[1]) 天谷 0.5703838467597961 中嶋 0.5688130259513855 中澤 0.5668359398841858 赤木 0.565341055393219 青木 0.5472140908241272 梶谷 0.5470311641693115 斉藤 0.5417250990867615 佐山 0.541033923625946 石毛 0.5390052199363708 松井 0.5352978706359863
その他にもいくつか.
out = model.most_similar(positive=["女性", "王"], negative=["男"]) for x in out: print(x[0], x[1]) 王族 0.5936665534973145 国王 0.5321540832519531 王室 0.5042459964752197 君主 0.49709588289260864 スルターン 0.4776388108730316 ムスリム 0.47091352939605713 王妃 0.4708373248577118 異教徒 0.4697801470756531 アノーヤター 0.4674013555049896 諸侯 0.4593404531478882 out = model.most_similar(positive=["イスラム教", "旧約聖書"], negative=["コーラン"]) KeyError: "word '旧約聖書' not in vocabulary"
前半の例では王妃が割りと上位に来てて期待通り.
そうでなくても,"王"関連のワードが出て来るWord2Vecの性能に驚き.
自然言語処理は僕の専門分野ではないので,どの程度の性能がでるのかは知らなかったけど,思っていたよりはよかったかな.
更にデータセット増やせば結構良い結果が得られそう.
一方後半の例ではエラーが発生.
ユダヤ教とかが出てほしかったけど,まさか"旧約聖書"が無いとは...
あれかな.多分MeCabの分かち書きの分割が上手く行っていないorデータセットにそもそも含まれていないって感じだと思う.
前者の方があり得るかな.その辺も勉強しないとな.