0
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?

GPUなし+オフライン+Raspberry Pi(4GB)で動かすカメラAI - SLM多段パイプライン

0
Last updated at Posted at 2026-04-19

はじめに

『Raspberry Pi』とは、低コストの小型コンピュータです。
フルスペックのLLMを動かすことは到底リソースが足りず、
通常は、クラウドやSaaS上で動作するAIを利用しなければ、AIを利用することはできません。

ですが、『1つの万能なAI(LLM)』を使う考えではなく、『得意なことに特化した小さなAIを組み合わせる』という設計思想に切り替えると、話が変わります。

本記事では、USBカメラ映像から『動きを検出』し、『英語で画像説明文を生成』し、『日本語に翻訳する』という3段階のパイプラインを、 オフライン(Offline) で Raspberry Pi のみで動かした実装をご紹介します。

なぜ『1つのLLM』では動かないのか?

メモリの壁

Raspberry Pi 4(4GB)でLLMを動かす場合、使えるRAMはシステム消費分を引いて実質2〜3GB程度です。
ですが、一般的な多言語/画像対応の大型LLMは、最低でも8〜16GBのVRAMを要求します。

『画像認識』と『日本語対応』の特徴を同時に持つモデルは、このメモリ量では動作困難です。

モデル AI種別 画像認識 日本語対応 必要メモリ目安 Pi4 (4GB)で動作 備考
Llama 3.2 Vision 11B (大型マルチモーダルLLM) マルチモーダルLLM △ (限定的) 10GB〜
OpenCV 画像処理ライブラリ △ (動き検知など) 数百MB 本稿のStage1で利用
moondream VLM(視覚言語モデル) 2〜4GB程度 本稿のStage2で利用
Qwen2.5:1.5b-instruct SLM(テキストLLM) 2GB程度 本稿のStage3で利用

前提

ここで利用する機器や仕様は次のとおりです。

  • Raspberry Pi : Raspberry Pi 4 4GB
  • USBカメラ : logi HD 1080p
  • GPU搭載有無 : GPUなし
  • microSD : KIOXIA EXCERIA 256GB

システム構成

以下、システム構成となります。

image.png

ステージ 利用技術 入力 出力 説明
CameraAI_Stage_1 OpenCV (フレーム差分) USBカメラ映像 JPG + JSON 動き検出後、5秒後のキャプチャを取得
CameraAI_Stage_2 moondream (Ollama) JPG JSON 画像認識し、英語キャプションを出力
CameraAI_Stage_3 Qwen2.5:1.5b (Ollama) JSON JSON 英語から日本語へ翻訳

セットアップ

ここからは、実際にRaspberry Pi上で動かすための準備手順を書きます。

Ollama のインストール

まず、モデルをローカルで動かすためにOllamaをインストールします。
これを利用することで、LLM/SLMなどのダウンロードや管理、インタラクティブのチャットツールの利用ができるようになります。

curl -fsSL https://ollama.com/install.sh | sh

モデルの取得

今回利用するmoondreamとQwen2.5:1.5b-instructをダウンロードします。

ollama pull moondream
ollama pull qwen2.5:1.5b-instruct

Python環境の準備

Python環境を整えるために、Conda(Miniforge)を導入し、そこに環境を構築します。

Conda(Miniforge)のインストール

##### 事前準備
sudo localedef -f UTF-8 -i ja_JP ja_JP
##### 1. Miniforge (linux-aarch64 版) のインストーラをダウンロード
cd /tmp
curl -LO https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-aarch64.sh

##### 2. インストーラを実行
bash Miniforge3-Linux-aarch64.sh
###### Proceed with initialization? [yes|no] -> yes

Conda環境設定の読み込み

##### ターミナルをいったん閉じて開き直すか、もしくは:
source ~/.bashrc

##### conda が使えるか確認
conda --version
###### conda 26.1.1などが表示されればOK

Python仮想環境の作成(cameraai環境)

##### 仮想環境 cameraai を作成
conda create -n cameraai python=3.11 -y

##### 環境を有効化
conda activate cameraai

##### 必要なパッケージをインストール
pip install opencv-python ollama

プログラムの配備

まず、作業用ディレクトリを作成し、今回のスクリプト群を配置します。

