Лет 7 назад ребята из NASA опубликовали на github опенсурс проект «Ames Stereo Pipeline (ASP)» зачем не это нужно? Читать чужой код сложно, но очень интересно, особенно когда это связано с космическими проектами, другими словами — бесплатная программа, разработанная лабораторией Эймса, с помощью которой можно делать крутые и детализированные 3D‑модели местности. Работает это просто: берёте пару снимков одной и той же территории, сделанных с разных углов (например, с дрона или даже из космоса), и ASP автоматически обрабатывает их и превращает в реалистичную трёхмерную карту или модель.

тут вид на ту самую лабу Эймса
тут вид на ту самую лабу Эймса

NASA активно использует ASP, чтобы изучать рельеф Марса, Луны и других планет. Но самое классное — этот инструмент абсолютно доступен каждому, так что вы можете легко использовать его и в своих проектах: делать 3D-карты любимых мест, моделировать ландшафты, или даже создавать трёхмерные изображения своего двора и домашних животных.

Давайте я Вам покажу что интересного они использовали в своем коде

"parallel_stereo"

  1. Разбиение на тайлы (tiles)

    Изображения разбиваются на небольшие участки — тайлы (например, 1024×1024 пикселя). Каждый тайл обрабатывается независимо: применяется корреляция, рассчитывается диспаритность. Это позволяет масштабировать обработку на большие изображения и использовать параллелизм эффективно. 

    Я использовал их логику и подход, используемый в ASP, но адаптировал для общего понимания и собственного использования:

    def split_image_to_tiles(image_path, tile_size, output_folder):
     image = Image.open(image_path)
     width, height = image.size
    # тут мы рассчитываем количество тайлов по гризонтали и вертикали
    cols = math.ceil(width / tile_size)
    rows = math.ceil(height / tile_size)
    
    tile_id = 0
    
    for row in range(rows):
        for col in range(cols):
            left = col * tile_size
            upper = row * tile_size
            right = min((col + 1) * tile_size, width)
            lower = min((row + 1) * tile_size, height)
    
            # режем тайл
            tile = image.crop((left, upper, right, lower))
            tile_filename = f"{output_folder}/tile_{row}_{col}.png"
            tile.save(tile_filename)
    
            print(f"Сохранили тайл #{tile_id}: {tile_filename}")
            tile_id += 1
    
  1. Параллелизм: процессы + потоки

    • Процессы (--processes) запускают несколько экземпляров обработки тайлов.

    • Потоки (--threads-multiprocess, --threads-singleprocess) внутри каждого процесса ускоряют алгоритмы (в т. ч. блок-матчинг, SGM/MGM).

      По умолчанию: для алгоритмов SGM/MGM используется 8 потоков на процесс, а число процессов автоматически выставляется как (число ядер) ÷ 8.

выдернуто из их документации

Processes and threads

It is suggested that after this program is started, one examine how well it uses the CPUs and memory on all nodes, especially at the correlation stage.

One may want to set the --processes, --threads-multiprocess, and --threads-singleprocess options …

Note that the SGM and MGM algorithms can be quite memory-intensive. For these, by default, the number of threads is set to 8, and the number of processes is the number of cores divided by the number of threads, on each node. Otherwise, the default is to use as many processes as there are cores

parallel_stereo комбинирует многопроцессность (разные тайлы → разные процессы) и многопоточность(каждый процесс распараллеливает вычисление SGM/MGM-алгоритмов внутри себя) через параметры:

разбиваем задачу на тайлы и запускаем для каждого тайла вызов stereo_corr параллельно через ProcessPoolExecutor.

import os
import math
import subprocess
from concurrent.futures import ProcessPoolExecutor

# ----- Чиллим с настройками -----
left_image = "left.tif"   # краш левый шот, сюда путь кидаем
right_image = "right.tif"  # и сюда — правый шот
output_dir = "results"     # папка, где всё сольём

tile_size = 1024            # размерчик блока для резки, типа 1024 пикселя
threads_per_process = 8      # сколько потоков дергать на каждый процесс (SGM/MGM)
threads_single = 8           # потоки для единичных задач, типа прогонки фильтров

