创建ASCII艺术世界地图

时间:2019-03-28 20:06:57

标签: python gis shapely map-projections ascii-art

我想根据this GeoJSON file渲染ASCII艺术世界地图。

我的基本方法是将GeoJSON加载到Shapely中,使用pyproj将点转换为Mercator,然后对ASCII艺术网格的每个字符的几何形状进行命中测试。

将本初子午线居中时,看起来(编辑:主要是)确定:

centered at lon = 0

但是以纽约市(lon_0=-74)为中心,却突然陷入僵局:

enter image description here

我相当确定我对这里的预测做错了。 (而且将ASCII地图坐标转换为经/纬度可能比转换整个几何图形更有效,但我不确定如何。)

import functools
import json
import shutil
import sys

import pyproj
import shapely.geometry
import shapely.ops


# Load the map
with open('world-countries.json') as f:
  countries = []
  for feature in json.load(f)['features']:
    # buffer(0) is a trick for fixing polygons with overlapping coordinates
    country = shapely.geometry.shape(feature['geometry']).buffer(0)
    countries.append(country)

mapgeom = shapely.geometry.MultiPolygon(countries)

# Apply a projection
tform = functools.partial(
  pyproj.transform,
  pyproj.Proj(proj='longlat'),  # input: WGS84
  pyproj.Proj(proj='webmerc', lon_0=0),  # output: Web Mercator
)
mapgeom = shapely.ops.transform(tform, mapgeom)

# Convert to ASCII art
minx, miny, maxx, maxy = mapgeom.bounds
srcw = maxx - minx
srch = maxy - miny
dstw, dsth = shutil.get_terminal_size((80, 20))

for y in range(dsth):
  for x in range(dstw):
    pt = shapely.geometry.Point(
      (srcw*x/dstw) + minx,
      (srch*(dsth-y-1)/dsth) + miny  # flip vertically
    )
    if any(country.contains(pt) for country in mapgeom):
      sys.stdout.write('*')
    else:
      sys.stdout.write(' ')
  sys.stdout.write('\n')

1 个答案:

答案 0 :(得分:4)

我在底部进行了编辑,发现了新问题(为什么没有加拿大以及Shapely和Pyproj的不可靠性)


尽管它不能完全解决问题,但我相信这种态度比使用pyproc和Shapely具有更大的潜力,将来,如果您将做更多的Ascii艺术,将给您带来更多的可能性和灵活性。首先,我将写优点和缺点。

PS:最初,我想在您的代码中发现问题,但由于pyproj返回了一些错误,我在运行它时遇到了问题。

PROS

1)我能够提取所有点(加拿大确实不见了)并旋转图像

2)处理速度非常快,因此您可以创建动画Ascii艺术

3)无需循环即可一次完成打印

CONS(已知问题,可解决)

1)这种姿态绝对不能正确地转换地理坐标-太平面了,它应该看起来更像球形

2)我没有花时间尝试找出填充边框的解决方案,因此只有边框带有“ *”。因此,这种态度需要找到填补国家的算法。我认为这不应该是问题,因为JSON文件包含分开的国家

3)您需要在numpy-opencv(可以改用PIL)和Colorama之外再增加2个库,因为我的示例是动画的,我需要通过将光标移至(0,0)而不是使用os来“清理”终端.system('cls')

4)我使它只能在 python 3 中运行。在python 2中它也可以工作,但是sys.stdout.buffer出现错误

将终端上的字体大小更改为最低点,以使打印的字符适合终端。字体更小,分辨率更高

动画应看起来像地图在“旋转” enter image description here

我只用了一点点代码来提取数据。步骤在评论中

import json
import sys
import numpy as np
import colorama
import sys
import time
import cv2

#understand terminal_size as how many letters in X axis and how many in Y axis. Sorry not good name
if len(sys.argv)>1:   
    terminal_size = (int(sys.argv[1]),int(sys.argv[2]))
else:
    terminal_size=(230,175)
with open('world-countries.json') as f:
    countries = []
    minimal = 0 # This can be dangerous. Expecting negative values
    maximal = 0 # Expecting bigger values than 0
    for feature in json.load(f)['features']: # getting data  - I pretend here, that geo coordinates are actually indexes of my numpy array
        indexes = np.int16(np.array(feature['geometry']['coordinates'][0])*2)
        if indexes.min()<minimal:
            minimal = indexes.min()
        if indexes.max()>maximal:
            maximal = indexes.max()
        countries.append(indexes) 

    countries = (np.array(countries)+np.abs(minimal)) # Transform geo-coordinates to image coordinates
correction = np.abs(minimal) # because geo-coordinates has negative values, I need to move it to 0 - xaxis

colorama.init()

def move_cursor(x,y):
    print ("\x1b[{};{}H".format(y+1,x+1))

