Tuesday, February 21, 2012

Accelerometer Revisited

I wrote about handling accelerometer in iOS from the browser before (Detecting Device Movement, Accelerometer Demo) but I wasn’t happy enough with my code and I promised to return with the third part. Today I returned to the topic with advanced features – handling data smoothing and calibration.

Smoothing

In my previous article I’ve been complaining that the accelerometer data is very “jittery”. This could cause a problem in the application so a way of smoothing that data to remove biggest peaks is preferable.

For that purpose, my component implements low-pass filter. This is a very simple kind of filter that removes high-frequency component of the signal (i.e. quick changes in the input) and leaves only low-frequency component (i.e. slow changes in the input). The code below does it for the horizontal direction and equivalent method SetVertical (not shown) does it for the vertical direction.

procedure TW3DisplayAcceleration.SetHorizontal(value: float);
begin if (FFilterFactor > 0) and (FFilterFactor < 1) then FHorizontal := FFilterFactor * value + (1 - FFilterFactor) * FHorizontal else FHorizontal := value;
end;

The code takes new reading (value) and uses only a FFilterFactor part of it (FFilterFactor must be between 0 and 1). The rest (1 – FFilterFactor) is taken from the previous (filtered) horizontal acceleration, stored in the FHorizontal field and result is stored back into this field.

As you can see, filtering is controlled by a filter factor, which you can configure by setting TGpW3Motion.MotionData.DisplayAccel.FilterFactor. Default filter factor is 0.5. Only “display” acceleration (explained in the previous article) is filtered, all other data is left intact so you can use it for your own processing.

You may be wondering how the filter factor works and how different values affect the smoothing. I hope the following two tables will explain it.

Let’s say that the device was completely stable with acceleration equal to zero in one direction. (We’ll only be looking at one direction as filtering in the other direction works completely the same.) Then the acceleration suddenly changes to 1 and stays there for very long time. What happens to the filtered acceleration (which was initially zero) if filter factor is 0.1, 0.5, and 0.9? [Decimal commas instead of decimal points are used in the table as this is a standard way to display floating point in my country.]

sensor acceleration

filtered acceleration
 f = 0,1f = 0,5f = 0,9
00,00000,00000,0000
10,10000,50000,9000
10,19000,75000,9900
10,27100,87500,9990
10,34390,93750,9999
10,40950,96881,0000
10,46860,98441,0000
10,52170,99221,0000
10,56950,99611,0000
10,61260,99801,0000
10,65130,99901,0000

With the filter factor of 0.1, filtered acceleration takes long time to adapt. After ten measurements, filtered value is around 0.65, still a long way from 1. If the filter factor is 0.5, changes happen faster and if the filter factor is 0.9, filtered acceleration approaches 1 very quickly.

Before making any conclusions, let’s look at the second simulation. Now the initial acceleration is 1 (and the filtered acceleration is also 1 because the acceleration hadn’t changed for a long time). Acceleration suddenly jumps to –1, to 3 and then back to 1. How do the filters behave?

sensor accelerationfiltered acceleration
 f = 0,1f = 0,5f = 0,9
11,00001,00001,0000
-10,80000,0000-0,8000
31,02001,50002,6200
11,01801,25001,1620
11,01621,12501,0162

Low filter factor (0.1) keeps the filtered acceleration close to 1. It changes only a little. High filter factor (0.9) follows the input in higher degree – it jumps to –0.8, to 2.6 and than quickly returns to 1. Medium filter factor data lies between those two extremes.

We can see now that low filter factor adapts to changes very slowly but that also makes it very good at filtering out sudden changes. High filter factor adapts to changes very quickly but is not very good at filtering, it leaves most of quick changes intact. Medium filter factor is good at both (or good at neither, as some may say).

Which filter factor to use? Well, this depends on you and the application you’re writing. If the filter factor is low, you won’t get much noise but the application will respond to changes in acceleration very slowly. Reverse holds for high filter factor. It’s best to test with few different values and see when the application behaves the best.

Calibration

Another feature I thought interesting was calibration. In some applications it helps a lot if the user can say “from now on pretend that the device is level”. Solution is very simple – just subtract this “current position” (actually, acceleration values at that moment) from following measurements – but is made much harder by the jitter in the input. What if the user “sets the level” in the same moment that accelerometer returned a very weird reading?

To solve this problem, I’ve written a circular buffer class. TW3DisplayAcceleration stores last ten raw acceleration measurements in this buffer and then uses average value for calibration.

Buffer is implemented as an array of Variants which function as ad-hoc objects (and I’ll explain this in a moment).

type
TW3MeasurementBuffer = class(IW3MeasurementBuffer)
private
FBuffer: array of Variant;
FBufferSize: integer;
FLastEl: integer;
FNumEl: integer;
public
constructor Create(bufferSize: integer);
procedure Add(x, y, z: float);
procedure Clear;
function Count: integer;
procedure Get(item: integer; var x, y, z: float);
end;

Most of the code is boilerplate, but two examples may help you understand Smart better.

