Python 表示和求解给定图像的迷宫

Python 表示和求解给定图像的迷宫,python,algorithm,matlab,image-processing,maze,Python,Algorithm,Matlab,Image Processing,Maze,对于给定的图像,表示和求解迷宫的最佳方法是什么 如上图所示,给定一幅JPEG图像,读入该图像、将其解析为某种数据结构并解决迷宫的最佳方法是什么?我的第一反应是逐像素读取图像,并将其存储在布尔值列表数组中:对于白色像素为True,对于非白色像素为False,颜色可以丢弃。这种方法的问题是,图像可能不是像素完美的。我的意思是,如果墙上某个地方有一个白色像素,它可能会创建一个非预期的路径 经过一番思考后,我想到的另一种方法是将图像转换为SVG文件——这是在画布上绘制的路径列表。这样,路径可以读入相同种

对于给定的图像,表示和求解迷宫的最佳方法是什么

如上图所示,给定一幅JPEG图像,读入该图像、将其解析为某种数据结构并解决迷宫的最佳方法是什么?我的第一反应是逐像素读取图像,并将其存储在布尔值列表数组中:对于白色像素为True,对于非白色像素为False,颜色可以丢弃。这种方法的问题是,图像可能不是像素完美的。我的意思是,如果墙上某个地方有一个白色像素,它可能会创建一个非预期的路径

经过一番思考后,我想到的另一种方法是将图像转换为SVG文件——这是在画布上绘制的路径列表。这样,路径可以读入相同种类的列表布尔值,其中True表示路径或墙,False表示可移动空间。如果转换不是100%准确,并且没有完全连接所有墙,从而产生间隙,则会出现此方法的问题

转换为SVG的另一个问题是线条不是完全笔直。这将导致路径为三次bezier曲线。使用由整数索引的布尔值的列表数组,曲线将不容易传输,并且必须计算曲线上的所有点,但不会与列表索引完全匹配

我认为,虽然这些方法中的一种可能有效,但可能不会,但考虑到如此大的图像,它们的效率非常低,而且存在一种更好的方法。如何以最高效和/或最简单的方式实现这一点?有没有最好的办法

然后是迷宫的求解。如果我使用前两种方法中的任何一种,我最终将得到一个矩阵。根据,表示迷宫的一个好方法是使用树,而解决它的一个好方法是使用。如何从图像创建树?有什么想法吗

TL;博士 解析的最佳方法?什么样的数据结构?所述结构如何帮助/阻碍解决问题

更新 正如@Thomas推荐的那样,我已经尝试使用numpy实现@Mikhail用Python编写的内容。我觉得这个算法是正确的,但它并没有按预期的那样工作。代码如下。PNG库是


我会选择布尔矩阵选项。如果您发现标准Python列表对此效率太低,那么可以使用numpy.bool数组。1000x1000像素迷宫的存储空间只有1MB

不要费心创建任何树或图形数据结构。这只是一种思考的方式,但不一定是在记忆中表现它的好方式;布尔矩阵更容易编码,也更高效

然后使用A*算法来解决它。对于距离启发式,使用曼哈顿距离x+距离y

通过行、列坐标的元组表示节点。每当算法调用邻域时,只需在四个可能的邻域上循环,注意图像的边缘即可

如果您发现它仍然太慢,可以在加载之前尝试缩小图像的大小。在这个过程中,小心不要失去任何狭窄的道路

也许在Python中也可以进行1:2的降尺度,检查是否确实丢失了任何可能的路径。这是一个有趣的选择,但需要更多的思考。

这里有一个解决方案

将图像转换为灰度而不是二进制,调整颜色的权重,使最终灰度图像大致一致。只需在Photoshop中的图像->调整->黑白中控制滑块即可。 通过在Photoshop的“图像->调整->阈值”中设置适当的阈值,将图像转换为二进制图像。 确保“阈值”选择正确。使用具有0公差、点采样、连续、无抗锯齿的魔棒工具。检查选择中断处的边是否为错误阈值引入的假边。事实上,这个迷宫的所有内部点从一开始就可以进入。 在迷宫上添加人工边界,以确保虚拟旅行者不会绕过迷宫: 用您喜欢的语言实现BFS,并从一开始就运行它。我更喜欢这个任务。正如@Thomas已经提到的,没有必要弄乱图的常规表示。您可以直接使用二值化图像。 以下是BFS的MATLAB代码:

函数路径=solve\u mazeimg\u文件 %%初始数据 img=imreadimg_文件; img=rgb2灰度img; 迷宫=img>0; 开始=[985 398]; 终点=[26 399]; %%初始BFS n=纽美尔迷宫; Q=zerosn,2; M=零[sizemaze 2]; 正面=0; 后退=1; 函数pushp,d q=p+d; 如果mazeq1,q2&&Mq1,q2,1==0 前=前+1; Qfront,:=q; Mq1,q2,:=reprograpep[12]; 终止 终止 pushstart[0]; d=[01;0-1;10;-10]; %%运行BFS
而back使用队列进行阈值连续填充。将像素推到ent的左侧 然后启动循环。如果队列中的像素足够暗,则其颜色为高于阈值的浅灰色,并且所有相邻像素都被推到队列中

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")
解决方案是灰色墙和彩色墙之间的走廊。注意,这个迷宫有多种解决方案。而且,这似乎只起作用