move = 0 # 'rotate' the globe
for i in range(1000):
    image = np.zeros(shape=[maximal+correction+1,maximal+correction+1]) #creating clean image

    move -=1 # you need to rotate with negative values
    # because negative one are by numpy understood. Positive one will end up with error
    for i in countries: # VERY STRANGE,because parsing the json, some countries has different JSON structure
        if len(i.shape)==2:
            image[i[:,1],i[:,0]+move]=255 # indexes that once were geocoordinates now serves to position the countries in the image
        if len(i.shape)==3:
            image[i[0][:,1],i[0][:,0]+move]=255


    cut = np.where(image==255) # Bounding box
    if move == -1: # creating here bounding box - removing empty edges - from sides and top and bottom - we need space. This needs to be done only once
        max_x,min_x = cut[0].max(),cut[0].min()
        max_y,min_y = cut[1].max(),cut[1].min()


    new_image = image[min_x:max_x,min_y:max_y] # the bounding box
    new_image= new_image[::-1] # reverse, because map is upside down
    new_image = cv2.resize(new_image,terminal_size) # resize so it fits inside terminal

    ascii = np.chararray(shape = new_image.shape).astype('|S4') #create container for asci image
    ascii[:,:]='' #chararray contains some random letters - dunno why... cleaning it
    ascii[:,-1]='\n' #because I pring everything all at once, I am creating new lines at the end of the image
    new_image[:,-1]=0 # at the end of the image can be country borders which would overwrite '\n' created one step above
    ascii[np.where(new_image>0)]='*' # transforming image array to chararray. Better to say, anything that has pixel value higher than 0 will be star in chararray mask
    move_cursor(0,0) # 'cleaning' the terminal for new animation
    sys.stdout.buffer.write(ascii) # print into terminal
    time.sleep(0.025) # FPS

也许最好解释一下代码中的主要算法是什么。我喜欢尽可能使用numpy。整个问题是,我假装图像中的坐标或任何可能的坐标(在您的情况下为地理坐标)都是矩阵索引。然后,我有2个矩阵-实像和Charray作为蒙版。然后,我在Real图像中获取有趣像素的索引,对于Charray Mask中的相同索引,我分配了我想要的任何字母。因此,整个算法不需要一个循环。

关于未来的可能性

想象一下,您还将获得有关地形(海拔)的信息。假设您以某种方式创建了世界地图的灰度图像,其中灰色阴影表示高度。这样的灰度图像将具有形状x,y。您将准备形状为[x,y,256]的 3Dmatrix 。对于3D矩阵中256个层中的每一层,您分配一个字母“ .... ;;;; ###依此类推”以表示阴影。 准备好之后,您可以拍摄灰度图像,其中任何像素实际上都具有3个坐标:x,y和阴影值。因此,您的grascale地图图像-> x,y,shade中将有 3个索引数组。您的新字符将只是提取带有图层字母的 3Dmatrix ,因为:

#Preparation phase
x,y = grayscale.shape
3Dmatrix = np.chararray(shape = [x,y,256])
table = '    ......;;;;;;;###### ...'
for i in range(256):
    3Dmatrix[:,:,i] = table[i]
x_indexes = np.arange(x*y)
y_indexes = np.arange(x*y)
chararray_image = np.chararray(shape=[x,y])

# Ready to print
...

shades = grayscale.reshape(x*y)
chararray_image[:,:] = 3Dmatrix[(x_indexes ,y_indexes ,shades)].reshape(x,y)

由于此过程中没有循环,您可以一次打印所有字符数组,因此实际上可以将电影打印到具有巨大FPS的终端中

例如,如果您有旋转地球的镜头,则可以制作这样的内容-(250 * 70个字母),渲染时间为0.03658s

enter image description here

您当然可以将其发挥到极致并在终端中进行超分辨率,但是最终的FPS效果不佳:0.23157s,大约是4-5 FPS。 有趣的是,这种姿态FPS令人吃惊,但是终端根本无法处理打印,因此之所以如此之低的FPS是由于终端的局限性而不是计算的限制,因为此高分辨率的计算花费了0.00693s,即144 FPS

enter image description here


大编辑-与上述某些陈述相矛盾

我不小心打开了原始json文件,然后发现,加拿大和俄罗斯的坐标完全正确。 我错误地依靠我们俩都没有加拿大这一事实,所以我希望我的代码还可以。在JSON内部,数据具有不同的NOT-UNIFIED结构。俄罗斯和加拿大拥有“ Multipolygon”,因此您需要对其进行迭代。

这是什么意思??不要依赖Shapely和pyproj。显然,他们不能提取某些国家,如果他们不能可靠地提取,您就不能指望他们做任何更复杂的事情。

修改代码后,一切正常

代码:这是正确加载文件的方法

...
with open('world-countries.json') as f:
    countries = []
    minimal = 0
    maximal = 0
    for feature in json.load(f)['features']: # getting data  - I pretend here, that geo coordinates are actually indexes of my numpy array

        for k in range((len(feature['geometry']['coordinates']))):
            indexes = np.int64(np.array(feature['geometry']['coordinates'][k]))
            if indexes.min()<minimal:
                minimal = indexes.min()
            if indexes.max()>maximal:
                maximal = indexes.max()
            countries.append(indexes) 

...

enter image description here