283

画像を与えられた迷路を表現して解決するための最良の方法は何ですか?

スコープイシュー134の表紙画像

(上記のように)JPEG画像が与えられた場合、それを読み取り、データ構造に解析し、迷路を解決するための最良の方法は何ですか?私の最初の本能は、ピクセルごとに画像を読み取り、それをブール値のリスト(配列)に格納することです。True白のピクセルとFalse白以外のピクセル(色は破棄できます)の場合です。この方法の問題は、画像が「ピクセルパーフェクト」ではない可能性があることです。つまり、壁のどこかに白いピクセルがあると、意図しないパスが作成される可能性があるということです。

もう1つの方法(少し考えた後で思いついた)は、画像をSVGファイルに変換することです。これはキャンバスに描画されたパスのリストです。このようにして、パスを同じ種類のリスト(ブール値)に読み込むことができます。ここでTrue、はパスまたは壁をFalse示し、移動可能なスペースを示します。この方法の問題は、変換が100%正確でなく、すべての壁を完全に接続しておらず、ギャップが生じている場合に発生します。

また、SVGへの変換に関する問題は、線が「完全に」まっすぐではないことです。これにより、パスは3次ベジェ曲線になります。整数でインデックス付けされたブール値のリスト(配列)を使用すると、曲線は簡単に転送されず、曲線上に並ぶすべてのポイントを計算する必要がありますが、リストのインデックスと正確に一致するわけではありません。

これらの方法の1つは機能するかもしれませんが(おそらくそうではないかもしれませんが)、そのような大きな画像を考えると、それらはひどく非効率的であり、より良い方法が存在すると思います。これはどのように(最も効率的におよび/または最も複雑さを抑えて)行われるのですか?最善の方法さえありますか?

次に、迷路の解決が行われます。最初の2つの方法のいずれかを使用すると、基本的に行列になります。この答えによると、迷路を表現する良い方法は木を使用することであり、それを解決する良い方法はA*アルゴリズムを使用することです。画像からどのようにツリーを作成しますか?何か案は?

TL; DR
解析する最良の方法は?どのようなデータ構造に?構造はどのように解決を助け/妨げますか?

更新
@Thomas が推奨するように、@ MikhailがPythonで記述したものを、を使用して実装しようとしましたnumpy。アルゴリズムは正しいと思いますが、期待どおりに機能していません。(以下のコード。)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, [])
4

10 に答える 10

244

これが解決策です。

  1. イメージをグレースケール (まだバイナリではない) に変換し、色の重みを調整して、最終的なグレースケール イメージがほぼ均一になるようにします。Photoshop の [画像] -> [調整] -> [白黒] でスライダーを制御するだけで簡単に行うことができます。
  2. Photoshop の [イメージ] -> [調整] -> [しきい値] で適切なしきい値を設定して、イメージをバイナリに変換します。
  3. しきい値が正しく選択されていることを確認してください。Magic Wand Tool を 0 許容値、ポイント サンプル、連続、アンチエイリアシングなしで使用します。選択が途切れるエッジが、間違ったしきい値によって導入された偽のエッジではないことを確認してください。実際、この迷路のすべての内部ポイントは最初からアクセス可能です。
  4. 迷路に人工的な境界線を追加して、仮想旅行者が迷路を歩き回らないようにします:)
  5. お好みの言語で幅優先探索(BFS) を実装し、最初から実行します。このタスクにはMATLABの方が適しています。@Thomas が既に述べたように、グラフの通常の表現をいじる必要はありません。二値化された画像を直接操作できます。

BFS の MATLAB コードは次のとおりです。

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などでこれを実装するのに問題はないはずです。

そして、ここに答えがあります:

ここに画像の説明を入力してください

于 2012-10-21T07:20:56.137 に答える
164

このソリューションは Python で記述されています。画像の準備について指摘してくれた Mikhail に感謝します。

