Представление и решение лабиринта с учетом изображения

Каков наилучший способ представления и решения лабиринта с учетом изображения?

The cover image of The Scope Issue 134

Учитывая изображение в формате JPEG (как показано выше), какой лучший способ его прочитать, проанализировать его в некоторой структуре данных и решить лабиринт? Мой первый инстинкт - прочитать изображение в пикселях по пикселям и сохранить его в списке (массиве) логических значений: True для белого пикселя и False для небелого пикселя (цвета могут быть отброшены), Проблема с этим методом заключается в том, что изображение не может быть "идеальным пикселем". Под этим я просто подразумеваю, что если есть белый пиксель где-то на стене, он может создать непреднамеренный путь.

Еще один способ (который пришел ко мне после некоторого раздумья) - преобразовать изображение в SVG файл - это список путей, нарисованных на холсте. Таким образом, пути могут быть прочитаны в один и тот же список (логические значения), где True указывает путь или стену, False, указывающий пространство, способное путешествовать. Проблема с этим методом возникает, если преобразование не является на 100% точным и не полностью соединяет все стены, создавая пробелы.

Также проблема с преобразованием в SVG заключается в том, что строки не "идеально" прямые. Это приводит к тому, что пути являются кубическими кривыми безье. С помощью списка (массива) логических значений, индексированных целыми числами, кривые не будут переноситься легко, и все точки, которые строятся на кривой, должны быть вычислены, но не будут точно соответствовать индексам списка.

Я предполагаю, что хотя один из этих методов может работать (хотя, вероятно, и нет), что они очень неэффективны при таком большом изображении и что существует лучший способ. Как это лучше (наиболее эффективно и/или с наименьшей сложностью)? Есть ли лучший способ?

Затем приходит решение лабиринта. Если я использую один из первых двух методов, я, по сути, получаю матрицу. Согласно этому ответу, хорошим способом представления лабиринта является использование дерева, и хорошим способом его решения является использование алгоритм A *. Как создать дерево из изображения? Любые идеи?

TL; DR
Лучший способ разобрать? В какую структуру данных? Как бы упомянутая структура помогла/препятствовала решению?

UPDATE
Я попробовал свои силы при реализации того, что @Mikhail написал на Python, используя numpy, как рекомендовал @Thomas. Я чувствую, что алгоритм правильный, но он не работает, как надеялся. (Код ниже.) Библиотека PNG PyPNG.

import png, numpy, Queue, operator, itertools

def is_white(coord, image):
  """ Returns whether (x, y) is approx. a white pixel."""
  a = True
  for i in xrange(3):
    if not a: break
    a = image[coord[1]][coord[0] * 3 + i] > 240
  return a

def bfs(s, e, i, visited):
  """ Perform a breadth-first search. """
  frontier = Queue.Queue()
  while s != e:
    for d in [(-1, 0), (0, -1), (1, 0), (0, 1)]:
      np = tuple(map(operator.add, s, d))
      if is_white(np, i) and np not in visited:
        frontier.put(np)
    visited.append(s)
    s = frontier.get()
  return visited

def main():
  r = png.Reader(filename = "thescope-134.png")
  rows, cols, pixels, meta = r.asDirect()
  assert meta['planes'] == 3 # ensure the file is RGB
  image2d = numpy.vstack(itertools.imap(numpy.uint8, pixels))
  start, end = (402, 985), (398, 27)
  print bfs(start, end, image2d, [])

Ответ 1

