web-dev-qa-db-fra.com

Représenter et résoudre un labyrinthe à partir d'une image

Quelle est la meilleure façon de représenter et de résoudre un labyrinthe à partir d’une image?

The cover image of The Scope Issue 134

Avec une image JPEG (comme on le voit ci-dessus), quel est le meilleur moyen de la lire, de l’analyser dans une structure de données et de résoudre le labyrinthe? Mon premier instinct est de lire l'image pixel par pixel et de la stocker dans une liste (tableau) de valeurs booléennes: True pour un pixel blanc et False pour un pixel non blanc (les couleurs peuvent être ignorées). Le problème avec cette méthode est que l'image peut ne pas être "pixel perfect". Par cela, je veux simplement dire que s'il y a un pixel blanc quelque part sur un mur, cela peut créer un chemin involontaire.

Une autre méthode (qui m'est venue après un peu de réflexion) consiste à convertir l'image en un fichier SVG - qui est une liste de chemins tracés sur un canevas. De cette façon, les chemins pourraient être lus dans le même type de liste (valeurs booléennes) où True désignerait un chemin ou un mur, False indiquant un espace pouvant être parcouru. Un problème avec cette méthode se pose si la conversion n'est pas précise à 100% et ne connecte pas complètement tous les murs, créant des lacunes.

Un autre problème avec la conversion en SVG est que les lignes ne sont pas "parfaitement" droites. Il en résulte que les chemins sont des courbes de Bézier cubiques. Avec une liste (tableau) de valeurs booléennes indexées par des entiers, les courbes ne seraient pas facilement transférées et tous les points de cette ligne sur la courbe devraient être calculés, mais ne correspondraient pas exactement à la liste des index.

Je suppose qu’une de ces méthodes peut fonctionner (mais probablement pas) qu’elles sont terriblement inefficaces compte tenu d’une image aussi large et qu’il existe un meilleur moyen. Comment cela fonctionne-t-il le mieux (le plus efficacement et/ou avec le moins de complexité)? Y a-t-il même un meilleur moyen?

Vient ensuite la résolution du labyrinthe. Si j'utilise l'une des deux premières méthodes, je finirai par obtenir une matrice. Selon cette réponse , un bon moyen de représenter un labyrinthe consiste à utiliser un arbre, et un bon moyen de le résoudre consiste à utiliser l’algorithme A * . Comment créer un arbre à partir de l'image? Des idées?

TL; DR
Le meilleur moyen d’analyser? Dans quelle structure de données? Comment cette structure aiderait-elle/empêcherait-elle la résolution?

METTRE À JOUR
J'ai essayé d'implémenter ce que @ Mikhail a écrit en Python, en utilisant numpy, comme @Thomas l'a recommandé. Je pense que l'algorithme est correct, mais il ne fonctionne pas comme prévu. (Code ci-dessous.) La bibliothèque PNG est 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, [])
241
Whymarrh

Voici une solution.

  1. Convertit l'image en niveaux de gris (pas encore binaire), en ajustant les poids pour les couleurs afin que l'image finale en niveaux de gris soit approximativement uniforme. Vous pouvez le faire simplement en contrôlant les curseurs dans Photoshop dans Image -> Réglages -> Noir et blanc.
  2. Convertissez une image en binaire en définissant le seuil approprié dans Photoshop dans Image -> Réglages -> Seuil.
  3. Assurez-vous que le seuil est bien sélectionné. Utilisez l'outil Baguette magique avec une tolérance de 0, un échantillon ponctuel, contigu, sans anti-aliasing. Vérifiez que les arêtes auxquelles des sauts de sélection sont définis ne sont pas de fausses arêtes introduites par un seuil incorrect En fait, tous les points intérieurs de ce labyrinthe sont accessibles depuis le début.
  4. Ajoutez des frontières artificielles sur le labyrinthe pour vous assurer que le voyageur virtuel ne s'y promènera pas :)
  5. Implémentez width-first search (BFS) dans votre langue préférée et lancez-le depuis le début. Je préfère MATLAB pour cette tâche. Comme @Thomas l'a déjà mentionné, il n'est pas nécessaire de jouer avec la représentation régulière des graphiques. Vous pouvez travailler avec une image binarisée directement.

Voici le code MATLAB pour 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

C’est vraiment très simple et standard, il ne devrait pas y avoir de difficulté à implémenter ceci dans Python ou autre.

Et voici la réponse:

 Enter image description here

223
Mikhail

