Google街景全景图下载

 描述:

        下载指定两个坐标点间或某区域的全景图片,由于是访问Google,所以网络方面需要

        本文档仅供学习分享,提供思路

目的:

        本地实现静态仿Google街景效果

参考资料:

        Google文档:https://developers.google.com/maps/documentation/streetview/request-streetview?hl=en

        全景图下载:https://github.com/Gage-Irwin/Street-View-Downloader

        Google服务Python库:https://github.com/googlemaps/google-maps-services-python

目录

一、前提准备

二、获取Pano_id

2.1 获取两个坐标点间Pano_id

2.2 获取指定区域的Pano_id

三、下载全景图


一、前提准备

  1. 魔法工具(自行查找)
  2. 注册Google API key

二、获取Pano_id

Pano_id为Google街景全景图唯一ID

2.1 获取两个坐标点间Pano_id

Google街景全景图下载_第1张图片

获取两点间距

def geo_distance(lat1, lon1, lat2, lon2):
    """
    计算两点间距离(m)
    """
    return geodesic((lat1, lon1), (lat2, lon2)).m

计算方位角

def geo_degree(lat1, lon1, lat2, lon2):
    """
    公式计算两点间方位角
    方位角:是与正北方向、顺时针之间的夹角
    """
    lat1_rad = lat1 * math.pi / 180
    lon1_rad = lon1 * math.pi / 180
    lat2_rad = lat2 * math.pi / 180
    lon2_rad = lon2 * math.pi / 180

    y = math.sin(lon2_rad - lon1_rad) * math.cos(lat2_rad)
    x = math.cos(lat1_rad) * math.sin(lat2_rad) - \
        math.sin(lat1_rad) * math.cos(lat2_rad) * math.cos(lon2_rad - lon1_rad)

    brng = math.atan2(y, x) * 180 / math.pi

    return (brng + 360.0) % 360.0

按步长拆分间距,根据方位角预判点位置

def get_fill_points(location1, location2, distance, step, c=True):
    lng_diff = location2[1] - location1[1]
    lat_diff = location2[0] - location1[0]
    n = math.ceil(distance / step)
    points = list()
    if n != 0:
        a = lng_diff / n
        b = lat_diff / n
        for i in range(n):
            lng = location1[1] + a * i
            lat = location1[0] + b * i
            points.append((round(lat, 7), round(lng, 7)))
    if c:
        points.insert(0, location1)
        points.append(location2)
    return points

根据经纬度、方位角、距离计算目标经纬度

def get_location(lat, lon, distance, degree):
    """
    根据经纬度、方位角、距离计算目标经纬度
    """
    RE = 6371.393 * 1000  # 赤道半径,单位:m
    re = RE * math.cos(lat / 180 * math.pi)  # 纬度切面的半径
    lon2 = lon + distance * math.sin(degree / 180 * math.pi) / (re * 2 * math.pi) * 360  # 求切面长度占比,与已知点经度相加
    lat2 = lat + distance * math.cos(degree / 180 * math.pi) / (re * 2 * math.pi) * 360
    return lat2, lon2

完成代码为

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import sys
import csv
import math
import time

import requests

import geo_distance, geo_degree, get_location, get_fill_points

def join_url(url: str, filed_dict: dict):
    """
    拼接URL参数
    """
    filed_list = []
    for k, v in filed_dict.items():
        filed_list.append(str(k) + "=" + str(v))
    return url + "?" + "&".join(filed_list)


def generate_location(location1: tuple, location2: tuple, step):
    """
    生成位置信息
    """
    m = math.ceil(geo_distance(*location1, *location2))
    degree = geo_degree(*location1, *location2)
    for i in range(0, m + 1, step):
        lat, lon = get_location(*location1, i, degree)
        yield round(lat, 7), round(lon, 7)


def write_csv(urls, write_path, file_name, pano_ids, data_list):
    headers = {
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36'
    }
    for i in range(len(urls)):
        if urls:
            url = urls.pop()
        else:
            continue
        try:
            response = requests.get(url, headers=headers)
        except Exception as e:
            print(e)
            urls.append(url)
            write_csv(urls, write_path, file_name, pano_ids, data_list)
        else:
            if response.status_code != 200:
                print(f"{url}: 请求失败")
            else:
                data = response.json()
                if data["status"] == "OK":
                    print(url)
                    # print(data)
                    pano_id = data.get("pano_id")
                    if pano_id not in pano_ids:
                        data_list.insert(1, [
                            url.split("location=")[1].split(",")[0],
                            url.split("location=")[1].split(",")[1],
                            pano_id
                        ])
                        pano_ids.append(pano_id)
                else:
                    print(f"该位置{url}无图片")
                    data_list.append([
                        url.split("location=")[1].split(",")[0],
                        url.split("location=")[1].split(",")[1],
                        ""
                    ])
        time.sleep(0.03)

    file_name = file_name + ".csv"
    if not os.path.exists(write_path):
        os.makedirs(write_path)
    path = os.path.join(write_path, file_name)
    if not os.path.exists(path):
        print(f"write to {path}")
        print(f"获取有效图片{len(data_list)}")
        for index, data in enumerate(data_list):
            if index == 0:
                continue
            data.insert(0, str(index))
        col = csv.writer(open(path, 'w', newline=""))
        col.writerows(data_list)


