创建 ASCII 艺术世界地图
Creating an ASCII art world map
我想渲染给定的 ASCII 艺术世界地图 this GeoJSON file。
我的基本方法是将 GeoJSON 加载到 Shapely, transform the points using pyproj 到墨卡托,然后对我的 ASCII 艺术网格的每个字符的几何图形进行命中测试。
当以本初子午线为中心时,它看起来(编辑:大部分)没问题:
但以纽约市 (lon_0=-74
) 为中心,突然变得混乱:
我很确定我这里的预测有问题。 (并且将 ASCII 地图坐标转换为 lat/lon 可能比转换整个几何图形更有效,但我不确定如何。)
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')
我在底部编辑了一下,发现新问题(为什么没有加拿大,Shapely和Pyproj不可靠)
尽管它不能完全解决问题,但我相信这种态度比使用 pyproc 和 Shapely 更有潜力,并且在未来,如果你会做更多的 Ascii 艺术,会给你更多的可能性和灵活性。先写优缺点吧
PS:最初我想在你的代码中找到问题,但我在使用它时遇到了问题 运行,因为 pyproj 返回了一些错误。
优点
1)我能够提取所有点(加拿大真的不见了)并旋转图像
2) 处理速度非常快,因此您可以创建 动画 Ascii 艺术。
3)一次打印完成,无需循环
缺点(已知问题,可解决)
1) 这种态度绝对没有正确翻译地理坐标 - 太平面了,它应该看起来更球形
2) 我没有花时间尝试找出填充边框的解决方案,所以只有边框有'*'。因此这种态度需要找到算法来填补国家。我认为这应该不是问题,因为 JSON 文件包含分隔的国家/地区
3) 除了 numpy - opencv(你可以使用 PIL)和 Colorama,你还需要 2 个额外的库,因为我的示例是动画的,我需要通过将光标移动到 (0,0) 来 'clean' 终端而不是使用 os.system('cls')
4) 我只在python 3中做到了运行。在 python 2 中它也有效,但我在 sys.stdout.buffer
中遇到错误
将终端上的字体大小更改为最低点,以便打印的字符适合终端。字体越小,分辨率越高
动画应该看起来像地图 'rotating'
我用了一点你的代码来提取数据。步骤在评论里
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 作为蒙版。然后,我在真实图像中获取有趣像素的索引,并在 Charray Mask 中为相同的索引分配我想要的任何字母。多亏了这一点,整个算法不需要一个循环。
关于未来的可能性
想象一下,您还将获得有关地形(高度)的信息。假设您以某种方式创建了世界地图的灰度图像,其中灰色阴影表示高度。这样的灰度图像将具有形状 x,y。您将准备形状 = [x,y,256] 的 3Dmatrix。对于 3D 矩阵中 256 层中的每一层,您分配一个字母“....;;;;### 等等”来表示阴影。
准备好后,您可以拍摄灰度图像,其中任何像素实际上都有 3 个坐标:x、y 和阴影值。因此,您的 grascale 地图图像 -> x、y、阴影中将有 3 个索引数组 。您的新 charray 将简单地提取带有图层字母的 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
你当然可以把它发挥到极致,在你的终端上做超分辨率,但是得到的 FPS 并不是那么好:0.23157s,大约是 4-5 FPS。 有趣的是,这个态度FPS是巨大的,但是终端根本无法处理打印,所以这个低FPS是由于终端的限制而不是计算作为计算这个高分辨率用了0.00693s,也就是144 FPS.
大编辑 - 与上述某些陈述相矛盾
我不小心打开原始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)
...
我想渲染给定的 ASCII 艺术世界地图 this GeoJSON file。
我的基本方法是将 GeoJSON 加载到 Shapely, transform the points using pyproj 到墨卡托,然后对我的 ASCII 艺术网格的每个字符的几何图形进行命中测试。
当以本初子午线为中心时,它看起来(编辑:大部分)没问题:
但以纽约市 (lon_0=-74
) 为中心,突然变得混乱:
我很确定我这里的预测有问题。 (并且将 ASCII 地图坐标转换为 lat/lon 可能比转换整个几何图形更有效,但我不确定如何。)
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')
我在底部编辑了一下,发现新问题(为什么没有加拿大,Shapely和Pyproj不可靠)
尽管它不能完全解决问题,但我相信这种态度比使用 pyproc 和 Shapely 更有潜力,并且在未来,如果你会做更多的 Ascii 艺术,会给你更多的可能性和灵活性。先写优缺点吧
PS:最初我想在你的代码中找到问题,但我在使用它时遇到了问题 运行,因为 pyproj 返回了一些错误。
优点
1)我能够提取所有点(加拿大真的不见了)并旋转图像
2) 处理速度非常快,因此您可以创建 动画 Ascii 艺术。
3)一次打印完成,无需循环
缺点(已知问题,可解决)
1) 这种态度绝对没有正确翻译地理坐标 - 太平面了,它应该看起来更球形
2) 我没有花时间尝试找出填充边框的解决方案,所以只有边框有'*'。因此这种态度需要找到算法来填补国家。我认为这应该不是问题,因为 JSON 文件包含分隔的国家/地区
3) 除了 numpy - opencv(你可以使用 PIL)和 Colorama,你还需要 2 个额外的库,因为我的示例是动画的,我需要通过将光标移动到 (0,0) 来 'clean' 终端而不是使用 os.system('cls')
4) 我只在python 3中做到了运行。在 python 2 中它也有效,但我在 sys.stdout.buffer
中遇到错误将终端上的字体大小更改为最低点,以便打印的字符适合终端。字体越小,分辨率越高
动画应该看起来像地图 'rotating'
我用了一点你的代码来提取数据。步骤在评论里
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 作为蒙版。然后,我在真实图像中获取有趣像素的索引,并在 Charray Mask 中为相同的索引分配我想要的任何字母。多亏了这一点,整个算法不需要一个循环。
关于未来的可能性
想象一下,您还将获得有关地形(高度)的信息。假设您以某种方式创建了世界地图的灰度图像,其中灰色阴影表示高度。这样的灰度图像将具有形状 x,y。您将准备形状 = [x,y,256] 的 3Dmatrix。对于 3D 矩阵中 256 层中的每一层,您分配一个字母“....;;;;### 等等”来表示阴影。 准备好后,您可以拍摄灰度图像,其中任何像素实际上都有 3 个坐标:x、y 和阴影值。因此,您的 grascale 地图图像 -> x、y、阴影中将有 3 个索引数组 。您的新 charray 将简单地提取带有图层字母的 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
你当然可以把它发挥到极致,在你的终端上做超分辨率,但是得到的 FPS 并不是那么好:0.23157s,大约是 4-5 FPS。 有趣的是,这个态度FPS是巨大的,但是终端根本无法处理打印,所以这个低FPS是由于终端的限制而不是计算作为计算这个高分辨率用了0.00693s,也就是144 FPS.
大编辑 - 与上述某些陈述相矛盾
我不小心打开原始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)
...