pythonpython-3.xgoogle-mapspython-imaging-librarygoogle-maps-static-api

How to scale an overlay image based on the Google Maps zoom level


I am trying to paste an overlay image on a static google map screenshot. Here is the code snippet -

import requests
from PIL import Image
import io

API_KEY = '***'

plot_center =  [75.49927187059548, 26.613400716542262]

zoom = 18
image_size = "640x480"

# Download Google image
url = f"https://maps.googleapis.com/maps/api/staticmap?center={plot_center[1]},{plot_center[0]}&zoom={zoom}&size={image_size}&key={API_KEY}&maptype=satellite"
response = requests.get(url)
google_image = Image.open(io.BytesIO(response.content))


# Download the overlay image
response = requests.get("https://storage.googleapis.com/kawa-public/4ed05009-8a61-4eb9-8b17-faa8d0d65ae3/kvi/kvi-s2-2023-07-03T15-01-25.838Z.png")
overlay_image = Image.open(io.BytesIO(response.content))


output_image = Image.new("RGBA", google_image.size)
output_image.paste(google_image, (0, 0))

overlay_position = (
    (google_image.width - overlay_image.width) // 2,
    (google_image.height - overlay_image.height) // 2,
)

output_image.paste(overlay_image, overlay_position, mask=overlay_image)
output_image.save("output_image.png")

The current output just pastes the overlay image to the center of the map which -

enter image description here

I want the overlay image to cover the farm plot in which it is currently in.


Solution

  • Google Maps uses a Mercator projection, so it uses this formula:

    meters_per_pixel = 156543.03392 * Math.cos(latLng.lat() * Math.PI / 180) / Math.pow(2, zoom)

    We can deduct from this formula that for a given location, the only variable is the zoom, and therefore the meters per pixel are inversely proportional to 2^zoom.

    You can figure out a correct factor of the size of your overlay image for a given zoom (let's say 18 for precision), then divide that factor by 2 for each unzoom operation.

    With some trial and error, I found that 3.8 is a correct overlay zoom factor for a zoom of 18, then you can divide it by 2 for each zoom less, giving this code:

    # ...
    overlay_image = Image.open(io.BytesIO(response.content))
    factor = 3.8 / (2 ** (18 - zoom))
    overlay_image = overlay_image.resize(
        (int(overlay_image.width * factor), int(overlay_image.height * factor))
    )
    # ...