瓦片批量下载与拼接程序2

Jul 12,2017   10591 words   38 min

Tags: Python

在之前的这篇博客中,实现了瓦片批量下载和拼接。针对发现的几个问题,对之前的代码做了一些修改。

一、瓦片下载

考虑到下载瓦片有可能出现中途错误退出的情况,因此增加了“断点续传”功能。 例如如果上次下载带20%突然意外程序退出了,那么下次重新输入和之前一样的数据,程序便会自动检查哪些瓦片下载过了。 然后从没有下载的瓦片开始下载。对于已经下载好的瓦片,如果不是“黑色”,则直接跳过。 如果有瓦片但是黑色的,重新尝试下载。 加入这个功能后,也可以利用程序对下载好的瓦片进行检查了。 程序会尝试重新下载黑色瓦片。

1.修改后的代码

修改后的完整代码如下:

# coding=utf-8
import urllib2 as ulb
import numpy as np
import PIL.ImageFile as ImageFile
import cv2
import math
import random
import time
import os

# 免费代理IP不能保证永久有效,如果不能用可以更新
# https://www.goubanjia.com/
proxy_list = [
    '117.143.109.167:80',
    '117.143.109.130:80',
    '117.143.109.136:80',
    '117.143.109.163:80',
    '61.136.163.245:3128',
    '180.97.250.89:80',
    '180.97.250.90:80',
    '117.143.109.161:80',
    '166.111.77.32:80',
    '61.136.163.245:3128'
]

# 收集到的常用Header
my_headers = [
    "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14",
    "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)",
    'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11',
    'Opera/9.25 (Windows NT 5.1; U; en)',
    'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)',
    'Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.5 (like Gecko) (Kubuntu)',
    'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12',
    'Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.2.9',
    "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.7 (KHTML, like Gecko) Ubuntu/11.04 Chromium/16.0.912.77 Chrome/16.0.912.77 Safari/535.7",
    "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:10.0) Gecko/20100101 Firefox/10.0 "
]

# 用于存放获取失败瓦片的url、path
err_urls = []
err_paths = []

# 记录经过尝试仍然失败的瓦片
err_final_url = []

t = 0.1
maxTryNum = 5
z = 18

# 获取瓦片函数
def getTile(url):
    # 每次执行前先暂停t秒
    time.sleep(t)

    # 随机选择IP、Header
    proxy = random.choice(proxy_list)
    header = random.choice(my_headers)

    print proxy, 'sleep:', t, header

    # 基于选择的IP构建连接
    urlhandle = ulb.ProxyHandler({'http': proxy})
    opener = ulb.build_opener(urlhandle)
    ulb.install_opener(opener)

    # 按照最大尝试次数连接
    for tries in range(maxTryNum):
        try:
            # 用urllib2库链接网络图像
            response = ulb.Request(url)

            # 增加Header伪装成浏览器
            response.add_header('User-Agent', header)
            # 打开网络图像文件句柄
            fp = ulb.urlopen(response)

            # 定义图像IO
            p = ImageFile.Parser()

            # 开始图像读取
            while 1:
                s = fp.read(1024)
                if not s:
                    break
                p.feed(s)

            # 得到图像
            im = p.close()
            # 将图像转换成numpy矩阵
            arr = np.array(im)
            # 将通道顺序变成BGR,以便OpenCV可以正确保存
            arr = arr[:, :, ::-1]
            return arr
        # 抛出异常
        except ulb.HTTPError, e:
            # 持续尝试
            if tries < (maxTryNum - 1):
                # 404错误直接退出
                if e.code == 404:
                    print '***404 Not Found***'
                    arr = np.zeros((256, 256, 3), np.uint8)
                    # 将该url、path记录到list中
                    err_urls.append(url)
                    err_paths.append(path)
                    break
                # 403错误直接退出
                elif e.code == 403:
                    print '!!!403 Forbidden!!!'
                    arr = np.zeros((256, 256, 3), np.uint8)
                    err_urls.append(url)
                    err_paths.append(path)
                    break
                # 打印尝试次数
                print (tries + 1), "time(s) to access", url
                continue
            else:
                # 输出失败信息
                print "Has tried", maxTryNum, "times to access", url, ", all failed!"
                arr = np.zeros((256, 256, 3), np.uint8)
                err_urls.append(url)
                err_paths.append(path)
    # 统一返回arr
    return arr

