LCOV - code coverage report
Current view: top level - AP_OpticalFlow - AP_OpticalFlow_Calibrator.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 105 133 78.9 %
Date: 2026-09-09 11:02:08 Functions: 12 12 100.0 %

          Line data    Source code
       1             : /*
       2             :    This program is free software: you can redistribute it and/or modify
       3             :    it under the terms of the GNU General Public License as published by
       4             :    the Free Software Foundation, either version 3 of the License, or
       5             :    (at your option) any later version.
       6             : 
       7             :    This program is distributed in the hope that it will be useful,
       8             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
       9             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      10             :    GNU General Public License for more details.
      11             : 
      12             :    You should have received a copy of the GNU General Public License
      13             :    along with this program.  If not, see <http://www.gnu.org/licenses/>.
      14             : */
      15             : 
      16             : #include "AP_OpticalFlow_config.h"
      17             : 
      18             : #if AP_OPTICALFLOW_CALIBRATOR_ENABLED
      19             : 
      20             : #include "AP_OpticalFlow_Calibrator.h"
      21             : #include <AP_InternalError/AP_InternalError.h>
      22             : 
      23             : #include <GCS_MAVLink/GCS.h>
      24             : #include <AP_Logger/AP_Logger.h>
      25             : #include <AP_AHRS/AP_AHRS.h>
      26             : 
      27             : const uint32_t AP_OPTICALFLOW_CAL_TIMEOUT_SEC = 120;        // calibration timesout after 120 seconds
      28             : const uint32_t AP_OPTICALFLOW_CAL_STATUSINTERVAL_SEC = 3;   // status updates printed at 3 second intervals
      29             : const float AP_OPTICALFLOW_CAL_YAW_MAX_RADS = radians(20);  // maximum yaw rotation (must be low to ensure good scaling)
      30             : const float AP_OPTICALFLOW_CAL_ROLLPITCH_MIN_RADS = radians(20);    // minimum acceptable roll or pitch rotation
      31             : const float AP_OPTICALFLOW_CAL_SCALE_MIN = 0.20f;           // min acceptable scaling value.  If resulting scaling is below this then the calibration fails
      32             : const float AP_OPTICALFLOW_CAL_SCALE_MAX = 4.0f;            // max acceptable scaling value.  If resulting scaling is above this then the calibration fails
      33             : const float AP_OPTICALFLOW_CAL_FITNESS_THRESH = 0.5f;       // min acceptable fitness
      34             : const float AP_OPTICALFLOW_CAL_RMS_FAILED = 1.0e30f;        // calc_mean_squared_residuals returns this value when it fails to calculate a good value
      35             : 
      36             : extern const AP_HAL::HAL& hal;
      37             : 
      38             : // start the calibration
      39           1 : void AP_OpticalFlow_Calibrator::start()
      40             : {
      41             :     // exit immediately if already running
      42           1 :     if (_cal_state == CalState::RUNNING) {
      43           0 :         return;
      44             :     }
      45             : 
      46           1 :     _cal_state = CalState::RUNNING;
      47           1 :     _start_time_ms = AP_HAL::millis();
      48             : 
      49             :     // clear samples buffers
      50           1 :     _cal_data[0].num_samples = 0;
      51           1 :     _cal_data[1].num_samples = 0;
      52             : 
      53           1 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FlowCal: Started");
      54             : }
      55             : 
      56           1 : void AP_OpticalFlow_Calibrator::stop()
      57             : {
      58             :     // exit immediately if already stopped
      59           1 :     if (_cal_state == CalState::NOT_STARTED) {
      60           0 :         return;
      61             :     }
      62             : 
      63           1 :     _cal_state = CalState::NOT_STARTED;
      64             : 
      65           1 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FlowCal: Stopped");
      66             : }
      67             : 
      68             : // update the state machine and calculate scaling
      69       18751 : bool AP_OpticalFlow_Calibrator::update()
      70             : {
      71             :     // prefix for reporting
      72       18751 :     const char* prefix_str = "FlowCal:";
      73             :     (void)prefix_str;  // in case !HAL_GCS_ENABLED
      74             : 
      75             :     // while running add samples
      76       18751 :     if (_cal_state == CalState::RUNNING) {
      77       11051 :         uint32_t now_ms = AP_HAL::millis();
      78             :         uint32_t timestamp_ms;
      79       11051 :         Vector2f flow_rate, body_rate, los_pred;
      80       11051 :         if (AP::ahrs().getOptFlowSample(timestamp_ms, flow_rate, body_rate, los_pred)) {
      81       11051 :             add_sample(timestamp_ms, flow_rate, body_rate, los_pred);
      82             : 
      83             :             // while collecting samples display percentage complete
      84       11051 :             if (now_ms - _last_report_ms > AP_OPTICALFLOW_CAL_STATUSINTERVAL_SEC * 1000UL) {
      85          19 :                 _last_report_ms = now_ms;
      86          19 :                 GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s x:%d%% y:%d%%",
      87             :                                 prefix_str,
      88             :                                 (int)((_cal_data[0].num_samples * 100.0 / AP_OPTICALFLOW_CAL_MAX_SAMPLES)),
      89             :                                 (int)((_cal_data[1].num_samples * 100.0 / AP_OPTICALFLOW_CAL_MAX_SAMPLES)));
      90             :             }
      91             : 
      92             :             // advance state once sample buffers are full
      93       11051 :             if (sample_buffers_full()) {
      94           1 :                 _cal_state = CalState::READY_TO_CALIBRATE;
      95           1 :                 GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s samples collected", prefix_str);
      96             :             }
      97             :         }
      98             : 
      99             :         // check for timeout
     100       11051 :         if (now_ms - _start_time_ms > AP_OPTICALFLOW_CAL_TIMEOUT_SEC * 1000UL) {
     101           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s timeout", prefix_str);
     102           0 :             _cal_state = CalState::FAILED;
     103             :         }
     104             :     }
     105             : 
     106             :     // start calibration
     107       18751 :     if (_cal_state == CalState::READY_TO_CALIBRATE) {
     108             :         // run calibration and mark failure or success
     109           1 :         if (run_calibration()) {
     110           1 :             _cal_state = CalState::SUCCESS;
     111           1 :             return true;
     112             :         } else {
     113           0 :             _cal_state = CalState::FAILED;
     114             :         }
     115             :     }
     116             : 
     117             :     // return indicating calibration is not complete
     118       18750 :     return false;
     119             : }
     120             : 
     121             : // get final scaling values
     122             : // scaling values used during sample collection should be multiplied by these scalars
     123           1 : Vector2f AP_OpticalFlow_Calibrator::get_scalars()
     124             : {
     125             :     // return best scaling values
     126           1 :     return Vector2f{_cal_data[0].best_scalar, _cal_data[1].best_scalar};
     127             : }
     128             : 
     129             : // add new sample to the calibrator
     130       11051 : void AP_OpticalFlow_Calibrator::add_sample(uint32_t timestamp_ms, const Vector2f& flow_rate, const Vector2f& body_rate, const Vector2f& los_pred)
     131             : {
     132             :     // return immediately if not running
     133       11051 :     if (_cal_state != CalState::RUNNING) {
     134       10497 :         return;
     135             :     }
     136             : 
     137             :     // check for duplicates
     138       11051 :     if (timestamp_ms == _last_sample_timestamp_ms) {
     139       10497 :         return;
     140             :     }
     141         554 :     _last_sample_timestamp_ms = timestamp_ms;
     142             : 
     143             :     // check yaw rotation is low
     144         554 :     const Vector3f gyro = AP::ahrs().get_gyro();
     145         554 :     if (fabsf(gyro.z) > AP_OPTICALFLOW_CAL_YAW_MAX_RADS) {
     146           0 :         return;
     147             :     }
     148             : 
     149             :     // check enough roll or pitch movement and record sample
     150         554 :     const bool rates_x_sufficient = (fabsf(body_rate.x) >= AP_OPTICALFLOW_CAL_ROLLPITCH_MIN_RADS) && (fabsf(flow_rate.x) >= AP_OPTICALFLOW_CAL_ROLLPITCH_MIN_RADS);
     151         554 :     if (rates_x_sufficient && (_cal_data[0].num_samples < ARRAY_SIZE(_cal_data[0].samples))) {
     152             : # if HAL_LOGGING_ENABLED
     153          50 :         log_sample(0, _cal_data[0].num_samples, flow_rate.x, body_rate.x, los_pred.x);
     154             : #endif
     155          50 :         _cal_data[0].samples[_cal_data[0].num_samples].flow_rate = flow_rate.x;
     156          50 :         _cal_data[0].samples[_cal_data[0].num_samples].body_rate = body_rate.x;
     157          50 :         _cal_data[0].samples[_cal_data[0].num_samples].los_pred = los_pred.x;
     158          50 :         _cal_data[0].num_samples++;
     159             :     }
     160         554 :     const bool rates_y_sufficient = (fabsf(body_rate.y) >= AP_OPTICALFLOW_CAL_ROLLPITCH_MIN_RADS) && (fabsf(flow_rate.y) >= AP_OPTICALFLOW_CAL_ROLLPITCH_MIN_RADS);
     161         554 :     if (rates_y_sufficient && (_cal_data[1].num_samples < ARRAY_SIZE(_cal_data[1].samples))) {
     162             : # if HAL_LOGGING_ENABLED
     163          50 :         log_sample(1, _cal_data[1].num_samples, flow_rate.y, body_rate.y, los_pred.y);
     164             : #endif
     165          50 :         _cal_data[1].samples[_cal_data[1].num_samples].flow_rate = flow_rate.y;
     166          50 :         _cal_data[1].samples[_cal_data[1].num_samples].body_rate = body_rate.y;
     167          50 :         _cal_data[1].samples[_cal_data[1].num_samples].los_pred = los_pred.y;
     168          50 :         _cal_data[1].num_samples++;
     169             :     }
     170             : }
     171             : 
     172             : // returns true once the sample buffer is full
     173       11051 : bool AP_OpticalFlow_Calibrator::sample_buffers_full() const
     174             : {
     175       11051 :     return ((_cal_data[0].num_samples >= ARRAY_SIZE(_cal_data[0].samples)) && (_cal_data[1].num_samples >= ARRAY_SIZE(_cal_data[1].samples)));
     176             : }
     177             : 
     178             : // run calibration algorithm for both axis
     179             : // returns true on success and updates _cal_data[0,1].best_scale and best_scale_fitness
     180           1 : bool AP_OpticalFlow_Calibrator::run_calibration()
     181             : {
     182             :     // run calibration for x and y axis
     183           1 :     const bool calx_res = calc_scalars(0, _cal_data[0].best_scalar, _cal_data[0].best_scalar_fitness);
     184           1 :     const bool caly_res = calc_scalars(1, _cal_data[1].best_scalar, _cal_data[1].best_scalar_fitness);
     185             : 
     186           1 :     return calx_res && caly_res;
     187             : }
     188             : 
     189             : // Run Gauss Newton fitting algorithm for all samples of the given axis
     190             : // returns a scalar and fitness (lower numbers mean a better result) in the arguments provided
     191           2 : bool AP_OpticalFlow_Calibrator::calc_scalars(uint8_t axis, float& scalar, float& fitness)
     192             : {
     193             :     // prefix for reporting
     194           2 :     const char* prefix_str = "FlowCal:";
     195             :     (void)prefix_str;  // in case !HAL_GCS_ENABLED
     196           2 :     const char* axis_str = axis == 0 ? "x" : "y";
     197             :     (void)axis_str;  // in case !HAL_GCS_ENABLED
     198             : 
     199             :     // check we have samples
     200             :     // this should never fail because this method should only be called once the sample buffer is full
     201           2 :     const uint8_t num_samples = _cal_data[axis].num_samples;
     202           2 :     if (num_samples == 0) {
     203           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s failed because no samples", prefix_str);
     204           0 :         return false;
     205             :     }
     206             : 
     207             :     // calculate total absolute residual from all samples
     208           2 :     float total_abs_residual = 0;
     209         102 :     for (uint8_t i = 0; i < num_samples; i++) {
     210         100 :         const sample_t& samplei = _cal_data[axis].samples[i];
     211         100 :         total_abs_residual += fabsf(calc_sample_residual(samplei, 1.0));
     212             :     }
     213             : 
     214             :     // if there are no residuals then scaling is perfect
     215           2 :     if (is_zero(total_abs_residual)) {
     216           0 :         scalar = 1.0;
     217           0 :         fitness = 0;
     218           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s perfect scalar%s of 1.0", prefix_str, axis_str);
     219           0 :         return true;
     220             :     }
     221             : 
     222             :     // for each sample calculate the residual and scalar that best reduces the residual
     223           2 :     float best_scalar_total = 0;
     224         102 :     for (uint8_t i = 0; i < num_samples; i++) {
     225             :         float sample_best_scalar;
     226         100 :         const sample_t& samplei = _cal_data[axis].samples[i];
     227         100 :         if (!calc_sample_best_scalar(samplei, sample_best_scalar)) {
     228             :             // failed to find the best scalar for a single sample
     229             :             // this should never happen because of checks when capturing samples
     230           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s failed because of zero flow rate", prefix_str);
     231           0 :             INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
     232           0 :             return false;
     233             :         }
     234         100 :         const float sample_residual = calc_sample_residual(samplei, 1.0);
     235         100 :         best_scalar_total += sample_best_scalar * fabsf(sample_residual) / total_abs_residual;
     236             :     }
     237             : 
     238             :     // check for out of range results
     239           2 :     if (best_scalar_total < AP_OPTICALFLOW_CAL_SCALE_MIN) {
     240           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s scalar%s:%4.3f too low (<%3.1f)", prefix_str, axis_str, (double)best_scalar_total, (double)AP_OPTICALFLOW_CAL_SCALE_MIN);
     241           0 :         return false;
     242             :     }
     243           2 :     if (best_scalar_total > AP_OPTICALFLOW_CAL_SCALE_MAX) {
     244           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s scalar%s:%4.3f too high (>%3.1f)", prefix_str, axis_str, (double)best_scalar_total, (double)AP_OPTICALFLOW_CAL_SCALE_MAX);
     245           0 :         return false;
     246             :     }
     247             : 
     248             :     // check for poor fitness
     249           2 :     float fitness_new = calc_mean_squared_residuals(axis, best_scalar_total);
     250           2 :     if (fitness_new > AP_OPTICALFLOW_CAL_FITNESS_THRESH) {
     251           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s scalar%s:%4.3f fit:%4.3f too high (>%3.1f)", prefix_str, axis_str, (double)scalar, (double)fitness_new, (double)AP_OPTICALFLOW_CAL_FITNESS_THRESH);
     252           0 :         return false;
     253             :     }
     254             : 
     255             :     // success if fitness has improved
     256           2 :     float fitness_orig = calc_mean_squared_residuals(axis, 1.0);
     257           2 :     if (fitness_new <= fitness_orig) {
     258           2 :         scalar = best_scalar_total;
     259           2 :         fitness = fitness_new;
     260           2 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s scalar%s:%4.3f fit:%4.2f", prefix_str, axis_str, (double)scalar, (double)fitness);
     261           2 :         return true;
     262             :     }
     263             : 
     264             :     // failed to find a better scalar than 1.0
     265           0 :     scalar = 1.0;
     266           0 :     fitness = fitness_orig;
     267           0 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s no better scalar%s:%4.3f (fit:%4.3f > orig:%4.3f)", prefix_str, axis_str, (double)best_scalar_total, (double)fitness_new, (double)fitness_orig);
     268           0 :     return false;
     269             : }
     270             : 
     271             : // calculate a single sample's residual
     272         400 : float AP_OpticalFlow_Calibrator::calc_sample_residual(const sample_t& sample, float scalar) const
     273             : {
     274         400 :     return (sample.body_rate + ((sample.flow_rate * scalar) - sample.los_pred));
     275             : }
     276             : 
     277             : // calculate the scalar that minimises the residual for a single sample
     278             : // returns true on success and populates the best_scalar argument
     279         100 : bool AP_OpticalFlow_Calibrator::calc_sample_best_scalar(const sample_t& sample, float& best_scalar) const
     280             : {
     281             :     // if sample's flow_rate is zero scalar has no effect
     282             :     // this should never happen because samples should have been checked before being added
     283         100 :     if (is_zero(sample.flow_rate)) {
     284           0 :         return false;
     285             :     }
     286         100 :     best_scalar = (sample.los_pred - sample.body_rate) / sample.flow_rate;
     287         100 :     return true;
     288             : }
     289             : 
     290             : // calculate mean squared residual for all samples of a single axis (0 or 1) given a scalar parameter
     291           4 : float AP_OpticalFlow_Calibrator::calc_mean_squared_residuals(uint8_t axis, float scalar) const
     292             : {
     293             :     // sanity check axis
     294           4 :     if (axis >= 2) {
     295           0 :         return AP_OPTICALFLOW_CAL_RMS_FAILED;
     296             :     }
     297             : 
     298             :     // calculate and sum residuals of each sample
     299           4 :     float sum = 0.0f;
     300           4 :     uint16_t num_samples = 0;
     301         204 :     for (uint8_t i = 0; i < _cal_data[axis].num_samples; i++) {
     302         200 :         sum += sq(calc_sample_residual(_cal_data[axis].samples[i], scalar));
     303         200 :         num_samples++;
     304             :     }
     305             : 
     306             :     // return a huge residual if no samples
     307           4 :     if (num_samples == 0) {
     308           0 :         return AP_OPTICALFLOW_CAL_RMS_FAILED;
     309             :     }
     310             : 
     311           4 :     sum /= num_samples;
     312           4 :     return sum;
     313             : }
     314             : 
     315             : #if HAL_LOGGING_ENABLED
     316             : // log all samples
     317         100 : void AP_OpticalFlow_Calibrator::log_sample(uint8_t axis, uint8_t sample_num, float flow_rate, float body_rate, float los_pred)
     318             : {
     319             :     // @LoggerMessage: OFCA
     320             :     // @Description: Optical Flow Calibration sample
     321             :     // @Field: TimeUS: Time since system startup
     322             :     // @Field: Axis: Axis (X=0 Y=1)
     323             :     // @Field: Num: Sample number
     324             :     // @Field: FRate: Flow rate
     325             :     // @Field: BRate: Body rate
     326             :     // @Field: LPred: Los pred
     327             : 
     328         100 :     AP::logger().Write(
     329             :         "OFCA",
     330             :         "TimeUS,Axis,Num,FRate,BRate,LPred",
     331             :         "s#-EEE",
     332             :         "F00000",
     333             :         "QBBfff",
     334             :         AP_HAL::micros64(),
     335             :         (unsigned)axis,
     336             :         (unsigned)sample_num,
     337             :         (double)flow_rate,
     338             :         (double)body_rate,
     339             :         (double)los_pred);
     340         100 : }
     341             : #endif  // HAL_LOGGING_ENABLED
     342             : 
     343             : #endif  // AP_OPTICALFLOW_CALIBRATOR_ENABLED

Generated by: LCOV version 1.14