Захват встроенного изображения карты Google с помощью Python без использования браузера

Я заметил, что на странице Google Maps вы можете получить ссылку «встроить», чтобы поместить ее в iframe и загрузить карту в браузере. (здесь нет новостей)

Размер изображения можно настроить так, чтобы он был очень большим, поэтому я заинтересован в получении некоторых больших изображений в виде отдельных файлов .PNG.

В частности, я хотел бы определить прямоугольную область из ограничивающей рамки (верхние правые и нижние левые координаты) и получить соответствующее изображение с соответствующим коэффициентом масштабирования.

Но мой вопрос: как я могу использовать Python для получения «пиксельного содержимого» этой карты в качестве объекта изображения?

(Мое обоснование таково: если браузер может получить и отобразить такой контент изображения, то Python тоже должен это делать).

РЕДАКТИРОВАТЬ: это содержимое HTML-файла, в котором показана моя примерная карта:

<iframe 
    width="2000"
    height="1500"
    frameborder="0"
    scrolling="yes"
    marginheight="0"
    marginwidth="0"
    src="http://maps.google.com.br/maps?hl=pt-BR&amp;ll=-30.027489,-51.229248&amp;spn=1.783415,2.745209&amp;z=10&amp;output=embed"/>

РЕДАКТИРОВАТЬ: я сделал, как предложил Нед Бэтчелдер, и прочитал содержимое вызова urllib.urlopen(), используя адрес src, взятый из iframe выше. В результате получилось много кода javascript, который, я думаю, имеет отношение к JavaScript API Google Maps. Итак, остается вопрос: как я могу сделать что-то полезное из всего этого материала в Python, чтобы получить изображение карты?

РЕДАКТИРОВАТЬ: эта ссылка содержит довольно важную информацию о том, как Карты Google разбивают свои карты: http://www.codeproject.com/KB/scrapbook/googlemap.aspx

также: http://econym.org.uk/gmap/howitworks.htm


person heltonbiker    schedule 20.09.2011    source источник


Ответы (8)


Благодарю за все ответы. В итоге я решил проблему другим способом, используя статический API Google Maps и некоторые формулы для преобразования из пространства координат в пространство пикселей, чтобы я мог получать точные изображения, которые хорошо «сшиваются» вместе.

Кому интересно, вот код. Если это кому-то поможет, пожалуйста, прокомментируйте!

=============================

import Image, urllib, StringIO
from math import log, exp, tan, atan, pi, ceil

EARTH_RADIUS = 6378137
EQUATOR_CIRCUMFERENCE = 2 * pi * EARTH_RADIUS
INITIAL_RESOLUTION = EQUATOR_CIRCUMFERENCE / 256.0
ORIGIN_SHIFT = EQUATOR_CIRCUMFERENCE / 2.0

def latlontopixels(lat, lon, zoom):
    mx = (lon * ORIGIN_SHIFT) / 180.0
    my = log(tan((90 + lat) * pi/360.0))/(pi/180.0)
    my = (my * ORIGIN_SHIFT) /180.0
    res = INITIAL_RESOLUTION / (2**zoom)
    px = (mx + ORIGIN_SHIFT) / res
    py = (my + ORIGIN_SHIFT) / res
    return px, py

def pixelstolatlon(px, py, zoom):
    res = INITIAL_RESOLUTION / (2**zoom)
    mx = px * res - ORIGIN_SHIFT
    my = py * res - ORIGIN_SHIFT
    lat = (my / ORIGIN_SHIFT) * 180.0
    lat = 180 / pi * (2*atan(exp(lat*pi/180.0)) - pi/2.0)
    lon = (mx / ORIGIN_SHIFT) * 180.0
    return lat, lon

############################################

# a neighbourhood in Lajeado, Brazil:

upperleft =  '-29.44,-52.0'  
lowerright = '-29.45,-51.98'

zoom = 18   # be careful not to get too many images!

############################################

ullat, ullon = map(float, upperleft.split(','))
lrlat, lrlon = map(float, lowerright.split(','))

# Set some important parameters
scale = 1
maxsize = 640

# convert all these coordinates to pixels
ulx, uly = latlontopixels(ullat, ullon, zoom)
lrx, lry = latlontopixels(lrlat, lrlon, zoom)

