mash-sensor-tof-pc/src/sensor/people_counter.py

190 lines
6.6 KiB
Python
Raw Normal View History

from typing import Dict
2021-11-03 00:37:17 +01:00
from sensor.tof_sensor import ToFSensor, Directions
2021-10-07 01:37:48 +02:00
from datetime import datetime
2021-12-03 16:48:26 +01:00
import threading
2021-10-07 01:37:48 +02:00
COUNTING_CB = "counting"
TRIGGER_CB = "trigger"
CHANGE_CB = "changes"
2021-12-13 18:15:48 +01:00
START_TIME = "start_time"
END_TIME = "end_time"
TRIGGER_DISTANCES = "trigger_distances"
END_DISTANCE = "end_distance"
2021-10-07 01:37:48 +02:00
class PeopleCounter ():
def __init__(self, sensor: ToFSensor) -> None:
self.sensor = sensor
self.callbacks = {COUNTING_CB: [], TRIGGER_CB: [], CHANGE_CB: []}
2021-10-07 01:37:48 +02:00
self.maxTriggerDistance = 120 # In cm
def hookCounting(self, cb) -> None:
self.callbacks[COUNTING_CB].append(cb)
def unhookCounting(self, cb) -> None:
self.callbacks[COUNTING_CB].remove(cb)
def hookTrigger(self, cb) -> None:
self.callbacks[TRIGGER_CB].append(cb)
def unhookTrigger(self, cb) -> None:
self.callbacks[TRIGGER_CB].remove(cb)
def hookChange(self, cb) -> None:
self.callbacks[CHANGE_CB].append(cb)
def unhookChange(self, cb) -> None:
self.callbacks[CHANGE_CB].remove(cb)
def getInitialDirectionState(self) -> Dict:
return {
Directions.INSIDE: [],
Directions.OUTSIDE: []
}
2021-10-07 01:37:48 +02:00
def run(self) -> None:
self.keepRunning = True
direction = Directions.INSIDE
self.directionState = self.getInitialDirectionState()
2021-10-07 01:37:48 +02:00
self.sensor.open()
2021-10-07 01:37:48 +02:00
while self.keepRunning:
# Switch to other direction
direction: Directions = Directions.other(direction)
self.sensor.setDirection(direction)
distance: float = self.sensor.getDistance()
2021-12-13 18:15:48 +01:00
changed: bool = self.updateState(direction, distance)
2021-10-07 01:37:48 +02:00
if changed:
countChange: int = self.getCountChange(self.directionState)
# Hooks
2021-12-03 18:37:14 +01:00
th = threading.Thread(target=self.handleCallbacks, args=(countChange,))
2021-12-03 18:33:20 +01:00
th.start()
2021-10-07 12:33:00 +02:00
2021-12-03 20:38:48 +01:00
# Reset state if state is finalised
if not self.isDirectionTriggered(Directions.INSIDE) and not self.isDirectionTriggered(Directions.OUTSIDE):
2021-12-03 18:03:41 +01:00
self.directionState = self.getInitialDirectionState()
self.sensor.close()
2021-10-07 01:37:48 +02:00
def getCountChange(self, directionState) -> int:
# Is valid?
for direction in Directions:
# Is there at least one record for every direction?
2021-12-03 17:39:38 +01:00
if len(directionState[direction]) <= 0:
return 0
# Did every record start and end?
if directionState[direction][0][START_TIME] is None or directionState[direction][-1][END_TIME] is None:
2021-10-07 01:37:48 +02:00
return 0 # Return no change if not valid
# Get times into variables
insideStart = directionState[Directions.INSIDE][0][START_TIME]
insideEnd = directionState[Directions.INSIDE][-1][END_TIME]
outsideStart = directionState[Directions.OUTSIDE][0][START_TIME]
outsideEnd = directionState[Directions.OUTSIDE][-1][END_TIME]
2021-10-07 01:37:48 +02:00
# In what direction is the doorframe entered and left?
# Entering doorframe in the inside direction
enteringInside: bool = outsideStart < insideStart
# Leaving dooframe in the inside direction
leavingInside: bool = outsideEnd < insideEnd
# They have to be the same, otherwise they switch directions in between
if enteringInside != leavingInside:
# Someone did not go all the way
# Either
# Inside -######-
# Outside ---##---
# or
# Inside ---##---
# Outside -######-
return 0
# Are those times overlapping or disjunct?
if insideEnd < outsideStart or outsideEnd < insideStart:
# They are disjunct
# Either
# Inside -##-----
# Outside -----##-
# or
# Inside -----##-
# Outside -##-----
return 0
# What direction is the person taking?
if enteringInside:
# Entering the inside
# Inside ---####-
# Outside -####---
return 1
else:
# Leaving the inside
# Inside -####---
# Outside ---####-
return -1
def isTriggerDistance(self, distance: float) -> bool:
#! TODO: Should be based on the distance from the ground, not them the sensor
return distance <= self.maxTriggerDistance
2021-12-03 18:33:20 +01:00
def handleCallbacks(self, countChange: int):
self.handleChangeCallbacks(countChange)
self.handleCountingCallbacks(countChange)
self.handleTriggerCallbacks()
2021-10-07 01:37:48 +02:00
2021-12-03 16:43:36 +01:00
def handleCountingCallbacks(self, countChange: int) -> None:
# Only notify counting on actual count change
2021-10-07 01:37:48 +02:00
if countChange == 0:
return
for cb in self.callbacks[COUNTING_CB]:
2021-12-03 18:33:20 +01:00
cb(countChange)
2021-10-07 01:37:48 +02:00
def handleTriggerCallbacks(self) -> None:
triggerState = {
2021-12-03 20:38:48 +01:00
Directions.INSIDE: self.isDirectionTriggered(Directions.INSIDE),
Directions.OUTSIDE: self.isDirectionTriggered(Directions.OUTSIDE)
}
for cb in self.callbacks[TRIGGER_CB]:
2021-12-03 18:33:20 +01:00
cb(triggerState)
2021-12-03 16:43:36 +01:00
def handleChangeCallbacks(self, countChange: int) -> None:
for cb in self.callbacks[CHANGE_CB]:
2021-12-03 18:33:20 +01:00
cb(countChange, self.directionState)
2021-12-03 20:38:48 +01:00
def isDirectionTriggered(self, direction: Directions) -> bool:
2021-12-03 21:11:57 +01:00
return len(self.directionState[direction]) > 0 and self.directionState[direction][-1][END_TIME] is None
2021-12-03 16:43:36 +01:00
2021-12-13 18:15:48 +01:00
def updateState(self, direction: Directions, distance: float) -> bool:
triggered: bool = self.isTriggerDistance(distance)
2021-12-03 18:21:09 +01:00
previouslyTriggered = False
if len(self.directionState[direction]) > 0:
2021-12-03 18:21:09 +01:00
previouslyTriggered = self.directionState[direction][-1][END_TIME] is None
2021-10-07 01:37:48 +02:00
2021-12-03 18:21:09 +01:00
if triggered and not previouslyTriggered:
2021-10-07 01:37:48 +02:00
# Set as new beginning for this direction
self.directionState[direction].append({
START_TIME: datetime.now(),
2021-12-13 18:15:48 +01:00
END_TIME: None,
TRIGGER_DISTANCES: [distance],
END_DISTANCE: None
})
2021-10-07 01:37:48 +02:00
return True
2021-12-03 18:21:09 +01:00
elif not triggered and previouslyTriggered:
# Set as end for this direction
self.directionState[direction][-1][END_TIME] = datetime.now()
2021-12-13 18:15:48 +01:00
self.directionState[direction][-1][END_DISTANCE] = distance
2021-10-07 01:37:48 +02:00
return True
2021-12-13 18:15:48 +01:00
elif previouslyTriggered:
# Add distance at least
self.directionState[direction][-1][TRIGGER_DISTANCES].append(distance)
2021-10-07 01:37:48 +02:00
return False