Unwrapped spherical projection panorama image from Google Street View

Ever wanted to download Google Street View panoramas programmatically?

I reverse-engineered their URL query scheme using the Live HTTP Headers plug-in for Firefox. From there, I just parsed through the XML returned to download each jpeg tile and then stitch them together in order.

I used a variation of this code in combination with a few other programs to parse a GPS log, download the street view panorama for each position in the log, and then stitch each frame together into a video documenting my trip on the M5 bus.

import processing.core.*;
import processing.net.*;
import processing.xml.*;

// get a hold of this for the xml library
// better way to do this part?
PApplet main = this;

void setup() {
  // we usually end up with 3328 x 1664 pixel images
  // let's undersample considerably for a more sane window size
  size(832, 416);

  // your google maps api key goes here
  // sign up: http://code.google.com/apis/maps/signup.html
  String apiKey = "REPLACE ME WITH YOUR VERY OWN API KEY";

  // pick a location
  // takes an address or a lat / lon
  String location = "721 broadway, new york, ny";

  // generate the panorama
  Panorama pano = new Panorama(location, apiKey);

  // show the image
  image(pano.fullPano, 0, 0, width, height);

  // save the full-res image to the sketch folder
  pano.fullPano.save(location + ".jpg");

  // skip the draw loop
  noLoop();
}

class Panorama {
  float lat;
  float lon;
  String panoId;
  int imageWidth;
  int imageHeight;
  int tileWidth;
  int tileHeight;
  int xTileCount;
  int yTileCount;
  PImage[][] tiles;
  PImage fullPano;
  String apiKey;

  // constructor
  Panorama(String address, String _apiKey) {
    apiKey = _apiKey;
    String[] location = geocode(address);
    lon = float(location[0]);
    lat = float(location[1]);
    fetchInfo(lat, lon);
    buildImage();
  }

  void fetchInfo(float lat, float lon) {
    String url = "http://maps.google.com/cbk?output=xml&ll=" + lat + "," + lon;

    XMLElement response = new XMLElement(main, url);

    XMLElement kid = response.getChild(0);
    imageWidth = kid.getIntAttribute("image_width");
    imageHeight = kid.getIntAttribute("image_height");
    tileWidth = kid.getIntAttribute("tile_width");
    tileHeight = kid.getIntAttribute("tile_height");
    panoId = kid.getStringAttribute("pano_id");

    xTileCount = imageWidth / tileWidth;
    yTileCount = imageHeight / tileHeight;

    println("Panorama ID: " + panoId);
    println("X Tile Count: " + xTileCount);
    println("Y Tile Count: " + yTileCount);
  }

  void buildImage() {
    fullPano = createImage(imageWidth, imageHeight, RGB);
    fullPano.loadPixels();

    for (int xPos = 0; xPos<= xTileCount; xPos++) {
      for (int yPos = 0; yPos<= yTileCount; yPos++) {
        // &.jpg fools processing into handling it
        String imageUrl = "http://cbk0.google.com/cbk?output=tile&panoid=" + panoId + "&zoom=3&x=" + xPos + "&y=" + yPos + "&.jpg";
        fullPano.set(xPos * tileWidth, yPos * tileHeight, loadImage(imageUrl));
        println("Loaded tile " + xPos + "::" + yPos);
      }
    }

    fullPano.updatePixels();
  }

  String[] geocode(String address) {
    String cleanAddress = address.replace(' ', '+');
    String url = "http://maps.google.com/maps/geo?q=" + cleanAddress + "&output=xml&oe=utf8&sensor=false&key=" + apiKey;
    println(url);
    XMLElement location = new XMLElement(main, url);
    XMLElement kid = location.getChild("Response/Placemark/Point/coordinates");
    String rawCoordinates = kid.getContent();
    String[] latlon = shorten(split(rawCoordinates, ',')); // ditch the 0
    println(latlon);
    return latlon;
  }
}