# calculate total pixel dimensions of final image
dx, dy = lrx - ulx, uly - lry

# calculate rows and columns
cols, rows = int(ceil(dx/maxsize)), int(ceil(dy/maxsize))

# calculate pixel dimensions of each small image
bottom = 120
largura = int(ceil(dx/cols))
altura = int(ceil(dy/rows))
alturaplus = altura + bottom


final = Image.new("RGB", (int(dx), int(dy)))
for x in range(cols):
    for y in range(rows):
        dxn = largura * (0.5 + x)
        dyn = altura * (0.5 + y)
        latn, lonn = pixelstolatlon(ulx + dxn, uly - dyn - bottom/2, zoom)
        position = ','.join((str(latn), str(lonn)))
        print x, y, position
        urlparams = urllib.urlencode({'center': position,
                                      'zoom': str(zoom),
                                      'size': '%dx%d' % (largura, alturaplus),
                                      'maptype': 'satellite',
                                      'sensor': 'false',
                                      'scale': scale})
        url = 'http://maps.google.com/maps/api/staticmap?' + urlparams
        f=urllib.urlopen(url)
        im=Image.open(StringIO.StringIO(f.read()))
        final.paste(im, (int(x*largura), int(y*altura)))
final.show()
person heltonbiker    schedule 27.10.2011
comment
Очень хорошо! Я заставил его работать, но мне нужно было внести несколько небольших изменений, как здесь: gist.github.com/BenElgar/ 0d5b3e7cc89cb2180c6e. Обратите внимание, что это Python 2, но заставить его работать в Python 3 не должно быть слишком сложно. Также имейте в виду, что это технически нарушает условия использования API статических карт, как подробно описано здесь: developers.google.com/maps/terms#section_10_1_3 - person Ben Elgar; 05.08.2015
comment
Вау, эти математики ужасны!! См. мой ответ: stackoverflow.com/a/50536888/1840698 - person enigmaticPhysicist; 26.05.2018
comment
ОШИБКА: вы должны сделать maxsize -= bottom перед его использованием, иначе alturaplus превысит ограничение в 640 пикселей, и API обрежет изображение. В частности, чтобы исправить это, просто удалите строку bottom = 120 и вставьте строки bottom = 120 и maxsize -= bottom ниже строки maxsize = 640. Также, возможно, захочется переименовать largura и altura в ширину и высоту соответственно. - person Wood; 26.07.2018

Вместо того, чтобы пытаться использовать ссылку для встраивания, вы должны напрямую обратиться к API Google, чтобы получить изображения в виде статической графики. Вот ссылка на API статических изображений Карт Google — похоже, вы можете просто передайте параметры long/lat в URL-адресе так же, как и для обычного встраиваемого. Например:

http://maps.googleapis.com/maps/api/staticmap?center=-30.027489,-51.229248&size=600x600&zoom=14&sensor=false

дает вам обзор на уровне улицы 600x600 с центром в координатах, которые вы указали выше, что, похоже, является Порту-Алегри в Бразилии. Теперь вы можете использовать urlopen и PIL, как предлагает Нед:

from cStringIO import StringIO
import Image
import urllib

url = "http://maps.googleapis.com/maps/api/staticmap?center=-30.027489,-51.229248&size=800x800&zoom=14&sensor=false"
buffer = StringIO(urllib.urlopen(url).read())
image = Image.open(buffer)
person Daniel Roseman    schedule 20.09.2011
comment
Это моя альтернатива на данный момент, но я все еще хочу попытаться сделать это по плану Б, поскольку для создания тех изображений мегапиксельного размера, к которым я стремлюсь, потребуется сшивание изображений. Получение координат для углов изображения не всегда легко или очевидно, и, поскольку встроенные карты объединяются автоматически, возможно, изучение javascript API может дать некоторое представление. Спасибо за ваши советы и за хороший фрагмент кода! - person heltonbiker; 21.09.2011

Изменить: код в этом ответе был улучшен и упрощен, здесь: https://stackoverflow.com/ а/50536888/5859283


