给定图像表示和解决迷宫的最佳方法是什么?
给定JPEG图像(如上所示),读取它的最佳方法是什么,将其解析为某些数据结构并解决迷宫?我的第一直觉是逐像素地读取图像并将其存储在布尔值的列表(数组)中:白色像素为True
,非白色像素为False
(颜色)可以丢弃)。这种方法的问题是图像可能不是“像素完美”。我只是说,如果墙上某处有白色像素,可能会造成意想不到的路径。
另一种方法(经过一番考虑后来找我)是将图像转换为SVG文件 - 这是在画布上绘制的路径列表。这样,路径可以被读入相同类型的列表(布尔值),其中True
表示路径或墙,False
表示可行进空间。如果转换不是100%准确,并且未完全连接所有墙壁,则会产生此方法的问题,从而产生间隙。
转换为SVG的另一个问题是线条不是“完美”直线。这导致路径是立方贝塞尔曲线。使用由整数索引的布尔值列表(数组),曲线不会轻易转移,并且必须计算曲线上所有的点,但不会与列表索引完全匹配。
我认为虽然这些方法中的一种可能有效(尽管可能不是),但鉴于这么大的图像,它们的效率非常低,并且存在更好的方法。如何最好(最有效和/或最简单)完成?还有最好的办法吗?
然后是解决迷宫的问题。如果我使用前两种方法中的任何一种,我基本上会得到一个矩阵。根据{{3}},表示迷宫的好方法是使用树,解决问题的好方法是使用this answer。如何从图像中创建树?有什么想法吗? TL; DR
最好的解析方法?进入什么数据结构?该结构将如何帮助/阻碍解决?
更新
正如@Thomas推荐的那样,我已尝试使用numpy
来实现@Mikhail用Python编写的内容。我觉得这个算法是正确的,但它没有像希望的那样工作。 (下面的代码。)PNG库是A* algorithm。
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, [])
答案 0 :(得分:227)
这是一个解决方案。
以下是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或其他任何地方实施这一点都不会有困难。
以下是答案:
答案 1 :(得分:154)
此解决方案是用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 __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 :(得分:78)
我尝试自己实施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是一种启发式搜索算法,因此您需要提供一个函数来估算剩余成本(此处为:距离),直到达到目标为止。除非您对次优解决方案感到满意,否则不应高估成本。这里保守的选择是manhattan (or taxicab) distance,因为它代表了所用Von Neumann邻域网格上两点之间的直线距离。 (在这种情况下,不会高估成本。)
然而,这会大大低估手头给定迷宫的实际成本。因此,我添加了另外两个距离度量平方欧氏距离和曼哈顿距离乘以4进行比较。然而,这些可能会高估实际成本,因此可能会产生不理想的结果。以下是代码:
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发布的图像)。动画在主循环的10000次迭代后每次显示一个新帧。
广度优先搜索:
曼哈顿A-Star距离:
A-Star Squared Euclidean距离:
A-Star Manhattan Distance乘以4:
结果表明,对于所使用的启发式方法,迷宫的探索区域差异很大。因此,平方欧氏距离甚至会产生与其他指标不同(次优)的路径。
关于A-Star算法在终止运行时的性能,请注意,与广度优先搜索(BFS)相比,距离和成本函数的大量评估相加,而BFS只需要评估“每个候选人职位的“目标性”。这些额外功能评估(A-Star)的成本是否超过了大量要检查的节点(BFS)的成本,特别是性能是否对您的应用程序来说是一个问题,这是个人感知的问题当然不能得到普遍回答。
以下。利用迷宫的维数,即搜索树的分支因子,穷举搜索(穷举搜索)的缺点呈指数增长。随着复杂性的增加,这样做变得越来越不可行,并且在某些时候,您对任何结果路径非常满意,无论是(近似)最佳还是不是。
答案 3 :(得分:35)
树搜索太多了。迷宫沿着解决方案路径本质上是可分离的。
(感谢Reddit的rainman002向我指出这一点。)
因此,您可以快速使用connected components来识别迷宫墙的连接部分。这会迭代像素两次。
如果要将其转换为解决方案路径的漂亮图表,则可以使用带结构元素的二元运算来填充每个连接区域的“死胡同”路径。
接下来是MATLAB的演示代码。它可以使用tweaking来更好地清理结果,使其更具通用性,并使其运行更快。 (有时候不是凌晨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);
答案 4 :(得分:23)
使用队列进行阈值连续填充。将入口左侧的像素推入队列,然后启动循环。如果排队的像素足够暗,则它是浅灰色(高于阈值),并且所有邻居都被推入队列。
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")
解决方案是灰墙和彩色墙之间的走廊。注意这个迷宫有多种解决方案。而且,这似乎只是起作用。
答案 5 :(得分:22)
在这里:maze-solver-python(GitHub)
我很开心玩这个并延长Joseph Kern的答案。不要减损它;我只是为其他可能有兴趣玩这个的人做了一些小小的补充。
这是一个基于python的求解器,它使用BFS来查找最短路径。我当时的主要补充是:
就目前而言,这个样本迷宫的起点/终点是硬编码的,但我计划扩展它,以便你可以选择合适的像素。
答案 6 :(得分:5)
我会选择matrix-of-bools选项。如果您发现标准Python列表效率太低,则可以使用numpy.bool
数组。存储1000x1000像素的迷宫只需1 MB。
不要为创建任何树或图形数据结构而烦恼。这只是一种思考它的方式,但不一定是在记忆中表现它的好方法;布尔矩阵更容易编码,效率更高。
然后使用A *算法来解决它。对于距离启发式,请使用曼哈顿距离(distance_x + distance_y
)。
通过(row, column)
坐标的元组表示节点。每当算法(Wikipedia pseudocode)调用“邻居”时,循环四个可能的邻居就很简单(请注意图像的边缘!)。
如果您发现它仍然太慢,您可以在加载前尝试缩小图像。注意不要在此过程中丢失任何狭窄的路径。
也许可以在Python中进行1:2的缩减,检查你实际上没有丢失任何可能的路径。一个有趣的选择,但它需要更多的思考。
答案 7 :(得分:5)
以下是一些想法。
(1.图像处理:)
1.1将图像加载为RGB像素图。在C#中使用system.drawing.bitmap
是微不足道的。在没有简单支持成像的语言中,只需将图像转换为portable pixmap format(PPM)(Unix文本表示,生成大文件)或一些您可以轻松阅读的简单二进制文件格式,例如BMP或TGA。 Unix中的ImageMagick或Windows中的IrfanView。
1.2如前所述,您可以通过将每个像素的(R + G + B)/ 3作为灰色调的指示符来简化数据,然后对该值进行阈值处理以生成黑白表。接近200,假设0 =黑色,255 =白色,将取出JPEG伪像。
(2.解决方案:)
2.1深度优先搜索:初始化具有起始位置的空堆栈,收集可用的后续移动,随机选择一个并推入堆栈,继续直到结束或deadend。在通过弹出堆栈进行后退回溯时,您需要跟踪地图上访问的位置,这样当您收集可用的移动时,您永远不会两次使用相同的路径。动画非常有趣。
2.2广度优先搜索:之前提到过,类似于上面但仅使用队列。动画也很有趣。这类似于图像编辑软件中的泛洪填充。我想你可以使用这个技巧在Photoshop中解决迷宫。
2.3墙跟随器:从几何学上讲,迷宫是折叠/旋绕管。如果你把手放在墙上,你最终会找到出口;)这并不总是有效。有一定的假设:完美的迷宫等,例如,某些迷宫包含岛屿。看一看;这很吸引人。
(3.评论:)
这是棘手的。如果以一些简单的数组形式表示,很容易解决迷宫,每个元素是具有北,东,南和西墙和访问标志字段的单元类型。但是考虑到你试图用手绘草图来做这件事,它会变得混乱。老实说,我认为试图使草图合理化会让你疯狂。这类似于相当复杂的计算机视觉问题。也许直接进入图像地图可能更容易,也更浪费。
答案 8 :(得分:0)
这是使用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")
Voila!
如果您不填写一些边框像素(哈!)...
完全公开:我发现自己之前非常similar question问过自己。然后,通过SO的魔力,发现这是最重要的“相关问题”之一。我以为我会把这个迷宫作为一个额外的测试用例...我很高兴地发现,在那里的答案也几乎不需要修改就可以用于该应用程序。
答案 9 :(得分:0)
一个好的解决方案是,而不是按像素查找邻居,而是按单元完成,因为走廊可以有15px,因此在同一走廊中它可以执行左右操作,而如果这样做是如果位移是一个立方体,它将是一个简单的动作,例如UP,DOWN,LEFT或RIGHT