Cette solution est écrite en Python. Merci Mikhail pour les conseils sur la préparation des images.

Une recherche animée en largeur d'abord:

Animated version of BFS

Le labyrinthe terminé:

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 __== '__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])

Remarque: Marque un pixel gris visité blanc. Cela supprime la nécessité d'une liste de sites visités, mais nécessite un second chargement du fichier image à partir du disque avant de tracer un chemin (si vous ne voulez pas d'image composite du chemin final et de TOUS les chemins empruntés).

Une version vierge du labyrinthe que j'ai utilisé.

152
Joseph Kern

J'ai moi-même essayé d'implémenter A-Star search pour résoudre ce problème. J'ai suivi de près l'implémentation de Joseph Kern pour le framework et l'algorithme pseudocode donné ici :

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 []

Comme A-Star est un algorithme de recherche heuristique, vous devez créer une fonction qui estime le coût restant (ici, la distance) jusqu'à ce que l'objectif soit atteint. À moins que vous ne soyez à l'aise avec une solution non optimale, cela ne doit pas surestimer les coûts. Le choix le plus prudent serait ici la distance manhattan (ou taxi) , car elle représente la distance en ligne droite entre deux points de la grille pour le quartier utilisé de Von Neumann. (Ce qui, dans ce cas, ne surestimerait jamais le coût.)

Cela sous-estime toutefois sensiblement le coût réel du labyrinthe concerné. Par conséquent, j'ai ajouté deux autres métriques de distance au carré distance euclidienne et la distance manhattan multipliée par quatre à des fins de comparaison. Celles-ci pourraient toutefois surestimer les coûts réels et pourraient donc donner des résultats non optimaux.

Voici le code:

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])

Voici quelques images pour une visualisation des résultats (inspirée de celle publiée par Joseph Kern ). Les animations montrent une nouvelle image après 10000 itérations de la boucle while principale.

Largeur-première recherche:

Breadth-First Search

A-Star Manhattan Distance:

A-Star Manhattan Distance

Distance euclidienne au carré A-Star:

A-Star Squared Euclidean Distance

A-Star Manhattan Distance multipliée par quatre:

A-Star Manhattan Distance multiplied by four

Les résultats montrent que les régions explorées du labyrinthe diffèrent considérablement selon les heuristiques utilisées. En tant que tel, la distance euclidienne au carré produit même un chemin (sous-optimal) différent des autres métriques.

En ce qui concerne les performances de l’algorithme A-Star en termes de temps d’exécution jusqu’à la fin, notons que de nombreuses évaluations des fonctions de distance et de coût s’additionnent par rapport au modèle Breadth-First Search (BFS) qui n’a besoin que d’évaluer le "degré de pertinence" de chaque poste candidat. Que le coût de ces évaluations de fonctions supplémentaires (A-Star) l'emporte sur le coût du plus grand nombre de nœuds à vérifier (BFS) et en particulier du point de savoir si les performances représentent un problème pour votre application, cela dépend de la perception individuelle et ne peut bien sûr pas être répondu généralement. 

Une chose qui peut être dite en général sur le point de savoir si un algorithme de recherche informé (tel que A-Star) pourrait être le meilleur choix par rapport à une recherche exhaustive (par exemple, BFS) est la suivante. Avec le nombre de dimensions du labyrinthe, c’est-à-dire le facteur de ramification de l’arbre de recherche, l’inconvénient d’une recherche exhaustive (pour effectuer une recherche exhaustive) croît de façon exponentielle. Avec la complexité croissante, il devient de moins en moins faisable de le faire et à un moment donné, vous êtes plutôt satisfait du chemin de résultat , qu'il soit (approximativement) optimal ou non.

75
moooeeeep

La recherche dans les arbres, c'est trop. Le labyrinthe est intrinsèquement séparable le long du ou des chemins de la solution.

