addet test set

This commit is contained in:
marcinator_2 committed 2026-10-14 17:40:31 +02:00
1 parent d370f992ce
commit b0f35bf3cc
21 files changed
+1796 -183

No files matched your search

+58 -19
View File
@@ -13,14 +13,22 @@ from io import BytesIO
from PIL import Image
from .motion import MotionDetector
DEFAULT_CONFIG = {"enabled": False, "line": [[0.2, 0.55], [0.8, 0.55]],
DEFAULT_CONFIG = {"enabled": False, "method": "person_line", "line": [[0.2, 0.55], [0.8, 0.55]],
"direction": 1, "region": [0.1, 0.15, 0.9, 0.95]}
def validate_config(value):
# Migrate previous saved configurations and old clients without changing
# their enabled setting or requiring a new geometry setup.
if isinstance(value, dict) and 'method' not in value:
value = {**value, 'method': 'person_line'}
if set(value) != set(DEFAULT_CONFIG) or type(value["enabled"]) is not bool:
raise ValueError("Ungültige Erkennungseinstellungen")
if value['method'] not in ('person_line', 'motion_test'):
raise ValueError('Unbekannte Erkennungsmethode')
if type(value["direction"]) is not int or value["direction"] not in (-1, 1):
raise ValueError("Richtung muss -1 oder 1 sein")
line, region = value["line"], value["region"]
@@ -180,7 +188,7 @@ class DetectionService:
self.last_valid = None
self.started = self.clock()
self.fps = 0.
self.result = {"frame_number": 0, "people": [], "image_base64": None}
self.result = {"frame_number": 0, "people": [], "image_base64": None, "motion": None}
def reset_state(self):
self.revision += 1
@@ -211,7 +219,7 @@ class DetectionService:
self.reset_state()
self.started = self.clock()
self.last_valid = None
self.result = {"frame_number": 0, "people": [], "image_base64": None}
self.result = {"frame_number": 0, "people": [], "image_base64": None, "motion": None}
return self.get_config()
def get_config(self):
@@ -243,8 +251,10 @@ class DetectionService:
elif self.clock() - (self.last_valid if self.last_valid is not None else self.started) > 2:
self.fail("Keine gültige Erkennung seit mehr als zwei Sekunden; zurücksetzen")
def process(self, people, number, jpeg, captured_at, version):
def process(self, people, number, jpeg, captured_at, version, motion=None):
event_mode = None
event_method = None
event_region = None
with self.lock:
now = self.clock()
if self.stop_event.is_set():
@@ -261,17 +271,26 @@ class DetectionService:
if not self.fault:
self.error = None
image = Image.open(BytesIO(jpeg))
people, crossing = self.tracker.update(people, self.config, now, image.width/image.height)
self.result = {"frame_number": number, "people": people,
if self.config['method'] == 'motion_test':
if motion is None:
self.fail('Bewegungs-Test liefert kein Ergebnis; zurücksetzen')
return
people = []
crossing = motion['trigger']
else:
motion = None
people, crossing = self.tracker.update(people, self.config, now, image.width/image.height)
self.result = {"frame_number": number, "people": people, "motion": motion,
"image_base64": base64.b64encode(jpeg).decode("ascii")}
if not self.config["enabled"] or self.fault or self.controller.stopped:
self.armed = False
self.free_since = None
return
rx1, ry1, rx2, ry2 = self.config["region"]
# Auch unbestätigte Personenrahmen verhindern eine falsche Freigabe.
occupied = any(p["box"][0] <= rx2 and p["box"][2] >= rx1 and
p["box"][1] <= ry2 and p["box"][3] >= ry1 for p in people)
# In motion test mode, a quiet image is NOT proof of no visitors.
occupied = (not motion['ready'] or motion['moving']) if motion is not None else any(
p["box"][0] <= rx2 and p["box"][2] >= rx1 and
p["box"][1] <= ry2 and p["box"][3] >= ry1 for p in people)
if occupied:
self.free_since = None
else:
@@ -288,17 +307,22 @@ class DetectionService:
self.free_since = None
if accepted:
event_mode = self.controller.last_event["mode"]
event_method = self.config['method']
event_region = list(self.config['region'])
# SQLite darf UI und unabhängigen Watchdog nicht blockieren.
if event_mode is not None:
try:
self.events.record_detection_trigger(event_mode)
if event_method == 'motion_test':
self.events.record_motion_trigger(event_mode, jpeg=jpeg, region=event_region)
else:
self.events.record_detection_trigger(event_mode, jpeg=jpeg, region=event_region)
except Exception as exc:
self.fail(f"Ereignis nicht gespeichert: {exc}")
self.fail(f"Ereignis/Bild nicht gespeichert: {exc}")
def status(self):
with self.lock:
age = None if self.last_valid is None else self.clock()-self.last_valid
return {"enabled": self.config["enabled"], "ready": age is not None and age <= 2 and not self.error,
return {"enabled": self.config["enabled"], "method": self.config["method"], "ready": age is not None and age <= 2 and not self.error,
"error": self.config_error or self.error, "reset_required": self.fault,
"armed": self.armed and not self.fault and not self.controller.stopped
and self.controller.generation == self.generation
@@ -309,7 +333,7 @@ class DetectionService:
with self.lock:
status = self.status()
fresh = status["ready"]
return {**self.result, "people": self.result["people"] if fresh else [],
return {**self.result, "motion": self.result["motion"] if fresh else None, "people": self.result["people"] if fresh else [],
"image_base64": self.result["image_base64"] if fresh else None,
"status": status}
@@ -325,27 +349,42 @@ class DetectionService:
self.thread.join(timeout=3)
def _run(self):
detector, last_number, last_completed = None, 0, None
detector, detector_key, last_number, last_completed = None, None, 0, None
while not self.stop_event.is_set():
began = self.clock()
version = None
try:
if detector is None:
detector = self.factory(self.model_path)
with self.lock:
if self.controller.generation != self.generation:
self.reset_state()
version = self.revision
config = self.get_config()
# A fresh motion baseline is required for every reconfiguration,
# controller change and reset. Never reuse pre-change optical flow.
key = (config['method'], version)
if detector is None or detector_key != key:
detector = MotionDetector() if config['method'] == 'motion_test' else self.factory(self.model_path)
detector_key = key
last_completed = None
number, jpeg, captured_at = self.camera.latest_timed()
if jpeg is not None and number != last_number:
last_number = number
people = detector.detect(jpeg)
self.process(people, number, jpeg, captured_at, version)
if config['method'] == 'motion_test':
motion = detector.detect(jpeg, config, captured_at)
self.process([], number, jpeg, captured_at, version, motion=motion)
else:
people = detector.detect(jpeg)
self.process(people, number, jpeg, captured_at, version)
completed = self.clock()
with self.lock:
self.fps = 0. if last_completed is None else 1/max(0.001, completed-last_completed)
last_completed = completed
except Exception as exc:
self.fail(f"Personenerkennung nicht verfügbar: {exc}")
# An old in-flight model failure must not fault a newly selected
# method. A current failure still uses the usual reset latch.
with self.lock:
if version == self.revision and self.controller.generation == self.generation:
self.fail(f"Erkennung nicht verfügbar: {exc}")
detector = None
self.stop_event.wait(1)
self.stop_event.wait(max(0, 0.2-(self.clock()-began)))