# 用于对失败的瓦片重新获取
def errTile(url):
    # 每次执行前先暂停t秒
    time.sleep(t)

    # 随机选择IP、Header
    proxy = random.choice(proxy_list)
    header = random.choice(my_headers)

    print proxy, 'sleep:', t, header

    # 基于选择的IP构建连接
    urlhandle = ulb.ProxyHandler({'http': proxy})
    opener = ulb.build_opener(urlhandle)
    ulb.install_opener(opener)

    # 按照最大尝试次数连接
    for tries in range(maxTryNum):
        try:
            # 用urllib2库链接网络图像
            response = ulb.Request(url)

            # 增加Header伪装成浏览器
            response.add_header('User-Agent', header)
            # 打开网络图像文件句柄
            fp = ulb.urlopen(response)

            # 定义图像IO
            p = ImageFile.Parser()

            # 开始图像读取
            while 1:
                s = fp.read(1024)
                if not s:
                    break
                p.feed(s)

            # 得到图像
            im = p.close()
            # 将图像转换成numpy矩阵
            arr = np.array(im)
            # 将通道顺序变成BGR,以便OpenCV可以正确保存
            arr = arr[:, :, ::-1]
            return arr
        # 抛出异常
        except ulb.HTTPError, e:
            # 持续尝试
            if tries < (maxTryNum - 1):
                # 404错误直接退出
                if e.code == 404:
                    print '***404 Not Found***'
                    arr = np.zeros((256, 256, 3), np.uint8)
                    err_final_url.append(url)
                    break
                # 403错误直接退出
                elif e.code == 403:
                    print '!!!403 Forbidden!!!'
                    arr = np.zeros((256, 256, 3), np.uint8)
                    err_final_url.append(url)
                    break
                # 打印尝试次数
                print (tries + 1), "time(s) to access", url
                continue
            else:
                # 输出失败信息
                print "Has tried", maxTryNum, "times to access", url, ", all failed!"
                arr = np.zeros((256, 256, 3), np.uint8)
                err_final_url.append(url)
    # 统一返回arr
    # 将通道顺序变成BGR,以便OpenCV可以正确保存
    arr = arr[:, :, ::-1]
    return arr


# 由x、y、z计算瓦片行列号
def calcXY(lat, lon, z):
    x = math.floor(math.pow(2, int(z) - 1) * ((lon / 180.0) + 1))
    tan = math.tan(lat * math.pi / 180.0)
    sec = 1.0 / math.cos(lat * math.pi / 180.0)
    log = math.log(tan + sec)
    y = math.floor(math.pow(2, int(z) - 1) * (1 - log / math.pi))
    return int(x), int(y)


# 字符串度分秒转度
def cvtStr2Deg(deg, min, sec):
    result = int(deg) + int(min) / 60.0 + float(sec) / 3600.0
    return result


# 获取经纬度
def getNum(str):
    split = str.split(',')
    du = split[0].split('°')[0]
    fen = split[0].split('°')[1].split('\'')[0]
    miao = split[0].split('°')[1].split('\'')[1].split('"')[0]
    split1 = cvtStr2Deg(du, fen, miao)
    du = split[1].split('°')[0]
    fen = split[1].split('°')[1].split('\'')[0]
    miao = split[1].split('°')[1].split('\'')[1].split('"')[0]
    split2 = cvtStr2Deg(du, fen, miao)
    return split1, split2


# 获取经纬度
def getNum2(str):
    split = str.split(',')
    split1 = float(split[0].split('N')[0])
    split2 = float(split[1].split('E')[0])
    return split1, split2


# 用户输入更新后的IP文件,如果没有则用代码中的默认IP
ip_path = raw_input("Input the path of IP list file(input \'no\' means use default IPs):\n")
# 判断是否输入IP文件
if ip_path != 'no':
    proxy_list = []
    file = open(ip_path)
    lines = file.readlines()
    for line in lines:
        proxy_list.append(line.strip('\n'))
    print proxy_list.__len__(), 'IPs are loaded.'

# 输入两次请求间的暂停时间
t = input("Input the interval time(second) of requests(e.g. 0.1):\n")

# 输入最大尝试连接次数
maxTryNum = input("Input max  number of try connection(e.g. 5):\n")

# 输入影像层数
z = raw_input("Input image level(0-18):\n")

# 输入左上角点经纬度并计算行列号
lt_raw = raw_input("Input lat & lon at left top(e.g. 30.52N,114.36E):\n")
lt_lat, lt_lon = getNum2(lt_raw)
lt_X, lt_Y = calcXY(lt_lat, lt_lon, z)