アニメーション化された幅優先検索:

BFSのアニメーション版

完成した迷路:

完成した迷路

#!/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])

注:訪問した白いピクセル グレーをマークします。これにより、訪問済みリストが不要になりますが、パスを描画する前に、ディスクからイメージ ファイルを 2 回ロードする必要があります (最終的なパスと取得したすべてのパスの複合イメージが必要ない場合)。

私が使用した迷路の空白バージョン。

于 2012-11-01T09:40:23.417 に答える
83

この問題に対して 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 はヒューリスティック検索アルゴリズムであるため、目標に到達するまでの残りのコスト (ここでは距離) を推定する関数を考え出す必要があります。次善のソリューションに慣れていない限り、コストを過大評価するべきではありません。保守的な選択は、ここではマンハッタン (またはタクシー) の距離です。これは、使用されているフォン ノイマン近傍のグリッド上の 2 点間の直線距離を表すためです。(この場合、コストを過大評価することはありません。)

ただし、これは手元にある特定の迷路の実際のコストを大幅に過小評価することになります。したがって、比較のために、ユークリッド距離の 2 乗とマンハッタン距離の 4 倍の 2 つの距離メトリックを追加しました。ただし、これらは実際のコストを過大評価する可能性があるため、最適ではない結果が生じる可能性があります。

コードは次のとおりです。

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

結果を視覚化するためのいくつかの画像を次に示します ( Joseph Kernによって投稿されたものに触発されました)。アニメーションは、メインの while ループを 10000 回繰り返した後に新しいフレームを表示します。

幅優先検索:

幅優先探索

A-Star マンハッタン距離:

A-Star マンハッタン ディスタンス

A-Star 二乗ユークリッド距離:

A-Star 二乗ユークリッド距離

A-Star マンハッタン距離の 4 倍:

A-Star マンハッタン距離の 4 倍

結果は、迷路の探索された領域が、使用されているヒューリスティックによってかなり異なることを示しています。そのため、二乗ユークリッド距離は、他のメトリックとは異なる (次善の) パスを生成します。

終了までの実行時間に関する A-Star アルゴリズムのパフォーマンスに関しては、距離とコスト関数の多くの評価が、それぞれの候補地。これらの追加の機能評価 (A-Star) のコストが、チェックするノード数の増加 (BFS) のコストを上回るかどうか、特にパフォーマンスがアプリケーションの問題であるかどうかは、個人の認識の問題です。もちろん一概には答えられません。

インフォームド サーチ アルゴリズム (A-Star など) が網羅的サーチ (BFS など) よりも優れているかどうかについて、一般的に言えることは次のとおりです。迷路の次元数、つまり検索ツリーの分岐係数に応じて、網羅的探索 (網羅的に探索する) の欠点が指数関数的に大きくなります。複雑さが増すにつれて、そうすることがますます現実的ではなくなり、ある時点で、(ほぼ)最適であるかどうかにかかわらず、結果パスにほとんど満足しています。

于 2013-05-20T19:33:31.647 に答える
39

ツリー検索が多すぎます。迷路は、ソリューション パスに沿って本質的に分離可能です。

(これを指摘してくれた Reddit のrainman002に感謝します。)

このため、連結要素を使用して、迷路の壁の連結部分をすばやく特定できます。これは、ピクセルを 2 回繰り返します。

それをソリューション パスの優れた図に変換したい場合は、構造化要素を使用してバイナリ操作を使用して、接続された各領域の「行き止まり」の経路を埋めることができます。

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

現在のコードの結果

于 2012-10-24T08:33:15.350 に答える
24

しきい値の連続充填にキューを使用します。入口の左側のピクセルをキューにプッシュしてから、ループを開始します。キューに入れられたピクセルが十分に暗い場合、そのピクセルは明るい灰色(しきい値を超える)になり、すべての隣接ピクセルがキューにプッシュされます。

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