mkdir -p ~/WORKSPACE/cameraai
cd ~/WORKSPACE/cameraai

mkdir handoff logs

このディレクトリに、以下 4 つのファイルを作成します。

  • cameraai_stage_1_motion_capture.py
  • cameraai_stage_2_scene_caption.py
  • cameraai_stage_3_translate_ja_qwen.py
  • cameraai_run_all_stages.sh
    以降では、それぞれの中身を記載します。

Stage1: OpenCVで動き検出+5秒後キャプチャ

動きがあった瞬間ではなく、5秒後の静止フレームを採用することで、ブレの少ない画像を VLM に渡せるようにしています。

cameraai_stage_1_motion_capture.py
# cameraai_stage_1_motion_capture.py

import cv2
import json
import os
import time

CAMERA_INDEX      = int(os.getenv("CAMERAAI_CAMERA_INDEX", 0))
FRAME_WIDTH       = int(os.getenv("CAMERAAI_FRAME_WIDTH", 640))
FRAME_HEIGHT      = int(os.getenv("CAMERAAI_FRAME_HEIGHT", 480))
FPS               = int(os.getenv("CAMERAAI_FPS", 15))

# 撮りすぎ防止のため、しきい値を少し厳しめに
MOTION_RATIO      = float(os.getenv("CAMERAAI_MOTION_RATIO", 0.05))  # 0.02 → 0.05
DIFF_THRESH       = int(os.getenv("CAMERAAI_DIFF_THRESH", 25))

# 「検出 → カウントダウン → 撮影」の待ち時間
CAPTURE_DELAY_SEC = float(os.getenv("CAMERAAI_CAPTURE_DELAY_SEC", 5.0))
COOLDOWN_SEC      = float(os.getenv("CAMERAAI_EVENT_COOLDOWN_SEC", 8.0))

# 起動直後は 5 秒間ウォームアップし、その間は撮影しない
WARMUP_SEC        = float(os.getenv("CAMERAAI_WARMUP_SEC", 5.0))

HANDOFF_DIR = "handoff"
STAGE1_JPG  = os.path.join(HANDOFF_DIR, "cameraai_stage_1_motion_capture.jpg")
STAGE1_JSON = os.path.join(HANDOFF_DIR, "cameraai_stage_1_motion_capture.json")

os.makedirs(HANDOFF_DIR, exist_ok=True)

def handoff_busy() -> bool:
    """handoff/ にファイルが残っている間は、前回イベント処理中とみなす"""
    return os.path.exists(STAGE1_JPG) or os.path.exists(STAGE1_JSON)

cap = cv2.VideoCapture(CAMERA_INDEX)
cap.set(cv2.CAP_PROP_FRAME_WIDTH,  FRAME_WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
cap.set(cv2.CAP_PROP_FPS, FPS)

print(f"[INIT] warming up camera for {WARMUP_SEC} seconds...", flush=True)
warmup_start = time.time()

prev_gray   = None
frame_count = 0

# ウォームアップ期間: 「通常状態」の学習とみなす
while time.time() - warmup_start < WARMUP_SEC:
    ok, frame = cap.read()
    if not ok:
        time.sleep(0.1)
        continue
    frame_count += 1
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (21, 21), 0)
    prev_gray = gray

print("[INIT] warmup finished. Start motion detection.", flush=True)

capture_scheduled_at = None
trigger_info         = {}
idle_announced       = False