Constructor creates the storage by allocating a dynamic array of Variants and then creates empty JavaScript objects for each slot. [BTW, nothing special has to be done in the destructor as the JavaScript is garbage collected.] It does this by assigning a {}, an empty object, to a temporary Variant and then storing this Variant in the slot. [This will be simplified before the Smart is released as Eric is planning a language extension to create Variant objects.]

constructor TW3MeasurementBuffer.Create(bufferSize: integer);
var
i: integer;
newEl: Variant;
begin
inherited Create;
FBufferSize := bufferSize;
FBuffer.SetLength(FBufferSize);
for i := 0 to FBufferSize - 1 do begin
asm @newEl = {}; end;
FBuffer[i] := newEl;
end;
Clear;
end;

Add method takes three numbers (I’m only using two at the moment) and stores them in a slot. As the slot is a Variant which is a JavaScript object, we can simply store x, y and y into its X, Y and Z fields. It doesn’t matter that those fields were never declared – you can store anything in a JavaScript object. [It helps a lot if you think about a JavaScript object as of a hash – and that holds even for arrays.] Just keep in mind that Variant fields are case-sensitive as they translate directly to JavaScript, which is a case-sensitive language.

procedure TW3MeasurementBuffer.Add(x, y, z: float);
begin
Inc(FLastEl);
if FLastEl >= FBufferSize then
FLastEl := 0;
FBuffer[FLastEl].X := x;
FBuffer[FLastEl].Y := y;
FBuffer[FLastEl].Z := z;
end;

Each time an accelerometer reading arrives, Update method subtracts the current calibration, uses SetXXX to set filtered properties and stores raw readings in the circular buffer.

procedure TW3DisplayAcceleration.Update(horizontal, vertical: float);
begin
horizontal := horizontal - FZeroHoriz;
vertical := vertical - FZeroVert;
SetHorizontal(horizontal);
SetVertical(vertical);
FBuffer.Add(horizontal, vertical, 0);
end;

Calibrate method just iterates over accumulated data, creates and average and stores it in the “offset'” fields FZeroHoriz and FZeroVert (this happens inside the CalibrateOffs call). The assumption here is that the device was mostly stationary during last few measurements (which is typically the case during the calibration).

procedure TW3DisplayAcceleration.Calibrate;
var
i: integer;
sumH, sumV: float;
x, y, z: float;
begin
if FBuffer.Count = 0 then
Exit;
sumH := 0; sumV := 0;
for i := 0 to FBuffer.Count - 1 do begin
FBuffer.Get(i, x, y, z);
sumH := sumH + x + ZeroHorizontal;
sumV := sumV + y + ZeroVertical;
end;
CalibrateOffs(sumH / FBuffer.Count, sumV / FBuffer.Count);
end;

Alternatively, application can call CalibrateOffs directly and provide calibration offsets. As you’ll see in the demo below, this feature can be used to reset recalibrated accelerometer to default behaviour.

User Interface

The user interface in the demo program has also been updated.

First – and very important – change is that the calculated velocity now changes a floating point representation of the object’s position (previously it was changing FRect position directly). This makes the rectangle glide around much more smoothly.

procedure TFormMain.HandleMotionChange(Sender: TObject; info: TW3MotionData);
const  
  CSpeedUp = 200;
begin  
  FvX := FvX + info.DisplayAccel.Horizontal * info.Interval;  
  FvY := FvY + info.DisplayAccel.Vertical * info.Interval;  

  LogMotion(info);

  FX := BounceX(FX + FvX * info.Interval * CSpeedUp);
  FY := BounceY(FY + FvY * info.Interval * CSpeedUp);

  FRect.BeginUpdate;
  FRect.Left := Round(FX);
  FRect.Top := Round(FY);
  FRect.EndUpdate;
end;

The second change is that a tap on the rectangle calls the calibration. Object speed is set to zero to stop it from moving around.

procedure TFormMain.HandleClick(Sender: TObject);
begin
FMotion.MotionData.DisplayAccel.Calibrate;
FvX := 0;
FvY := 0;
end;

The third and last change is that Resize (called also when the device orientation changes) resets calibration offsets to zero (undoing the last call to Calibrate).

procedure TFormMain.Resize;
begin
inherited;
FMotion.MotionData.DisplayAccel.CalibrateOffs(0, 0);
FvX := 0; FvY := 0;
FRect.Width := 200;
FRect.Height := 200;
FRect.Top := (Height - FRect.Height) div 2;
FRect.Left:= (Width - FRect.Width) div 2;
FRect.Invalidate;
FX := FRect.Left;
FY := FRect.Top;
end;

Obligatory Links

Demo program (requires iOS 4.2+).

GpW3Motion unit.

Main form.

2 comments:

  1. Interesting.. I want to try it with an ipad and look at the code, but the links to the files are all dead.

    ReplyDelete
    Replies
    1. Files have moved. Please use the ones in the http://www.thedelphigeek.com/2012/05/smartms-demos.html post.

      Delete