Основываясь на отличном ответе от heltonbiker с изменениями от BenElgar, ниже приведен обновленный код для Python 3 и добавлен доступ к ключу API, надеюсь, это кому-нибудь пригодится:

"""
Stitch together Google Maps images from lat, long coordinates
Based on work by heltonbiker and BenElgar
Changes: 
  * updated for Python 3
  * added Google Cloud Static Maps API key field (now required for access)
  * handle http request exceptions
"""

import requests
from io import BytesIO
from math import log, exp, tan, atan, pi, ceil
from PIL import Image
import sys

EARTH_RADIUS = 6378137
EQUATOR_CIRCUMFERENCE = 2 * pi * EARTH_RADIUS
INITIAL_RESOLUTION = EQUATOR_CIRCUMFERENCE / 256.0
ORIGIN_SHIFT = EQUATOR_CIRCUMFERENCE / 2.0
GOOGLE_MAPS_API_KEY = 'change this to your API key'

def latlontopixels(lat, lon, zoom):
    mx = (lon * ORIGIN_SHIFT) / 180.0
    my = log(tan((90 + lat) * pi/360.0))/(pi/180.0)
    my = (my * ORIGIN_SHIFT) /180.0
    res = INITIAL_RESOLUTION / (2**zoom)
    px = (mx + ORIGIN_SHIFT) / res
    py = (my + ORIGIN_SHIFT) / res
    return px, py

def pixelstolatlon(px, py, zoom):
    res = INITIAL_RESOLUTION / (2**zoom)
    mx = px * res - ORIGIN_SHIFT
    my = py * res - ORIGIN_SHIFT
    lat = (my / ORIGIN_SHIFT) * 180.0
    lat = 180 / pi * (2*atan(exp(lat*pi/180.0)) - pi/2.0)
    lon = (mx / ORIGIN_SHIFT) * 180.0
    return lat, lon


def get_maps_image(NW_lat_long, SE_lat_long, zoom=18):
  
  ullat, ullon = NW_lat_long
  lrlat, lrlon = SE_lat_long
  
  # Set some important parameters
  scale = 1
  maxsize = 640
  
  # convert all these coordinates to pixels
  ulx, uly = latlontopixels(ullat, ullon, zoom)
  lrx, lry = latlontopixels(lrlat, lrlon, zoom)
  
  # calculate total pixel dimensions of final image
  dx, dy = lrx - ulx, uly - lry
  
  # calculate rows and columns
  cols, rows = int(ceil(dx/maxsize)), int(ceil(dy/maxsize))
  
  # calculate pixel dimensions of each small image
  bottom = 120
  largura = int(ceil(dx/cols))
  altura = int(ceil(dy/rows))
  alturaplus = altura + bottom
  
  # assemble the image from stitched
  final = Image.new("RGB", (int(dx), int(dy)))
  for x in range(cols):
      for y in range(rows):
          dxn = largura * (0.5 + x)
          dyn = altura * (0.5 + y)
          latn, lonn = pixelstolatlon(ulx + dxn, uly - dyn - bottom/2, zoom)
          position = ','.join((str(latn), str(lonn)))
          print(x, y, position)
          urlparams = {'center': position,
                        'zoom': str(zoom),
                        'size': '%dx%d' % (largura, alturaplus),
                        'maptype': 'satellite',
                        'sensor': 'false',
                        'scale': scale}
          if GOOGLE_MAPS_API_KEY is not None:
            urlparams['key'] = GOOGLE_MAPS_API_KEY
            
          url = 'http://maps.google.com/maps/api/staticmap'
          try:                  
            response = requests.get(url, params=urlparams)
            response.raise_for_status()
          except requests.exceptions.RequestException as e:
            print(e)
            sys.exit(1)
            
          im = Image.open(BytesIO(response.content))                  
          final.paste(im, (int(x*largura), int(y*altura)))
          
  return final

############################################

if __name__ == '__main__':
  
  # a neighbourhood in Lajeado, Brazil:
  NW_lat_long =  (-29.44,-52.0)
  SE_lat_long = (-29.45,-51.98)
  
  zoom = 18   # be careful not to get too many images!
  
  result = get_maps_image(NW_lat_long, SE_lat_long, zoom=18)
  result.show()
