Private
Public Access
79 lines
3.6 KiB
Python
79 lines
3.6 KiB
Python
"""Optischer Bewegungs-Test, ohne Personenmodell oder Besucherzuordnung."""
|
|
|
|
from io import BytesIO
|
|
|
|
from PIL import Image
|
|
|
|
|
|
class MotionDetector:
|
|
"""Bildbewegung im Bereich entlang der sichtbaren Pfeilnormalen prüfen.
|
|
|
|
Feste, konservative Startwerte für den Nahbereichstest, keine Zusage für
|
|
Außenbetrieb. Nur zwei aufeinanderfolgende gerichtete Beobachtungen lösen
|
|
aus. Ein ruhiges Bild bedeutet hier keine Personen-/Bereichsfreiheit.
|
|
"""
|
|
|
|
def __init__(self):
|
|
import cv2
|
|
import numpy as np
|
|
self.cv, self.np = cv2, np
|
|
self.previous = None
|
|
self.previous_at = None
|
|
self.directed_count = 0
|
|
|
|
def detect(self, jpeg, config, captured_at):
|
|
cv, np = self.cv, self.np
|
|
image = Image.open(BytesIO(jpeg)).convert('L')
|
|
scale = min(1., 240 / max(image.size))
|
|
size = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
|
|
gray = np.asarray(image.resize(size, Image.Resampling.BILINEAR))
|
|
height, width = gray.shape
|
|
x1, y1, x2, y2 = config['region']
|
|
region = gray[round(y1*height):round(y2*height), round(x1*width):round(x2*width)]
|
|
if min(region.shape) < 8:
|
|
raise ValueError('Bewegungs-Test: Bereich im Kamerabild zu klein')
|
|
previous, previous_at = self.previous, self.previous_at
|
|
self.previous = region.copy()
|
|
self.previous_at = captured_at
|
|
result = {'ready': False, 'moving': False, 'directed': False,
|
|
'trigger': False, 'vector': [0., 0.], 'changed_fraction': 0.,
|
|
'confirmed_frames': 0, 'note': 'Neue Bildbasis; kurz ruhig halten'}
|
|
if (previous is None or previous.shape != region.shape or
|
|
captured_at <= previous_at or captured_at - previous_at > 0.8):
|
|
self.directed_count = 0
|
|
return result
|
|
result['ready'] = True
|
|
changed = cv.absdiff(previous, region) > 12
|
|
changed_fraction = float(changed.mean())
|
|
result['changed_fraction'] = round(changed_fraction, 5)
|
|
# Small near-camera motions must not be diluted by a large quiet ROI.
|
|
result['moving'] = changed_fraction >= 0.001
|
|
if not result['moving']:
|
|
result['note'] = 'Bild ruhig / Änderung zu klein'
|
|
self.directed_count = 0
|
|
return result
|
|
flow = cv.calcOpticalFlowFarneback(previous, region, None,
|
|
0.5, 3, 15, 3, 5, 1.2, 0)
|
|
magnitude = np.linalg.norm(flow, axis=2)
|
|
# Only changed pixels with meaningful, bounded displacement vote.
|
|
mask = changed & (magnitude >= 0.35) & (magnitude <= 30)
|
|
vectors = flow[mask]
|
|
if len(vectors) < 6:
|
|
result['note'] = 'Bildänderung erkannt; Richtung noch nicht messbar'
|
|
self.directed_count = 0
|
|
return result
|
|
a, b = config['line']
|
|
dx, dy = (b[0]-a[0])*width, (b[1]-a[1])*height
|
|
normal = np.array([-dy, dx], dtype=np.float32)
|
|
normal *= config['direction'] / np.linalg.norm(normal)
|
|
projection = vectors @ normal
|
|
aligned = (projection >= 0.35) & (projection / np.linalg.norm(vectors, axis=1) >= 0.65)
|
|
result['vector'] = [round(float(v), 2) for v in np.median(vectors, axis=0)]
|
|
result['directed'] = bool(aligned.mean() >= 0.65)
|
|
self.directed_count = self.directed_count + 1 if result['directed'] else 0
|
|
result['confirmed_frames'] = min(2, self.directed_count)
|
|
result['note'] = ('Bewegung in Pfeilrichtung' if result['directed']
|
|
else 'Bewegung nicht in Pfeilrichtung')
|
|
result['trigger'] = self.directed_count >= 2
|
|
return result
|