Вот решение.

  • Преобразование изображения в оттенки серого (еще не двоичные), корректировка веса для цветов, чтобы окончательное изображение в оттенках серого было примерно равномерным. Вы можете сделать это просто, управляя ползунками в Photoshop в Image → Adjustments → Black and White.
  • Преобразуйте изображение в двоичный файл, установив соответствующий порог в Photoshop в Image → Adjustments → Threshold.
  • Убедитесь, что порог выбран правильно. Используйте инструмент Magic Wand Tool с точностью 0, точечным образцом, смежным, без сглаживания. Проверьте, что ребра, при которых разрывы выбора не являются ложными краями, введенными неправильным порогом. Фактически, все внутренние точки этого лабиринта доступны с самого начала.
  • Добавьте искусственные границы в лабиринт, чтобы виртуальный путешественник не обошел его:)
  • Внесите поиск в ширину (BFS) на вашем любимом языке и запустите его с самого начала. Я предпочитаю MATLAB для этой задачи. Как уже упоминал @Thomas, нет необходимости возиться с регулярным представлением графиков. Вы можете напрямую работать с бинаризованным изображением.

Вот код MATLAB для BFS:

function path = solve_maze(img_file)
  %% Init data
  img = imread(img_file);
  img = rgb2gray(img);
  maze = img > 0;
  start = [985 398];
  finish = [26 399];

  %% Init BFS
  n = numel(maze);
  Q = zeros(n, 2);
  M = zeros([size(maze) 2]);
  front = 0;
  back = 1;

  function push(p, d)
    q = p + d;
    if maze(q(1), q(2)) && M(q(1), q(2), 1) == 0
      front = front + 1;
      Q(front, :) = q;
      M(q(1), q(2), :) = reshape(p, [1 1 2]);
    end
  end

  push(start, [0 0]);

  d = [0 1; 0 -1; 1 0; -1 0];

  %% Run BFS
  while back <= front
    p = Q(back, :);
    back = back + 1;
    for i = 1:4
      push(p, d(i, :));
    end
  end

  %% Extracting path
  path = finish;
  while true
    q = path(end, :);
    p = reshape(M(q(1), q(2), :), 1, 2);
    path(end + 1, :) = p;
    if isequal(p, start) 
      break;
    end
  end
end

Это действительно очень простой и стандартный, не должно быть трудностей при реализации этого в Python или что-то еще.

И вот ответ:

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

Ответ 2

Это решение написано на Python. Спасибо Михаилу за указатели на подготовку изображений.

Анимационный поиск по ширине:

Animated version of BFS

Завершенный лабиринт:

Completed Maze

#!/usr/bin/env python

import sys

from Queue import Queue
from PIL import Image

start = (400,984)
end = (398,25)

def iswhite(value):
    if value == (255,255,255):
        return True

def getadjacent(n):
    x,y = n
    return [(x-1,y),(x,y-1),(x+1,y),(x,y+1)]

def BFS(start, end, pixels):

    queue = Queue()
    queue.put([start]) # Wrapping the start tuple in a list

    while not queue.empty():

        path = queue.get() 
        pixel = path[-1]

        if pixel == end:
            return path

        for adjacent in getadjacent(pixel):
            x,y = adjacent
            if iswhite(pixels[x,y]):
                pixels[x,y] = (127,127,127) # see note
                new_path = list(path)
                new_path.append(adjacent)
                queue.put(new_path)

    print "Queue has been exhausted. No answer was found."


if __name__ == '__main__':

    # invoke: python mazesolver.py <mazefile> <outputfile>[.jpg|.png|etc.]
    base_img = Image.open(sys.argv[1])
    base_pixels = base_img.load()

    path = BFS(start, end, base_pixels)

    path_img = Image.open(sys.argv[1])
    path_pixels = path_img.load()

    for position in path:
        x,y = position
        path_pixels[x,y] = (255,0,0) # red

    path_img.save(sys.argv[2])

Примечание. Отмечает белый посещенный пиксель серый. Это устраняет необходимость в посещенном списке, но для этого требуется вторая загрузка файла изображения с диска, прежде чем рисовать путь (если вам не требуется составное изображение конечного пути и ВСЕ пути).

Чистая версия лабиринта, который я использовал.

Ответ 3

Я попробовал реализовать A-Star для поиска этой проблемы. Внимательно следили за реализацией Джозефа Керна для фреймворка и псевдокода алгоритма, приведенного здесь