person 4Oh4    schedule 12.12.2017
comment
Используя это снова год спустя, ключ API теперь является обязательным для доступа. Учетная запись Google Cloud и ключ для Maps Static API — это то, что вам нужно. - person 4Oh4; 15.09.2020

Это Daniel Roseman answer для людей, использующих python 3.x:

Код Python 3.x:

from io import BytesIO
from PIL import Image
from urllib import request
import matplotlib.pyplot as plt # this is if you want to plot the map using pyplot

url = "http://maps.googleapis.com/maps/api/staticmap?center=-30.027489,-51.229248&size=800x800&zoom=14&sensor=false"

buffer = BytesIO(request.urlopen(url).read())
image = Image.open(buffer)

# Show Using PIL
image.show()

# Or using pyplot
plt.imshow(image)
plt.show()
person Tanasis    schedule 19.09.2016
comment
ОШИБКА: вы должны сделать maxsize -= bottom перед его использованием, иначе alturaplus превысит ограничение в 640 пикселей, и API обрежет изображение. В частности, чтобы исправить это, просто удалите строку bottom = 120 и вставьте строки bottom = 120 и maxsize -= bottom ниже строки maxsize = 640. Также, возможно, захочется переименовать largura и altura в ширину и высоту соответственно. - person Wood; 26.07.2018

Ответ @ 4Oh4 правильный, но математика намного сложнее, чем должна быть. Преобразования между градусами и радианами происходят гораздо чаще, чем нужно. Радиус Земли вызывается без всякой причины — он отменяется во всех расчетах. Смещение добавляется к координатам пикселей без всякой причины. Вырез логотипа намного больше, чем должен быть. И еще несколько мелочей, которые были прописаны в изменениях. Вот моя версия:

#!/usr/bin/env python
"""
Stitch together Google Maps images from lat, long coordinates
Based on work by heltonbiker and BenElgar
Changes: 
* updated for Python 3
* added Google Maps API key (compliance with T&C, although can set to None)
* handle http request exceptions

With contributions from Eric Toombs.
Changes:
* Dramatically simplified the maths.
* Set a more reasonable default logo cutoff.
* Added global constants for logo cutoff and max image size.
* Translated a couple presumably Portuguese variable names to English.
"""

import requests
from io import BytesIO
from math import log, exp, tan, atan, ceil
from PIL import Image
import sys

# circumference/radius
tau = 6.283185307179586
# One degree in radians, i.e. in the units the machine uses to store angle,
# which is always radians. For converting to and from degrees. See code for
# usage demonstration.
DEGREE = tau/360

ZOOM_OFFSET = 8
GOOGLE_MAPS_API_KEY = None  # set to 'your_API_key'

# Max width or height of a single image grabbed from Google.
MAXSIZE = 640
# For cutting off the logos at the bottom of each of the grabbed images.  The
# logo height in pixels is assumed to be less than this amount.
LOGO_CUTOFF = 32


def latlon2pixels(lat, lon, zoom):
    mx = lon
    my = log(tan((lat + tau/4)/2))
    res = 2**(zoom + ZOOM_OFFSET) / tau
    px = mx*res
    py = my*res
    return px, py

def pixels2latlon(px, py, zoom):
    res = 2**(zoom + ZOOM_OFFSET) / tau
    mx = px/res
    my = py/res
    lon = mx
    lat = 2*atan(exp(my)) - tau/4
    return lat, lon