(Merci à rainman002 de Reddit de me l'avoir signalé.)

De ce fait, vous pouvez rapidement utiliser composants connectés pour identifier les sections connectées du mur de labyrinthe. Cette itère sur les pixels deux fois.

Si vous souhaitez transformer cela en un diagramme de Nice du ou des chemins de solution, vous pouvez ensuite utiliser des opérations binaires avec des éléments structurants pour renseigner les chemins "sans issue" de chaque région connectée.

Le code de démonstration pour MATLAB suit. Vous pouvez utiliser des modifications pour améliorer le résultat, le rendre plus généralisable et le rendre plus rapide. (Parfois quand il n'est pas 2h30 du matin.)

% 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

34
Jim Gray

Utilise une file d'attente pour un remplissage continu de seuil. Pousse le pixel à gauche de l'entrée dans la file d'attente puis commence la boucle. Si un pixel en file d'attente est suffisamment sombre, il est coloré en gris clair (au-dessus du seuil) et tous les voisins sont placés dans la file d'attente.

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")

La solution est le couloir entre mur gris et mur coloré. Notez que ce labyrinthe a plusieurs solutions. En outre, cela semble simplement fonctionner.

Solution

23
kylefinn

Ici vous allez: maze-solver-python (GitHub)

enter image description here

Je me suis amusé à jouer avec cela et à prolonger la réponse de Joseph Kern . Pour ne pas nuire à cela; Je viens de faire quelques ajouts mineurs pour quiconque pourrait être intéressé à jouer avec cela.

C'est un solveur basé sur Python qui utilise BFS pour trouver le chemin le plus court. Mes principaux ajouts à l'époque sont:

  1. L'image est nettoyée avant la recherche (c.-à-d. Convertie en noir et blanc pur)
  2. Générer automatiquement un GIF.
  3. Générer automatiquement un fichier AVI.

Dans l'état actuel des choses, les points de départ et d'arrivée sont codés en dur pour cet exemple de labyrinthe, mais je prévois de l'étendre de sorte que vous puissiez choisir les pixels appropriés.

22
stefano

Voici quelques idées.

(1. Traitement de l'image :)

1.1 Chargez l'image en tant que _RGB pixel map. Dans C # il est trivial d'utiliser system.drawing.bitmap. Dans les langues ne prenant pas en charge l’imagerie, convertissez simplement l’image en format pixmap portable (PPM) (représentation textuelle Unix produisant de gros fichiers) ou en un format de fichier binaire simple et lisible, comme . BMP ou TGA . ImageMagick sous Unix ou IrfanView sous Windows.

1.2 Vous pouvez, comme mentionné précédemment, simplifier les données en prenant (R + G + B)/3 pour chaque pixel comme indicateur de ton gris et en appliquant un seuil à la valeur afin de produire un tableau noir et blanc. Une valeur proche de 200 en supposant que 0 = noir et 255 = blanc supprime les artefacts JPEG.

(2. Solutions :)

2.1 Recherche en profondeur d'abord: Initiez une pile vide avec l'emplacement de départ, collectez les mouvements de suivi disponibles, choisissez-en un au hasard et appuyez sur la pile, poursuivez jusqu'à ce que vous atteigniez la fin ou une impasse. Pour revenir en arrière en faisant sauter la pile, vous devez savoir quelles positions ont été visitées sur la carte. Ainsi, lorsque vous collectez les mouvements disponibles, vous ne suivez jamais le même chemin deux fois. Très intéressant à animer.

2.2 Recherche en largeur d'abord: Mentionné précédemment, comme ci-dessus mais en n'utilisant que des files d'attente. Aussi intéressant à animer. Cela fonctionne comme un flou dans un logiciel de retouche d'image. Je pense que vous pourrez peut-être résoudre un labyrinthe dans Photoshop en utilisant cette astuce.

2.3 Suiveur de mur: géométriquement, un labyrinthe est un tube plié/alambiqué. Si vous gardez la main sur le mur, vous finirez par trouver la sortie;) Cela ne fonctionne pas toujours. Il existe certaines hypothèses: labyrinthes parfaits, etc., par exemple, certains labyrinthes contiennent des îles. Cherchez-le; c'est fascinant.

(3. Commentaires :)

C'est le plus délicat. Il est facile de résoudre les labyrinthes s’il est représenté par un simple tableau formel, chaque élément étant un type de cellule avec des murs nord, est, sud et ouest et un champ de drapeau visité. Cependant, étant donné que vous essayez de faire cela, étant donné un croquis dessiné à la main, cela devient compliqué. Honnêtement, je pense qu'essayer de rationaliser l'esquisse vous rendra fou. Cela s'apparente à des problèmes de vision par ordinateur qui sont assez impliqués. Peut-être aller directement sur la carte-image sera peut-être plus facile mais plus onéreux.

5
lino

J'irais pour l'option de matrice de bools. Si vous trouvez que les listes Python standard sont trop inefficaces pour cela, vous pouvez utiliser un tableau numpy.bool à la place. Le stockage pour un labyrinthe de 1000 x 1000 pixels est alors de 1 Mo seulement.

Ne vous embêtez pas pour créer des structures de données arborescentes ou graphiques. C'est juste une façon de penser à ce sujet, mais pas nécessairement un bon moyen de le représenter en mémoire; une matrice booléenne est à la fois plus facile à coder et plus efficace.

Ensuite, utilisez l'algorithme A * pour le résoudre. Pour l'heuristique de distance, utilisez la distance de Manhattan (distance_x + distance_y).

Représenter les nœuds par un tuple de coordonnées (row, column). Chaque fois que l'algorithme ( pseudo-code Wikipedia ) appelle des "voisins", il suffit de boucler sur les quatre voisins possibles (attention aux bords de l'image!).

Si vous trouvez qu'elle est toujours trop lente, vous pouvez essayer de réduire la taille de l'image avant de la charger. Veillez à ne pas perdre de chemins étroits dans le processus.

Peut-être qu'il est également possible d'effectuer une réduction d'échelle 1: 2 en Python, en vérifiant que vous ne perdez pas les chemins possibles. Une option intéressante, mais qui nécessite un peu plus de réflexion.

5
Thomas

Voici une solution en utilisant R. 

### download the image, read it into R, converting to something we can play with...
library(jpeg)
url <- "https://i.stack.imgur.com/TqKCM.jpg"
download.file(url, "./maze.jpg", mode = "wb")
jpg <- readJPEG("./maze.jpg")

### reshape array into data.frame
library(reshape2)
img3 <- melt(jpg, varnames = c("y","x","rgb"))
img3$rgb <- as.character(factor(img3$rgb, levels = c(1,2,3), labels=c("r","g","b")))

## split out rgb values into separate columns
img3 <- dcast(img3, x + y ~ rgb)

RVB en niveaux de gris, voir: https://stackoverflow.com/a/27491947/2371031

# convert rgb to greyscale (0, 1)
img3$v <- img3$r*.21 + img3$g*.72 + img3$b*.07
# v: values closer to 1 are white, closer to 0 are black

## strategically fill in some border pixels so the solver doesn't "go around":
img3$v2 <- img3$v
img3[(img3$x == 300 | img3$x == 500) & (img3$y %in% c(0:23,988:1002)),"v2"]  = 0

# define some start/end point coordinates
pts_df <- data.frame(x = c(398, 399),
                     y = c(985, 26))

# set a reference value as the mean of the start and end point greyscale "v"s
ref_val <- mean(c(subset(img3, x==pts_df[1,1] & y==pts_df[1,2])$v,
                  subset(img3, x==pts_df[2,1] & y==pts_df[2,2])$v))

library(sp)
library(gdistance)
spdf3 <- SpatialPixelsDataFrame(points = img3[c("x","y")], data = img3["v2"])
r3 <- rasterFromXYZ(spdf3)

# transition layer defines a "conductance" function between any two points, and the number of connections (4 = Manhatten distances)
# x in the function represents the greyscale values ("v2") of two adjacent points (pixels), i.e., = (x1$v2, x2$v2)
# make function(x) encourages transitions between cells with small changes in greyscale compared to the reference values, such that: 
# when v2 is closer to 0 (black) = poor conductance
# when v2 is closer to 1 (white) = good conductance
tl3 <- transition(r3, function(x) (1/max( abs( (x/ref_val)-1 ) )^2)-1, 4) 

## get the shortest path between start, end points
sPath3 <- shortestPath(tl3, as.numeric(pts_df[1,]), as.numeric(pts_df[2,]), output = "SpatialLines")

## fortify for ggplot
sldf3 <- fortify(SpatialLinesDataFrame(sPath3, data = data.frame(ID = 1)))

# plot the image greyscale with start/end points (red) and shortest path (green)
ggplot(img3) +
  geom_raster(aes(x, y, fill=v2)) +
  scale_fill_continuous(high="white", low="black") +
  scale_y_reverse() +
  geom_point(data=pts_df, aes(x, y), color="red") +
  geom_path(data=sldf3, aes(x=long, y=lat), color="green")

Voila! 

solution that correctly finds shortest path

C'est ce qui se passe si vous ne remplissez pas certains pixels de la bordure (Ha!) ...

solution version where the solver goes around the maze

Divulgation complète: j’ai demandé et répondu à une question similaire moi-même avant de trouver celui-ci. Puis, grâce à la magie de SO, celui-ci figure parmi les «questions connexes». Je pensais utiliser ce labyrinthe comme test supplémentaire ... Je suis très heureux de constater que ma réponse à cette question fonctionne également pour cette application avec très peu de modifications.

0
Brian D