def AStar(start, goal, neighbor_nodes, distance, cost_estimate):
    def reconstruct_path(came_from, current_node):
        path = []
        while current_node is not None:
            path.append(current_node)
            current_node = came_from[current_node]
        return list(reversed(path))

    g_score = {start: 0}
    f_score = {start: g_score[start] + cost_estimate(start, goal)}
    openset = {start}
    closedset = set()
    came_from = {start: None}

    while openset:
        current = min(openset, key=lambda x: f_score[x])
        if current == goal:
            return reconstruct_path(came_from, goal)
        openset.remove(current)
        closedset.add(current)
        for neighbor in neighbor_nodes(current):
            if neighbor in closedset:
                continue
            if neighbor not in openset:
                openset.add(neighbor)
            tentative_g_score = g_score[current] + distance(current, neighbor)
            if tentative_g_score >= g_score.get(neighbor, float('inf')):
                continue
            came_from[neighbor] = current
            g_score[neighbor] = tentative_g_score
            f_score[neighbor] = tentative_g_score + cost_estimate(neighbor, goal)
    return []

Поскольку A-Star - это алгоритм эвристического поиска, вам нужно придумать функцию, которая оценивает оставшуюся стоимость (здесь: расстояние) до достижения цели. Если вам не нравится субоптимальное решение, оно не должно переоценивать стоимость. Консервативным выбором здесь будет расстояние manhattan (или taxicab), поскольку это представляет собой прямолинейное расстояние между двумя точками сетки для использованного Фон Нейман. (Что в данном случае никогда не будет переоценивать стоимость.)

Это, однако, значительно преувеличивало бы фактическую стоимость данного лабиринта под рукой. Поэтому я добавил две другие метрики расстояния, эвакуированные по эвклидовым расстояниям, а расстояние для манхэттенов, умноженное на четыре для сравнения. Однако они могут переоценить фактическую стоимость и, следовательно, могут привести к неоптимальным результатам.

Здесь код:

import sys
from PIL import Image

def is_blocked(p):
    x,y = p
    pixel = path_pixels[x,y]
    if any(c < 225 for c in pixel):
        return True
def von_neumann_neighbors(p):
    x, y = p
    neighbors = [(x-1, y), (x, y-1), (x+1, y), (x, y+1)]
    return [p for p in neighbors if not is_blocked(p)]
def manhattan(p1, p2):
    return abs(p1[0]-p2[0]) + abs(p1[1]-p2[1])
def squared_euclidean(p1, p2):
    return (p1[0]-p2[0])**2 + (p1[1]-p2[1])**2

start = (400, 984)
goal = (398, 25)

# invoke: python mazesolver.py <mazefile> <outputfile>[.jpg|.png|etc.]

path_img = Image.open(sys.argv[1])
path_pixels = path_img.load()

distance = manhattan
heuristic = manhattan

path = AStar(start, goal, von_neumann_neighbors, distance, heuristic)

for position in path:
    x,y = position
    path_pixels[x,y] = (255,0,0) # red

path_img.save(sys.argv[2])

Вот некоторые изображения для визуализации результатов (вдохновленные тем, что был опубликован Джозефом Керном). Анимация показывает новый кадр после 10000 итераций основного цикла while.

Поиск по ширине и ширине:

Breadth-First Search

A-Star Манхэттен Расстояние:

A-Star Manhattan Distance

Квадратное эвклидовое расстояние A-звезды:

A-Star Squared Euclidean Distance

A-Star Манхэттен Расстояние, умноженное на четыре:

A-Star Manhattan Distance multiplied by four

Результаты показывают, что исследуемые области лабиринта значительно отличаются для используемых эвристик. Таким образом, квадратичное эвклидово расстояние даже создает другой (субоптимальный) путь как другие метрики.