# ----- Готовим папочку, чтоб без конского лага -----
os.makedirs(output_dir, exist_ok=True)  # если нет папки — создаём, не ругаемся

# ----- Вычисляем, сколько у нас ядер и процессов -----
cpu_cores = os.cpu_count() or 1  # узнаём, сколько ядер у компа
processes = max(cpu_cores // threads_per_process, 1)  # типа делим ядра на потоки
print(f"Ядрышек понаходили: {cpu_cores}, будем юзать процессов: {processes}")

# ----- Функция, которая бахает обработку одного тайла -----
def process_tile(tile_id):
    # создаём папку для результатов этого блока
    out_tile_dir = os.path.join(output_dir, f"tile_{tile_id}")
    os.makedirs(out_tile_dir, exist_ok=True)

    # собираем команду, чтобы позвать C++ шнягу
    cmd = [
        "stereo_corr",
        "--threads-multiprocess", str(threads_per_process),  # потоки внутри процесса
        "--threads-singleprocess", str(threads_single),       # потоки для единичной прогонки
        "--corr-tile-size", str(tile_size),                  # какой размер тайла юзаем
        "--tile-id", tile_id,                                # метка блока, чтобы не спалиться
        left_image, right_image, out_tile_dir                 # вход и выход
    ]
    print("Ну что, стартуем тайл:", " ".join(cmd))  # чилловый вывод для дебага
    subprocess.run(cmd, check=True)  # бежим и не паримся

# ----- Готовим список тайлов -----
# просто представим, что у нас фотка 10240x7680
image_width, image_height = 10240, 7680
cols = math.ceil(image_width / tile_size)  # чёт рассчитываем кол-во столбцов
rows = math.ceil(image_height / tile_size) # и строк
# мапим их в строки вида "row_col"
tile_ids = [f"{r}_{c}" for r in range(rows) for c in range(cols)]

# ----- Юзаем параллельность, как профи -----
with ProcessPoolExecutor(max_workers=processes) as executor:
    # раздаём тайлы на обработку, пусть каждый процесс таскает свою порцию
    executor.map(process_tile, tile_ids)

print("Все блоки обработаны – гуляем дальше!")

После того как все тайлы прошли через stereo_corr (и, при необходимости, через stereo_blend, stereo_rfne, stereo_fltr, stereo_tri), parallel_stereo автоматически собирает «разрозненные» файлы из подпапок в единые выходные изображения. Вот как это происходит «под капотом»:

  1. Виртуальный VRT-мозаик

    В каждой подпапке, созданной для конкретного тайла, лежат результаты обработки:

    • для шагов 0–4 — файлы диспаритности (*D.tif, B.tif, RD.tif, *F.tif и т.д.),

    • для шага 5 — точечное облако (*PC.tif).

    Вместо того чтобы сразу читать/копировать их поблочно, ASP создаёт GDAL VRT (Virtual Raster) — XML-описание, в котором указывается, какие файлы и в каком порядке «склеить». Эти VRT-файлы ведут себя как обычные GeoTIFF’ы, но занимают гораздо меньше места и читаются практически мгновенно

  1. Сборка окончательных TIFF’ов

    Если пользователь не указал --keep-only essential или unchanged, то по окончании всех этапов и после генерации VRT’ов вызывается внутренняя обёртка, которая запускает gdal_translate (или эквивалентную логику) на VRT, чтобы получить единый GeoTIFF

схема
схема

Итого, этап «сборки» в parallel_stereo происходит так:

  1. Генерируются VRT-файлы, описывающие виртуальную мозаичную картину из тайлов.

  2. Через GDAL (или аналогичный API) эти VRT переводятся в единые GeoTIFF’ы.

  3. В случае DEM: на объединённое облако точек (run-PC.tif) вызывается point2dem, и получается готовая DEM.

Таким образом ASP умеет не только «раскладывать и считать» по тайлам, но и «склеивать обратно» результаты в привычные большие растры без лишних усилий со стороны пользователя.

В следующей статье попробую выложить готовый код для всех и продемонстрировать что получилось.

Комментарии (1)


  1. QwertyOFF
    18.07.2025 22:44

    Прекрасная статья! В следующий раз пожалуйста используйте более толковую модель для ее генерации.