while True:
    ok, frame = cap.read()
    if not ok:
        time.sleep(0.1)
        continue

    frame_count += 1
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (21, 21), 0)

    if prev_gray is None:
        prev_gray = gray
        continue

    diff   = cv2.absdiff(prev_gray, gray)
    _, th  = cv2.threshold(diff, DIFF_THRESH, 255, cv2.THRESH_BINARY)
    motion_ratio = th.sum() / 255 / th.size
    prev_gray    = gray

    # handoff が埋まっている間は何もしない
    if handoff_busy():
        if capture_scheduled_at is not None:
            print("[INFO] handoff busy; cancel scheduled capture.", flush=True)
        capture_scheduled_at = None
        idle_announced = False
        continue

    # 完全 idle 状態のときだけ、新しいイベントを受付
    if capture_scheduled_at is None and not idle_announced:
        print("[STATE] pipeline idle; waiting for next motion event", flush=True)
        idle_announced = True

    # イベント検出: motion_ratio をログに出しつつ、しきい値以上で反応
    if capture_scheduled_at is None and motion_ratio > MOTION_RATIO:
        now = time.time()
        capture_scheduled_at = now + CAPTURE_DELAY_SEC
        trigger_info = {
            "trigger_frame_count": frame_count,
            "motion_ratio": round(motion_ratio, 6),
            "detected_at": now,
        }
        print(
            f"[EVENT] motion detected frame={frame_count} "
            f"motion_ratio={motion_ratio:.6f} "
            f"capture_in={CAPTURE_DELAY_SEC:.1f}s",
            flush=True,
        )

        # ここでカウントダウンを開始(撮影前に必ずカウントダウン)
        remaining = int(CAPTURE_DELAY_SEC)
        while remaining > 0:
            # 途中で handoff が埋まったらキャンセル
            if handoff_busy():
                print("[INFO] handoff became busy during countdown; cancel capture.", flush=True)
                capture_scheduled_at = None
                break
            print(f"[COUNTDOWN] capture in {remaining} sec", flush=True)
            time.sleep(1.0)
            remaining -= 1

        # handoff busy によるキャンセル時は次のフレームへ
        if capture_scheduled_at is None:
            continue

    # 撮影タイミングになったら、現在のフレームを保存
    if capture_scheduled_at is not None and time.time() >= capture_scheduled_at:
        cv2.imwrite(STAGE1_JPG, frame)
        meta = {
            "frame_count": frame_count,
            "capture_delay_sec": CAPTURE_DELAY_SEC,
            **trigger_info,
        }
        with open(STAGE1_JSON, "w", encoding="utf-8") as f:
            json.dump(meta, f, ensure_ascii=False)

        print(
            f"[PRODUCE] delayed_capture frame={frame_count} "
            f"(trigger={trigger_info.get('trigger_frame_count')} "
            f"motion_ratio={trigger_info.get('motion_ratio')})",
            flush=True,
        )

        capture_scheduled_at = None
        idle_announced = False

        # 撮りすぎ防止のクールダウン
        time.sleep(COOLDOWN_SEC)

Stage2: moondreamで英語キャプション生成

handoff/ に jpg + json が置かれるのを待ち、moondream を使って英語キャプションを生成します。

cameraai_stage_2_scene_caption.py
# cameraai_stage_2_scene_caption.py

import json
import os
import time

import ollama

VLM_MODEL  = os.getenv("CAMERAAI_VLM_MODEL", "moondream")
VLM_PROMPT = os.getenv(
    "CAMERAAI_VLM_PROMPT",
    "Describe"
)
POLL_SEC   = float(os.getenv("CAMERAAI_STAGE2_POLL_SEC", 0.2))

HANDOFF_DIR = "handoff"
STAGE1_JPG  = os.path.join(HANDOFF_DIR, "cameraai_stage_1_motion_capture.jpg")
STAGE1_JSON = os.path.join(HANDOFF_DIR, "cameraai_stage_1_motion_capture.json")
STAGE2_JSON = os.path.join(HANDOFF_DIR, "cameraai_stage_2_scene_caption.json")

while True:
    # 入力がそろうまで待機
    if not (os.path.exists(STAGE1_JPG) and os.path.exists(STAGE1_JSON)):
        time.sleep(POLL_SEC)
        continue

    # まだ前回の出力が残っている場合は待機
    if os.path.exists(STAGE2_JSON):
        time.sleep(POLL_SEC)
        continue

    with open(STAGE1_JSON, encoding="utf-8") as f:
        meta = json.load(f)

    print(f"[CAPTION] start image={os.path.basename(STAGE1_JPG)}", flush=True)
    t0 = time.time()

    with open(STAGE1_JPG, "rb") as img_f:
        img_bytes = img_f.read()

    res = ollama.chat(
        model=VLM_MODEL,
        messages=[{
            "role": "user",
            "content": VLM_PROMPT,
            "images": [img_bytes],
        }],
    )
    caption_en = res["message"]["content"].strip()
    elapsed    = round(time.time() - t0, 2)

    out = {
        **meta,
        "caption_en": caption_en,
        "caption_elapsed_sec": elapsed,
        "model": VLM_MODEL,
        "prompt": VLM_PROMPT,
    }
    with open(STAGE2_JSON, "w", encoding="utf-8") as f:
        json.dump(out, f, ensure_ascii=False)

    print(f"[CAPTION] done elapsed={elapsed}s", flush=True)
    print(f"[CAPTION] EN: {caption_en}", flush=True)

