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
|
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!")
|
||||||
|
|||||||
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Start the application
|
||||||
|
print("Starting app")
|
||||||
|
app.set_producer(RealtimeCSIProducer())
|
||||||
app.start()
|
app.start()
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user