init: camera server Flask MJPEG
This commit is contained in:
29
CLAUDE.md
Normal file
29
CLAUDE.md
Normal file
@ -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
|
||||
145
camera_server.py
Normal file
145
camera_server.py
Normal file
@ -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 """
|
||||
<html>
|
||||
<head><title>pi-test-cam</title></head>
|
||||
<body style="background:#111;color:#eee;font-family:monospace;padding:20px">
|
||||
<h2>pi-test-cam</h2>
|
||||
<img src="/stream" width="640" height="360"
|
||||
style="border:1px solid #444;border-radius:4px"><br><br>
|
||||
|
||||
<div style="display:flex;gap:20px;margin-top:10px">
|
||||
<label>Brightness
|
||||
<input type="range" min="-100" max="100" value="0"
|
||||
oninput="update('brightness', this.value)">
|
||||
</label>
|
||||
<label>Contrast
|
||||
<input type="range" min="50" max="300" value="100"
|
||||
oninput="update('contrast', this.value/100)">
|
||||
</label>
|
||||
<label>Saturation
|
||||
<input type="range" min="0" max="300" value="100"
|
||||
oninput="update('saturation', this.value/100)">
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
<a href="/snapshot" style="color:#4af">📸 Snapshot</a>
|
||||
|
||||
<script>
|
||||
function update(key, value) {
|
||||
fetch('/settings', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({[key]: parseFloat(value)})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@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)
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
flask
|
||||
opencv-python
|
||||
Reference in New Issue
Block a user