关于python:表示和求解给定图像的迷宫

Representing and solving a maze given an image

在给定图像的情况下,表示和求解迷宫的最佳方法是什么?

The cover image of The Scope Issue 134

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

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

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

我认为,尽管这些方法中的一种可能有效(尽管可能不有效),但考虑到如此大的图像,它们效率极低,而且存在更好的方法。如何做到最好(最有效和/或最不复杂)?有没有最好的办法?

然后是迷宫的求解。如果我使用前两种方法中的任何一种,我基本上都会得到一个矩阵。根据这个答案,表示迷宫的一个好方法是使用树,解决迷宫的一个好方法是使用A*算法。如何从图像创建树?有什么想法吗?

DR最好的分析方法?进入什么数据结构?结构如何帮助/阻碍解决?

更新我已经尝试过按照@thomas的建议,使用numpy来实现@mikhail用python编写的内容。我觉得这个算法是正确的,但它没有按预期工作。(下面的代码。)PNG库是Pypng。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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, [])


这是一个解决办法。

  • 转换图像到灰度(不是二元),调整颜色的重量,因此最终的灰度图像大致均匀。在图像调整中,你可以简单地控制滑板。
  • 将图像转换到二进制,通过在图像——>调整——>阈值中设置适当阈值。
  • 确定门槛是正确的。使用魔法工具,零容忍,点样品,毗连,无反异形。检查选择性断裂的边缘不是由错误的阈值引入的假边缘。事实上,这片沼泽的所有内点都可以从起始点进入。
  • 将人造边框添加到maze上以确保虚拟旅行者不会绕着它行走:)
  • 在你最喜爱的语言中进行第一次搜索,并从起始开始运行。我喜欢这项任务。As@Thomas Already mentioned,there is no need to mess with regular representation of graphs.你可以直接使用二进制图像。
  • 这是BFS的MATLAB代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    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或任何东西中实现这一点不应该是困难的。

    And here is the answer:

    MGX1〔0〕


    这个解决方案是用python编写的。感谢米哈伊尔在图像准备上的指点。

    动画宽度优先搜索:

    Animated version of BFS

    完成的迷宫:

    Completed Maze

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    #!/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])

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

    我用的迷宫的空白版本。


    我试着用A星搜索这个问题。随后,Joseph Kern对框架和此处给出的算法伪代码进行了详细的实施:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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是一种启发式搜索算法,因此您需要想出一个函数来估计剩余成本(这里是距离),直到达到目标为止。除非你对一个次优的解决方案感到满意,否则它不应该高估成本。这里保守的选择是曼哈顿(或出租车)距离,因为这代表了使用过的von neumann社区网格上两点之间的直线距离。(在这种情况下,不会高估成本。)

    然而,这将大大低估现有迷宫的实际成本。因此,我添加了另外两个距离度量平方欧几里得距离和曼哈顿距离乘以4进行比较。然而,这些可能高估实际成本,因此可能产生次优结果。

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    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次迭代后显示一个新的帧。

    广度优先搜索:

    Breadth-First Search

    A星曼哈顿距离:

    A-Star Manhattan Distance

    A星平方欧几里得距离:

    A-Star Squared Euclidean Distance

    A星曼哈顿距离乘以4:

    A-Star Manhattan Distance multiplied by four

    结果表明,对于所使用的启发式方法,迷宫的探索区域有很大差异。因此,平方欧几里得距离甚至产生一个不同的(次优)路径作为其他指标。

    关于A星算法在运行时到终止时的性能,请注意,与只需要评估每个候选位置的"目标性"的广度优先搜索(BFS)相比,距离和成本函数的许多评估相加。这些额外的功能评估(a-star)的成本是否大于要检查的节点数量(bfs)的成本,特别是性能是否对您的应用程序来说是一个问题,这是一个个体感知的问题,当然不能得到普遍的回答。

    一般来说,与全面搜索(如bfs)相比,知情搜索算法(如a-star)是否是更好的选择,可以这样说。随着迷宫维数的增加,即搜索树的分支因子,穷尽搜索(穷尽搜索)的缺点呈指数增长。随着复杂性的增加,这样做变得越来越不可行,并且在某一点上,您对任何结果路径都非常满意,无论它(近似)是最佳的还是非最佳的。


    树搜索太多了。玉米在溶液通道上是可分离的。

    谢谢你给我指点这个

    由于这一点,您可以快速地使用连接的部件来识别连接的墙壁部分。这个项目在像素twice上。

    如果你想转到一个很好的溶液路径图中,你可以使用二进制操作的结构元素来填充每个连接区的"死亡结束"路径。

    Demo Code for Matlab follows.它可以用两个字来清理结果,使结果更为普遍,并使结果更为迅速。(有时不是2:30米)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    % 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);

    MGX1〔2〕


    用尾巴连续填充阈值。将像素移到尾部,然后开始旋转。如果一个尾巴像素足够黑暗,它的颜色是灰色的(上面的阈值),所有的邻居都被推到尾巴上。

    ZZU1

    解决方案是灰墙和彩墙之间的走廊。Note this maze has multiple solutions.同样,这件事也要求我们工作。

    MGX1〔1〕


    给你:迷宫解算器python(Github)

    enter image description here

    我玩得很开心,还扩展了约瑟夫·克恩的回答。不要贬低它;我只是为其他可能有兴趣玩这个的人做了一些小的补充。

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

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


    这里有一些想法。

    (1)。图像处理:)

    1.1将图像加载为RGB像素映射。在c中,使用system.drawing.bitmap很简单。在不支持图像处理的语言中,只需将图像转换为可移植的PixMap格式(PPM)(一种Unix文本表示,生成大型文件)或一些简单的二进制文件格式,您可以轻松读取,如BMP或TGA。Unix中的ImageMagick或Windows中的IrfanView。

    1.2如前所述,您可以通过将每个像素的(r+g+b)/3作为灰度指示器来简化数据,然后阈值生成黑白表。假设0=black和255=white接近200,则会删除jpeg工件。

    (2)。解决方案:

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

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

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

    (3)。评论:

    这是个棘手的问题。如果用简单的数组形式表示,每个元素都是具有北、东、南、西墙和访问标志字段的单元类型,则很容易解决迷宫问题。然而,如果你想用手绘草图来完成这项工作,它会变得杂乱无章。我真的认为试图使草图合理化会让你发疯。这类似于相当复杂的计算机视觉问题。也许直接进入图像地图可能更容易,但更浪费。


    我要去看图表图标选项I'm going for the matrix-of-bools option.如果你发现标准Python名单对这一点太无效,你可以使用一个numpy.bool阵列。存储一个1000x1000像素Maze,然后才是11mb。

    不要与任何树木或图表数据结构混为一谈。这只是一种思考它的方式,但不需要一种很好的方式在记忆中表达它;一个布尔矩阵很容易代码和效率。

    然后使用一个算法来解决它。为了启发性的距离,使用曼哈顿的距离。

    Represent nodes by a tuple of (row, column)coordinates.当Never the algorithm(wikipedia pseudocode)calls for"neighbours",it's a simple matter of looping over the four possible neighbours(mind the edges of the image!)页:1

    如果你发现这还太慢,你可以试着在装载之前把图像降下来。注意不要失去过程中任何狭窄的路径。

    也许你可以在Python身上做1:2下载查看你是否真的失去了任何可能的路径一个有兴趣的选择,但它需要更多的思考。


    这里有一个使用R的解决方案。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ### 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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    # 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

    完全公开:在我找到这个问题之前,我自己也问过并回答了一个非常相似的问题。然后通过这样的魔力,找到了这一个作为头号"相关问题"之一的问题。我想我会用这个迷宫作为额外的测试案例…我很高兴地发现,我的答案也适用于这个应用程序,修改很少。