Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved TDoA outlier filter #1237

Merged
merged 9 commits into from
Mar 1, 2023
4 changes: 2 additions & 2 deletions src/modules/interface/kalman_core/mm_tdoa.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*
* Crazyflie control firmware
*
* Copyright (C) 2021 Bitcraze AB
* Copyright (C) 2023 Bitcraze AB
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -28,4 +28,4 @@
#include "kalman_core.h"

// Measurements of a UWB Tx/Rx
void kalmanCoreUpdateWithTDOA(kalmanCoreData_t* this, tdoaMeasurement_t *tdoa);
void kalmanCoreUpdateWithTDOA(kalmanCoreData_t* this, tdoaMeasurement_t *tdoa, const uint32_t nowMs);
3 changes: 2 additions & 1 deletion src/modules/interface/outlierFilter.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
* || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
*
* Copyright (C) 2011-2019 Bitcraze AB
* Copyright (C) 2011-2023 Bitcraze AB
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -29,6 +29,7 @@

bool outlierFilterValidateTdoaSimple(const tdoaMeasurement_t* tdoa);
bool outlierFilterValidateTdoaSteps(const tdoaMeasurement_t* tdoa, const float error, const vector_t* jacobian, const point_t* estPos);
bool outlierFilterValidateTdoaIntegrator(const tdoaMeasurement_t* tdoa, const float error, const uint32_t nowMs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a comment, with kalmanCoreUpdateWithTDOA we use all caps for TDOA, but here it is only Tdoa. Shouldn't all be TDOA for concistency?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make a separate PR for this, it might be too much of a rabbit hole for now.


typedef struct {
uint32_t openingTimeMs;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/src/estimator_kalman.c
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ static void updateQueuedMeasurements(const uint32_t nowMs, const bool quadIsFlyi
kalmanCoreRobustUpdateWithTDOA(&coreData, &m.data.tdoa);
}else{
// standard KF update
kalmanCoreUpdateWithTDOA(&coreData, &m.data.tdoa);
kalmanCoreUpdateWithTDOA(&coreData, &m.data.tdoa, nowMs);
}
break;
case MeasurementTypePosition:
Expand Down
12 changes: 11 additions & 1 deletion src/modules/src/kalman_core/mm_tdoa.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
// TODO krri What is this used for? Do we still need it?
TESTABLE_STATIC uint32_t tdoaCount = 0;

void kalmanCoreUpdateWithTDOA(kalmanCoreData_t* this, tdoaMeasurement_t *tdoa)
// Enable this to use the old step outlier filter
// This filter is deprecated and will be removed after October 2023
// #define USE_STEP_OUTLIER_FILTER 1


void kalmanCoreUpdateWithTDOA(kalmanCoreData_t* this, tdoaMeasurement_t *tdoa, const uint32_t nowMs)
{
if (tdoaCount >= 100)
{
Expand Down Expand Up @@ -71,6 +76,7 @@ void kalmanCoreUpdateWithTDOA(kalmanCoreData_t* this, tdoaMeasurement_t *tdoa)
h[KC_STATE_Y] = (dy1 / d1 - dy0 / d0);
h[KC_STATE_Z] = (dz1 / d1 - dz0 / d0);

#if USE_STEP_OUTLIER_FILTER
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be part of the kbuild config?

vector_t jacobian = {
.x = h[KC_STATE_X],
.y = h[KC_STATE_Y],
Expand All @@ -84,6 +90,10 @@ void kalmanCoreUpdateWithTDOA(kalmanCoreData_t* this, tdoaMeasurement_t *tdoa)
};

bool sampleIsGood = outlierFilterValidateTdoaSteps(tdoa, error, &jacobian, &estimatedPosition);
#else
bool sampleIsGood = outlierFilterValidateTdoaIntegrator(tdoa, error, nowMs);
#endif

if (sampleIsGood) {
kalmanCoreScalarUpdate(this, &H, error, tdoa->stdDev);
}
Expand Down
93 changes: 85 additions & 8 deletions src/modules/src/outlierFilter.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*
* Crazyflie control firmware
*
* Copyright (C) 2011-2018 Bitcraze AB
* Copyright (C) 2011-2023 Bitcraze AB
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -30,6 +30,12 @@
#include "log.h"
#include "debug.h"


static bool isDistanceDiffSmallerThanDistanceBetweenAnchors(const tdoaMeasurement_t* tdoa);


// The step TDoA outlier filter is deprecated and will be removed

#define BUCKET_ACCEPTANCE_LEVEL 3
#define MAX_BUCKET_FILL 10
#define FILTER_CLOSE_DELAY_COUNT 30
Expand All @@ -55,19 +61,12 @@ filterLevel_t filterLevels[FILTER_LEVELS] = {
};


static bool isDistanceDiffSmallerThanDistanceBetweenAnchors(const tdoaMeasurement_t* tdoa);
static float distanceSq(const point_t* a, const point_t* b);
static float sq(float a) {return a * a;}
static void addToBucket(filterLevel_t* filter);
static void removeFromBucket(filterLevel_t* filter);
static int updateBuckets(float errorDistance);



bool outlierFilterValidateTdoaSimple(const tdoaMeasurement_t* tdoa) {
return isDistanceDiffSmallerThanDistanceBetweenAnchors(tdoa);
}

bool outlierFilterValidateTdoaSteps(const tdoaMeasurement_t* tdoa, const float error, const vector_t* jacobian, const point_t* estPos) {
bool sampleIsGood = false;

Expand Down Expand Up @@ -102,6 +101,84 @@ bool outlierFilterValidateTdoaSteps(const tdoaMeasurement_t* tdoa, const float e
return sampleIsGood;
}

// Simple TDoA outlier filter
bool outlierFilterValidateTdoaSimple(const tdoaMeasurement_t* tdoa) {
return isDistanceDiffSmallerThanDistanceBetweenAnchors(tdoa);
}


// Integrator TDoA outlier filter -------------------------------------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comments! But perhaps it should be more according to doxygen convention and put it closer to the function. If we ever want to make doxygen comments for these, it might get lost

// This filter uses an "integrator" to track the ratio of good/samples. Samples with an errors (measurement - predicted)
// larger than the acceptance level will be discarded. The acceptance level is based on the integrator, with a
// hysteresis to avoid rapid changes. When the filter is open, all samples are let through, usually at startup to let
// the kalman filter converge. When the filter is closed, only samples with an error < the acceptance level, this should
// be most of the time.
// The acceptance level is based on the standard deviation used for the tdoa samples, which should be based on the
// noise level in the system.


// The maximum size of the integrator. This size determines the time [in ms] needed to open/close the filter.
static const float INTEGRATOR_SIZE = 300.0f;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should make a struct for these values and initialize the avalues when the kalman filter is initialize? What do you think? It will make it more suitable for wrapping and the bindings?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it would make it possible to use this filter for something else


// The level when the filter open up to let all samples through
static const float INTEGRATOR_FORCE_OPEN_LEVEL = INTEGRATOR_SIZE * 0.1f;

// The level when the filter closes again
static const float INTEGRATOR_RESUME_ACTION_LEVEL = INTEGRATOR_SIZE * 0.9f;

static float integrator;
static uint32_t latestUpdateMs;
static bool isFilterOpen = true;


bool outlierFilterValidateTdoaIntegrator(const tdoaMeasurement_t* tdoa, const float error, const uint32_t nowMs) {
// The accepted error when the filter is closed
const float acceptedDistance = tdoa->stdDev * 2.5f;

// The level used to determine if a sample is added or removed from the integrator
const float integratorTriggerDistance = tdoa->stdDev * 2.0f;


bool sampleIsGood = false;

// Discard samples that are physically impossible, most likely measurement error
if (isDistanceDiffSmallerThanDistanceBetweenAnchors(tdoa)) {
uint32_t dtMs = nowMs - latestUpdateMs;
// Limit dt to minimize the impact on the integrator if we have not received samples for a long time (or at start up)
dtMs = fminf(dtMs, INTEGRATOR_SIZE / 10.0f);

if (fabsf(error) < integratorTriggerDistance) {
integrator += dtMs;
integrator = fminf(integrator, INTEGRATOR_SIZE);
} else {
integrator -= dtMs;
integrator = fmaxf(integrator, 0.0f);
}

if (isFilterOpen) {
// The filter is open, let all samples through
sampleIsGood = true;

if (integrator > INTEGRATOR_RESUME_ACTION_LEVEL) {
// We have recovered and converged, close the filter again
isFilterOpen = false;
}
} else {
// The filter is closed, let samples with a small error through
sampleIsGood = (fabsf(error) < acceptedDistance);

if (integrator < INTEGRATOR_FORCE_OPEN_LEVEL) {
// We have got lots of outliers lately, the kalman filter may have diverged. Open up to try to recover
isFilterOpen = true;
}
}

latestUpdateMs = nowMs;
}

return sampleIsGood;
}


#define LH_MS_PER_FRAME (1000 / 120)
static const int32_t lhMinWindowTimeMs = -2 * LH_MS_PER_FRAME;
Expand Down
10 changes: 5 additions & 5 deletions test/modules/src/kalman_core/test_mm_tdoa.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ void testThatScalarUpdateIsCalledInSimpleCase() {
};

setKalmanCoreScalarUpdateExpectationsSingleCall(&this, expectedHm, expectedError, expectedStdMeasNoise);
outlierFilterValidateTdoaSteps_IgnoreAndReturn(true);
outlierFilterValidateTdoaIntegrator_IgnoreAndReturn(true);

// Test
kalmanCoreUpdateWithTDOA(&this, &measurement);
kalmanCoreUpdateWithTDOA(&this, &measurement, 0);

// Assert
assertScalarUpdateWasCalled();
Expand All @@ -77,7 +77,7 @@ void testThatSampleWhereDroneIsInSamePositionAsAnchorIsIgnored() {
};

// Test
kalmanCoreUpdateWithTDOA(&this, &measurement);
kalmanCoreUpdateWithTDOA(&this, &measurement, 0);

// Assert
assertScalarUpdateWasNotCalled();
Expand All @@ -99,10 +99,10 @@ void testThatScalarUpdateIsNotCalledWhenTheOutlierFilterIsBlocking() {
.stdDev = 0.123,
};

outlierFilterValidateTdoaSteps_IgnoreAndReturn(false);
outlierFilterValidateTdoaIntegrator_IgnoreAndReturn(false);

// Test
kalmanCoreUpdateWithTDOA(&this, &measurement);
kalmanCoreUpdateWithTDOA(&this, &measurement, 0);

// Assert
assertScalarUpdateWasNotCalled();
Expand Down