def get_maps_image(NW_lat_long, SE_lat_long, zoom=18):

    ullat, ullon = NW_lat_long
    lrlat, lrlon = SE_lat_long

    # convert all these coordinates to pixels
    ulx, uly = latlon2pixels(ullat, ullon, zoom)
    lrx, lry = latlon2pixels(lrlat, lrlon, zoom)

    # calculate total pixel dimensions of final image
    dx, dy = lrx - ulx, uly - lry

    # calculate rows and columns
    cols, rows = ceil(dx/MAXSIZE), ceil(dy/MAXSIZE)

    # calculate pixel dimensions of each small image
    width = ceil(dx/cols)
    height = ceil(dy/rows)
    heightplus = height + LOGO_CUTOFF

    # assemble the image from stitched
    final = Image.new('RGB', (int(dx), int(dy)))
    for x in range(cols):
        for y in range(rows):
            dxn = width * (0.5 + x)
            dyn = height * (0.5 + y)
            latn, lonn = pixels2latlon(
                    ulx + dxn, uly - dyn - LOGO_CUTOFF/2, zoom)
            position = ','.join((str(latn/DEGREE), str(lonn/DEGREE)))
            print(x, y, position)
            urlparams = {
                    'center': position,
                    'zoom': str(zoom),
                    'size': '%dx%d' % (width, heightplus),
                    'maptype': 'satellite',
                    'sensor': 'false',
                    'scale': 1
                }
            if GOOGLE_MAPS_API_KEY is not None:
                urlparams['key'] = GOOGLE_MAPS_API_KEY

            url = 'http://maps.google.com/maps/api/staticmap'
            try:                  
                response = requests.get(url, params=urlparams)
                response.raise_for_status()
            except requests.exceptions.RequestException as e:
                print(e)
                sys.exit(1)

            im = Image.open(BytesIO(response.content))                  
            final.paste(im, (int(x*width), int(y*height)))

    return final

############################################

if __name__ == '__main__':
    # a neighbourhood in Lajeado, Brazil:
    NW_lat_long =  (-29.44*DEGREE, -52.0*DEGREE)
    SE_lat_long = (-29.45*DEGREE, -51.98*DEGREE)

    zoom = 18   # be careful not to get too many images!

    result = get_maps_image(NW_lat_long, SE_lat_long, zoom=18)
    result.show()
person enigmaticPhysicist    schedule 25.05.2018
comment
Это немного проще! - person 4Oh4; 22.01.2019
comment
Привет, Как я могу добавить линии и булавки на сгенерированную карту? Я задал этот вопрос здесь: stackoverflow.com/questions/56555592/ - person SingaporePythonProgrammer; 12.06.2019
comment
Используя это снова год спустя, ключ API теперь является обязательным для доступа. Учетная запись Google Cloud и ключ для Maps Static API — это то, что вам нужно. - person 4Oh4; 15.09.2020

Более лаконичный метод, совместимый с Python 2.x,

from io import BytesIO
import Image
import urllib

url = "http://maps.googleapis.com/maps/api/staticmap?center=52.50058,13.31316&size=800x800&zoom=14"
buffer = BytesIO(urllib.urlopen(url).read())
image = Image.open(buffer)
image.save("map.png")
person BBSysDyn    schedule 10.11.2016

Самый простой способ захватить/сохранить изображение статической карты Google (в формате png):

import requests

img = open('tmp.png','wb')
img.write(requests.get('https://maps.googleapis.com/maps/api/staticmap?center=33.0456,131.3009&zoom=12&size=320x385&key=YOUR_API_KEY').content)
img.close()
person Nabeel Ahmed    schedule 27.09.2017

urllib.urlopen откроет URL-адрес, результат будет иметь метод .read(), который вы можете использовать для получения байтов изображения. cStringIO имеет файлоподобный объект, основанный на строке в памяти. В PIL есть функция Image.open, которая открывает нечто похожее на файл, чтобы дать вам объект изображения. Объекты изображения могут быть запрошены об их значениях пикселей.

person Ned Batchelder    schedule 20.09.2011
comment
Использование urllib, как вы сказали, дает мне код javascript (я думаю), а не объект изображения. Это имеет смысл, так как встроенная карта позволяет осуществлять навигацию (она не генерируется с использованием статического API Google Maps, который сам по себе имеет серьезные ограничения на размер изображения). - person heltonbiker; 20.09.2011
comment
@heltonbiker: извините, я не знаю, как помочь с более сложной проблемой. Первоначальное описание звучало более прямолинейно. - person Ned Batchelder; 20.09.2011
comment
Я очень благодарен за ваше понимание и интерес. На самом деле, я использую ваше предложение со статическим API Google Maps. Заданная проблема, хотя, возможно, проще решается с помощью некоторого javascript напрямую. Если я найду хороший ответ, я опубликую его здесь. Спасибо еще раз! - person heltonbiker; 21.09.2011