Stage3: Qwen2.5で英語→日本語翻訳

Stage2 が出力した英語キャプションを、Qwen2.5 で日本語 1 文に翻訳し、logs/ に追記していきます。

cameraai_stage_3_translate_ja_qwen.py
# cameraai_stage_3_translate_ja_qwen.py

import json
import os
import re
import time

import ollama

TRANSLATE_MODEL = os.getenv("CAMERAAI_TRANSLATE_MODEL", "qwen2.5:1.5b-instruct")
TEMPERATURE     = float(os.getenv("CAMERAAI_TRANSLATE_TEMPERATURE", 0))
POLL_SEC        = float(os.getenv("CAMERAAI_STAGE3_POLL_SEC", 0.2))

HANDOFF_DIR = "handoff"
STAGE1_JPG  = os.path.join(HANDOFF_DIR, "cameraai_stage_1_motion_capture.jpg")
STAGE1_JSON = os.path.join(HANDOFF_DIR, "cameraai_stage_1_motion_capture.json")
STAGE2_JSON = os.path.join(HANDOFF_DIR, "cameraai_stage_2_scene_caption.json")
LOG_FILE    = os.path.join("logs", "cameraai_results_qwen.jsonl")

os.makedirs("logs", exist_ok=True)

PROMPT_TMPL = """\
You are a careful bilingual translation assistant.
Translate the following English image caption into natural Japanese.
Rules:
- Keep it to one Japanese sentence.
- Do not add new information.
- Do not remove objects or relations mentioned in the English caption.
- If the English sentence is long, compress wording but keep all objects.
- Return valid JSON only.
JSON schema:
{{"caption_ja": "natural Japanese translation"}}
English caption:
"{caption_en}"
"""

def extract_json_object(text: str):
    m = re.search(r"\{.*\}", text, re.DOTALL)
    return json.loads(m.group()) if m else {}

while True:
    if not os.path.exists(STAGE2_JSON):
        time.sleep(POLL_SEC)
        continue

    with open(STAGE2_JSON, encoding="utf-8") as f:
        src = json.load(f)

    caption_en = src["caption_en"]
    print(f"[TRANSLATE] start model={TRANSLATE_MODEL}", flush=True)
    t0 = time.time()

    res = ollama.chat(
        model=TRANSLATE_MODEL,
        messages=[{
            "role": "user",
            "content": PROMPT_TMPL.format(caption_en=caption_en),
        }],
        options={"temperature": TEMPERATURE},
    )
    raw = res["message"]["content"].strip()
    parsed = extract_json_object(raw)
    caption_ja = parsed.get("caption_ja", raw)
    elapsed    = round(time.time() - t0, 2)

    result = {
        **src,
        "caption_ja": caption_ja,
        "translate_elapsed_sec": elapsed,
        "translate_model": TRANSLATE_MODEL,
        "translated_at": time.time(),
    }

    with open(LOG_FILE, "a", encoding="utf-8") as f:
        f.write(json.dumps(result, ensure_ascii=False) + "\n")

    print(f"[TRANSLATE] done elapsed={elapsed}s", flush=True)
    print(f"[RESULT] EN: {caption_en}", flush=True)
    print(f"[RESULT] JA: {caption_ja}", flush=True)

    # handoff を空にして Stage1 に「処理完了」を伝える
    for path in [STAGE1_JPG, STAGE1_JSON, STAGE2_JSON]:
        if os.path.exists(path):
            os.remove(path)
    print("[ACK] cleared handoff files", flush=True)

一括起動スクリプト

3つのStageをまとめて立ち上げるシェルスクリプトです。

cameraai_run_all_stages.sh
#!/bin/bash
set -e

mkdir -p handoff logs