def start(road_data: list, write_path: str, step=5):
    s_url = "https://maps.googleapis.com/maps/api/streetview/metadata"
    _dict = {
        "key": "your Google key",
        "location": ""
    }
    url_list = []
    for road_locations, road_name in road_data:
        pano_ids = []
        data_list = [["id", "lat", "lng", "pano_id"]]
        _roads = road_locations.split("|")
        for index, road in enumerate(_roads):
            try:
                location1 = (float(road.split("_")[1]), float(road.split("_")[0]))
                location2 = (float(_roads[index + 1].split("_")[1]), float(_roads[index + 1].split("_")[0]))
            except IndexError as e:
                print(e)
                _dict["location"] = str(road.split("_")[1]) + "," + str(road.split("_")[0])
                url_list.append(join_url(s_url, _dict))
                continue
            m = math.ceil(geo_distance(*location1, *location2))
            # 较长直线距离,根据方位角计算
            print(f"两点{location1} > {location2}距离{m}, 开始计算直线坐标点,步长为:{step}")
            points = get_fill_points(location1, location2, m, step)
            for point in points:
                _dict["location"] = str(point[0]) + "," + str(point[1])
                url_list.append(join_url(s_url, _dict))
        print(url_list)
        print(f"该道路:{road_name}有{len(url_list)}个坐标点")
        write_csv(url_list, write_path, road_name, pano_ids, data_list)


if __name__ == '__main__':
    _path = "./"
    roads = ["121.0125937_24.8587082|121.012562_24.8586107", "五一路"]
    start(roads, _path)

自定义修改main方法后,执行代码,生成指定道路的pano_id csv文件

id,lat,lng,pano_id
1,24.8660544,120.9971263,e_mIi84_ZEfqSaJ-I2cAtA
2,24.8659941,120.9970576,byKKp4vuDwVhQ6fTE2h_Kw
3,24.8659337,120.9969889,JbPNGE6ZRMrPwgTt-86fOg
4,24.8658733,120.9969201,yjzAWCjp2x-5-TfPIF2AmA
5,24.865813,120.9968514,8LRF3xd_GHrasdAi7IkwWw

2.2 获取指定区域的Pano_id

使用street view download 360(需付费)工具下载Pano_id后,执行下载脚本。

三、下载全景图

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import time
import requests
from PIL import Image
from io import BytesIO


# helper functions in increase image down and right
def increase_right(img, pixels):
    width, height = img.size
    new_width = width + pixels
    result = Image.new(img.mode, (new_width, height), (250, 250, 250))
    result.paste(img, (0, 0))
    return result


def increase_down(img, pixels):
    width, height = img.size
    new_height = height + pixels
    result = Image.new(img.mode, (width, new_height), (250, 250, 250))
    result.paste(img, (0, 0))
    return result


