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

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

Follow me!

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

PAGE TOP