python -u cameraai_stage_1_motion_capture.py &
PID1=$!
python -u cameraai_stage_2_scene_caption.py &
PID2=$!
python -u cameraai_stage_3_translate_ja_qwen.py &
PID3=$!

trap "kill $PID1 $PID2 $PID3 2>/dev/null" EXIT INT TERM

wait

実行

実行方法

conda activate cameraai
cd ~/WORKSPACE/cameraai

# 前回の handoff を消してから起動
rm -f handoff/*.jpg handoff/*.json

bash cameraai_run_all_stages.sh

実行結果の確認

カメラの前で動きを見せると、
Stage1 が 5 秒後のフレームを handoff/ に保存
Stage2 が moondream で英語キャプションを生成
Stage3 が Qwen2.5 で日本語に翻訳し、logs/cameraai_results_qwen.jsonl に 1 行 JSON を追記
という流れでパイプラインが回ります。


(cameraai) hiro_to_int@pi4:~/WORKSPACE/cameraai $ bash cameraai_run_all_stages.sh
[INIT] warming up camera for 5.0 seconds...
[INIT] warmup finished. Start motion detection.
[STATE] pipeline idle; waiting for next motion event
[EVENT] motion detected frame=107 motion_ratio=0.177288 capture_in=5.0s
[COUNTDOWN] capture in 5 sec
[COUNTDOWN] capture in 4 sec
[COUNTDOWN] capture in 3 sec
[COUNTDOWN] capture in 2 sec
[COUNTDOWN] capture in 1 sec
[PRODUCE] delayed_capture frame=107 (trigger=107 motion_ratio=0.177288)
[CAPTION] start image=cameraai_stage_1_motion_capture.jpg
[CAPTION] done elapsed=210.31s
[CAPTION] EN: The image features a pink stuffed animal, likely a Pokemon character or toy, lying on its back on top of a bed. The plush toy is positioned in such a way that it appears to be resting comfortably and facing the camera directly. The bed occupies most of the background, showcasing the main subject of the photo - the adorable Pokemon plushie.
[TRANSLATE] start model=qwen2.5:1.5b-instruct
[TRANSLATE] done elapsed=46.39s
[RESULT] EN: The image features a pink stuffed animal, likely a Pokemon character or toy, lying on its back on top of a bed. The plush toy is positioned in such a way that it appears to be resting comfortably and facing the camera directly. The bed occupies most of the background, showcasing the main subject of the photo - the adorable Pokemon plushie.
[RESULT] JA: ピンクのぬいぐるみが、 likely ポケモンキャラクターまたは玩具で、ベッドの上に寝転がっています。このぬいぐるとカメラに向かって直接見つめています。背景には主な被写体であるポケモンぬいぐりが占めるスペースが広く、ベッドは全体的に占めており、その中にいるポケモンぬいぐりがゆっくりと寝転がっています。
[ACK] cleared handoff files
[STATE] pipeline idle; waiting for next motion event
logs/cameraai_results_qwen.jsonl
{
    "frame_count": 107,
    "capture_delay_sec": 5.0,
    "trigger_frame_count": 107,
    "motion_ratio": 0.177288,
    "detected_at": 1776545852.8470438,
    "caption_en": "The image features a pink stuffed animal, likely a Pokemon character or toy, lying on its back on top of a bed. The plush toy is positioned in such a way that it appears to be resting comfortably and facing the camera directly. The bed occupies most of the background, showcasing the main subject of the photo - the adorable Pokemon plushie.",
    "caption_elapsed_sec": 210.31,
    "model": "moondream",
    "prompt": "Describe",
    "caption_ja": "ピンクのぬいぐるみが、 likely ポケモンキャラクターまたは玩具で、ベッドの上に寝転がっています。このぬいぐるとカメラに向かって直接見つめています。背景には主な被写体であるポケモンぬいぐりが占めるスペースが広く、ベッドは全体的に占めており、その中にいるポケモンぬいぐりがゆっくりと寝転がっています。",
    "translate_elapsed_sec": 46.39,
    "translate_model": "qwen2.5: 1.5b-instruct",
    "translated_at": 1776546114.9013596
}

各ステージのポイント

Stage1: 動き検出+5秒後キャプチャ

Stage1 は「撮りすぎない」「ブレたフレームを渡さない」ことを重視しています。

  • 起動直後 5 秒間はウォームアップとして、撮影は一切しない
  • フレーム差分から計算した motion_ratio が 0.05 を超えたときだけイベント扱い
  • 動きを検出したらすぐには撮らず、ログでカウントダウンしながら 5 秒待つ
  • カウントダウン中に handoff が埋まったら、そのイベントはキャンセルして次を待つ

これにより、「起動時のノイズで大量に撮れる」「少し手を動かしただけで連写になる」といった挙動を避けています。

Stage2: moondream で英語キャプション生成

Stage2 は、画像から説明文を作る VLM (Vision Language Model) のステージです。

  • 入力は Stage1 が保存した JPG + JSON
  • 出力は caption_en と処理時間 caption_elapsed_sec
  • 処理時間を考慮し、プロンプトは "Describe" のようなシンプルなものを採用している。

Stage3: Qwen2.5 で英語→日本語翻訳

Stage3 は、英語キャプションを日本語 1〜2 文に翻訳するステージです。

  • Qwen2.5 に渡すプロンプトで、「要約ではなく翻訳」「オブジェクトを削らない」ことを明示
  • 英語キャプションをそのまま埋め込むのではなく、English caption: "{caption_en}" のように
    ダブルクォートで 1 つの文字列として渡すことで、長文でも途中で切れずに翻訳されるようになりました。
  • Qwen の出力は JSON で返させていますが、取り出しているのは caption_ja だけです。
    caption_en も一緒に生成させてしまうと、元の英語キャプションを「要約」したり書き換えてしまうことがあったため、
    英語は 必ず Stage2 で生成したものだけを使うこととしています。

実際、この "{caption_en}" に変える前は、カンマ以降の情報が無視されてしまうことがあり、短い訳しか返ってこない場面がありました。
プロンプト側で「文字列の境界」を明確にしてあげることが、地味に効いたポイントです。

今後の発展アイデア

今回は、あくまで「Raspberry Pi 4 (4GB) だけでどこまでできるか」という制約つきの構成でしたが、
ここから先の発展として、次のような方向性が考えられます。

  • Stage2 の高速化

    • 入力画像を 1280x960 ではなく 512px 四方程度に縮小してから moondream に渡す
    • それでも遅い場合は、Stage2/3 だけを GPU 付き PC にオフロードし、Pi は Stage1 専任にする
  • 翻訳品質のチューニング

    • 「2 文まで OK」にして、読みやすさを優先する
    • 「です・ます調」「だ・である調」のどちらで話すかをプロンプトで固定する
    • Stage3の翻訳は『Argos Translate』の利用も検討した方が良いかと思われる
  • 応用例

    • 監視カメラ代わりに、「人が通ったときだけ日本語で説明ログを残す」
    • 工場やガレージで、「特定の場所に物が置かれたときだけログを取る」
    • 視覚に課題がある人向けに、「手元のモノをかざすと日本語で説明してくれる」端末として使う

まとめ

本記事では、Raspberry Pi 4 (4GB) という小さなマシン上で、

  • Stage1: OpenCV による動き検出と「5秒後」の静止フレーム取得
  • Stage2: moondream による画像 → 英語キャプション生成
  • Stage3: Qwen2.5:1.5b による英語 → 日本語翻訳

という 3 段構成のパイプラインを構築し、USB カメラの映像を「日本語の説明文」に変換するところまで実装しました。

ここで重要だったのは、

  • フルスペックのマルチモーダル LLM はメモリ的に載らない → 1 台に「全部入り」を使わない
  • 代わりに、OpenCV / VLM / SLM それぞれの 得意分野に処理を分割 する

といった設計上の工夫でした。

Raspberry Pi のようなリソース制約の厳しい環境でも、

  • 古典的な画像処理(OpenCV)
  • 軽量 VLM(moondream)
  • 小さめの言語モデル(Qwen2.5:1.5b)

をうまく組み合わせることで、「巨大な LLM がなければできないと思っていた処理」も、
十分に現実的な形で動かせることが分かったと思います。

エッジでのAI利用でも組み合わせや用途によっては、十分に活用できる可能性がありますので、それぞれの利用シーンに応じた検討を実施して行って頂けたらと思います。

参考リンク

0
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
0
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?