前回はファイルの保存とグラフの作成について学びました。
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ページのHTMLapp.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:8000PCやスマホを同じネットワークにつなぎ(モバイルホットスポットやデザリングなどで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
def main():
cam = Picamera2()
# 4:3 のプレビュー設定
preview_cfg = cam.create_preview_configuration(main={"size": (640, 480)})
cam.configure(preview_cfg)
cam.start()
time.sleep(0.5)
# センサー全体を使う(デジタルズーム OFF)
try:
sw, sh = cam.camera_properties["PixelArraySize"] # 例: 2592x1944
cam.set_controls({"ScalerCrop": (0, 0, int(sw), int(sh))})
except:
pass
# 撮影(画面表示なし / 自動露出を安定させて保存)
time.sleep(1.0) # 少し待つと色合いが安定
cam.capture_file("photo.jpg")
print("Saved: photo.jpg")
cam.stop()
if __name__ == "__main__":
main()実行すると、プログラムと同じフォルダ内に 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, redirect, url_for
from picamera2 import Picamera2
from pathlib import Path
import time, threading
app = Flask(__name__, static_folder="static", static_url_path="/static")
STATIC_DIR = Path(__file__).parent / "static"
STATIC_DIR.mkdir(exist_ok=True)
LOCK = threading.Lock()
@app.get("/")
def index():
return send_from_directory("static", "index.html", max_age=0)
@app.get("/capture")
def capture():
with LOCK:
cam = Picamera2()
try:
cam.configure(cam.create_preview_configuration(main={"size": (640, 480)}))
cam.start()
time.sleep(0.6) # 露出/WB安定
# ★デジタルズーム解除
try:
sw, sh = cam.camera_properties["PixelArraySize"]
cam.set_controls({"ScalerCrop": (0, 0, int(sw), int(sh))})
except Exception:
# 取得できない環境でも動くようフォールバック
cam.set_controls({"ScalerCrop": (0, 0, 1600, 1200)})
# 稀に反映が遅れる環境向け
time.sleep(0.2)
# プレビュー構成のまま撮る
cam.capture_file(str(STATIC_DIR / "capture.jpg"))
finally:
cam.stop(); cam.close()
# キャッシュ回避のためタイムスタンプを付与してトップへ
return redirect(url_for("index", t=int(time.time())))
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=1696600000HTML の <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, threading, atexit, time, logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
# --- カメラ初期化(4:3 + RGB888 = 3ch固定でアルファ混入を防ぐ)---
cam = Picamera2()
preview_cfg = cam.create_preview_configuration(
main={"size": (1280, 960), "format": "RGB888"} # ★ここが重要
)
cam.configure(preview_cfg)
cam.start()
time.sleep(0.5)
try:
sw, sh = cam.camera_properties["PixelArraySize"]
cam.set_controls({"ScalerCrop": (0, 0, int(sw), int(sh))})
except Exception:
pass
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():
try:
with lock:
arr = cam.capture_array("main") # 想定: RGB(3ch)。環境により4chになる場合あり
# --- 保険:4ch(RGBA/BGRA)で来た場合はRGBへ変換 ---
if arr.ndim == 3 and arr.shape[2] == 4:
# BGRAの可能性が高いのでまずBGRA→RGBを試す
try:
import cv2
arr = cv2.cvtColor(arr, cv2.COLOR_BGRA2RGB)
except Exception:
# OpenCVが使えない環境でもPillowでアルファを落とす
im = Image.fromarray(arr)
im = im.convert("RGB") # RGBAでもBGRAでもアルファを除去できる
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
# --- 通常ルート:RGB(3ch)をJPEG化 ---
im = Image.fromarray(np.ascontiguousarray(arr)).convert("RGB")
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)
@app.route("/")
def index():
return """
<!doctype html>
<html lang="ja"><head>
<meta charset="utf-8"><title>📷 自動更新カメラ(4:3, non-zoom)</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:.95rem}
code{background:#f5f5f5;padding:2px 6px;border-radius:4px}
</style></head><body>
<div class="wrap">
<h1>📷 カメラ画像(3秒ごと自動更新 / 4:3)</h1>
<p class="hint">まず <a href="/capture" target="_blank">/capture</a> で単体動作を確認してください。</p>
<img id="cam" src="/capture" alt="camera image">
<p class="hint">更新間隔は下の <code>setInterval</code>(ミリ秒)で調整できます。</p>
</div>
<script>
setInterval(()=>{document.getElementById("cam").src="/capture?t="+Date.now()},3000);
</script>
</body></html>
"""
@app.route("/health")
def health():
return "OK"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)次回はこちら

