ラズパイ×ローカルLLM で完全無料の自律型AI植物監視システムを構築した
はじめに
自宅を植物で彩りたいんだが、どうしてもお世話が苦手だ。
何度もかっこいいと思う植物を購入し、枯らしてきた。
水をあげてない、肥料を与えすぎ、植え替えしようとして根っこを切りすぎた。
家族は特に観葉植物には興味がない。私しか欲しがらない。
よって、お世話すべきはもちろん私である。
お世話したくないわけではない。単純に忘れてしまう。ほんとにただそれだけ。
お世話しようと瞬間は思っても、別のことをやっていると植物のことが抜け落ちてしまう。
なので、そんな私でも育て上げられるように完全自律型のAI植物監視システムを構築した。
クラウドAPIは使わない。月額課金もゼロ。Raspberry Pi 1台で完結する。
カメラで撮影して、ローカルのAIが画像を分析して、Slack で「お水あげて」と教えてくれる。
電源を挿しておけば、あとは勝手にやってくれる。そういうのが欲しかった。
本記事では、Raspberry Pi 4B 上で Vision Language Model(VLM)をローカル実行し、定点撮影した植物画像をAIが自動分析、結果をSlackに通知するシステムを構築したので紹介する。
最新ハードは不要
先に言っておくと、このシステムに最新の高性能ハードウェアは一切不要。
使ったのは、買ったけど活用する機会を失って眠っていた Pi 4B(8GB)、別のプロジェクトで余った USB-SSD、数年前に買った Pi Camera v1.3。今なら Pi 5 も出ているし、Camera Module 3 もある。でもそんなものはいらない。
家にあるもので、これだけのことができる。 そこがこのプロジェクトの一番伝えたいところだったりする。
このシステムでできること
- 完全ローカル — クラウドAPI不使用、月額課金ゼロ
- 完全自律 — systemd で24時間無人稼働、電源抜き差ししても自動復帰
- AI画像分析 — Ollama + Qwen3-VL:2b で植物の状態をローカル推論
- Slack通知 — 分析結果を画像付きで自動投稿。異常時はアラート
- ランニングコスト — 電気代のみ(月額約100〜150円)
- 開発もAI — Claude Code で TDD 開発。テスト 157 個
システム全体像
使用技術
| カテゴリ | 技術 |
|---|---|
| ハードウェア | Raspberry Pi 4B (8GB), SSD 1TB, Pi Camera v1.3 |
| OS | Raspberry Pi OS Bookworm (64-bit) |
| 言語 | Python 3.11 |
| VLM | Ollama 0.6.x + Qwen3-VL:2b |
| Web | Flask + Plotly.js |
| DB | SQLite |
| 通知 | Slack Bot (slack-sdk) |
| 運用 | systemd (timer + service) |
| テスト | pytest (157 tests) |
1. ハードウェアセットアップ
なぜ SSD ブートか
SD カードでの運用は「半年でカードが壊れた」という話をよく聞く。AIの推論結果やカメラ画像を毎日書き込み続けるなら、なおさら怖い。USB-SSD にしておけば耐久性も速度も段違いで、精神衛生上よい。SSD ブートの手順は公式ドキュメントに詳しいので割愛する。
Pi Camera v1.3 の接続
CSI(Camera Serial Interface)ケーブルで接続する。ここで最初の罠にハマった。詳しくは後述するが、ケーブルの向きを間違えると半日溶ける。
先に言っておく: ケーブルの青い面がイーサネットポート側を向くように挿す。逆でもコネクタに入るし、i2c でも検出される。だがデータは来ない。
# カメラ動作確認
$ rpicam-still -o test.jpg --width 640 --height 480
$ ls -la test.jpg
-rw-r--r-- 1 iris iris 75747 ... test.jpg # 成功!
ハードウェア費用
| パーツ | 今回使ったもの | 新品で買う場合 (参考) |
|---|---|---|
| Raspberry Pi | 4B 8GB(棚で眠ってた) | 4B 8GB: ¥10,000〜 / Pi 5 は不要 |
| ストレージ | USB-SSD 1TB(余りもの) | ¥8,000〜 |
| カメラ | Pi Camera v1.3(数年前に購入) | ¥1,500〜 / Module 3 は不要 |
| 電源 | 手持ちの 5V 3A アダプタ | ¥1,500 |
| 今回の出費 | ¥0 | 全部買っても 約 ¥20,000〜 |
私の場合、全て家にあったものなので追加出費はゼロ。ランニングコストは電気代のみ。Pi 4B の消費電力は約 5〜7W。24時間つけっぱなしでも月額 約100〜150円。コンビニでペットボトルのジュースを1本買うより安い。
……2万円あれば、市販の植物監視カメラが買える気もする。
でもこっちには自由がある。ラズパイは他のプロジェクトと共存できるし、センサーやカメラを増やせる。分析ロジックもプロンプトも自分で変えられる。SLACKの自分専用チャンネルに勝手に投稿してくれる。何より、完全オリジナルの専用監視カメラエージェントが動いている。コードは半永久的に自分のもの。
……安いか? 安いということにしておく。
2. ソフトウェア環境構築
OS とPython 環境
# OS 確認
$ cat /etc/os-release | grep PRETTY
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
$ uname -m
aarch64
# Python venv 構築
$ python3 -m venv .venv --system-site-packages
$ source .venv/bin/activate
# 必要パッケージ
$ pip install flask plotly structlog slack-sdk python-dotenv sqlalchemy ollama
--system-site-packagesは picamera2 のために必要。picamera2 は apt パッケージで入るため、venv からシステムパッケージを参照できるようにする。
Ollama のインストール
# ワンライナーでインストール
$ curl -fsSL https://ollama.com/install.sh | sh
# 自動で systemd サービスとして登録される
$ systemctl status ollama
● ollama.service - Ollama Service
Active: active (running)
# Qwen3-VL:2b モデルをダウンロード
$ ollama pull qwen3-vl:2b
pulling manifest
pulling ... 100%
なぜ Qwen3-VL:2b なのか
正直、選択肢がほとんどなかった。Pi 4B で動く VLM で、日本語が使えるものを探した結果がこれ。
| モデル | サイズ | VRAM | 日本語 | Pi 4B 4GB |
|---|---|---|---|---|
| LLaVA 7B | ~4.5GB | 5GB+ | △ | ✗ メモリ不足 |
| moondream2 0.5B | ~0.8GB | 1GB | ✗ 英語のみ | ○ 動くが日本語不可 |
| Qwen3-VL:2b | ~2.7GB | 3GB | ◎ ネイティブ | ○ ギリギリ動く |
| Qwen3-VL:7b | ~6GB | 7GB+ | ◎ | ✗ メモリ不足 |
Pi 4B 4GB で動作し、日本語でJSON出力できるVLMは、現時点で Qwen3-VL:2b がほぼ唯一の選択肢。選んだというより、消去法で残った。
3. AI分析パイプラインの実装
ここがシステムの心臓部。「撮影→分析→保存→通知」を1本のパイプラインでつなぐ。
非力だからこその設計思想
Pi 4B + 2B パラメータモデルという組み合わせは、お世辞にもパワフルとは言えない。だからこそ、「なんでもやらせる」のではなく「決まった情報を決まったルールで出してもらう」 という設計にした。
- 入力は固定: 640x480 の JPEG 1枚だけ
- 出力も固定: 10項目の JSON。自由記述させない
- 質問は1回: 「この画像を分析して、このJSONを埋めろ」それだけ
汎用的なチャットをさせたら破綻する。でも「この写真の植物は元気?枯れてない?」くらいの判定なら、2B モデルでも実用になる。非力なハードで AI を動かすコツは、AI にさせることを極限まで絞ることだった。
アーキテクチャ
実際のパイプラインコード (scripts/run_pipeline.py) より抜粋:
class Pipeline:
"""capture -> analyze -> store -> notify の統合パイプライン."""
def run(self) -> dict:
"""パイプラインを実行し、結果サマリを返す."""
result_summary = {
"capture": False,
"analyze": False,
"store": False,
"notify": False,
"error": None,
}
# Step 1: Capture
try:
metadata = self._capture.capture()
result_summary["capture"] = True
logger.info("pipeline_capture_ok", image=str(metadata.file_path))
except Exception as e:
logger.error("pipeline_capture_failed", error=str(e))
result_summary["error"] = f"capture: {e}"
return result_summary # 撮影失敗したらここで中断
# Step 2: Analyze
try:
analysis = self._analyzer.analyze(metadata.file_path)
result_summary["analyze"] = True
logger.info(
"pipeline_analyze_ok",
growth_stage=analysis.growth_stage.value,
has_anomaly=analysis.has_anomaly,
)
except Exception as e:
logger.error("pipeline_analyze_failed", error=str(e))
result_summary["error"] = f"analyze: {e}"
return result_summary # 分析失敗したらここで中断
# Step 3: Store (continue even if fails)
try:
record_id = self._storage.save(analysis)
result_summary["store"] = True
logger.info("pipeline_store_ok", record_id=record_id)
except Exception as e:
logger.error("pipeline_store_failed", error=str(e))
result_summary["error"] = f"store: {e}"
# ← 保存失敗しても続行
# Step 4: Notify (graceful degradation - continue even if fails)
if self._notifier:
try:
if analysis.has_anomaly:
self._notifier.post_alert(analysis)
else:
self._notifier.post_report(analysis)
result_summary["notify"] = True
logger.info("pipeline_notify_ok")
except Exception as e:
logger.error("pipeline_notify_failed", error=str(e))
result_summary["error"] = f"notify: {e}"
# ← 通知失敗してもOK
return result_summary
Graceful Degradation がポイント。Slack が落ちていても撮影とDB保存は止めない。データさえ取れていれば後からどうにでもなる。
VLMBackend Protocol — 差し替え可能な設計
実際の Protocol 定義 (backend/src/analyzer.py) より:
class VLMBackend(Protocol):
"""VLM バックエンドのプロトコル."""
def caption(self, image_path: Path) -> str: ...
def query(self, image_path: Path, question: str) -> str: ...
この Protocol を使うことで、VLM バックエンドを自由に差し替えられる:
実際の初期化コード (scripts/run_pipeline.py) より:
# Ollama + Qwen3-VL VLM backend
from backend.src.vlm_ollama import OllamaVLM
vlm = OllamaVLM(
model=os.getenv("OLLAMA_MODEL", "qwen3-vl:2b"),
host=os.getenv("OLLAMA_HOST", "http://localhost:11434"),
)
analyzer = PlantAnalyzer(vlm=vlm)
# クラウド API(将来の拡張)
vlm = CloudVLM(api_key="...")
# テスト用モック
vlm = MockVLM(response='{"growth_stage": "seed", ...}')
プロンプト設計 — JSON構造化出力
ここが「エコ設計」の要。VLM に自然言語で自由に語らせたら、回答は毎回フォーマットが違うし、パースも大変だし、推論時間も長くなる。最初から「このJSONのフィールドを埋めろ。それ以外は何も返すな」と指示する。VLM の仕事を「穴埋め問題」に落とし込む。
実際のプロンプト (backend/src/analyzer.py) より:
ANALYSIS_PROMPT = """\
この植物の画像を分析して、以下のJSON形式で回答してください。必ずJSON形式のみで回答し、それ以外のテキストは含めないでください。
{
"description": "画像の説明(日本語)",
"growth_stage": "種子/発芽/苗/成長期/成熟/不明",
"health_comment": "健康状態の詳細コメント(日本語)",
"has_anomaly": true/false,
"anomaly_details": "異常がある場合の詳細(なければ空文字)",
"color_score": 1-10の整数(葉色の鮮やかさ、10が最高),
"turgor": "ぷくぷく/普通/しおれ",
"etiolation": true/false(徒長しているか),
"new_growth": true/false(新芽が見えるか),
"soil_moisture": "湿り/乾燥/不明"
}"""
JSONパースの堅牢化
「JSONだけ返せ」と言っても、VLM は言うことを聞かない。マークダウンのコードブロックで囲んできたり、末尾にカンマを残したりする。防御的パーサーが必須。
実際のパーサー実装 (backend/src/analyzer.py) より:
def _parse_json_response(self, text: str) -> dict:
"""VLM レスポンスから JSON を抽出してパース."""
# Remove markdown code blocks
cleaned = re.sub(r"```(?:json)?\s*", "", text)
cleaned = re.sub(r"```", "", cleaned)
cleaned = cleaned.strip()
# Fix trailing commas before } or ]
cleaned = re.sub(r",\s*([}\]])", r"\1", cleaned)
try:
return json.loads(cleaned)
except json.JSONDecodeError:
logger.warning("json_parse_failed", raw_response=text[:200])
return {}
4. Slack 通知の実装
ここが「忘れっぽい自分」にとって一番大事なところ。分析結果を Slack に飛ばしてくれれば、スマホの通知で「あ、水やらなきゃ」と思い出せる。現在は多肉植物を種子から発芽させてるところなので100均のボックスに水をため常に土が湿ってる状態にしてるから、私の環境では当分水やりが必要にならないんだが、例えばカビとかが生えてきたときは重要な気付きになる。
実際のSlack投稿例
実際にシステムが投稿した Slack メッセージ。朝起きてスマホを見たらこれが来ている生活。
🟢 植物モニタリングレポート
━━━━━━━━━━━━━━━━━━━━━━━━━━
説明:
緑色の若い植物の芽が土から出ている様子。土壌は湿っている。
成長段階:
germination
健康状態: 🟢
健康的に成長しています。葉は緑色で、新しい成長が見られます。
分析時間:
531.23秒
色スコア:
7/10
張り:
ぷくぷく
新芽:
🌱 あり
土壌水分:
湿り
━━━━━━━━━━━━━━━━━━━━━━━━━━
推論に約8分半かかっている。リアルタイムではないが、植物の状態変化は数時間〜数日単位なので全く問題ない。
今すぐ状態報告が欲しいなら自分で見に行けばいい。ラズパイとLLMが頑張って毎回8分半なら、めちゃくちゃ優秀なスコアだし何も問題ない。
Slack Bot の作成
- Slack API で新規 App 作成
-
OAuth & Permissions で以下のスコープを追加:
-
chat:write— メッセージ投稿 -
files:write— 画像アップロード -
channels:read— チャンネル一覧取得
-
- Bot User OAuth Token をコピー →
.envに設定
# .env
SLACK_BOT_TOKEN=xoxb-xxxx-xxxx-xxxx
SLACK_CHANNEL=#green-lab
投稿フォーマット
通常の分析レポートと、異常検出時のアラートで投稿を分ける。
実際の Slack 投稿実装 (backend/src/notifier.py) より:
def post_report(self, result: AnalysisResult) -> bool:
blocks = self._build_report_blocks(result)
try:
self._client.chat_postMessage(
channel=self._channel,
text=f"植物レポート: {result.caption}",
blocks=blocks,
)
if result.image_path.exists():
self._upload_image(result.image_path, result.caption)
logger.info("slack_report_posted", channel=self._channel)
return True
except SlackApiError as e:
logger.error("slack_post_failed", error=str(e))
return False
def post_alert(self, result: AnalysisResult) -> bool:
text = (
f"<!channel> 植物アラート!\n"
f"*異常検出*: {result.anomaly_details}\n"
f"画像: `{result.image_path.name}`"
)
try:
self._client.chat_postMessage(
channel=self._channel,
text=text,
)
if result.image_path.exists():
self._upload_image(result.image_path, f"ALERT: {result.anomaly_details}")
logger.info("slack_alert_posted", channel=self._channel)
return True
except SlackApiError as e:
logger.error("slack_alert_failed", error=str(e))
return False
5. systemd で24時間自律運用
タイマー + サービスの構成
# /etc/systemd/system/greenlab-camera.timer
[Timer]
OnCalendar=*-*-* 07,10,13,16,19:00:00 ←私はLEDを点灯させる時間に合わせて設定している
Persistent=true
RandomizedDelaySec=120
# /etc/systemd/system/greenlab-camera.service
[Service]
Type=oneshot
User=iris
WorkingDirectory=/home/iris/green-lab
ExecStartPre=/usr/bin/touch /tmp/greenlab-pipeline-running
ExecStartPre=/bin/sleep 3
ExecStart=/home/iris/green-lab/.venv/bin/python scripts/run_pipeline.py
ExecStopPost=/bin/rm -f /tmp/greenlab-pipeline-running
MemoryMax=3G
TimeoutStartSec=1800
1日5回(7, 10, 13, 16, 19時)、自動で撮影→AI分析→Slack通知が走る。私は何もしなくていい。これが欲しかった。
カメラ競合の解決
Web ダッシュボードのライブカメラと、分析パイプラインが同時にカメラを掴もうとすると Pipeline handler in use by another process エラーになる。
解決策: ファイルロック方式
パイプライン起動
→ /tmp/greenlab-pipeline-running を touch
→ カメラスレッドがロックファイルを検出
→ picamera2 を stop/close して解放
→ パイプラインが撮影・推論
→ ロックファイルを rm
→ カメラスレッドが自動で再取得
実際のカメラスレッド実装 (web/routes/camera.py) より:
PIPELINE_LOCK = Path("/tmp/greenlab-pipeline-running")
def _camera_loop():
"""バックグラウンドで picamera2 からフレームを取得し続ける.
パイプライン実行中はカメラを解放して待機する."""
global _frame, _running
try:
from picamera2 import Picamera2
while _running:
# パイプライン実行中はカメラを解放して待つ
if PIPELINE_LOCK.exists():
with _lock:
_frame = None
while PIPELINE_LOCK.exists() and _running:
time.sleep(2)
if not _running:
break
# カメラ起動
picam2 = Picamera2()
config = picam2.create_video_configuration(main={"size": (640, 480)})
picam2.configure(config)
picam2.start()
# フレーム取得ループ (パイプラインロックが現れたら停止)
while _running and not PIPELINE_LOCK.exists():
buf = io.BytesIO()
picam2.capture_file(buf, format="jpeg")
with _lock:
_frame = buf.getvalue()
time.sleep(0.1)
# カメラ解放
picam2.stop()
picam2.close()
except Exception:
_running = False
リブート耐性
# サービスの有効化
$ sudo systemctl enable greenlab-web.service
$ sudo systemctl enable greenlab-camera.timer
# リブートテスト
$ sudo reboot
# ... 再起動後 ...
$ systemctl status greenlab-web # → active (running)
$ systemctl list-timers # → greenlab-camera.timer active
電源を抜いて挿し直しても、全て自動で復帰する。ブレーカーが落ちても大丈夫。
6. 8bit RPG風ダッシュボード(おまけ)
せっかくなので、ダッシュボードも遊び心全開にした。ファミコン風の8bit RPGスタイル。植物たちが住む「GREENLAB」という世界観で、モンスターが画面を歩き回る。
デザインコンセプト
- フォント: Press Start 2P(ファミコン風ピクセルフォント)
- カラー: 紺色ベース + RPG パレット(金
#F8C830、シアン#58D8F8、緑#00D800) - エフェクト: CRT スキャンライン、ちらつきアニメーション
- モンスター: Canvas で描画するピクセルアートモンスター5種(GLOOP, KRAB, PUFF, BEEP, WIGGLY)が画面を歩き回る
![]() |
![]() |
/* RPGウィンドウ */
.rpg {
background: #000C30;
border: 4px solid #FFFFFF;
box-shadow:
inset 0 0 0 2px #000060,
inset 0 0 0 4px #000C30,
inset 0 0 0 6px #8888FF,
8px 8px 0 rgba(0,0,0,.85);
}
/* CRTスキャンライン */
body::after {
content: '';
position: fixed; inset: 0;
background: repeating-linear-gradient(
0deg, transparent 0, transparent 2px,
rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px
);
}
ログイン画面は「ぼうけんのしょ」。ダッシュボードには植物の4ゾーンがRPGのキャラクターカード風に並ぶ。ライブカメラ映像の確認や手動撮影もここからできる。
実用性はともかく、毎日見るものだから楽しいほうがいい。
7. 実際にやってみて — ハードルと学び
正直に、うまくいかなかったことも多い。
推論精度の課題
サンプル画像4枚(seed → germination → seedling → vegetative)で精度検証してみた:
| 画像 | 実際 | VLM判定 | 正否 |
|---|---|---|---|
| 土のみ | seed | seedling | ❌ |
| 発芽初期 | germination | seedling | ❌ |
| 双葉展開 | seedling | seedling | ✅ |
| 茂った苗 | vegetative | seedling | ❌ |
4枚中1枚しか正解できなかった。 正直がっかりした。ただ、2B パラメータのモデルに細かな成長段階の区別を求めるのは酷かもしれない。私の育ててる環境もアクリルボックスの外から撮影しているので湿度による水滴など判別がしずらくなる状況というものもある。一方で color_score(葉色の鮮やかさ)は画像の緑色量に比例して妥当な値を返しており、「緑が増えてきた」「色が薄くなってきた」レベルの変化検知には使える。完璧じゃなくても、忘れっぽい私に「ちょっと見に行ったほうがいいかも」と教えてくれるだけで十分価値がある。
推論時間の実測値
実際のシステムログから抽出した推論時間:
| 日時 | 画像 | 推論時間 | 備考 |
|---|---|---|---|
| 3/15 19:10 | 640x480 JPEG | 589.99秒(約10分) | 通常稼働 |
| 3/16 07:25 | 640x480 JPEG | 1486.35秒(約25分) | 朝イチで遅い |
| 3/16 10:11 | 640x480 JPEG | 531.23秒(約9分) | 最速記録 |
| - | VS Code Server 起動中 | - | OOM でクラッシュ |
私の Pi 4B は 8GB モデルなので問題なく自律運用できている。ただし 4GB モデルだった場合、Qwen3-VL:2b の推論(約2.7GB)だけでメモリがほぼ埋まるため、VLM 推論中に他の作業をすることは難しかったと思う。これから構築する人は、8GB モデルをおすすめしたい。
デバッグ地獄 — CSI ケーブルの罠
これが一番つらかった。ある日、カメラが突然動かなくなった。
$ rpicam-still -o test.jpg
[0:00:25.567890123] [1234] ERROR Camera camera_manager.cpp:284 Can't open camera manager
ERROR: failed to create camera component
でも i2cdetect では認識される:
$ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- 64 -- -- -- -- -- -- -- -- -- -- -- # ← カメラモジュール検出
70: -- -- -- -- -- -- -- --
原因調査に6時間。結論: CSIケーブルの青い面の向きが逆だった。
正しい向き:
- カメラ側: 青い面が基板と反対側
- Pi側: 青い面がイーサネットポート側
逆向きでもコネクタには刺さる。i2cでも検出される。だがデータ転送だけエラーになる、という罠。これから Pi Camera を使う人は、この1行だけ覚えて帰ってほしい。青い面はイーサネットポート側。
Claude Code による TDD 開発
このプロジェクトの開発は、Anthropic の Claude Code(CLI版 Claude)を使って行った。
テスト駆動開発(TDD)で進めており、RED → GREEN → REFACTOR のサイクルを徹底した。テストは 157 個。カメラやVLMのようなハードウェア依存のコードも、Protocol パターンとモックで全てテスト可能にしてある。
Claude Code の良いところは、SSH 越しに Pi に直接入って作業できること。「このファイル読んで」「テスト書いて」「動かして」が全部ターミナルで完結する。Pi のような非力なマシンでも、コードを書くのは Claude、推論を走らせるのは Pi、という分業ができる。
メモリ管理の工夫
systemd サービスでメモリ上限を設定することで、システム全体のクラッシュを防いでいる:
# /etc/systemd/system/greenlab-camera.service
[Service]
MemoryMax=3G # 4GB のうち 3GB まで
CPUQuota=80% # CPU も 80% に制限
これにより、推論が暴走してもシステムは生き残る。
8. 今後の展望
- センサー統合: BME280(温湿度)、DS18B20(土温)、ADS1115(土壌水分)を Pi に直結。VLMの分析結果と組み合わせて総合判定
- タイムラプス動画: 蓄積された定点撮影画像から成長タイムラプスを自動生成
- VLM精度向上: プロンプトチューニング、または Qwen3-VL:7b が動く環境(Pi 5 8GB?)への移行
- Firebase認証: ダッシュボードに認証を追加し、外出先からセキュアにアクセス
まとめ
植物を枯らし続けてきた私が、Raspberry Pi 4B + Ollama + Qwen3-VL:2b で完全ローカルAI植物監視システムを構築した。
かかったコスト:
- 初期投資: ¥0(家にあったもので全部まかなえた)
- 新品で揃えても約 ¥20,000〜。最新ハードは不要
- ランニングコスト: 電気代 月額約100〜150円
- API課金: ¥0
できたこと:
- 1日5回の自動撮影 + AI分析 + Slack通知
- ブラウザでのライブカメラ表示
- リブート後の完全自動復帰
- 157個のテストによる品質保証
正直な限界:
- 2B モデルの推論精度は完璧ではない(成長段階の細かな区別が苦手)
- 推論に10〜23分かかる(リアルタイムではない)
- 8GB モデルでも推論中はメモリの大半を使う。4GB だと厳しい
だからこそうまくいったこと:
- 「なんでもやらせる」ではなく「決まった情報を決まったルールで出す」エコ設計
- 入力も出力も固定。VLM の仕事を穴埋め問題に絞ったから、2B モデルでも実用になった
- 非力なハードでAIを活かすコツは、AIにさせることを極限まで絞ること
それでも、「寝ている間も旅行中も、AIが植物を見守ってSlackで報告してくれる」のは想像以上に安心感がある。朝起きてSlackに「健康的に成長しています」と来ていると、それだけでちょっと嬉しい。
完全無料で、自分だけのAIが自分の植物を見守る。私でも、これなら続けられそうだ。
今後やりたいこと
推論能力の向上が一番の課題。監視している植物の種類は決まっていて、突然増えたりはしない。だからこそ、汎用的なプロンプトではなく「この子たちに必要な情報は何か」を整理して、種ごとに最適化したプロンプトを組みたい。エコ設計の延長線上にある改善だ。
カメラを3台に増設して、今のボックス構成を全方向からカバーしたい。現状は1台のカメラで1方向しか見えていないので、死角がある。Pi Camera は安いので、CSI マルチプレクサか USB カメラを追加すればハード側は解決できる。
あとは撮影データが溜まり続けているので、タイムラプス動画を自動生成する仕組みも作りたい。
リポジトリ: GitHub - almlog/green-lab
必要パッケージ一覧:
# システムパッケージ
sudo apt install python3-picamera2 mosquitto mosquitto-clients
# Pythonパッケージ
pip install flask plotly structlog slack-sdk ollama
pip install python-dotenv sqlalchemy pytest paho-mqtt
質問・議論歓迎: 「うちのラズパイでも動くか?」「別のVLMモデル試したら?」など、コメントお待ちしています!