Что касается производительности алгоритма A-Star с точки зрения времени выполнения до завершения, обратите внимание, что большая оценка функций расстояния и стоимости складывается по сравнению с Breadth-First Search (BFS), которая требует только оценки "враждебности" каждой позиции кандидата. Независимо от того, превышает ли стоимость этих дополнительных оценок функций (A-Star) стоимость большего количества узлов для проверки (BFS), и особенно независимо от того, является ли производительность проблемой для вашего приложения вообще, является вопросом индивидуального восприятия и, конечно, не может быть в целом ответ.

В общем случае можно сказать, что лучший алгоритм поиска (например, A-Star) может быть лучшим выбором по сравнению с исчерпывающим поиском (например, BFS). С числом измерений лабиринта, то есть фактором ветвления дерева поиска, недостаток исчерпывающего поиска (для поиска исчерпывающе) растет экспоненциально. С ростом сложности становится все меньше и меньше того, что можно сделать, и в какой-то момент вы в значительной степени довольны любым результатом, будь то (приблизительно) оптимальным или нет.

Ответ 4

Поиск дерева слишком велик. Лабиринт неотделим по пути пути (путей) решения.

(Спасибо rainman002 от Reddit за то, что указали это мне.)

Из-за этого вы можете быстро использовать связанные компоненты, чтобы идентифицировать связанные разделы стены лабиринта. Это повторяется по пикселям дважды.

Если вы хотите превратить это в красивую диаграмму пути (путей) решения, вы можете затем использовать двоичные операции со структурирующими элементами для заполнения путей "тупика" для каждой связанной области.

Ниже приведен демо-код для MATLAB. Он может использовать настройку, чтобы лучше очистить результат, сделать его более обобщаемым и заставить его работать быстрее. (Иногда, когда это не 2:30 утра.)

% read in and invert the image
im = 255 - imread('maze.jpg');

% sharpen it to address small fuzzy channels
% threshold to binary 15%
% run connected components
result = bwlabel(im2bw(imfilter(im,fspecial('unsharp')),0.15));

% purge small components (e.g. letters)
for i = 1:max(reshape(result,1,1002*800))
    [count,~] = size(find(result==i));
    if count < 500
        result(result==i) = 0;
    end
end

% close dead-end channels
closed = zeros(1002,800);
for i = 1:max(reshape(result,1,1002*800))
    k = zeros(1002,800);
    k(result==i) = 1; k = imclose(k,strel('square',8));
    closed(k==1) = i;
end

% do output
out = 255 - im;
for x = 1:1002
    for y = 1:800
        if closed(x,y) == 0
            out(x,y,:) = 0;
        end
    end
end
imshow(out);

result of current code

Ответ 5

Использует очередь для порогового непрерывного заполнения. Выдвигает пиксель слева от входа в очередь и затем запускает цикл. Если пиксель в очереди достаточно темный, он окрашен в светло-серый (выше порога), и все соседи помещаются в очередь.

from PIL import Image
img = Image.open("/tmp/in.jpg")
(w,h) = img.size
scan = [(394,23)]
while(len(scan) > 0):
    (i,j) = scan.pop()
    (r,g,b) = img.getpixel((i,j))
    if(r*g*b < 9000000):
        img.putpixel((i,j),(210,210,210))
        for x in [i-1,i,i+1]:
            for y in [j-1,j,j+1]:
                scan.append((x,y))
img.save("/tmp/out.png")

Решение - это коридор между серой стеной и цветной стеной. Обратите внимание, что этот лабиринт имеет несколько решений. Кроме того, это просто работает.

Solution

Ответ 6

Здесь вы найдете: maze-solver-python (GitHub)

enter image description here

Мне понравилось играть с этим и продлить на ответ Джозефа Керна. Не умалять его; Я просто сделал некоторые незначительные дополнения для всех, кто может быть заинтересован в том, чтобы поиграть с этим.

Это решатель на основе python, который использует BFS для поиска кратчайшего пути. В настоящее время моими основными дополнениями являются:

  • Изображение очищается перед поиском (т.е. преобразуется в черно-белый)
  • Автоматически создавать GIF.
  • Автоматическое создание AVI.

