Code Poetry
and Text Adventures

by catid posted (>30 days ago) 6:13pm Mon. Jan 6th 2014 PST
The accelerometer on the mobile device will include the gravitational force, which is a constant.  To normalize the input x,y,z acceleration data to be relative to g = 1 across all devices, I take a very low-pass filter to the amplitude of each measurement to arrive at the long-term magnitude average, and then normalize the x,y,z values with that magnitude.

These samples are stored with their amplitudes in a sample array.

The algorithm I settled on will activate every 500 milliseconds with any collected data.  Pseudo-code:

(1) Find largest amplitude in the set.

(2) Calculate the dot product of largest amplitude sample with each sample.

(3) From first to last, ignore the dot products that have absolute value under some threshold.

(4) Calculate the number of times the sign of the remaining high-amplitude dot products change signs.

(5) If the number of zero crossings exceeds 4, then it's a shake.

Delete half the oldest half of the sample data and continue collecting from there, so that shakes that wrap between triggers are not lost.

The advantage of zero crossings is that periodic behavior can be qualified by rate of motion change per unit time.  This is also a very inexpensive approach.

Since the dot product is used, shakes towards the ground will be detected but not shakes that are side to side.  I'm not sure if this is a problem yet.

Full test JS code:

    this.samples = [];
    this.sampleCount = 0;
    this.lastSample = {};
    this.lastCheck = 0;

    this.watchingShakes = true;

    NATIVE.events.registerHandler('deviceGravity', bind(this, function(evt) {
      if (!this.watchingShakes) {
        return;
      }

      var x = evt.x, y = evt.y, z = evt.z;
      var amp = Math.sqrt(x*x + y*y + z*z);

      // Eliminate scaling issues between devices, normalized to gravity
      var smoothedAmp = this.smoothedAmp;
      if (!smoothedAmp) {
        smoothedAmp = amp;
      } else {
        smoothedAmp = smoothedAmp * 0.95 + amp * 0.05;
      }
      this.smoothedAmp = smoothedAmp;

      // Normalize by smoothed amplitude
      x = x / smoothedAmp;
      y = y / smoothedAmp;
      z = z / smoothedAmp;

      // This is not tied to any particular time interval so may
      // behave differently on different devices
      var dx = x - this.lx;
      var dy = y - this.ly;
      var dz = z - this.lz;
      this.lx = x;
      this.ly = y;
      this.lz = z;

      var sampleIndex = this.sampleCount++;
      var sample = this.samples[sampleIndex];
      if (!sample) {
        this.samples[sampleIndex] = {
          'x': dx,
          'y': dy,
          'z': dz,
          'amp': amp
        };
      } else {
        sample.x = dx;
        sample.y = dy;
        sample.z = dz;
        sample.amp = amp;
      }

      // Every second,
      var now = +new Date();
      if (now - this.lastCheck > 500) {
        // Find the most intense acceleration in the set
        var best = this.samples[0];
        var bestAmp = best && best.amp;
        for (var i = 1; i < this.sampleCount; i++) {
          var sample = this.samples[i];
          var amp = sample.amp;

          if (amp > bestAmp) {
            bestAmp = amp;
            best = sample;
          }
        }

        // If useful data,
        if (best) {
          var last = 0;
          var zeroCrossings = 0;

          // For each sample,
          for (var j = 0; j < this.sampleCount; j++) {
            var sample = this.samples[j];
            var dot = sample.x * best.x + sample.y * best.y + sample.z * best.z;

            // If there is some motion,
            if (Math.abs(dot) > 0.0015) {
              if ((dot < 0 && last > 0) || (dot > 0 && last < 0)) {
                ++zeroCrossings;
              }

              last = dot;
            }
          }

          // If 4 shakes in ~0.75 seconds,
          if (zeroCrossings >= 4) {
            this.updatePosition();
          }
        }

        this.lastCheck = now;

        // Keep half the data
        var half = Math.floor(this.sampleCount / 2);
        this.samples = this.samples.splice(half, this.sampleCount);
        this.sampleCount = half;
      }
    }));
last edit by catid edited (>30 days ago) 6:31pm Mon. Jan 6th 2014 PST