From abf08fc6ef0c52f5f641b2438627dff6bd19f9e9 Mon Sep 17 00:00:00 2001 From: nicoboy Date: Mon, 8 Jun 2026 17:00:56 +0200 Subject: [PATCH] init: camera server Flask MJPEG --- CLAUDE.md | 29 ++++++++++ camera_server.py | 145 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 3 files changed, 176 insertions(+) create mode 100644 CLAUDE.md create mode 100644 camera_server.py create mode 100644 requirements.txt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..746a794 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +# pi-test-cam — Contexte projet + +## Matériel +- Raspberry Pi Zero 2W (pi-wireclaw, 192.168.1.132) +- Caméra OV5647 connectée sur port CSI +- OS : Raspberry Pi OS Bookworm Lite (SSH uniquement) + +## Rôle +Serveur de test caméra pour le projet WireClaw (drone d'inspection industrielle). +En prod, le Pi Zero rejoindra le hotspot WireClaw (10.42.0.0/24) créé par le Jetson. + +## Stack +- Python 3, Flask, OpenCV +- Packages système apt (pas de virtualenv) +- Fichiers dans ~/pi-test-cam/ + +## Endpoints Flask (port 5000) +- GET /stream → flux MJPEG +- GET /snapshot → photo JPEG +- GET /settings → lire les réglages +- POST /settings → modifier brightness, contrast, saturation + +## Réseau WireClaw complet +- Jetson : 192.168.1.84 / 10.42.0.1 (FastAPI :8765, hotspot) +- ESP32 : 192.168.1.122 / 10.42.0.77 (MAVLink relais) +- Pi Zero : 192.168.1.132 / 10.42.0.XX (caméra) + +## Repo +git.syoul.fr/nicoboy/pi-test-cam \ No newline at end of file diff --git a/camera_server.py b/camera_server.py new file mode 100644 index 0000000..e879476 --- /dev/null +++ b/camera_server.py @@ -0,0 +1,145 @@ +from flask import Flask, Response, request, jsonify +import cv2 +import threading +import time +import numpy as np + +app = Flask(__name__) + +# Paramètres image modifiables à chaud +settings = { + "brightness": 0, # -100 à +100 + "contrast": 1.0, # 0.5 à 3.0 + "saturation": 1.0, # 0.0 à 3.0 +} + +frame_lock = threading.Lock() +current_frame = None + +def apply_settings(frame): + # Contrast + Brightness + frame = cv2.convertScaleAbs( + frame, + alpha=settings["contrast"], + beta=settings["brightness"] + ) + # Saturation via HSV + hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV).astype("float32") + hsv[:, :, 1] *= settings["saturation"] + hsv[:, :, 1] = np.clip(hsv[:, :, 1], 0, 255) + frame = cv2.cvtColor(hsv.astype("uint8"), cv2.COLOR_HSV2BGR) + return frame + +def capture_loop(): + global current_frame + # Sur Pi Zero avec OV5647, /dev/video0 + cam = cv2.VideoCapture(0) + cam.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) + cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) + cam.set(cv2.CAP_PROP_FPS, 15) + + if not cam.isOpened(): + print("ERREUR : impossible d'ouvrir la caméra") + return + + print("Caméra initialisée") + while True: + ok, frame = cam.read() + if ok: + frame = apply_settings(frame) + with frame_lock: + current_frame = frame.copy() + time.sleep(0.066) # ~15fps + +def generate_mjpeg(): + while True: + with frame_lock: + if current_frame is None: + time.sleep(0.1) + continue + _, jpeg = cv2.imencode( + ".jpg", current_frame, + [cv2.IMWRITE_JPEG_QUALITY, 70] + ) + yield ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n\r\n" + + jpeg.tobytes() + + b"\r\n" + ) + time.sleep(0.066) + +@app.route("/") +def index(): + return """ + + pi-test-cam + +

pi-test-cam

+

+ +
+ + + +
+
+ 📸 Snapshot + + + + + """ + +@app.route("/stream") +def stream(): + return Response( + generate_mjpeg(), + mimetype="multipart/x-mixed-replace; boundary=frame" + ) + +@app.route("/snapshot") +def snapshot(): + with frame_lock: + if current_frame is None: + return "Pas de frame disponible", 503 + _, jpeg = cv2.imencode( + ".jpg", current_frame, + [cv2.IMWRITE_JPEG_QUALITY, 95] + ) + return Response(jpeg.tobytes(), mimetype="image/jpeg") + +@app.route("/settings", methods=["GET", "POST"]) +def handle_settings(): + if request.method == "POST": + data = request.json + for k in ["brightness", "contrast", "saturation"]: + if k in data: + settings[k] = float(data[k]) + return jsonify({"ok": True, "settings": settings}) + return jsonify(settings) + +if __name__ == "__main__": + t = threading.Thread(target=capture_loop, daemon=True) + t.start() + time.sleep(2) + print("Serveur démarré sur http://0.0.0.0:5000") + app.run(host="0.0.0.0", port=5000, threaded=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..31bc9c0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +opencv-python \ No newline at end of file