画像が与えられた迷路を表現し解決する最良の方法は何ですか?
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, [])
これが解決策です。
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 などで実装するのに困難はないはずです。
そして、ここに答えがあります:
このソリューションはPythonで書かれています。画像の準備に関する指示をしてくれたミハイルに感謝します。
アニメーション幅広検索:
完成した迷路:
#!/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回ロードする必要があります(最終パスとすべてのパスの合成イメージが必要ない場合)。
私はこの問題に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回繰り返した後、アニメーションはそれぞれ新しいフレームを表示します。
幅優先検索:
A-Starマンハッタン距離:
A-Star Squared Euclidean Distance:
A-Starマンハッタン距離に4を掛けた値:
結果は、迷路の探索された領域が、使用されているヒューリスティックによってかなり異なることを示しています。そのため、ユークリッド距離の2乗は、他のメトリックとは異なる(準最適)パスを生成します。
終了までの実行時間に関するA-Starアルゴリズムのパフォーマンスに関しては、距離とコスト関数の多くの評価が、「ゴール」の評価のみを必要とするBreadth-First Search(BFS)と比較されることに注意してください。各候補者の位置。これらの追加機能評価(A-Star)のコストがチェック対象の多数のノード(BFS)のコストを上回るかどうか、特にパフォーマンスがアプリケーションの問題であるかどうかは、個々の認識の問題ですもちろん、一般的に答えることはできません。
一般的に、徹底的な検索(例:BFS)と比較して、インフォームドサーチアルゴリズム(A-Starなど)の方が良い選択であるかどうかを言うことができること以下です。迷路の次元の数、つまり検索ツリーの分岐要因により、徹底的な検索(徹底的な検索)の欠点は指数関数的に増大します。複雑さが増すにつれて、そうすることはますます難しくなり、ある時点で(ほぼ)最適であるかどうかにかかわらず、結果のパスanyにかなり満足しています。
ツリー検索が多すぎます。迷路は、ソリューションパスに沿って本質的に分離可能です。
(これを指摘してくれた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);
しきい値の連続フィルにキューを使用します。入り口の左側のピクセルをキューにプッシュし、ループを開始します。キューに入れられたピクセルが十分に暗い場合は、明るい灰色(しきい値以上)になり、すべての隣接ピクセルがキューにプッシュされます。
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")
解決策は、灰色の壁と色付きの壁の間の廊下です。この迷路には複数の解決策があることに注意してください。また、これは単に機能しているように見えます。
ここに行きます: maze-solver-python (GitHub)
私はこれをいじって楽しんで、 Joseph Kern の答えを拡張しました。それを損なうことはありません。これをいじってみたいと思う人のために、ちょっとした追加をしただけです。
これは、BFSを使用して最短パスを見つけるPythonベースのソルバーです。当時の主な追加は次のとおりです。
現状では、このサンプル迷路の開始/終了ポイントはハードコーディングされていますが、適切なピクセルを選択できるように拡張する予定です。
ここにいくつかのアイデアがあります。
(1.画像処理:)
1.1 RGB ピクセルマップとして画像を読み込みます。 C# では、system.drawing.bitmap
を使用するのは簡単です。イメージングを簡単にサポートしていない言語では、イメージを portable pixmap format (PPM)(Unixテキスト表現、大きなファイルを生成)に変換するか、簡単に読むことができる単純なバイナリファイル形式に変換します。 BMP または TGA 。 ImageMagick Unixの場合 IrfanView Windowsの場合.
1.2前述のように、各ピクセルの(R + G + B)/ 3をグレートーンのインジケータとして使用し、値をしきい値処理して白黒テーブルを生成することにより、データを単純化できます。 0 =黒と255 =白を仮定すると200に近いものはJPEGアーティファクトを取り除きます。
(2.ソリューション:)
2.1深さ優先検索:開始位置で空のスタックを初期化し、利用可能なフォローアップの動きを収集し、ランダムに1つ選択してスタックにプッシュし、最後に到達するか行き止まりになるまで続行します。スタックをポップすることによる行き止まりのバックトラックでは、マップ上のどの位置にアクセスしたかを追跡する必要があるため、利用可能な動きを収集するときに同じパスを2回とることはありません。アニメーション化するのは非常に興味深い。
2.2幅優先検索:前に言及しました。上記と同様ですが、キューのみを使用します。アニメ化するのも面白い。これは、画像編集ソフトウェアの塗りつぶしのように機能します。このトリックを使用して、Photoshopの迷路を解決できるかもしれません。
2.3ウォールフォロワー:幾何学的に言えば、迷路は折り畳まれた/入り組んだチューブです。壁に手をかざすと、やがて出口が見つかります;)これは常に機能するとは限りません。たとえば、完全な迷路など、特定の仮定があります。たとえば、特定の迷路には島が含まれています。調べてください。それは魅力的です。
(3.コメント:)
これはトリッキーなものです。各要素が北、東、南、および西の壁と訪問済みのフラグフィールドを持つセルタイプである単純な配列形式で表されている場合、迷路を簡単に解決できます。しかし、手描きのスケッチを与えてこれを行おうとすると、面倒になります。私は正直に、スケッチを合理化しようとすることはあなたを夢中にさせると思います。これは、かなり複雑なコンピュータービジョンの問題に似ています。おそらく、イメージマップに直接アクセスする方が簡単であり、無駄が多いかもしれません。
マトリックスオブブールオプションを選択します。標準のPythonリストがこれに対して非効率的であることがわかった場合は、代わりにnumpy.bool
配列を使用できます。 1000x1000ピクセルの迷路のストレージはわずか1 MBです。
ツリーやグラフのデータ構造を作成する必要はありません。これは単なる考え方ですが、必ずしもメモリ内で表すのに適した方法ではありません。ブール行列は、コーディングが簡単で効率的です。
次に、A *アルゴリズムを使用して解決します。距離ヒューリスティックには、マンハッタン距離(distance_x + distance_y
)を使用します。
(row, column)
座標のタプルでノードを表します。アルゴリズム( Wikipedia pseudocode )が「隣人」を呼び出すときはいつでも、4つの可能な隣人をループする単純な問題です(画像の端に注意してください!)。
それでも遅い場合は、読み込む前に画像を縮小してみてください。プロセスで狭いパスを失わないように注意してください。
たぶん、Pythonで1:2のダウンスケーリングを行うことも可能です。実際に可能なパスを失わないことを確認します。興味深いオプションですが、もう少し考えが必要です。
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")
出来上がり!
これは、いくつかの境界ピクセルを埋めないと発生します(Ha!)...
完全な開示:これを見つける前に、私は非常に 類似した質問 を尋ねて答えました。それからSOの魔法を通して、これをトップの「関連する質問」の1つとして見つけました。この迷路を追加のテストケースとして使用すると思ったのですが、そこにある私の答えも、ほとんど変更を加えずにこのアプリケーションで機能することがわかりました。