※サンプルコードあります。
はじめに
アクセスいただきありがとうございます。「Djangoでなんか作ってみたいな」という思いでChatGPTを使いつつ、1週間くらいで作りました。自分の環境にだけ置いておくはもったいないと思い、このように記事にすることにしました。「Django使って1から自分の環境でWebアプリ動かしてみたい」 という方のお役に立てれば幸いです。ぜひご自身の環境でもお試しください。
制作物
テーマ
今回作成したもののテーマは 「事故画を撮らせない、絶対に笑顔が撮れるアプリ」 です。
私は、うまく笑えていなかったり、写真写りが悪くなってしまうった所謂「事故画」を撮られてしまったことがあります。私の経験上、事故画を撮られてしまうのはこちらが十分に準備しておらず、不意に写真を撮られてしまうときだと思います。じゃあ、 不意に写真を撮れなくして十分に準備できたタイミングで写真をとればいいじゃないか! ということで、カメラから取得した映像から写っている人の表情を分析し、笑顔が3秒続けばシャッターを切れば絶対に笑顔の写真が撮れると考えました。これで事故画が防げる!
作りました。ただ「笑顔が続けば撮るアプリ」はすでに存在するっぽいので、今回は拡張して 「指定された表情を3秒続けたら写真を撮るアプリ」 を作りました。このアプリでも間違いなくじ事故画にはならないので、まあいっか...
使用方法
- ①(画像では白い四角)
カメラから取得した映像が表示されます。上部に「Be surprise!」とあります。この部分で表情を指定するので、それに従った表情をしましょう。 - ②
「リトライボタン」です。一度確定した写真をもう一度取り直せます。 - ③
「表情変更ボタン」です。指定される表情をランダムに変更できます。 - ④(画像では薄灰色の四角枠)
撮影された画像がここでプレビュー表示されます。 - ⑤
「保存ボタン」です。④で表示されたプレビュー画像を実際にカメラロールに保存できます。
仕組み
実は「3秒待ってから写真を撮る」ではなく、「毎秒写真を撮って評価する」ということをしています。
カメラは常に起動し、映像を撮っています。その映像を1秒ごとにキャプチャし、キャプチャされた画像をサーバーに送信します。サーバー上でその画像内の人物の表情を評価し、評価された結果をWebブラウザに返します。サーバーから返された結果が3回同じだった場合、3回目にキャプチャされた画像を「確定した写真」としてプレビュー表示されます。
実行環境
OSやPythonのバージョンは以下のようになります。他にDjangoやferというPythonライブラリも主要なものになりますので、私の実行環境でのバージョンを記します。(2025/02/09現在)
- Windows 11 Pro 23H2
- Python 3.10.11
- Django 5.1.6
- fer 22.5.1
ディレクトリ構造
最終的に主要なファイルのディレクトリ構造は以下のようになります。これから追って作っていきますので、ここでは参考程度で大丈夫です。
camera_venv // 全体
├─camera_project // プロジェクト
│ ├─camera_app // アプリケーション
│ │ ├─__init__.py
│ │ ├─admin.py
│ │ ├─apps.py
│ │ ├─models.py
│ │ ├─test.py
│ │ ├─urls.py
│ │ └─views.py
│ ├─config // 設定ファイル
│ │ ├─__init__.py
│ │ ├─asgi.py
│ │ ├─settings.py
│ │ ├─urls.py
│ │ └─wsgi.py
│ ├─static // 静的ファイル
│ │ ├─css
│ │ │ └─camera.css
│ │ ├─icon
│ │ │ └─favicon.ico
│ │ └─js
│ │ └─camera.js
│ ├─templates // HTMLファイル
│ │ └─camera_app
│ │ └─camera.html
│ └─manage.py
└─.venv // 仮想環境
ディレクトリ構成は以下のサイトを参考にしました。
仮想環境の構築
Pythonのインストールを行っていない方は、まずインストールをお願いいたします。
ではまず、アプリケーション開発準備段階であるPythonの仮想環境構築を行いましょう。開発したいディレクトリに移動して、ターミナルを実行してください。ここではDドライブ直下に開発環境を構築していくことを想定しています。
ターミナルを実行したらコマンドで空のフォルダを作成し、そのフォルダ下に移動してください。ここで仮想環境を作ります。
D:\> mkdir camera_venv
D:\> cd camera_venv
D:\camera_venv>
仮想環境フォルダ下に移動できたら、コマンドで仮想環境を作り、仮想環境を実行します。
D:\camera_venv> python -m venv .venv
D:\camera_venv> ./.venv/Scripts/activate
(.venv) D:\camera_venv>
これで仮想環境の構築と実行ができました。コマンドで仮想環境を停止できます。開発が終わったら仮想環境を止めるようにしましょう。
(.venv) D:\camera_venv> deactivate
D:\camera_venv>
ライブラリのインストール
必要なライブラリをインストールします。最終的に以下のようなパッケージ構成になるはずです。ほとんどは今からインストールするライブラリに付随してインストールされます。(2025/02/09現在)
Package Version
---------------------------- -----------
absl-py 2.1.0
asgiref 3.8.1
astunparse 1.6.3
certifi 2025.1.31
charset-normalizer 3.4.1
colorama 0.4.6
contourpy 1.3.1
cycler 0.12.1
decorator 4.4.2
Django 5.1.6
facenet-pytorch 2.6.0
fer 22.5.1
ffmpeg 1.4
filelock 3.17.0
flatbuffers 25.1.24
fonttools 4.56.0
fsspec 2025.2.0
gast 0.6.0
google-pasta 0.2.0
grpcio 1.70.0
h5py 3.12.1
idna 3.10
imageio 2.37.0
imageio-ffmpeg 0.6.0
Jinja2 3.1.5
keras 3.8.0
kiwisolver 1.4.8
libclang 18.1.1
Markdown 3.7
markdown-it-py 3.0.0
MarkupSafe 3.0.2
matplotlib 3.10.0
mdurl 0.1.2
ml-dtypes 0.4.1
moviepy 1.0.3
mpmath 1.3.0
namex 0.0.8
networkx 3.4.2
numpy 1.26.4
opencv-contrib-python 4.11.0.86
opt_einsum 3.4.0
optree 0.14.0
packaging 24.2
pandas 2.2.3
pillow 10.2.0
pip 23.0.1
proglog 0.1.10
protobuf 5.29.3
Pygments 2.19.1
pyparsing 3.2.1
python-dateutil 2.9.0.post0
python-dotenv 1.0.1
pytz 2025.1
requests 2.32.3
rich 13.9.4
setuptools 65.5.0
six 1.17.0
sqlparse 0.5.3
sympy 1.13.3
tensorboard 2.18.0
tensorboard-data-server 0.7.2
tensorflow 2.18.0
tensorflow_intel 2.18.0
tensorflow-io-gcs-filesystem 0.31.0
termcolor 2.5.0
torch 2.2.2
torchvision 0.17.2
tqdm 4.67.1
typing_extensions 4.12.2
tzdata 2025.1
urllib3 2.3.0
Werkzeug 3.1.3
wheel 0.45.1
wrapt 1.17.2
Django
DjangoはWebアプリ開発をサポートするPythonのフレームワークです。仮想環境上でコマンドを実行し、Djangoをインストールします。
(.venv) D:\camera_venv> pip install django
他にも付随してライブラリがインストールされます。
FER
FERは画像に移っている人物の表情から感情を分析することができるライブラリです。仮想環境上でコマンドを実行し、インストールします。
(.venv) D:\camera_venv> pip install FER
他にも付随してライブラリがインストールされます。FERの実装については以下の記事を参考にしました。
Tensorflow
FERの一部の処理で用いられているため、インストールします。
pip install tensorflow
MoviePy
FERの処理の一部で用いられており、FERをインストールした際に一緒にインストールされます。しかし、サーバーを起動する際に、ModuleNotFoundError: No module named 'moviepy.editor'
というエラーが発生する場合があるため、FERと一緒にインストールされたMoviePyのバージョンを更新します。
python -m pip install moviepy==1.0.3
以下のサイトを参考にしました。
ディレクトリの作成
必要なディレクトリとファイルを作成します。この準備は以下のサイトを参考に行っています。
プロジェクトフォルダの作成
コマンドで新しい空のフォルダを作ります。このフォルダがプロジェクト全体のファイルになります。
(.venv) D:\camera_venv> mkdir camera_project
(.venv) D:\camera_venv> cd camera_project
設定ファイル・フォルダの作成
作成したプロジェクトフォルダに移動し、Djangoのコマンドを使って設定ファイルおよびフォルダを作ります。
(.venv) D:\camera_venv\camera_project> django-admin startproject config .
設定フォルダ内は以下のようなファイル構成になっているはずです。
config
├─__init__.py
├─asgi.py
├─settings.py
├─urls.py
└─wsgi.py
プロジェクトフォルダ下でコマンドを実行してサーバーを立ち上げてみましょう。
(.venv) D:\camera_venv\camera_project> python manage.py runserver
http://127.0.0.1:8000/
がデフォルトのURLとして設定されています。それにアクセスすると、以下のようなサンプルの画面を見ることができます。
ここでは一度ターミナルでCtrl + C
を行い、サーバーを停止しておきましょう。
アプリケーションの作成
プロジェクトフォルダ上でDjangoコマンドを使って、アプリケーション用のフォルダとファイルを作ります。
(.venv) D:\camera_venv\camera_project> python manage.py startapp camera_app
また、camera_appフォルダ上にurls.pyを作成してください。
アプリケーションフォルダは以下のようなファイル構成になっているはずです。
camera_app
├─migrations
├─__init__.py
├─admin.py
├─apps.py
├─models.py
├─test.py
├─urls.py
└─views.py
静的ファイル・フォルダの作成
CSSやJavascript、faviconをまとめて管理します。プロジェクト上にstaticフォルダを作成し、そこにcss、icon、jsのフォルダを作ります。
(.venv) D:\camera_venv\camera_project> mkdir static
(.venv) D:\camera_venv\camera_project> cd static
(.venv) D:\camera_venv\camera_project\static> mkdir css
(.venv) D:\camera_venv\camera_project\static> mkdir icon
(.venv) D:\camera_venv\camera_project\static> mkdir js
また、cssとjsのフォルダに新しく、camera.cssとcamera.jsを作成します。
最終的にファイル構造は下のようになります。
static
├─css
│ └─camera.css
├─icon
└─js
└─camera.js
HTMLファイル・フォルダの作成
HTMLファイルは別で空のフォルダを作成して管理します。プロジェクトフォルダ上にtemplatesフォルダを作成し、そこにcamera_appフォルダを作ります。
(.venv) D:\camera_venv\camera_project> mkdir templates
(.venv) D:\camera_venv\camera_project> cd templates
(.venv) D:\camera_venv\camera_project\templates> mkdir camera_app
camera_appフォルダにcamera.htmlを作ります。
ファイル構造は以下のようになるはずです。
templates
└─camera_app
└─camera.html
ファイルの編集
ディレクトリとファイルの追加が終わり、現在の全体の階層構造は以下のようになっているはずです。番号がついているファイルをここで編集していきます。
camera_venv
├─camera_project
│ ├─camera_app
│ │ ├─__init__.py
│ │ ├─admin.py
│ │ ├─apps.py
│ │ ├─models.py
│ │ ├─test.py
│ │ ├─urls.py // 1
│ │ └─views.py // 2
│ ├─config
│ │ ├─__init__.py
│ │ ├─asgi.py
│ │ ├─settings.py // 3
│ │ ├─urls.py // 4
│ │ └─wsgi.py
│ ├─static
│ │ ├─css
│ │ │ └─camera.css // 5
│ │ ├─icon
│ │ └─js
│ │ └─camera.js // 6
│ ├─templates
│ │ └─camera_app
│ │ └─camera.html // 7
│ └─manage.py
└─.venv
以下をコピーペーストしましょう。
1.camera_app/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('analyze/', views.analyze_expression_view),
path('', views.camera_view),
]
2.camera_app/views.py
import base64
import cv2
import json
import numpy as np
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from fer import FER # 表情分析ライブラリ
from django.shortcuts import render
def analyze_expression_view(request):
if request.method == 'POST':
data = request.json().get('image')
# 画像データをデコードして配列に変換
img_data = base64.b64decode(data.split(',')[1])
np_arr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
# 表情分析モデルで表情を分類
emo_detector = FER(mtcnn=True)
emotion = emo_detector.top_emotion(img) # 分析モデルの呼び出し
return JsonResponse({'emotion': emotion})
return JsonResponse({'error': 'Invalid request'}, status=400)
def analyze_expression_view(request):
if request.method == 'POST':
try:
# リクエストボディからJSONデータを読み込む
data = json.loads(request.body)
image_data = data.get('image')
if image_data is None:
return JsonResponse({'error': 'No image data'}, status=400)
# base64エンコードされた画像データをデコード
img_data = base64.b64decode(image_data.split(',')[1])
np_arr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
# 表情分析モデルを使って表情を分類
emo_detector = FER(mtcnn=True)
emotion = emo_detector.top_emotion(img) # 分析モデルの呼び出し
return JsonResponse({'emotion': emotion})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
return JsonResponse({'error': 'Invalid request'}, status=400)
def camera_view(request):
return render(request, 'camera_app/camera.html')
3.config/settings.py
…
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'camera_app', # この行を追加
]
…
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # この行を編集
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
…
STATICFILES_DIRS = [BASE_DIR / 'static'] # この行を追加
…
4.config/urls.py
from django.urls import include, path
urlpatterns = [
path('', include('camera_app.urls')),
]
5.static/css/camera.css
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f2e3c6;
color: #353634;
font-family: cursive;
}
.header {
font-family: cursive;
font-size: 2.5rem;
background-color: #ffc6a5;
padding: 20px;
text-align: left;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
#content {
display: flex;
justify-content: center;
}
#holder {
display: flex;
flex-direction: column;
width: fit-content;
}
#video-holder {
position: relative;
justify-content: center;
flex-direction: column;
width: fit-content;
}
#video-container {
position: relative;
display: inline-block;
padding: 40px;
margin: 30px;
background-color: #ffc6a5;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2);
border-radius: 10px;
}
#video {
display: block;
transform: scaleX(-1);
background-color: #FFF;
}
#video-text-container {
display: flex;
position: absolute;
justify-content: center;
width: 100%;
top: 20%;
}
#video-text {
font-size: 30px;
background-color: #f8f1e9;
opacity: 0.8;
}
#video-countdown-container {
display: flex;
position: absolute;
justify-content: center;
width: 100%;
top: 40%;
}
#video-countdown {
font-size: 130px;
opacity: 0.5;
text-shadow:1px 1px 0 #FFF, -1px -1px 0 #FFF,
-1px 1px 0 #FFF, 1px -1px 0 #FFF,
0px 1px 0 #FFF, 0-1px 0 #FFF,
-1px 0 0 #FFF, 1px 0 0 #FFF;
}
button {
color: #353634;
font-family: cursive;
font-size: 40px;
padding: 10px;
background-color: #ffc6a5;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
border-radius: 5px;
}
button:hover {
cursor: pointer;
background-color: #b38b73;
}
button:active {
background-color: #f8f1e9;
}
button:disabled {
color: #353634;
font-family: cursive;
font-size: 40px;
padding: 10px;
background-color: #ffc6a5;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
border-radius: 5px;
cursor: not-allowed;
}
#button-holder {
margin: 20px;
display: flex;
justify-content: space-between;
}
#button-holder2 {
margin: 20px;
display: flex;
justify-content: center;
}
#layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.overlay {
background-color: rgba(0, 0, 0, 0.5);
border-radius: 10px;
}
#preview {
height: 480px;
width: 640px;
padding: 40px;
margin: 30px;
background-color: #ffc6a5;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.2);
border-radius: 10px;
}
#preview-holder {
display: "flex";
}
ここでの色や命名規則は以下のサイトを参考にしました。
6.static/js/camera.js
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const video = document.getElementById('video');
const emotionResult = document.getElementById('emotionResult');
const emotions = ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"];
sessionStorage.setItem("count", 3);
const retry = document.getElementById('retry');
const randomEmotion = document.getElementById('random-emotion');
const save = document.getElementById('save');
save.disabled = true;
const videoText = document.getElementById("video-text");
const countdown = document.getElementById('video-countdown');
const layer = document.getElementById('layer');
const preview = document.getElementById('preview');
retry.addEventListener("mousedown", (e) => {
sessionStorage.setItem("count", 3);
const emotion = sessionStorage.getItem("emotion");
videoText.innerText = "Be " + emotion + " !";
countdown.innerText = "";
layer.classList.remove("overlay");
preview.src = '';
save.disabled = true;
});
randomEmotion.addEventListener("mousedown", (e) => {
sessionStorage.setItem("count", 3);
setEmotion();
countdown.innerText = "";
layer.classList.remove("overlay");
preview.src = '';
save.disabled = true;
});
save.addEventListener("mousedown", (e) => {
const link = document.createElement('a');
link.href = preview.src;
const emotion = sessionStorage.getItem("emotion");
link.download = "your_" + emotion + "_face.png";
link.click();
});
// カメラを開始する
async function startCamera() {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
}
// 何の表情をするのか設定
function setEmotion() {
const emotion = emotions[Math.floor(Math.random()*emotions.length)]
videoText.innerText = "Be " + emotion + " !";
sessionStorage.setItem("emotion", emotion);
}
// 定期的にフレームをキャプチャしサーバーに送信する
async function captureAndAnalyze() {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = canvas.toDataURL('image/png'); // 画像データをbase64形式でエンコード
try {
// サーバーに画像を送信
const response = await fetch('/analyze/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify({ image: imageData })
});
if (response.ok) {
const result = await response.json();
const count = Number(sessionStorage.getItem("count"));
if(count >= 0)
judgeEmotion(result.emotion[0], canvas);
} else {
console.error("サーバーエラー:", response.status);
}
} catch (error) {
console.error("リクエストエラー:", error);
}
setTimeout(captureAndAnalyze, 1000); // 500msごとにキャプチャ
}
function judgeEmotion(emotion, canvas) {
const emt = sessionStorage.getItem("emotion")
if(emotion === null){
countdown.innerText = "Failed!";
sessionStorage.setItem("count", 3);
}
else if(emotion !== emt){
countdown.innerText = emotion + " ?";
sessionStorage.setItem("count", 3);
}
else{
const count = Number(sessionStorage.getItem("count"));
if(count === 0){
countdown.innerText = "Great!";
videoText.innerText = "";
layer.classList.add("overlay");
preview.src = canvas.toDataURL('image/png');
save.disabled = false;
}
else
countdown.innerText = count;
sessionStorage.setItem("count", count - 1);
}
}
// カメラを開始して、リアルタイムで画像をキャプチャして分析
startCamera();
setEmotion();
captureAndAnalyze();
7.templates/camera_app/camera.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'css/camera.css' %}">
<link rel="icon" href="{% static 'icon/favicon.ico' %}" type="image/x-icon">
<title>Emo Pic!</title>
</head>
<body>
<div class="header">Emo Pic!</div>
<div id="content">
<div id="holder">
<div id="video-holder">
<div id="video-container">
<video id="video" autoplay width="640" height="480"></video>
<div id="layer"></div>
</div>
<div id="video-text-container">
<div id="video-text"></div>
</div>
<div id="video-countdown-container">
<div id="video-countdown"></div>
</div>
<!-- <div>Result: <span id="emotionResult">Please Wait...</span></div> -->
</div>
<div id="button-holder">
<button id="retry">Retry</button>
<button id="random-emotion">Random Emotion</button>
</div>
</div>
<div id="preview-holder">
<img id="preview"/>
<div id="button-holder2">
<button id="save">Save</button>
</div>
</div>
</div>
<!-- CSRFトークンをJavaScriptで利用できるように埋め込み -->
{% load static %}
<script src="{% static 'js/camera.js' %}"></script>
</body>
</html>
アプリ起動
最後にアプリを起動しましょう。先ほど使用したコマンドをターミナルに入力します。
(.venv) D:\camera_venv\camera_project> python manage.py runserver
すると、サーバーが起動され以下のような文字列が表示されるはずです。
Django version 5.1.3, using settings 'camera_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
ここに表示されたhttp://127.0.0.1:8000/
をCtrl + 左クリック
すると、下のような画面が表示されます。
場合によってはカメラの使用許可が聞かれますので、許可
すると左の真っ白な画面にカメラから取得した映像が表示されます。通常通り使用できれば、完成です。
ちなみに先ほどstatic/icon
ディレクトリを作成しました。このディレクトリ内にfavicon.ico
という名前で画像を保存すると、Webアプリのアイコンを指定できます。
おわりに
いかがでしたでしょうか。私は初めてDjangoを触りましたが、Ruby on Railsと少し似ているように感じました。データベースに今回触らなかったので、機会があればそれにも触れてみようと思います。最後まで読んでくださりありがとうございました。
GitHub