树搜索太多了。迷宫本质上是沿着解决方案路径分离的

感谢Reddit向我指出这一点

因此,您可以快速使用来识别迷宫墙的连接部分。这将在像素上迭代两次

如果你想把它变成一个很好的解决方案路径图,那么你可以使用带有结构元素的二进制操作来填充每个连接区域的死胡同

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);
这里有一些想法

一,。图像处理:

1.1将图像加载为像素贴图。在这种情况下,使用system.drawing.bitmap非常简单。在不支持简单成像的语言中,只需将图像转换为Unix文本表示形式,生成大文件或一些易于读取的简单二进制文件格式,如或。在Unix或Windows中

1.2如前所述,您可以通过将每个像素的R+G+B/3作为灰度的指示器,然后设置阈值来生成黑白表,从而简化数据。假设0=黑色,255=白色,则接近200的值将去除JPEG伪影

二,。解决方案:

2.1深度优先搜索:初始化带有起始位置的空堆栈,收集可用的后续移动,随机选择一个并推到堆栈上,继续进行,直到到达终点或死角。在通过弹出堆栈进行死区回溯时,您需要跟踪地图上访问过的位置,这样当您收集可用移动时,您就永远不会在同一条路径上重复两次。制作动画非常有趣

2.2广度优先搜索:前面提到过,与上面类似,但仅使用队列。动画也很有趣。这类似于图像编辑软件中的洪水填充。我想你可以用这个技巧在Photoshop中解决一个迷宫

2.3墙壁跟随器:从几何角度讲,迷宫是一个折叠/卷曲的管。如果你把手放在墙上,你最终会找到出口;这并不总是有效的。有一定的假设:完美迷宫等,例如,某些迷宫包含岛屿。一定要查一下;这是迷人的

三,。评论:


这是个棘手的问题。如果用简单的数组形式表示迷宫,每个元素都是一个单元类型,有北、东、南、西墙和一个访问过的旗子场,那么很容易解决迷宫问题。然而,考虑到你正试图这样做,一个手绘草图就会变得混乱。老实说,我认为试图使草图合理化会让你发疯。这类似于相当复杂的计算机视觉问题。直接进入图像映射可能更容易,但更浪费。

此解决方案是用Python编写的。感谢米哈伊尔在图像准备方面的指导

动画宽度优先搜索:

完成的迷宫:

注意:将白色像素标记为灰色。这样就不需要访问列表,但如果您不希望最终路径和所有路径的合成图像,则需要在绘制路径之前从磁盘上再次加载图像文件


我试着自己对这个问题进行A-Star搜索。密切关注框架和算法伪代码的实现:

def AStarstart、目标、邻居节点、距离、成本估算: def重建路径来源,当前节点: 路径=[] 当前_节点不是无时: path.APPENDER当前_节点 当前节点=来自[当前节点] 返回listreversedpath g_得分={开始:0} f_分数={开始:g_分数[开始]+成本{估计开始,目标} openset={start} 闭合集=集合 来自={开始:无} 而openset: 当前=最小值集,键=λx:f_分数[x] 如果当前==目标: 返回路径来源、目标 openset.removecurrent closedset.addcurrent 对于邻居节点中的邻居当前: 如果closedset中的邻居: 持续 如果邻居不在openset中: openset.addneighbor 暂定分数=当前分数+当前距离,邻居 如果暂定分数>=g分数.getneighbor,则浮动'inf': 持续 来自[邻居]=当前 g_分数[邻居]=暂定g_分数 f_分数[邻居]=暂定分数+成本估计邻居,目标 返回[] 由于A-Star是一种启发式搜索算法,您需要提出一个函数 在这里估计剩余成本:距离目标实现的距离。除非你对次优的解决方案感到满意,否则不应该高估成本。此处保守的选择是,因为这表示所用冯·诺依曼邻域网格上两点之间的直线距离。在这种情况下,不会高估成本

然而,这将大大低估手头给定迷宫的实际成本。因此,我添加了另外两个距离度量,即欧几里德距离的平方和曼哈顿距离乘以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])
以下是一些图片,这些图片的灵感来源于作者发布的图片。动画在主while循环的10000次迭代后显示一个新帧

广度优先搜索:

A星曼哈顿距离:

A星平方欧氏距离:


A星曼哈顿距离乘以四:

结果表明,对于所使用的启发式,迷宫的探索区域有很大的不同。因此,平方欧几里德距离甚至产生了与其他度量不同的次优路径

关于A-Star算法在终止前的运行时间方面的性能,请注意,与广度优先搜索BFS相比,许多距离和成本函数的评估相加,后者只需要评估每个候选位置的目标性。这些额外功能评估A-Star的成本是否超过了检查BFS的大量节点的成本,尤其是性能是否是应用程序的一个问题,这是个人的看法,当然不能普遍回答

