2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Django】初心者がカメラアプリを作ってみた

Posted at

※サンプルコードあります。

はじめに

 アクセスいただきありがとうございます。「Djangoでなんか作ってみたいな」という思いでChatGPTを使いつつ、1週間くらいで作りました。自分の環境にだけ置いておくはもったいないと思い、このように記事にすることにしました。「Django使って1から自分の環境でWebアプリ動かしてみたい」 という方のお役に立てれば幸いです。ぜひご自身の環境でもお試しください。

制作物

テーマ

 今回作成したもののテーマは 「事故画を撮らせない、絶対に笑顔が撮れるアプリ」 です。
 私は、うまく笑えていなかったり、写真写りが悪くなってしまうった所謂「事故画」を撮られてしまったことがあります。私の経験上、事故画を撮られてしまうのはこちらが十分に準備しておらず、不意に写真を撮られてしまうときだと思います。じゃあ、 不意に写真を撮れなくして十分に準備できたタイミングで写真をとればいいじゃないか! ということで、カメラから取得した映像から写っている人の表情を分析し、笑顔が3秒続けばシャッターを切れば絶対に笑顔の写真が撮れると考えました。これで事故画が防げる!
 作りました。ただ「笑顔が続けば撮るアプリ」はすでに存在するっぽいので、今回は拡張して 「指定された表情を3秒続けたら写真を撮るアプリ」 を作りました。このアプリでも間違いなくじ事故画にはならないので、まあいっか...

使用方法

スクリーンショット 2025-02-09 201923 - (1).png

  • ①(画像では白い四角)
     カメラから取得した映像が表示されます。上部に「Be surprise!」とあります。この部分で表情を指定するので、それに従った表情をしましょう。

  •  「リトライボタン」です。一度確定した写真をもう一度取り直せます。

  •  「表情変更ボタン」です。指定される表情をランダムに変更できます。
  • ④(画像では薄灰色の四角枠)
     撮影された画像がここでプレビュー表示されます。

  •  「保存ボタン」です。④で表示されたプレビュー画像を実際にカメラロールに保存できます。

仕組み

 実は「3秒待ってから写真を撮る」ではなく、「毎秒写真を撮って評価する」ということをしています。
prj.png
 カメラは常に起動し、映像を撮っています。その映像を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として設定されています。それにアクセスすると、以下のようなサンプルの画面を見ることができます。
image.png
 ここでは一度ターミナルで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

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

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

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

config/urls.py
from django.urls import include, path

urlpatterns = [
    path('', include('camera_app.urls')),
]

5.static/css/camera.css

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

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

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 + 左クリックすると、下のような画面が表示されます。

image.png

 場合によってはカメラの使用許可が聞かれますので、許可すると左の真っ白な画面にカメラから取得した映像が表示されます。通常通り使用できれば、完成です。

 ちなみに先ほどstatic/iconディレクトリを作成しました。このディレクトリ内にfavicon.icoという名前で画像を保存すると、Webアプリのアイコンを指定できます。

おわりに

 いかがでしたでしょうか。私は初めてDjangoを触りましたが、Ruby on Railsと少し似ているように感じました。データベースに今回触らなかったので、機会があればそれにも触れてみようと思います。最後まで読んでくださりありがとうございました。

GitHub

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?