How to calculate distance from a phone thrown in height

200

Correct me if I'm wrong, but if you're trying to calculate a phone's absolute position in space at any moment in time directly from (past and present) accelerometer data, that is actually very complex, mainly because the phone's accelerometer's frame of reference in terms of x, y, and z is the phone itself... and phones are not in a fixed orientation, especially when being thrown around, and besides... it will have zero acceleration while in the air, anyway.

It's sort of like being blindfolded and being taken on a space journey in a pod with rockets that fire in different directions randomly, and being expected to know where you are at the end. That would be technically possible if you knew where you were when you started, and you had the ability to track every acceleration vector you felt along the way... and integrate this with gyroscope data as well... converting all this into a single path.

But, luckily, we can still get the height thrown from the accelerometer indirectly, along with some other measurements.

This solution assumes that:

  • The sensors package provides acceleration values, NOT velocity values (even though it claims to provide velocity, strangely), because accelerometers themselves provide acceleration.
  • Total acceleration is equal to sqrt(x^2 + y^2 + z^2) regardless of phone orientation.
  • The accelerometer will read zero (or gravity only) during the throw
  • This article in wired is correct in that Height = (Gravity * Time^2) / 8

The way my code works is:

  • You (user) hold the "GO" button down.
  • When you throw the phone up, naturally you let go of the button, which starts the timer, and the phone starts listening to accelerometer events.
  • We assume that the total acceleration of the phone in the air is zero (or gravity only, depending on chosen accelerometer data type)... so we're not actually trying to calculate distance directly from the accelerometer data:
  • Instead, we are using the accelerometer ONLY to detect when you have caught the phone... by detecting a sudden change in acceleration using a threshold.
  • When this threshold is met, the timer is stopped.
  • Now we have a total time value for the throw from beginning to end and can calculate the height.

Side notes:

  • I'm using AccelerometerEvent (includes gravity), not UserAccelerometer event (does not include gravity), because I was getting weird numbers on my test device (non-zero at rest) using UserAccelerometerEvent.
  • It helps to catch the phone gently ***
  • My math could be complete off... I haven't had anyone else look at this yet... but at least this answer gets you started on a basic theory that works.
  • My phone landed in dog poo so I hope you accept this answer.

Limitations on Accuracy:

  • The height at which you let go, and catch are naturally going to be inconsistent.
  • The threshold is experimental.... test different values yourself. I've settled on 10.
  • There is probably some delay between the GO button depress and the timer beginning.
  • *** The threshold may not always be detected accurately, or at all if the deceleration ends too quickly because the frequency of accelerometer updates provided by the sensors package is quite low. Maybe there is a way to get updates at a higher frequency with a different package.
  • There is always the chance that the GO button could be depressed too early (while the phone is still in your hand) and so the acceleration will be non zero at that time, and perhaps enough to trigger the threshold.
  • Probably other things not yet considered.

Code:

