Raspberry Pi 5 にはカメラやネットワーク機能が搭載されており、
プログラムを使って Webページを自作し、カメラの画像を配信することができます。
この授業では、以下のことを学びます
- Webサーバとは何かを理解する
- Thonny を使って Flask のWebサーバを立てる
- Pi Camera で撮影した画像を Webブラウザで表示する

必要なライブラリのインストール
Raspberry Pi 5 では、初期状態では Web サーバや Pi Camera 用の Python ライブラリは入っていません。
まずは一度だけ、ターミナルを開いて以下のコマンドを実行し、必要なライブラリをインストールします。
sudo apt update
sudo apt install -y python3-flask python3-picamera2
- Flask:PythonでWebサーバを作るための軽量なライブラリ
- picamera2:Pi CameraをPythonから操作するための公式ライブラリ(Pi 5対応)
また、Pi Cameraを使うためにはカメラをフラットケーブルで接続後、再起動してください。


以下のコマンドで動作をテストできます。
libcamera-hello --list-cameras
以下のように表示されれば問題ありません。

以下のコマンドでカメラの写りを確認できます。
libcamera-still -o image.jpg

Webサーバについて
簡単な仕組み
Raspberry PiをWebサーバとして使うときの基本的な構成は以下の通りです
[ブラウザ(PCやスマホ)]
↓ HTTPリクエスト
[Raspberry Pi 上の Flask サーバ]
↓ カメラ操作や画像ファイルの読み出し
[Pi Camera / 画像ファイル]
- PCやスマホのブラウザで Raspberry Pi の IP アドレスにアクセスすると、
Flask がリクエストを受け取り、HTMLや画像データを送り返します。 - カメラを使う場合、Flask が撮影命令を出し、Pi Camera で撮った画像を保存したうえで、
その画像ファイルをブラウザに返す、という流れになります。
この仕組みを実際に自分で作れると、センサー情報や画像をブラウザ上に表示したり、
他の端末(スマホや別のPC)から確認したりできるようになります。
フォルダ構成
まずは作業用フォルダを作成し、プログラムとHTMLファイルを整理しておきます。
cam_web/
├─ app.py ← Flaskのプログラム(Python)
└─ static/ ← Webに配信するファイル(画像・HTMLなど)
└─ index.html ← WebページのHTML
app.py
は Flask を使ったWebサーバのプログラムです。static
フォルダは、Webブラウザからそのままアクセスできるファイル(HTML・画像)を置く場所です。
Flaskはこのフォルダを自動的に「静的ファイル置き場」として認識します。
この構成は Flask でWeb開発を行うときの基本形なので、必ずこのように作っておきましょう。

FlaskでWebサーバを立ててみる
まずは最小限のプログラムで、Raspberry Pi上にWebサーバを立ち上げてみます。
📄 app.py
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return "<h1>Hello from Raspberry Pi 5!</h1><p>Web server is running.</p>"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)
プログラムの解説
Flask(__name__)
でWebサーバを準備します。@app.get("/")
は「URLの/
(トップページ)にアクセスされたとき」という意味です。- 関数
index()
の返り値がそのままブラウザに表示されます。 app.run(host="0.0.0.0")
とすると、LAN内の他の端末(スマホ・PC)からもアクセス可能になります。
Thonnyで実行すると、コンソールに以下のように表示されます。
ここで表示されるIPアドレスを覚えておいてください。
* Running on http://0.0.0.0:8000
PCやスマホを同じネットワークにつなぎ(モバイルホットスポットやデザリングなどでRaspberry Piをつなげると楽です)、ブラウザのアドレス欄で以下を入力します。
ここで入力するIPアドレスは上記のThonnyのシェル上に出ているものを使います。
http://<Raspberry PiのIP>:8000/
「Hello from Raspberry Pi 5!」という文字が表示されれば成功です。

