Python Raspbeyyi Pi プログラム 回路 電子工作 高専3年生

【Raspberry Pi】電子工作入門③~カメラとWebサーバ~【Python】

前回はファイルの保存とグラフの作成について学びました。

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

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=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, 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)

次回はこちら

Follow me!

-Python, Raspbeyyi Pi, プログラム, 回路, 電子工作, 高専3年生
-, , , , ,

PAGE TOP