web-dev-qa-db-ja.com

画像が与えられた迷路の表現と解決

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

The cover image of The Scope Issue 134

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

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

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

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

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

TL; DR
解析する最良の方法は?どのデータ構造に?上記の構造は、解決にどのように役立ちますか?

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

これが解決策です。

  1. 画像をグレースケール(まだバイナリではない)に変換し、最終的なグレースケール画像がほぼ均一になるように色の重みを調整します。画像->調整->白黒でPhotoshopのスライダーを制御するだけで簡単にできます。
  2. 画像->調整->しきい値でPhotoshopで適切なしきい値を設定して、画像をバイナリに変換します。
  3. しきい値が正しく選択されていることを確認してください。マジックワンドツールは、許容値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 などで実装するのに困難はないはずです。

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

Enter image description here

229
Mikhail

このソリューションは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 __== '__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回ロードする必要があります(最終パスとすべてのパスの合成イメージが必要ない場合)。

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

154
Joseph Kern

私はこの問題にA-Star検索を実装しようとしました。フレームワークの Joseph Kern と、与えられたアルゴリズム擬似コード here によって実装に密接に続きました:

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回繰り返した後、アニメーションはそれぞれ新しいフレームを表示します。

幅優先検索:

Breadth-First Search

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

A-Star Manhattan Distance

A-Star Squared Euclidean Distance:

A-Star Squared Euclidean Distance

A-Starマンハッタン距離に4を掛けた値:

A-Star Manhattan Distance multiplied by four

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

終了までの実行時間に関するA-Starアルゴリズムのパフォーマンスに関しては、距離とコスト関数の多くの評価が、「ゴール」の評価のみを必要とするBreadth-First Search(BFS)と比較されることに注意してください。各候補者の位置。これらの追加機能評価(A-Star)のコストがチェック対象の多数のノード(BFS)のコストを上回るかどうか、特にパフォーマンスがアプリケーションの問題であるかどうかは、個々の認識の問題ですもちろん、一般的に答えることはできません。

一般的に、徹底的な検索(例:BFS)と比較して、インフォームドサーチアルゴリズム(A-Starなど)の方が良い選択であるかどうかを言うことができること以下です。迷路の次元の数、つまり検索ツリーの分岐要因により、徹底的な検索(徹底的な検索)の欠点は指数関数的に増大します。複雑さが増すにつれて、そうすることはますます難しくなり、ある時点で(ほぼ)最適であるかどうかにかかわらず、結果のパスanyにかなり満足しています。

78
moooeeeep

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

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

このため、 connected components を使用して、迷路の壁の接続されたセクションを識別することができます。これは、ピクセルを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);

result of current code

36
Jim Gray

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

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

23
kylefinn

ここに行きます: maze-solver-python (GitHub)

enter image description here

私はこれをいじって楽しんで、 Joseph Kern の答えを拡張しました。それを損なうことはありません。これをいじってみたいと思う人のために、ちょっとした追加をしただけです。

これは、BFSを使用して最短パスを見つけるPythonベースのソルバーです。当時の主な追加は次のとおりです。

  1. 画像は検索の前に消去されます(つまり、純粋な白黒に変換します)
  2. GIFを自動的に生成します。
  3. AVIを自動的に生成します。

現状では、このサンプル迷路の開始/終了ポイントはハードコーディングされていますが、適切なピクセルを選択できるように拡張する予定です。

22
stefano

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

(1.画像処理:)

1.1 RGB ピクセルマップとして画像を読み込みます。 C# では、system.drawing.bitmapを使用するのは簡単です。イメージングを簡単にサポートしていない言語では、イメージを portable pixmap format (PPM)(Unixテキスト表現、大きなファイルを生成)に変換するか、簡単に読むことができる単純なバイナリファイル形式に変換します。 BMP または TGAImageMagick Unixの場合 IrfanView Windowsの場合.

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

(2.ソリューション:)

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

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

2.3ウォールフォロワー:幾何学的に言えば、迷路は折り畳まれた/入り組んだチューブです。壁に手をかざすと、やがて出口が見つかります;)これは常に機能するとは限りません。たとえば、完全な迷路など、特定の仮定があります。たとえば、特定の迷路には島が含まれています。調べてください。それは魅力的です。

(3.コメント:)

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

5
lino

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

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

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

(row, column)座標のタプルでノードを表します。アルゴリズム( Wikipedia pseudocode )が「隣人」を呼び出すときはいつでも、4つの可能な隣人をループする単純な問題です(画像の端に注意してください!)。

それでも遅い場合は、読み込む前に画像を縮小してみてください。プロセスで狭いパスを失わないように注意してください。

たぶん、Pythonで1:2のダウンスケーリングを行うことも可能です。実際に可能なパスを失わないことを確認します。興味深いオプションですが、もう少し考えが必要です。

5
Thomas

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)

RGBからグレースケール、参照: 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")

出来上がり!

solution that correctly finds shortest path

これは、いくつかの境界ピクセルを埋めないと発生します(Ha!)...

solution version where the solver goes around the maze

完全な開示:これを見つける前に、私は非常に 類似した質問 を尋ねて答えました。それからSOの魔法を通して、これをトップの「関連する質問」の1つとして見つけました。この迷路を追加のテストケースとして使用すると思ったのですが、そこにある私の答えも、ほとんど変更を加えずにこのアプリケーションで機能することがわかりました。

0
Brian D