解決策は、灰色の壁と色付きの壁の間の廊下です。この迷路には複数の解決策があることに注意してください。また、これは単に機能しているように見えます。

解決

于 2012-10-24T02:41:16.557 に答える
5

ここにいくつかのアイデアがあります。

(1.画像処理:)

1.1 画像をRGBピクセル マップとして読み込みます。C#では、 を使用すると簡単system.drawing.bitmapです。画像処理を簡単にサポートしていない言語では、画像を ポータブル ピックスマップ形式(PPM) (Unix テキスト表現で、大きなファイルを生成します)、またはBMPTGAなどの読みやすい単純なバイナリ ファイル形式に変換するだけです。Unix のImageMagickまたはWindows のIrfanView 。

1.2 前述のように、各ピクセルの (R+G+B)/3 をグレー トーンの指標として取り、値をしきい値処理して白黒のテーブルを作成することで、データを単純化できます。0=黒、255=白と仮定すると、200 に近い値が JPEG アーティファクトを取り除きます。

(2.解決策:)

2.1 深さ優先検索: 開始位置で空のスタックを開始し、利用可能なフォローアップの動きを収集し、ランダムに 1 つを選択してスタックにプッシュし、最後に到達するか行き止まりになるまで続行します。スタックをポップしてデッドエンドのバックトラックを行う場合、マップ上でどの位置を訪れたかを追跡する必要があるため、利用可能な動きを収集するときに同じパスを 2 回たどることはありません。アニメ化するのはとても面白い。

2.2 幅優先検索: 前に述べた、上記と同様ですが、キューのみを使用します。アニメ化も面白い。これは、画像編集ソフトウェアの塗りつぶしのように機能します。このトリックを使用して、Photoshop で迷路を解くことができると思います。

2.3 Wall Follower: 幾何学的に言えば、迷路は折りたたまれた/回旋状のチューブです。壁に手を置いたままにしておくと、最終的には出口が見つかります ;) これは常に機能するとは限りません。完全な迷路などに関する特定の仮定があります。たとえば、特定の迷路には島が含まれています。調べてください。それは魅力的です。

(3. コメント:)

これはトリッキーなものです。各要素が北、東、南、西の壁と訪問済みフラグ フィールドを持つセル型である単純な配列形式で表現されている場合、迷路を解くのは簡単です。ただし、手描きのスケッチを指定してこれを実行しようとすると、面倒になります。正直なところ、スケッチを合理化しようとすると気が狂ってしまうと思います。これは、かなり複雑なコンピューター ビジョンの問題に似ています。イメージ マップに直接アクセスする方が簡単かもしれませんが、無駄が多いかもしれません。

于 2012-10-29T16:23:01.363 に答える
5

私はmatrix-of-boolsオプションを選びます。numpy.boolこれには標準の Python リストが非効率的であることがわかった場合は、代わりに配列を使用できます。1000x1000 ピクセルの迷路のストレージは、わずか 1 MB です。

ツリーまたはグラフのデータ構造を作成することを気にしないでください。これは単なる考え方にすぎませんが、必ずしもメモリ内で表現するのに適した方法ではありません。ブール行列は、コーディングが簡単で効率的です。

次に、A* アルゴリズムを使用して解決します。距離ヒューリスティックには、マンハッタン距離 ( distance_x + distance_y) を使用します。

(row, column)座標のタプルでノードを表します。アルゴリズム (ウィキペディアの疑似コード) が「隣人」を呼び出すときはいつでも、4 つの可能な隣人をループするだけです (画像の端に注意してください!)。

それでも遅すぎる場合は、画像をロードする前に縮小してみてください。その過程で狭い道を見失わないように注意してください。

おそらく、Python でも 1:2 のダウンスケーリングを実行して、可能なパスが実際に失われていないことを確認できます。興味深いオプションですが、もう少し考える必要があります。

于 2012-10-21T07:17:42.813 に答える