Ski Resort Trail Maps & GPS Data

Ski Resort Trail Maps & GPS Data

True coordinates on an untrue map: mapping real GPS data to ski resort trail maps.

Ski resort trail maps are the standard way that ski resorts show their layout, and have become a niche art form with well-recognised artists (Heinrich Berann, James Niehues). Oblique panoramas like this were traditionally painted by hand, but now will increasingly be computer-rendered. This page is me noodling around with these maps and modern AI tools.
I have skied at Shiga Kogen, Japan – the resort’s official trail map is below.

Shiga Kogen trail map
Fig. 1 Upscaled panorama trail map for Shiga Kogen, about 14 km across. Zoomable.

Shiga Kogen, in Nagano Prefecture, is one of Japan’s largest ski areas: 18 areas on one lift pass, about 45 lifts and 400 ha of terrain between 1,325 m and 2,307 m, and a venue of the 1998 Winter Olympics. Below is a satellite image of the resort from above, turned so north is on the left, matching the painting, which looks east.

Show
Season
N
Fig. 2 The resort on the ground, north to the left, about 14 km across. Amber outlines are the 18 ski areas; black lines are lifts. Summer: GSI aerial photograph, about 0.3 m per pixel. Winter: Sentinel-2 satellite image, 15 March 2024, 10 m per pixel, so roughly 30 times coarser: the runs show clearly, but it turns soft when you zoom in.

The map is the territory

I thought it would be fun to apply GPS tracking data to the trail map. Normally this would be a complete waste of time, but with modern AI coding tools it is now a feasible side project.

The mapping is built in steps:

  1. Match points. Find features visible on both the painting and real map data: every lift’s top and bottom station and the named peaks (88 in total), each with a true 3D position from the terrain model.
  2. Fit a camera. Solve for the one viewpoint, direction, lens and vertical stretch that best reproduces those 88 points. This gets the overall perspective right, but is still about 66 px off.
  3. Warp the rest. Add a smooth correction, pinned at the 88 points, for the artist’s local distortions.
  4. Snap to the painted runs. Nudge the mapping so OpenStreetMap’s ski runs land on the painted run lines (median error 8.5 → 3.5 px).
  5. Draw the GPS. Drop each GPS fix onto the terrain, push it through the mapping to a pixel on the painting, and place lift rides along the painted lift lines.
Show
Day
Speed
Fig. 3 The painting with four overlays. Ski areas: the 18 boundaries from OpenStreetMap, projected through the final mapping; the fill covers only ground the painting can see, and dashed edges lie behind a ridge. Camera only and Final mapping: a 400 m ground grid drawn with the camera only (step 2) and with the full mapping (step 4). GPS tracks: my phone’s GPS, resampled to 5 s, with play, day and time controls.

Conclusions

Fitting a camera to a hand-painted panorama, warping it, and pinning thousands of GPS fixes onto it is the kind of thing that would once have been a research project. I built this over two days of tinkering with Claude Code: Claude Opus 5.5 planned and wrote most of it, and Claude Sonnet 5.5 sub-agents handled research, reviews and parallel pieces of work. I described what I wanted, looked at the results and pushed back; the models found the data sources, wrote the Python and JavaScript, ran the fits and checked their own output.

Time
2 days, about 7 hours of active work
Model turns
1,157 (748 Opus, 409 Sonnet)
Tokens
271 million, 98% of them re-reads of cached context
API-equivalent cost
about US$93 (Opus $79, Sonnet $14)

Counted from the Claude Code session logs, priced at Anthropic API list rates on 1 October 2026 (Opus 5.5: $4 / $20 per million input / output tokens; Sonnet 5.5: $2 / $10; cache reads $0.20). I used a subscription, so this is what the same work would cost through the API, not what I paid.

Pipeline

Inputs

  • Painting 3,000×961
  • DEM (GSI 1 m)
  • OSM lifts + pistes
  • GPS (lat, lon, t)

Fit · offline · Python

1 Control points

  • cv2 ink threshold
  • open 3×3, components
  • 86 candidate markers
  • hand-matched to OSM
  • + 9 peaks = 88 pairs

OpenCV

2 Camera fit

  • pinhole, 10 params
  • heights × e
  • least_squares
  • soft-L1, bounded
  • multi-start init

SciPy

3 Thin-plate spline

  • paint − camera
  • over ground (x, z)
  • RBFInterpolator
  • thin_plate_spline
  • weighted smoothing

SciPy

4 Chamfer refine

  • distanceTransform
  • per run colour
  • 114 Gaussian nodes
  • least_squares
  • analytic Jacobian

OpenCV · SciPy

Portable

5 Lookup grid

  • portable copy of M
  • 40 m lat/lon grid
  • 182 × 360 nodes
  • u, v × 10 as ints
  • 709 KB JSON

NumPy

Tracks

6 Render

  • GPS projected offline with M
  • resampled to 5 s
  • packed Int16 u, v, state, km/h

Canvas 2D draws

The full method, data sources and a replication-grade specification are on the Methodology page.