実行を止めるときは Thonny のコンソールで Ctrl + C
を押します。
Pi Cameraの使用
Raspberry Piは簡単にカメラを接続することができます。
Pi CameraをPythonから制御して写真を撮ってみます。
以下のプログラムを実行してください。
from picamera2 import Picamera2
import time
picam2 = Picamera2()
picam2.start() # カメラを起動
time.sleep(2) # 露出やホワイトバランスが安定するまで待つ
picam2.capture_file("test.jpg") # 撮影してファイル保存
picam2.stop() # カメラを停止
実行すると、プログラムと同じフォルダ内に test.jpg
という画像ファイルが保存されます。
Raspberry Piのファイルブラウザなどで開いて、撮影できていることを確認してみましょう。
カメラ起動直後は明るさなどが安定しないため、time.sleep(2)
で2秒ほど待つのがきれいに撮るコツです。
Webページとのリンク
いよいよ、Pi Cameraで撮影した画像をWebページに表示してみましょう。
HTMLファイルとFlaskプログラムを少しずつ連携させていきます。
📄 static/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pi Camera Viewer</title>
</head>
<body>
<h1>📷 Pi Camera Viewer</h1>
<p><a href="/capture">📸 撮影する</a></p>
<img src="/static/capture.jpg" alt="Captured image" width="400">
</body>
</html>
このHTMLは、撮影ボタン(リンク)と、撮影結果の画像を表示するシンプルなWebページです。
📄 app.py
from flask import Flask, send_from_directory
from picamera2 import Picamera2
import time
app = Flask(__name__)
@app.get("/")
def index():
return send_from_directory("static", "index.html")
@app.get("/capture")
def capture():
# 必要なときだけその場で開いて、その場で閉じる
picam2 = Picamera2()
picam2.start()
time.sleep(2)
picam2.capture_file("static/capture.jpg")
picam2.stop()
picam2.close()
return send_from_directory("static", "index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True, use_reloader=False)
・ポイント
send_from_directory("static", "index.html")
→static/index.html
をブラウザに送ります。/capture
にアクセスするとカメラで撮影し、static/capture.jpg
に上書き保存します。
その後、再びトップページ(index.html)を表示します。
実行と確認
先ほどと同様にThonnyでプログラムを実行してからほかのデバイス等でそのIPアドレスにアクセスします。
http://<Raspberry PiのIP>:8000
「📸 撮影する」をクリックし、画像が更新されて表示されれば成功です。

問題6
いまのままでは、ブラウザでボタンを押さないと画像が切り替わりません。
これを、3秒ごとに自動的に最新の画像に切り替わるWebカメラに改良してください。
ヒント1:
ブラウザは同じURLの画像をキャッシュ(保存)するので、ただ <img src="/capture">
と書いても、実際には同じ画像が使い回されてしまいます。
これを避けるために、URLの末尾にダミーのパラメータ(例:現在時刻)を付けると、毎回違う画像として認識してくれます
/capture?t=1696600000
HTML の <script>
タグの中に JavaScript を書いて、自動的に動作させます。setInterval(function() { ... }, 3000);
のように書くと、3秒ごとに処理が実行されます。
ヒント2:
<img>
タグに id="cam"
を付けて、JavaScriptから document.getElementById("cam")
でアクセスできるようにしましょう。
ヒント3:
img.src = "/capture?t=" + Date.now();
のように書くと、キャッシュを回避できます。
回答例はこちら
from flask import Flask, make_response
from picamera2 import Picamera2
from PIL import Image
import numpy as np
import io
import threading
import atexit
import logging
# ------------------------
# Flask 基本設定
# ------------------------
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
# ------------------------
# カメラ初期化
# ------------------------
cam = Picamera2()
# 解像度は必要に応じて変更可(例: 1920x1080 など)
still_cfg = cam.create_still_configuration(
main={"size": (1280, 720)},
buffer_count=2
)
cam.configure(still_cfg)
cam.start()
# 複数リクエスト同時アクセスの衝突を防ぐためのロック
lock = threading.Lock()
# 終了時にカメラを確実に停止・解放
def _cleanup():
try:
cam.stop()
except Exception:
pass
try:
cam.close()
except Exception:
pass
atexit.register(_cleanup)
# ------------------------
# 画像取得エンドポイント
# ------------------------
@app.route("/capture")
def capture():
"""
現在のカメラ画像をJPEGで返す。
Picamera2 -> numpy配列 -> np.ascontiguousarray -> Pillow(JPEG) -> レスポンス
"""
try:
with lock:
# カメラからRGB配列を取得
arr = cam.capture_array("main")
# JPEGエンコードのために C-contiguous 化(ここが重要)
arr_c = np.ascontiguousarray(arr)
# PillowでJPEGにエンコード(メモリ上)
im = Image.fromarray(arr_c)
buf = io.BytesIO()
im.save(buf, format="JPEG", quality=90)
buf.seek(0)
# 応答
resp = make_response(buf.read())
resp.headers["Content-Type"] = "image/jpeg"
# キャッシュ回避(自動更新時に最新を取りやすくする)
resp.headers["Cache-Control"] = "no-store, max-age=0"
return resp
except Exception as e:
app.logger.exception("capture error: %s", e)
return ("capture failed", 500)
# ------------------------
# トップページ(自動更新UI)
# ------------------------
@app.route("/")
def index():
"""
3秒ごとに<img>のsrcを書き換えて最新画像を取得するページ。
URL末尾に ?t=タイムスタンプ を付けてブラウザキャッシュを回避。
"""
return """
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>📷 自動更新カメラ</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html,body { margin: 0; padding: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.wrap { padding: 24px; max-width: 900px; margin: 0 auto; }
img { max-width: 100%; height: auto; border: 1px solid #ccc; border-radius: 8px; }
.hint { color: #555; font-size: 0.95rem; }
code { background: #f5f5f5; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<div class="wrap">
<h1>📷 カメラ画像(3秒ごとに自動更新)</h1>
<p class="hint">
まず <a href="/capture" target="_blank">/capture</a> を直接開いて画像が出るか確認してください。<br>
画像が出れば、このページでも3秒ごとに最新画像へ切り替わります。
</p>
<img id="cam" src="/capture" alt="camera image">
<p class="hint">更新間隔を変えたい場合は、下の <code>setInterval</code> の数値(ミリ秒)を変更してください。</p>
</div>
<script>
// 3000ms = 3秒おきに最新画像へ置き換え
setInterval(function () {
const ts = Date.now(); // キャッシュ回避のためにタイムスタンプを付与
const img = document.getElementById("cam");
img.src = "/capture?t=" + ts;
}, 3000);
</script>
</body>
</html>
"""
# ------------------------
# 簡易ヘルスチェック
# ------------------------
@app.route("/health")
def health():
return "OK"
# ------------------------
# エントリポイント
# ------------------------
if __name__ == "__main__":
# debug=True はリローダでプロセスが2つ動き、カメラが競合する場合があるため False を推奨
app.run(host="0.0.0.0", port=5000, debug=False)
次回はこちら
Comming Soon