Elevation Correction

:: activitylog2

From having a nice and consistent elevation plot and an accurate total ascent/descent for an activity, to calculating effects of slope on performance, such as Grade Adjusted Pace for running, it is important to have accurate elevation data. Unfortunately, elevation data is one of the measures most prone to errors during an activity. On-line elevation services can be used to correct this data, but in ActivityLog2, I decided to use solution runs on the user’s computer only, and does not depend on cloud services.

Why does elevation data contain errors?

Elevation data recorded during a sport activity, such as running or cycling, can come from two sources: GPS (GNSS), or a barometric altimeter. Altitude readings from GPS suffer from most of the errors, which are inherent to how the system works. An internet search for “Why is GPS elevation inaccurate?” will return many results with explanations about this problem.

Most high end sport watches these days have a baromethric altimeter and calculate elvation data from changes in atmospheric pressure as the athlete climbs or descends. However, these devices are sensitive to changes in atmospheric pressure due to weather fronts passing over.

To verify the quality of barometric sensor readings, I analyzed some of my training data for my indoor rides, since my Garmin FR920 device would record altitude from its barometric sensor for all indoor rides. This is convenient because I had lots of readings, on many different days, for the same altitude. I wrote a blog post about this analysis but here is the summary:

  • My home has an elevation of about 26 meters above sea level, but the measurements range from –138 meters to +257 meters, which is quite a bit of error
  • Even within a single activity, which lasts between 1 and 1.5 hours, the elevation change is about 4 meters on average, with the maximum I’ve seen for my data is 14.5 meters.

I suspect most sport watches will initialize the barometric altimeter reading either from a GPS altitude reading or from weather data, since I have never seen a huge absolute error for outdoor rides, however the altitude change due to weather fronts is present even for these outdoor rides, which causes routes that are out-and-back or routes that contain laps to have elevation data that is different for the same road sections covered by the route.

How does ActivityLog2 do elevation correction?

When running, cycling, hiking, etc, most athletes will train on the same routes, or at least there will be an overlap for these routes. When these activities are imported into ActivityLog2, its database will contain several elevation data points for the same, or very close, locations. This means that, in order to find the best elevation estimate for a certain location, the ActivityLog2 can simply look at the elevation of nearby points and calculate the average for them.

To illustrate how this works, here is a climbing segment which I traversed many times in different activities, collecting 34′000 data samples along it from several years. A 3D view of the data points shows that, while the latitude/longitude seems to be quite good, all points are “on top of each other”, the elevation data seems to be slightly different for each activity:

And here is a 2D elevation plot of the same segment showing the minimum and maximum elevation recorded along the route, the green band, along with the average of these samples, or mean elevation, at each location, shown as a red line. For comparison, the blue line shows the elevation of the segment obtained from an online elevation service:

Implementation details

The elevation correction algorithm used by ActivityLog2 is quite straightforward: for each GPS point in an activity, it finds nearby points from previous activities in the database and calculates their average altitude. Unfortunately, a naive implementation will be very inefficient: finding nearby locations based on latitude and longitude can only be done with inspecting each data point, and, with millions of them, this can be very slow.

I first read about Google’s S2 Geometry library a few years ago and I was impressed by the possibility of building indexes for fast lookup of geographic data. This library allows associating a 64 bit integer with every region on Earth, with the smallest regions that can be indexed being less than 1 cm². Regions that are close to each other geographically have close integer IDs, which allows building indexes from sorted list of IDs.

Here is an overview of the S2 library and what it can do, however, I wrote my own Racket based implementation, the geoid package, based on the same ideas. The ActivityLog2 database stores a geoid (an 64bit integer) for each data point, and these geoids can be queried quickly, below is some code that can quickly scan millions of data points to find nearby altitude samples, the full elevation correction implementation is a bit more sophisticated, including some in-memory caching:

(define fetch-altitude-sql """
select altitude
 where altitude is not null
   and geoid >= ? and geoid < ?
(define (fetch-altitude-samples database geoid)
  (define-values (start end) (leaf-span geoid))
    (query-list database
                (geoid->sqlite-integer start)
                (geoid->sqlite-integer end)))
(define (nearby-samples database lat lon)
   ;; geoid for this location, an area about 1cm²
   (define g (lat-lon->geoid lat lon))
   ;; Get the larger enclosing GEOID (level 10 is about 8 m²)
   (define area-geoid (enclosing-geoid g 10))
   (fetch-altitude-samples database area-geoid))

What alternatives exist?

There are several online services available for elevation correction, where one can make a web request with a list of waypoints and get an elevation profile back. Most of these services cost money, although some do provide a free tier.

There are also free alternatives, such as Open-Elevation, which also allow hosting the site locally, although the actual elevation data is quite large and it is more complex to set up.

Final Thoughts

The approach of averaging the elevation from nearby locations works remarkably well, even when there are limited amounts of data, like a simple out-and-back route, where altitude points from the “out” part of the route can be averaged with the altitude from the “back” part of the route and vice versa. The approach is not limited to averaging data points from the same route: nearby locations can be looked up form any activity in the ActivityLog2 database.

Overall, the ActivityLog2 approach is perhaps the simplest option for a fitness application, especially when choosing to use only data available on the users machine, and avoid depending on external services. It does, however depend on traversing the route multiple times, which is the case for athletes training over the same routes, but it is not generic enough for all types of applications — for example, it cannot fix elevation for a route which is only “planned” but not executed yet and where there is no elevation data in the ActivityLog2 database.