class GMAP360:

    def __init__(
            self,
            sv_ids: list = None,
            download_path: str = None,
            zoom: int = 4,
            retry: int = 5,
            overwrite: bool = False,
    ):
        self.street_info = sv_ids
        if not os.path.exists(download_path):
            os.makedirs(download_path)
        self.download_path = download_path
        self.retry = retry
        # only zoom levels from 1-5 are allowed
        if 5 < zoom < 0:
            raise Exception(f"Incorrect zoom size: {zoom}, only sizes 1-5 are allowed.")
        self.zoom = zoom
        self.overwrite = overwrite
        self.start()

    def download_street_view(self, location_id: str, lat: str, lng: str, id: str):
        # Street view 360 images are loaded in chunks.
        # The chunks are layed out in a 2D plane with an X and Y cordinate.
        # The number of chunks is based on the resolution of the full image.
        # You can not get the resolution of the the full image so we loop over X and Y till we reach a 400 status code.

        # to try to prevent throttling by using a browser user agent
        headers = {
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.82 Safari/537.36'}
        # start from 0,0
        X = Y = 0
        # variable to know when the column ends so we don't have to check when each column ends.
        end_column = 0
        # the zoom level determines the size of each chunk
        # zoom level 1-4 are 512 and 5 is 256
        block_size = 512 if self.zoom <= 4 else 256
        # loop over X positions of chunks
        while True:
            # loop over Y positions of chunks
            while True:
                # if the current Y position is at the end don't waste a GET requests
                if Y == end_column and Y != 0:
                    break
                # build url for chunk
                response = requests.get(
                    f"https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=maps_sv.tactile&panoid={location_id}&x={X}&y={Y}&zoom={self.zoom}&nbt=1&fover=2",
                    headers=headers)
                if response.status_code == 400:
                    end_column = Y
                    break
                # load image from responce
                image = Image.open(BytesIO(response.content))
                # if first chunk create new image
                if X == 0 and Y == 0:
                    # create empty panorama image of size block_size by block_size
                    panorama = Image.new('RGB', (block_size, block_size), (250, 250, 250))
                elif end_column == 0 and Y != 0:
                    #
                    panorama = increase_down(panorama, block_size)
                # paste loaded image into panorama image
                panorama.paste(image, (X * block_size, Y * block_size))
                # move to next Y position
                Y += 1
                time.sleep(0.01)
            # reset Y position
            Y = 0
            # move to next X position
            X += 1
            response = requests.get(
                f"https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=maps_sv.tactile&panoid={location_id}&x={X}&y={Y}&zoom={self.zoom}&nbt=1&fover=2",
                headers=headers)
            # if no more X positions then we are done
            if response.status_code == 400:
                break
            # load image from responce
            image = Image.open(BytesIO(response.content))
            # increase panorama image right by block_size amount
            panorama = increase_right(panorama, block_size)
            # paste loaded image into panorama image
            panorama.paste(image, (X * block_size, Y * block_size))
        # save panorama image
        panorama.save(os.path.join(self.download_path, f"{id}_{lat}_{lng}.jpg"), format="JPEG")

    def download(self, location_id: str, retry: int, lat: str, lng: str, id: str):
        # wrapper function for self.download_street_view() to allow retrying
        try:
            self.download_street_view(location_id, lat, lng, id)
        except Exception as e:
            print('Download Failed!')
            print(e)
            if retry > 0:
                print('Retrying...')
                self.download(location_id, retry - 1, lat, lng, id)

    def start(self):
        for index, street in enumerate(self.street_info):
            id, lat, lng, location_id = street
            if not self.overwrite and os.path.exists(os.path.join(self.download_path, f"{id}_{lat}_{lng}.jpg")):
                print(f"Skipping download | Image {id}_{lat}_{lng}.jpg already exists")
                continue
            print(f"Downloading image {id}_{lat}_{lng}.jpg {index}/{len(self.street_info)}")
            if location_id:
                self.download(location_id, self.retry, lat, lng, id)


def generate_ids(file_path):
    with open(file_path, "r") as f:
        f.readline()
        return [data.replace("\n", "").split(",") for data in f.readlines()]


if __name__ == '__main__':
    import sys

    try:
        s_path = sys.argv[1]
        d_path = sys.argv[2]
    except:
        s_path = "./"
        d_path = "./"
    if not os.path.exists(s_path):
        print(f"查无此文件或目录:{s_path}")

    # 下载颗粒度,0-5,颗粒度越大,请求次数越多
    zoom = 3
    # 重重次数
    retry = 3
    # 覆盖以前创建的任何文件。
    overwrite = False
    if os.path.isdir(s_path):
        for file_name in os.listdir(s_path):
            if file_name.endswith(".csv"):
                csv_path = os.path.join(s_path, file_name)
                # 图片ID
                street_info = generate_ids(csv_path)
                # 存储路径
                f_name = os.path.splitext(file_name)[0]
                output_path = os.path.join(
                    d_path,
                    str(zoom),
                    f_name
                )
                print(output_path)
                GMAP360(
                    sv_ids=street_info,
                    download_path=output_path,
                    zoom=zoom, retry=retry,
                    overwrite=overwrite,
                )
    else:
        csv_path = s_path
        # 图片ID
        street_info = generate_ids(csv_path)
        # 存储路径
        f_name = os.path.splitext(os.path.basename(s_path))[0]
        output_path = os.path.join(
            d_path,
            str(zoom),
            f_name
        )
        print(output_path)
        GMAP360(
            sv_ids=street_info,
            download_path=output_path,
            zoom=zoom, retry=retry,
            overwrite=overwrite,
        )

使用说明:

  1. 修改源码支持参数

支持参数

类型

说明

zoom

int

下载颗粒度:0-5

颗粒度越大,请求次数越多,图片越清晰

retry

int

请求重试次数

overwrite

bool

是否覆盖已有文件

  1. 执行脚本
python street_download [panoid_path] [images_path]
# panoid_path:panoid文件位置
# images_path:保存图片路径

你可能感兴趣的:(python)