一般来说,与穷举搜索(如BFS)相比,A-Star等知情搜索算法是否是更好的选择。随着迷宫的维数(即搜索树的分支因子)的增加,穷举搜索的缺点以指数形式增长。随着复杂性的不断增加,这样做的可行性越来越低,在某种程度上,您对任何结果路径都非常满意,不管它是否近似最优。

给您:GitHub

我玩得很开心,并扩展了作者的答案。不要减损它;我只是为其他可能有兴趣玩这个的人做了一些小的补充

它是一个基于python的解算器,使用BFS查找最短路径。我当时的主要补充内容是:

图像在搜索前被清理,即转换为纯黑白 自动生成GIF。 自动生成AVI。
目前,这个示例迷宫的起点/终点是硬编码的,但我计划扩展它,以便您可以选择适当的像素。

这里有一个使用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到灰度,请参见:

如果不填充一些边界像素,就会发生这种情况哈


充分披露:在我找到这个之前,我自己问了一个问题并回答了一个问题。然后通过神奇的SO,发现这一个是最重要的相关问题之一。我想我应该用这个迷宫作为额外的测试用例。。。我很高兴地发现,我的答案也适用于这个应用程序,只需很少的修改。

好的解决方案是,不通过像素来查找邻居,而是通过单元来查找邻居,因为一条走廊可以有15px,所以在同一条走廊中,它可以采取向左或向右的操作,如果把位移当作一个立方体来做,这将是一个简单的动作,如上、下、左或右

我会将迷宫转换成黑白,并使用寻路细胞自动机方法来解决它。你需要只处理那个图像,还是处理许多类似的图像?也就是说,是否有一个特定于此特定图像的手动处理选项?@Whymarrh我不编写python代码,但我非常确定您应该将visitor.appends移动到for.if下,并将其替换为visitor.appendnp。顶点添加到队列后即被访问。实际上,这个数组应该命名为queued。您还可以在到达终点后终止BFS。@Whymarrh,您似乎也跳过了实现路径提取块。没有它,你只能知道是否可以到达终点,而不能知道如何到达终点。要知道是否有解决方案,联合查找和线性扫描是最快的算法。它不会提供路径,但会提供一组将路径作为子集的平铺。演示如何在mathematica中解迷宫。将该方法转换为python应该不是问题。我已经更新了这个问题。如果我选择使用RGB三元组代替布尔值,存储是否仍会比较?矩阵为2400*1200。和w
BFS上的A*是否对实时运行时间有显著影响?@Whymarrh,位深度可以缩小以进行补偿。每个像素2位对任何人来说都足够了。@Whymarrh好吧,就这张图片,你现在有了答案。你有什么具体问题吗?我清单中的第1-4项是我询问的手动处理。第5项是BFS——图形的最基本算法,但它可以直接应用于图像,而无需将像素转换为顶点,将邻域转换为边。我觉得你已经涵盖了所有内容。我正在尝试用DFS代替BFS来实现您在Python中所说的内容,只是因为我以前编写过一次。我稍后会回来更新问题/接受答案。@Whymarrh DFS不会为您找到最短路径,而BFS会。它们本质上是相同的,唯一的区别是基本结构。在这里,DFS的Stack FILO和BFS.BFS的queue FIFO是正确的选择,因为它生成最短路径,即使走廊宽度远大于1像素,也能给出合理的路径。OTOH DFS将倾向于以洪水填充模式探索走廊和没有希望的迷宫区域。@JosephKern路径不重叠任何墙壁。只需删除所有红色像素,就可以了。因为你非常棒,即使在回答了你的问题后,也能回来投票给我,所以我创建了BFS的动画gif,以帮助更好地可视化过程。很好,谢谢。对于像我一样希望玩这个游戏的其他人,我想根据我面临的困难分享我的建议。1将图像转换为纯黑白或修改“isWhite”函数以接受近白|黑。我写了一个“cleanImage”方法,对所有像素进行预处理,将它们转换为纯白色或黑色,否则算法无法找到路径。2将中的图像显式读取为RGB[base\u img=image.openimg\u in;base\u img=base\u img.convert'RGB']。若要获取gif,请输出多个图像,然后运行“convert-delay 5-loop 1*.jpg bfs.gif”。第13行中缺少缩进,这是一种有趣的原始分辨率,基于墙上手工方法。的确,这不是最好的,但我喜欢它。太好了,谢谢,它没有在BSD/Darwin/Mac上运行,一些依赖项和shell脚本需要稍作修改,适合那些想在Mac上尝试的人:[迷宫求解器python]:@HolgT:很高兴你发现它很有用。我欢迎对此的任何请求:A星曼哈顿距离乘以4?如果启发式可以高估距离,A星就不是A星。因此不能保证找到最短路径。你能像答案的其余部分一样添加解决方案图和算法来验证你的观点吗?如果你能把这些加在一起,给你的答案增加更多的权重,这样其他人就能更好地理解你的答案。
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])
### 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)
# 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")