improve motion detection examples

This commit is contained in:
Christos Falas 2025-05-16 00:41:31 +01:00
parent 6c33c8e37e
commit 7e983e0a64
No known key found for this signature in database
5 changed files with 126 additions and 92 deletions

View File

@ -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 detect changes in the environment
""" """
from queue import Queue
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
from where_fi.application import CSIApplication from where_fi.application import CSIApplication
from where_fi.collection.ingest import RealtimeCSIProducer 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 # Connect to a FeitCSI host
app = CSIApplication(visualise_raw=True) 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 # Stores historical data for each receiving antenna, for each subcarrier
historical: dict[tuple[int, int], Queue[np.complex64]] = {} historical = np.zeros(
(QUEUE_SIZE, config.subcarriers, config.antennas.count, 1), dtype=np.complex64
MAGN_THRESHOLD = 20 )
PHASE_THRESHOLD = 0.5 sample_position = 0
QUEUE_SIZE = 20
@app.on_process @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 It is used to detect changes in the environment caused by motion, by comparing each
entry in the matrix with a moving average entry in the matrix with a moving average
""" """
change = False global historical
for antenna in range(sample.shape[1]): global sample_position
for subcarrier in range(sample.shape[0]):
# Get the current subcarrier data
current = sample[subcarrier, antenna, 0]
# Get the historical data for this antenna and subcarrier historical[sample_position] = sample
if (antenna, subcarrier) not in historical: sample_position = (sample_position + 1) % QUEUE_SIZE
historical[(antenna, subcarrier)] = Queue(maxsize=QUEUE_SIZE)
historical_data = historical[(antenna, subcarrier)]
# Calculate the average of the historical data for this antenna and mean = np.mean(historical, axis=0)
# subcarrier magn_diff = np.abs(mean - sample)
mean = np.mean(historical_data.queue) phase_diff = np.abs(np.angle(mean) - np.angle(sample))
# If we have enough historical data, compare it with the current data app.visualise_data(magn_diff, "magn-diff")
if historical_data.full(): app.visualise_data(phase_diff, "phase-diff")
historical_data.get()
# Add the current sample to the historical data if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any():
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:
print("Motion detected!") print("Motion detected!")
else: else:
print("No motion detected!") print("No motion detected!")

View File

@ -1,24 +1,51 @@
"""Motion Detection example """Motion Detection example
This example shows how to use the CSI framework to connect to a FeitCSI host and This example shows how to use the CSI framework to connect to a FeitCSI host and
detect changes in the environment 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 as np
import numpy.typing as npt import numpy.typing as npt
import requests import requests
from where_fi.application import CSIApplication from where_fi.application import CSIApplication
from where_fi.collection.ingest import RealtimeCSIProducer 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 # Connect to a FeitCSI host
producer = RealtimeCSIProducer() app = CSIApplication(visualise_raw=True)
app = CSIApplication(producer)
# 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: class HomeAssistantBinarySensor:
@ -55,14 +82,8 @@ class HomeAssistantBinarySensor:
requests.post(self.url, json=data, headers=self.headers) 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") sensor = HomeAssistantBinarySensor("motion_detector", "Room Motion Detector")
MAGN_THRESHOLD = 20
PHASE_THRESHOLD = 0.5
QUEUE_SIZE = 2000
@app.on_process @app.on_process
def _(sample: npt.NDArray[np.complex64]) -> None: 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 It is used to detect changes in the environment caused by motion, by comparing each
entry in the matrix with a moving average entry in the matrix with a moving average
""" """
change = False global sample_position
for antenna in range(sample.shape[1]):
for subcarrier in range(sample.shape[0]):
# Get the current subcarrier data
current = sample[subcarrier, antenna, 0]
# Get the historical data for this antenna and subcarrier historical[sample_position] = sample
if (antenna, subcarrier) not in historical: sample_position = (sample_position + 1) % QUEUE_SIZE
historical[(antenna, subcarrier)] = Queue(maxsize=QUEUE_SIZE)
historical_data = historical[(antenna, subcarrier)]
# Calculate the average of the historical data for this antenna and mean = np.mean(historical, axis=0)
# subcarrier magn_diff = np.abs(mean - sample)
mean = np.mean(historical_data.queue) phase_diff = np.abs(np.angle(mean) - np.angle(sample))
# If we have enough historical data, compare it with the current data app.visualise_data(magn_diff, "magn-diff")
if historical_data.full(): app.visualise_data(phase_diff, "phase-diff")
historical_data.get()
# Add the current sample to the historical data if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any():
historical_data.put(current) print("Motion detected!")
sensor.update(True)
# Compare the current data with the historical data else:
if ( print("No motion detected!")
np.abs(mean - current) > MAGN_THRESHOLD sensor.update(False)
or np.abs(np.angle(mean) - np.angle(current)) > PHASE_THRESHOLD
):
change = True
sensor.update(change)
app.start() if __name__ == "__main__":
# Start the application
print("Starting app")
app.set_producer(RealtimeCSIProducer())
app.start()

View File

@ -222,7 +222,7 @@ class Preprocessor:
case "bandpass": case "bandpass":
h_hat = self.bandpass(h_hat) 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 self._last_sample = h_hat

View File

@ -67,6 +67,10 @@ class Webapp:
def add_data( def add_data(
self, dtype: figures.FigureId, new_data: npt.NDArray[np.complex128] self, dtype: figures.FigureId, new_data: npt.NDArray[np.complex128]
) -> None: ) -> 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) updates = figures.all_figures[dtype].update(new_data)
for fig_id, update in updates.items(): for fig_id, update in updates.items():
for client in self.figure_server.clients.get(fig_id, []): for client in self.figure_server.clients.get(fig_id, []):
@ -76,7 +80,7 @@ class Webapp:
while self.active: while self.active:
self.logger.debug("Listening for data") self.logger.debug("Listening for data")
try: try:
data = data_queue.get(timeout=0.5) data = data_queue.get(timeout=0.1)
self.add_data(data.dtype, data.data) self.add_data(data.dtype, data.data)
except queue.Empty: except queue.Empty:
pass pass

View File

@ -103,7 +103,28 @@ class PerSubcarrierFigure(SpecificFigure):
return reduce((lambda a, b: a | b), updates) 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. """A figure that plots data for each antenna separately.
This allows creating multiple figures, each having one line per antenna. This allows creating multiple figures, each having one line per antenna.
@ -118,18 +139,9 @@ class PerAntennaFigure(SpecificFigure):
figures: Sequence[SimpleLineChart], figures: Sequence[SimpleLineChart],
funcs: list[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]], funcs: list[Callable[[npt.NDArray[Any]], npt.NDArray[Any]]],
) -> None: ) -> None:
self.charts = figures super().__init__(
self.figures = [figure.figure for figure in figures] figures, funcs, [f"Antenna {i + 1}" for i in range(config.antennas.count)]
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)
class MusicEigenvalueHistogram(SpecificFigure): class MusicEigenvalueHistogram(SpecificFigure):