Как бы то ни было, начальные/конечные точки жестко закодированы для этого лабиринта образца, но я планирую расширить его таким образом, чтобы вы могли выбрать соответствующие пиксели.

Ответ 7

Я бы выбрал опцию matrix-of-bools. Если вы обнаружите, что стандартные списки Python слишком неэффективны для этого, вместо этого вы можете использовать массив numpy.bool. Хранение для лабиринта 1000x1000 пикселей составляет всего 1 МБ.

Не беспокойтесь о создании любых структур данных дерева или графика. Это просто способ думать об этом, но не обязательно хороший способ представить его в памяти; булевая матрица легче кодировать и более эффективно.

Затем используйте алгоритм A * для его решения. Для эвристического расстояния используйте расстояние Манхэттена (distance_x + distance_y).

Представляем узлы кортежем координат (row, column). Всякий раз, когда алгоритм (Wikipedia pseudocode) требует "соседей", это простой вопрос обхода четырех возможных соседей (обратите внимание на края изображения!).

Если вы обнаружите, что он все еще слишком медленный, вы можете попытаться уменьшить масштаб изображения перед его загрузкой. Будьте осторожны, чтобы не потерять узкие пути в процессе.

Возможно, возможно сделать уменьшенное масштабирование 1: 2 в Python, проверяя, что вы фактически не теряете никаких возможных путей. Интересный вариант, но ему нужно немного подумать.

Ответ 8

Вот некоторые идеи.

(1. Обработка изображений:)

1.1 Загрузите изображение как RGB пиксельная карта. В С# это тривиально, используя system.drawing.bitmap. На языках без простой поддержки для изображений просто преобразуйте изображение в портативный формат pixmap (PPM) (текстовое представление Unix, создает большие файлы) или некоторый простой формат двоичного файла, который вы можете легко прочитать, например BMP или TGA. ImageMagick в Unix или IrfanView в Windows.

1.2 Вы можете, как упоминалось ранее, упростить данные, взяв (R + G + B)/3 для каждого пикселя в качестве индикатора серого тона, а затем пороговое значение для создания черно-белой таблицы. Что-то близкое к 200, предполагая, что 0 = черный и 255 = белый, выведут артефакты JPEG.

(2. Решения:)

2.1 Поиск по глубине: Инициализируйте пустой стек с начальным местоположением, собирайте доступные последующие ходы, произвольно выбирайте их и нажимайте на стек, продолжайте до тех пор, пока не будет достигнут конец или нет. На мертвой основе, выбирая стек, вам нужно отслеживать, какие позиции были посещены на карте, поэтому, когда вы собираете доступные ходы, вы никогда не прокладываете один и тот же путь дважды. Очень интересно анимировать.

2.2 Поиск по ширине и ширине: ранее упоминалось, как описано выше, но только с использованием очередей. Также интересно анимировать. Это работает как наводнение в программном обеспечении для редактирования изображений. Я думаю, вы сможете решить лабиринт в Photoshop, используя этот трюк.

2.3 Наставник стены. Геометрически говоря, лабиринт представляет собой свернутую/извилистую трубку. Если вы держите руку на стене, вы, в конце концов, найдете выход;) Это не всегда работает. Есть определенные предположения: совершенные лабиринты и т.д., Например, некоторые лабиринты содержат острова. Посмотрите на него; это захватывающе.

(3. Комментарии:)

Это сложный вопрос. Легко разрешать лабиринты, если они представлены в некотором простом массиве, формальном с каждым элементом, являющимся типом ячейки с северными, восточными, южными и западными стенами и посещенным полем флага. Однако, учитывая, что вы пытаетесь сделать это, учитывая рисованный эскиз, он становится беспорядочным. Я честно считаю, что попытка рационализировать эскиз будет приводить вас в бешенство. Это сродни проблемам компьютерного зрения, которые достаточно привлекательны. Возможно, переход непосредственно на карту изображения может быть проще, но более расточительным.