improve motion detection examples
This commit is contained in:
parent
6c33c8e37e
commit
7e983e0a64
@ -3,24 +3,48 @@ This example shows how to use the CSI framework to connect to a FeitCSI host and
|
||||
detect changes in the environment
|
||||
"""
|
||||
|
||||
from queue import Queue
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from where_fi.application import CSIApplication
|
||||
from where_fi.collection.ingest import RealtimeCSIProducer
|
||||
from where_fi.config import config
|
||||
from where_fi.visualise import server as visualise
|
||||
|
||||
# Connect to a FeitCSI host
|
||||
app = CSIApplication(visualise_raw=True)
|
||||
|
||||
# Register visualisations
|
||||
app.register_figure(
|
||||
"magn-diff",
|
||||
visualise.figures.PerAntennaFigure(
|
||||
[
|
||||
visualise.figures.SimpleLineChart(
|
||||
"Magnitude diff", "Subcarrier", "Magnitude"
|
||||
),
|
||||
],
|
||||
[lambda x: x],
|
||||
),
|
||||
)
|
||||
app.register_figure(
|
||||
"phase-diff",
|
||||
visualise.figures.PerAntennaFigure(
|
||||
[
|
||||
visualise.figures.SimpleLineChart("Phase diff", "Subcarrier", "Phase"),
|
||||
],
|
||||
[lambda x: x],
|
||||
),
|
||||
)
|
||||
|
||||
MAGN_THRESHOLD = 0.2
|
||||
PHASE_THRESHOLD = 0.02
|
||||
QUEUE_SIZE = 20
|
||||
|
||||
# Stores historical data for each receiving antenna, for each subcarrier
|
||||
historical: dict[tuple[int, int], Queue[np.complex64]] = {}
|
||||
|
||||
MAGN_THRESHOLD = 20
|
||||
PHASE_THRESHOLD = 0.5
|
||||
QUEUE_SIZE = 20
|
||||
historical = np.zeros(
|
||||
(QUEUE_SIZE, config.subcarriers, config.antennas.count, 1), dtype=np.complex64
|
||||
)
|
||||
sample_position = 0
|
||||
|
||||
|
||||
@app.on_process
|
||||
@ -34,39 +58,20 @@ def _(sample: npt.NDArray[np.complex64]) -> None:
|
||||
It is used to detect changes in the environment caused by motion, by comparing each
|
||||
entry in the matrix with a moving average
|
||||
"""
|
||||
change = False
|
||||
for antenna in range(sample.shape[1]):
|
||||
for subcarrier in range(sample.shape[0]):
|
||||
# Get the current subcarrier data
|
||||
current = sample[subcarrier, antenna, 0]
|
||||
global historical
|
||||
global sample_position
|
||||
|
||||
# Get the historical data for this antenna and subcarrier
|
||||
if (antenna, subcarrier) not in historical:
|
||||
historical[(antenna, subcarrier)] = Queue(maxsize=QUEUE_SIZE)
|
||||
historical_data = historical[(antenna, subcarrier)]
|
||||
historical[sample_position] = sample
|
||||
sample_position = (sample_position + 1) % QUEUE_SIZE
|
||||
|
||||
# Calculate the average of the historical data for this antenna and
|
||||
# subcarrier
|
||||
mean = np.mean(historical_data.queue)
|
||||
mean = np.mean(historical, axis=0)
|
||||
magn_diff = np.abs(mean - sample)
|
||||
phase_diff = np.abs(np.angle(mean) - np.angle(sample))
|
||||
|
||||
# If we have enough historical data, compare it with the current data
|
||||
if historical_data.full():
|
||||
historical_data.get()
|
||||
app.visualise_data(magn_diff, "magn-diff")
|
||||
app.visualise_data(phase_diff, "phase-diff")
|
||||
|
||||
# Add the current sample to the historical data
|
||||
historical_data.put(current)
|
||||
|
||||
# Compare the current data with the historical data
|
||||
if (
|
||||
np.abs(mean - current) > MAGN_THRESHOLD
|
||||
or np.abs(np.angle(mean) - np.angle(current)) > PHASE_THRESHOLD
|
||||
):
|
||||
print(
|
||||
f"Change detected in antenna {antenna}, subcarrier {subcarrier}: "
|
||||
f"magn: {np.abs(mean - current)}, angle: {np.abs(np.angle(mean) - np.angle(current))}"
|
||||
)
|
||||
change = True
|
||||
if change:
|
||||
if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any():
|
||||
print("Motion detected!")
|
||||
else:
|
||||
print("No motion detected!")
|
||||
|
||||
@ -1,24 +1,51 @@
|
||||
"""Motion Detection example
|
||||
This example shows how to use the CSI framework to connect to a FeitCSI host and
|
||||
detect changes in the environment
|
||||
|
||||
Once motion is detected, the application will log the change to Home Assistant
|
||||
through an HTTP request.
|
||||
"""
|
||||
|
||||
import os
|
||||
from queue import Queue
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import requests
|
||||
|
||||
from where_fi.application import CSIApplication
|
||||
from where_fi.collection.ingest import RealtimeCSIProducer
|
||||
from where_fi.config import config
|
||||
from where_fi.visualise import server as visualise
|
||||
|
||||
# Connect to a FeitCSI host
|
||||
producer = RealtimeCSIProducer()
|
||||
app = CSIApplication(producer)
|
||||
app = CSIApplication(visualise_raw=True)
|
||||
|
||||
# Register visualisations
|
||||
app.register_figure(
|
||||
"magn-diff",
|
||||
visualise.figures.PerAntennaFigure(
|
||||
[
|
||||
visualise.figures.SimpleLineChart(
|
||||
"Magnitude diff", "Subcarrier", "Magnitude"
|
||||
),
|
||||
],
|
||||
[lambda x: x],
|
||||
),
|
||||
)
|
||||
app.register_figure(
|
||||
"phase-diff",
|
||||
visualise.figures.PerAntennaFigure(
|
||||
[
|
||||
visualise.figures.SimpleLineChart("Phase diff", "Subcarrier", "Phase"),
|
||||
],
|
||||
[lambda x: x],
|
||||
),
|
||||
)
|
||||
|
||||
MAGN_THRESHOLD = 0.2
|
||||
PHASE_THRESHOLD = 0.02
|
||||
QUEUE_SIZE = 20
|
||||
|
||||
# Stores historical data for each receiving antenna, for each subcarrier
|
||||
historical = np.zeros(
|
||||
(QUEUE_SIZE, config.subcarriers, config.antennas.count, 1), dtype=np.complex64
|
||||
)
|
||||
sample_position = 0
|
||||
|
||||
|
||||
class HomeAssistantBinarySensor:
|
||||
@ -55,14 +82,8 @@ class HomeAssistantBinarySensor:
|
||||
requests.post(self.url, json=data, headers=self.headers)
|
||||
|
||||
|
||||
# Stores historical data for each receiving antenna, for each subcarrier
|
||||
historical: dict[tuple[int, int], Queue[np.complex64]] = {}
|
||||
sensor = HomeAssistantBinarySensor("motion_detector", "Room Motion Detector")
|
||||
|
||||
MAGN_THRESHOLD = 20
|
||||
PHASE_THRESHOLD = 0.5
|
||||
QUEUE_SIZE = 2000
|
||||
|
||||
|
||||
@app.on_process
|
||||
def _(sample: npt.NDArray[np.complex64]) -> None:
|
||||
@ -75,36 +96,28 @@ def _(sample: npt.NDArray[np.complex64]) -> None:
|
||||
It is used to detect changes in the environment caused by motion, by comparing each
|
||||
entry in the matrix with a moving average
|
||||
"""
|
||||
change = False
|
||||
for antenna in range(sample.shape[1]):
|
||||
for subcarrier in range(sample.shape[0]):
|
||||
# Get the current subcarrier data
|
||||
current = sample[subcarrier, antenna, 0]
|
||||
global sample_position
|
||||
|
||||
# Get the historical data for this antenna and subcarrier
|
||||
if (antenna, subcarrier) not in historical:
|
||||
historical[(antenna, subcarrier)] = Queue(maxsize=QUEUE_SIZE)
|
||||
historical_data = historical[(antenna, subcarrier)]
|
||||
historical[sample_position] = sample
|
||||
sample_position = (sample_position + 1) % QUEUE_SIZE
|
||||
|
||||
# Calculate the average of the historical data for this antenna and
|
||||
# subcarrier
|
||||
mean = np.mean(historical_data.queue)
|
||||
mean = np.mean(historical, axis=0)
|
||||
magn_diff = np.abs(mean - sample)
|
||||
phase_diff = np.abs(np.angle(mean) - np.angle(sample))
|
||||
|
||||
# If we have enough historical data, compare it with the current data
|
||||
if historical_data.full():
|
||||
historical_data.get()
|
||||
app.visualise_data(magn_diff, "magn-diff")
|
||||
app.visualise_data(phase_diff, "phase-diff")
|
||||
|
||||
# Add the current sample to the historical data
|
||||
historical_data.put(current)
|
||||
|
||||
# Compare the current data with the historical data
|
||||
if (
|
||||
np.abs(mean - current) > MAGN_THRESHOLD
|
||||
or np.abs(np.angle(mean) - np.angle(current)) > PHASE_THRESHOLD
|
||||
):
|
||||
change = True
|
||||
|
||||
sensor.update(change)
|
||||
if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any():
|
||||
print("Motion detected!")
|
||||
sensor.update(True)
|
||||
else:
|
||||
print("No motion detected!")
|
||||
sensor.update(False)
|
||||
|
||||
|
||||
app.start()
|
||||
if __name__ == "__main__":
|
||||
# Start the application
|
||||
print("Starting app")
|
||||
app.set_producer(RealtimeCSIProducer())
|
||||
app.start()
|
||||
|
||||
@ -222,7 +222,7 @@ class Preprocessor:
|
||||
case "bandpass":
|
||||
h_hat = self.bandpass(h_hat)
|
||||
|
||||
logger.info(f"CSI shape: {h_hat.shape}")
|
||||
logger.debug(f"CSI shape: {h_hat.shape}")
|
||||
|
||||
self._last_sample = h_hat
|
||||
|
||||
|
||||
@ -67,6 +67,10 @@ class Webapp:
|
||||
def add_data(
|
||||
self, dtype: figures.FigureId, new_data: npt.NDArray[np.complex128]
|
||||
) -> None:
|
||||
self.logger.debug(f"Adding data to figure server {dtype}")
|
||||
if dtype not in figures.all_figures:
|
||||
self.logger.error(f"Figure {dtype} not found")
|
||||
return
|
||||
updates = figures.all_figures[dtype].update(new_data)
|
||||
for fig_id, update in updates.items():
|
||||
for client in self.figure_server.clients.get(fig_id, []):
|
||||
@ -76,7 +80,7 @@ class Webapp:
|
||||
while self.active:
|
||||
self.logger.debug("Listening for data")
|
||||
try:
|
||||
data = data_queue.get(timeout=0.5)
|
||||
data = data_queue.get(timeout=0.1)
|
||||
self.add_data(data.dtype, data.data)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
@ -103,7 +103,28 @@ class PerSubcarrierFigure(SpecificFigure):
|
||||
return reduce((lambda a, b: a | b), updates)
|
||||
|
||||
|
||||
class PerAntennaFigure(SpecificFigure):
|
||||
class LabelledMultiLineChart(SpecificFigure):
|
||||
def __init__(
|
||||
self,
|
||||
figures: Sequence[SimpleLineChart],
|
||||
funcs: list[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]],
|
||||
labels: Sequence[str],
|
||||
) -> None:
|
||||
self.charts = figures
|
||||
self.figures = [figure.figure for figure in figures]
|
||||
self.funcs = funcs
|
||||
self.labels = labels
|
||||
|
||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
||||
data_by_antenna = new_data[:, :, 0].T
|
||||
updates = [
|
||||
figure.update(func(data_by_antenna), labels=self.labels)
|
||||
for func, figure in zip(self.funcs, self.charts, strict=True)
|
||||
]
|
||||
return reduce((lambda a, b: a | b), updates)
|
||||
|
||||
|
||||
class PerAntennaFigure(LabelledMultiLineChart):
|
||||
"""A figure that plots data for each antenna separately.
|
||||
|
||||
This allows creating multiple figures, each having one line per antenna.
|
||||
@ -118,18 +139,9 @@ class PerAntennaFigure(SpecificFigure):
|
||||
figures: Sequence[SimpleLineChart],
|
||||
funcs: list[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]],
|
||||
) -> None:
|
||||
self.charts = figures
|
||||
self.figures = [figure.figure for figure in figures]
|
||||
self.funcs = funcs
|
||||
|
||||
def update(self, new_data: npt.NDArray[np.complex128]) -> FigureUpdate:
|
||||
data_by_antenna = new_data[:, :, 0].T
|
||||
antenna_labels = [f"Antenna {i}" for i in range(data_by_antenna.shape[0])]
|
||||
updates = [
|
||||
figure.update(func(data_by_antenna), labels=antenna_labels)
|
||||
for func, figure in zip(self.funcs, self.charts, strict=True)
|
||||
]
|
||||
return reduce((lambda a, b: a | b), updates)
|
||||
super().__init__(
|
||||
figures, funcs, [f"Antenna {i + 1}" for i in range(config.antennas.count)]
|
||||
)
|
||||
|
||||
|
||||
class MusicEigenvalueHistogram(SpecificFigure):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user