# 输入右下角点经纬度并计算行列号
rb_raw = raw_input("Input lat & lon at right bottom(e.g. 30.51N,114.37E):\n")
rb_lat, rb_lon = getNum2(rb_raw)
rb_X, rb_Y = calcXY(rb_lat, rb_lon, z)

# 计算行列号差值及瓦片数
cols = rb_X - lt_X
rows = rb_Y - lt_Y
tiles = cols * rows
count = 0

# 判断结果是否合理
if tiles <= 0:
    print 'Please check your input.'
    exit()
print tiles.__str__() + ' tiles will be downloaded.'

# 输入保存路径
base = raw_input("Input save path:\n")

print 'Now start...'

# 循环遍历,下载瓦片
for i in range(rows):
    for j in range(cols):
        # 拼接url
        url = 'https://mt2.google.cn/vt/lyrs=s&hl=zh-CN&gl=CN&x=' + (j + lt_X).__str__() + '&y=' + (
            i + lt_Y).__str__() + '&z=' + z.__str__()
        # 拼接输出路径
        path = base + '\\' + z.__str__() + '_' + (j + lt_X).__str__() + '_' + (i + lt_Y).__str__() + '.jpg'

        # 判断是否已有瓦片文件
        if os.path.exists(path):
            temp_tile = cv2.imread(path, 0)
            # 判断文件内容是否为空
            if temp_tile[100, 100] == 0:
                # 获取瓦片
                tile = getTile(url)
                # 保存瓦片
                cv2.imwrite(path, tile)
                # 计数变量增加
                count = count + 1
                # 输出进度信息
                print (round((float(count) / float(tiles)) * 100, 2)).__str__() + " % finished"
            else:
                # 计数变量增加
                count = count + 1
                print (round((float(count) / float(tiles)) * 100, 2)).__str__() + " % finished"
            continue
        else:
            # 获取瓦片
            tile = getTile(url)
            # 保存瓦片
            cv2.imwrite(path, tile)
            # 计数变量增加
            count = count + 1
            # 输出进度信息
            print (round((float(count) / float(tiles)) * 100, 2)).__str__() + " % finished"

# 输出下载完成信息
print rows * cols, 'in total,', (rows * cols - err_urls.__len__()), 'successful,', (err_urls.__len__()), 'unsuccessful.'

# 如果不成功瓦片列表不为0,再次尝试
if err_urls.__len__() != 0:
    print 'Trying for unsuccessful tiles again...'
    for k in range(err_urls.__len__()):
        # 获取瓦片
        tile = errTile(err_urls[k])
        # 保存瓦片
        cv2.imwrite(err_paths[k], tile)

# 如果最终不成功瓦片列表不为0,输出最终不成功瓦片url
if err_final_url.__len__() != 0:
    # 创建文件
    output = open(base + "\err_output.txt", 'w')
    output.write('Delete this file before join tiles together!\n')
    # 依次输出无法获取瓦片的url
    for i in range(err_final_url.__len__()):
        output.write(err_final_url[i] + '\n')
        print err_final_url[i]
    output.close()

二、瓦片拼接

1.范围计算bug

在使用时发现了拼图结果是正确的,但是计算的左上、右下角点经纬度计算错误,行列号也不对的情况。 如下所示,信息是错乱的。 经过排查,发现是读取瓦片存放到list中没有从小到大排序导致的问题。 如下图,可以看到读取的x和y的顺序大部分是对的,但是有些地方不太对。 按道理来说应该是从小到大增加的,但是从209844开始,就突然跳到了209776。这就产生了错误。 由于代码中是按照x、y列表的第一个元素和最后一个元素的组合来获取左上、右下瓦片的。 因此由于受到没有排序的影响会导致计算的并不是左上、右下对应的经纬度。在利用sort()函数对x、y的list进行排序后问题解决。

2.内存占用过大

在拼接很多瓦片的过程中发现内存占用太多,在我的笔记本上(6G内存),内存占用一度达到了99%,电脑也变得很卡。 因此针对这个问题进行了优化。主要是两方面,一是将很多存放中间变量的list使用完成后就置空释放内存。 二是分列读取影像,尽可能减少一次性写入内存的数据。之前采用的方式是一次性把所有瓦片装进内存进行处理。 优化后是一次将一列瓦片读取拼接放入list,拼接完成后删除该列影像数据。 优化后经过测试,拼接一张含5589个瓦片的大图,内存(共6G)初始状态是47%,运行时增加到60%,内存瞬间峰值为72%。 在运行过程中,电脑没有出现卡顿现象,还在写着这篇博客。相比于之前,性能提升效果明显。 上图是拼接后的大图,包含69行、81列,分辨率为17664×20736,大小为91.9MB。