import 'package:flutter/material.dart';
import 'package:sensors/sensors.dart';
import 'dart:async';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Phone Throw Height',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Phone Throw Height'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<StreamSubscription<dynamic>> _streamSubscriptions =
      <StreamSubscription<dynamic>>[];

  DateTime? startTime;
  DateTime? endTime;
  bool isBeingThrown = false;
  final double GRAVITATIONAL_FORCE = 9.80665;
  final double DECELERATION_THRESHOLD = 10; // <---- experimental
  List<double> accelValuesForAnalysis = <double>[];

  @override
  void initState() {
    super.initState();

    _streamSubscriptions
        .add(accelerometerEvents.listen((AccelerometerEvent event) {
      if (isBeingThrown) {
        double x_total = pow(event.x, 2).toDouble();
        double y_total = pow(event.y, 2).toDouble();
        double z_total = pow(event.z, 2).toDouble();

        double totalXYZAcceleration = sqrt(x_total + y_total + z_total);

        // only needed because we are not using UserAccelerometerEvent
        // (because it was acting weird on my test phone Galaxy S5)
        double accelMinusGravity = totalXYZAcceleration - GRAVITATIONAL_FORCE;

        accelValuesForAnalysis.add(accelMinusGravity);
        if (accelMinusGravity > DECELERATION_THRESHOLD) {
          _throwHasEnded();
        }
      }
    }));
  }

  void _throwHasEnded() {
    isBeingThrown = false;
    endTime = DateTime.now();
    Duration totalTime = DateTime.now().difference(startTime!);
    double totalTimeInSeconds = totalTime.inMilliseconds / 1000;
    // this is the equation from the wired article
    double heightInMeters =
        (GRAVITATIONAL_FORCE * pow(totalTimeInSeconds, 2)) / 8;

    Widget resetButton = TextButton(
      child: Text("LONG PRESS TO RESET"),
      onPressed: () {},
      onLongPress: () {
        startTime = null;
        endTime = null;
        print(accelValuesForAnalysis.toString());
        accelValuesForAnalysis.clear();
        Navigator.pop(context);
        setState(() {
          isBeingThrown = false;
        });
      },
    );

    AlertDialog alert = AlertDialog(
      title: Text("Throw End Detected"),
      content: Text("total throw time in seconds was: " +
          totalTimeInSeconds.toString() +
          "\n" +
          "Total height was: " +
          heightInMeters.toString() +
          " meters. \n"),
      actions: [
        resetButton,
      ],
    );

    showDialog(
      barrierDismissible: false,
      context: context,
      builder: (BuildContext context) {
        return alert;
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SizedBox.expand(
        child: Container(
          color: Colors.green,
          //alignment: Alignment.center,
          child: SizedBox.expand(
            child: (!isBeingThrown)
                ? TextButton(
                    child: Text("GO!",
                        style: TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                            fontSize: 40)),
                    onPressed: () {
                      setState(() {
                        isBeingThrown = true;
                        startTime = DateTime.now();
                      });
                    },
                  )
                : Center(
                    child: Text("weeeeeeeeee!",
                        style: TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                            fontSize: 40)),
                  ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    // cancel the stream from the accelerometer somehow!! ugh!!!
    for (StreamSubscription<dynamic> subscription in _streamSubscriptions) {
      subscription.cancel();
    }
    super.dispose();
  }
}
Share:
200
Happyriri
Author by

Happyriri

Updated on January 02, 2023

Comments

  • Happyriri
    Happyriri over 1 year

    In Flutter, there is the sensor package https://pub.dev/packages/sensors that allow to know the velocity X, Y and Z.

    My question is : how could I calculate the distance of a phone thrown in height ?

    Example : you throw your telephone, with your hand at 0.5 meter from the ground. The phone reaching 1 meter from your hand (so 1.5 meter from the ground).

    How can I get the 1 meter value ?

    Thanks all !

    Here is the code I have right now (you need to install sensors package):

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:sensors/sensors.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
    
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
    
      List _velocityY = [];
      DateTime time;
      List<double> distances = [];
    
      List<StreamSubscription<dynamic>> _streamSubscriptions =
      <StreamSubscription<dynamic>>[];
    
      @override
      void initState() {
        super.initState();
    
        _streamSubscriptions
          .add(userAccelerometerEvents.listen((UserAccelerometerEvent event)
        {
          setState(() {
            if (event.y.abs() > 0.1) {
              if (time != null) {
                _velocityY.add(event.y);
              }
              //print((new DateTime.now().difference(time).inSeconds));
              if (_velocityY.length > 0) {
                distances.add(_velocityY[_velocityY.length - 1] * (new DateTime.now().difference(time).inMicroseconds) / 1000);
              }
    
              time = new DateTime.now();
            }
    
            //print('time' + time.toString());
          });
        }));
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: ListView(
            children: [
              for(double distance in distances.reversed.toList())
                Text(
                  distance.toStringAsFixed(2),
                  style: Theme.of(context).textTheme.headline4,
                ),
            ],
          ),// This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }