Создание ASCII художественной карты мира - PullRequest
5 голосов
/ 28 марта 2019

Я бы хотел сделать карту мира ASCII с данными этого файла GeoJSON .

Мой основной подход - загрузить GeoJSON в Shapely , преобразоватьуказывает на Mercator pyproj , а затем проверяет геометрию каждого персонажа моей сетки 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 Ответ

5 голосов
/ 29 марта 2019

Я сделал правку внизу, обнаружив новую проблему (почему нет Канады и ненадежность Shapely и Pyproj)


Хотя это не совсем решает проблему,Я полагаю, что такое отношение имеет больший потенциал, чем использование pyproc и Shapely, и в будущем, если вы будете больше заниматься искусством Ascii, у вас будет больше возможностей и гибкости.Сначала я напишу плюсы и минусы.

PS: Изначально я хотел найти проблему в вашем коде, но у меня были проблемы с его запуском, потому что pyproj возвращал мне какую-то ошибку.

PROS

1) Мне удалось извлечь все точки (Канада действительно отсутствует) и повернуть изображение

2) Обработка очень быстрая, и поэтому вы можете создать Анимированный Ascii art .

3) Печать выполняется сразу без необходимости зацикливания

CONS (известные проблемы, решаемые)

1) Это отношение определенно неправильно переводит гео-координаты - слишком плоское, оно должно выглядеть более сферическим

2) Мне не потребовалось время, чтобы попытаться найти решение для заполнения границ, поэтому только границы имеют '*».Поэтому это отношение должно найти алгоритм для заполнения стран.Я думаю, что это не должно быть проблемой, так как файл JSON содержит страны, разделенные

3) Вам нужно 2 дополнительные библиотеки помимо numpy - opencv (вместо этого вы можете использовать PIL) и Colorama, потому что мой пример анимирован, и мне нужно было 'очистите терминал, переместив курсор на (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 матрицы - Real Image и Charray as Mask.Затем я беру индексы интересных пикселей в реальном изображении и для тех же индексов в маске Charray я назначаю любую букву, которую я хочу.Благодаря этому весь алгоритм не нуждается в одном цикле.

О будущих возможностях

Представьте, что у вас также будет информация о местности (высоте).Допустим, вы каким-то образом создаете серое изображение карты мира, где серые оттенки выражают высоту.Такое изображение в градациях серого будет иметь форму x, y.Вы подготовите 3Dmatrix с формой = [x, y, 256].Для каждого слоя из 256 в трехмерной матрице вы назначаете одну букву «.... ;;;; ### и т. Д.», Которая будет выражать тень.Когда вы это подготовите, вы можете взять свое изображение в градациях серого, где любой пиксель будет фактически иметь 3 координаты: x, y и значение оттенка.Таким образом, у вас будет 3 массива индексов из вашего изображения карты масштаба -> x, y, тень.Ваш новый 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)

Поскольку в этом процессе нет цикла, и вы можете печатать все chararray одновременно, вы можетена самом деле печатать фильм в терминал с огромным FPS

Например, если у вас есть кадры вращающейся земли, вы можете сделать что-то вроде этого - (250 * 70 букв), время рендеринга 0,03658 с

enter image description here

Вы, конечно, можете довести это до крайности и сделать суперразрешение в своем терминале, но результирующий FPS не так хорош: 0,23157 с, то есть примерно 4-5 FPS. Интересно отметить, что это отношение FPS является огромным, но терминал просто не может обрабатывать печать, поэтому этот низкий FPS обусловлен ограничениями терминала, а не расчетами, так как вычисление этого высокого разрешения заняло 0,00693 с, то есть 144 FPS .

enter image description here


БОЛЬШОЕ РЕДАКТИРОВАНИЕ - противоречащее некоторым из приведенных выше утверждений

Я случайно открыл raw json файл и выяснил, что там КАНАДА и РОССИЯ с полными правильными координатами. Я сделал ошибку, полагаясь на тот факт, что у нас обоих не было Канады в результате, поэтому я ожидал, что мой код в порядке . Внутри JSON данные имеют различную НЕ-УНИФИЦИРОВАННУЮ структуру. В России и Канаде есть «Мультиполигон», поэтому вам нужно повторить его.

Что это значит? Не полагайтесь на Shapely и pyproj. Очевидно, что они не могут извлечь некоторые страны, и если они не могут сделать это надежно, вы не можете ожидать, что они сделают что-нибудь более сложное.

После изменения кода все в порядке

CODE: как правильно загрузить файл

...
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

...