Создание карты мира искусства ASCII

Я хотел бы отобразить карту мира искусства ASCII с учетом этот файл GeoJSON.

Мой основной подход заключается в загрузке GeoJSON в Shapely, преобразовании точек с помощью pyproj для Mercator, а затем выполните проверку геометрии для каждого символа моей художественной сетки ASCII.

Это выглядит (редактировать: в основном) нормально, когда центрировано по нулевому меридиану:

с центром в долготе = 0

Но в центре внимания находится Нью-Йорк (lon_0=-74), и он внезапно выходит из строя:

введите здесь описание изображения

Я почти уверен, что делаю что-то не так с прогнозами здесь. (И, вероятно, было бы более эффективно преобразовать координаты карты 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')


person rgov    schedule 28.03.2019    source источник
comment
верхний выглядит нормально, если не считать Канаду   -  person philshem    schedule 28.03.2019
comment
Я забыл установить Канаду на свой виртуальный компьютер.   -  person rgov    schedule 28.03.2019
comment
нужно ли придерживаться shapely и pyproj?   -  person Martin    schedule 29.03.2019
comment
@Martin Не особенно, но я не хочу добавлять какие-либо тяжелые зависимости (matplotlib, pandas и т. д.)   -  person rgov    schedule 29.03.2019
comment
У меня есть быстрое решение для обработки, которое преобразует границы стран в координаты изображения. Это означает, что у вас нет многоугольников, только границы, что выглядит не очень хорошо, потому что в ascii-изображении много пустого места. Также вам нужно будет сделать небольшую коррекцию в отношении координат изображения, потому что я не понимаю картографию и не знаю, как именно преобразуются долгота и т. Д. В 2D. Единственная зависимость, которую я думаю, это numpy. Вам было бы интересно такое решение?   -  person Martin    schedule 29.03.2019
comment
@Мартин Конечно, поделитесь, пожалуйста. Мне также интересно узнать, есть ли ошибка в моем коде.   -  person rgov    schedule 29.03.2019
comment
2-й выглядит так, как будто вы забыли учесть антимеридиан, и линии соединились неправильно   -  person Ian Turton    schedule 29.03.2019
comment
На этапе «Преобразовать в ASCII-арт» вам необходимо учитывать значения, выходящие за пределы ширины терминала или отрицательные значения. Они должны быть завернуты (или выброшены) до того, как stdout.write поместит их в неправильное место.   -  person Daryll    schedule 29.03.2019


Ответы (1)


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


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

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

ПЛЮСЫ

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

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

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

МИНУСЫ (известные проблемы, решаемые)

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

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

3) Вам нужны 2 дополнительные библиотеки помимо numpy - opencv (вместо этого вы можете использовать PIL) и Colorama, потому что мой пример анимирован, и мне нужно было «очистить» терминал, переместив курсор на (0,0) вместо использования os.system( 'клс')

4) Я заставил его работать только в python 3. В python 2 это тоже работает, но я получаю сообщение об ошибке с sys.stdout.buffer

Измените размер шрифта на терминале на самую низкую точку, чтобы напечатанные символы помещались в терминале. Меньше шрифт, лучше разрешение

Анимация должна выглядеть так, будто карта «вращается» введите здесь описание изображения

Я использовал немного вашего кода для извлечения данных. Шаги в комментариях

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 Mask присваиваю любую букву, которую хочу. Благодаря этому всему алгоритму не требуется ни одного цикла.

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

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

введите здесь описание изображения

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

введите здесь описание изображения


БОЛЬШОЕ ИЗМЕНЕНИЕ — противоречащие некоторым из приведенных выше утверждений

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

Что это значит? Не полагайтесь на 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) 

...

введите здесь описание изображения

person Martin    schedule 29.03.2019