3.细节优化

在之前的程序中,无论拼接后的图像有多大,都会显示一下,其实没有这个必要。 例如上面的17664×20736影像,即使显示了也没有什么意义,而且还会占用大量内存。 因此优化后设置了判断机制,如果拼接后影像长或宽大于1080像素,则不显示了。如果小于这个大小,则显示。

4.修改后的代码
# coding=utf-8
import cv2
import numpy as np
import os.path
import math


# 计算经纬度
def calcLatLon(x, y, z, m, n):
    lon = (math.pow(2, 1 - z) * (x + m / 256.0) - 1) * 180.0
    lat = (360 * math.atan(math.pow(math.e, (1 - math.pow(2, 1 - z) * (y + n / 256.0)) * math.pi))) / math.pi - 90
    return lat, lon


layer = 18

# 记录x、y、瓦片
xs = []
ys = []
imgs = []
paths = []

# 用户输入存放影像的文件夹目录,如E:\L0
rootdir = raw_input("Input the parent path of images:\n") + "\\"

for parent, dirname, filenames in os.walk(rootdir):
    for filename in filenames:
        name = parent + filename
        # 附加到list
        paths.append(name)

        # 提取x、y、layer并保存在list中
        filename = filename.split('.')
        str = filename[0].split('_')
        if xs.__len__() == 0:
            layer = int(str[0])
        x = int(str[1])
        y = int(str[2])
        xs.append(x)
        ys.append(y)

print 'Images are loaded.'

# 去除list中的重复元素并排序
xs = list(set(xs))
xs.sort()
ys = list(set(ys))
ys.sort()

# 用于存放每一列的拼图
v_lines = []

# 先按照竖直方向拼成条带
for i in range(0, paths.__len__(), ys.__len__()):
    for item in paths[i:i + ys.__len__()]:
        img = cv2.imread(item)
        imgs.append(img)
    v_line = tuple(imgs)
    imgs = []
    v_tuple = np.vstack(v_line)
    v_lines.append(v_tuple)
    print 'Join images', round((i * 1.0 / paths.__len__()) * 100, 2), '% finished'

v_tuple = tuple(v_lines)

# 清空v_lines释放内存
v_lines = []

# 清空paths释放
paths = []

# 再按水平方向拼接
final = np.hstack(v_tuple)

# 清空v_tuple释放内存
v_tuple = []

print 'Writing image...'

# 输出拼接后的图像
cv2.imwrite(parent + "output.jpg", final)

# 输出相关信息
output = open(parent + "output.txt", 'w')
output.write('north-west point (x,y):' + (xs[0], ys[0]).__str__() + "\n")
output.write('north-west point (lat,lon):' + calcLatLon(xs[0], ys[0], layer, 0, 0).__str__() + "\n")
output.write('south-east point (x,y):' + (xs[-1], ys[-1]).__str__() + "\n")
output.write('south-east point (lat,lon):' + calcLatLon(xs[-1], ys[-1], layer, 255, 255).__str__() + "\n")
output.write('rows:' + xs.__len__().__str__() + "\n")
output.write('columns:' + ys.__len__().__str__() + "\n")
output.write('Output image size:' + final.shape[1].__str__() + ' * ' + final.shape[0].__str__())
output.close()

# 控制台中打印相关信息
print '\n'
print 'north-west point (x,y):', (xs[0], ys[0]).__str__()
print 'north-west point (lat,lon):', calcLatLon(xs[0], ys[0], layer, 0, 0)
print 'south-east point (x,y):', (xs[-1], ys[-1]).__str__()
print 'south-east point (lat,lon):', calcLatLon(xs[-1], ys[-1], layer, 255, 255)
print 'rows:', xs.__len__()
print 'columns:', ys.__len__()
print 'Output image size:', final.shape[1].__str__(), '*', final.shape[0].__str__()

print "------------------------"
print "Output files info:"
print parent + "output.jpg"
print parent + "output.txt"
print "------------------------"

# 显示图像,如果图片宽高大于1080则太大,就不在窗口显示了
if final.shape[1] <= 1080 and final.shape[0] <= 1080:
    cv2.imshow("final", final)
    cv2.waitKey(0)
else:
    pass

本文作者原创,未经许可不得转载,谢谢配合

返回顶部