首页 文章

代表和解决给定图像的迷宫

提问于
浏览
239

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

The cover image of The Scope Issue 134

给定JPEG图像(如上所示),读取它的最佳方法是什么,将其解析为一些数据结构并解决迷宫?我的第一直觉是逐个像素地读取图像并将其存储在布尔值的列表(数组)中:白色像素为 True ,非白色像素为 False (颜色可以丢弃) . 此方法的问题是图像可能不是"pixel perfect" . 我只是说,如果墙上的某个地方有白色像素,可能会产生意想不到的路径 .

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

转换为SVG的另一个问题是线条不是“完美”直线 . 这导致路径是立方贝塞尔曲线 . 使用由整数索引的布尔值的列表(数组),曲线不会轻易转移,并且必须计算曲线上所有的点,但不会与列表索引完全匹配 .

我假设虽然这些方法中的一种可能有用(尽管可能不是),但是如果这么大的图像,它们的效率很低,并且存在更好的方法 . 如何最好(最有效和/或最简单)完成?有没有最好的方法?

然后是迷宫的解决方案 . 如果我使用前两种方法中的任何一种,我基本上会得到一个矩阵 . 根据this answer,表示迷宫的好方法是使用树,解决它的好方法是使用A* algorithm . 如何从图像中创建树?有任何想法吗?

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

UPDATE
我已经尝试使用 numpy 来实现@Mikhail用Python编写的内容,正如@Thomas推荐的那样 . 我觉得这个算法是正确的,但它并没有像希望的那样工作 . (以下代码 . )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, [])

9 回答

  • 21

    我尝试自己实现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邻域网格上两点之间的直线距离 . (在这种情况下,不会高估成本 . )

    然而,这将显着低估手头给定迷宫的实际成本 . 因此,我添加了两个其他距离度量平方欧几里德距离和曼哈顿距离乘以四进行比较 . 然而,这些可能会高估实际成本,因此可能会产生不理想的结果 .

    这是代码:

    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平方欧几里德距离:

    A-Star Squared Euclidean Distance

    A-Star Manhattan Distance乘以4:

    A-Star Manhattan Distance multiplied by four

    结果表明,对于所使用的启发式方法,迷宫的探索区域差异很大 . 因此,平方欧氏距离甚至产生与其他度量不同的(次优的)路径 .

    关于A-Star算法在运行时直到终止的性能,请注意,与广度优先搜索(BFS)相比,大量的距离和成本函数评估总和需要评估“目标性” . 每个候选人的职位这些额外功能评估(A-Star)的成本是否超过了大量要检查的节点(BFS)的成本,尤其是性能是否对您的应用程序来说是一个问题,这是个人感知的问题当然不能得到普遍回答 .

    关于与穷举搜索(例如,BFS)相比,知情搜索算法(例如A-Star)是否可能是更好的选择,可以说是以下内容 . 利用迷宫的维数,即搜索树的分支因子,穷举搜索(彻底搜索)的缺点呈指数增长 . 随着复杂性的增加,这样做变得越来越不可行,而且在某些时候你也是如此非常满意任何结果路径,无论是(近似)最佳还是不是 .

  • 74

    这是一个解决方案 .

    • 将图像转换为灰度(尚未二进制),调整颜色的权重,使最终的灰度图像大致均匀 . 您可以通过在图像 - >调整 - >黑白中控制Photoshop中的滑块来实现 .

    • 通过在图像 - >调整 - >阈值中在Photoshop中设置适当的阈值,将图像转换为二进制 .

    • 确保选择正确的阈值 . 使用魔术棒工具,0容差,点样本,连续,无抗锯齿 . 检查选择中断的边缘不是错误阈值引入的假边缘 . 事实上,这个迷宫的所有内部点都可以从一开始就可以访问 .

    • 在迷宫上添加人工边框,以确保虚拟旅行者不会四处走动:)

    • 用您喜欢的语言实现breadth-first search(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
    

    这真的非常简单和标准,在_549603中实现这一点不应该有任何困难 .

    这是答案:

    Enter image description here

  • 5

    树搜索太多了 . 迷宫沿着溶液路径固有地是可分离的 .

    (感谢Reddit的rainman002向我指出这一点 . )

    因此,您可以快速使用connected components来识别迷宫墙的连接部分 . 这会迭代像素两次 .

    如果要将其转换为解决方案路径的漂亮图表,则可以使用带结构元素的二元运算来填充每个连接区域的“死胡同”路径 .

    下面是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

  • 33

    我会选择matrix-of-bools选项 . 如果您发现标准Python列表对此效率太低,则可以使用 numpy.bool 数组 . 然后,1000x1000像素迷宫的存储空间仅为1 MB .

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

    然后使用A *算法来解决它 . 对于距离启发式,请使用曼哈顿距离( distance_x + distance_y ) .

    通过 (row, column) 坐标的元组表示节点 . 每当算法(Wikipedia pseudocode)调用"neighbours"时,只需循环四个可能的邻居(请注意图像的边缘!) .

    如果您发现它仍然太慢,您可以在加载图像之前尝试缩小图像 . 注意不要在此过程中丢失任何狭窄的路径 .

    也许有可能在Python中进行1:2缩减,检查你实际上没有丢失任何可能的路径 . 一个有趣的选择,但它需要更多的思考 .

  • 0

    使用队列进行阈值连续填充 . 将入口左侧的像素推入队列,然后启动循环 . 如果排队的像素足够暗,则它是浅灰色(高于阈值),并且所有邻居都被推入队列 .

    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

  • 149

    你去:maze-solver-python(GitHub)

    enter image description here

    我玩得很开心,并且延伸到Joseph Kern的答案 . 不要减损它;我只是为其他可能有兴趣玩这个的人做了一些小小的补充 .

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

    • 图像在搜索前被清除(即转换为纯黑色和白色)

    • 自动生成GIF .

    • 自动生成AVI .

    就目前而言,这个样本迷宫的起点/终点是硬编码的,但我计划对其进行扩展,以便您可以选择合适的像素 .

  • 220

    这是一些想法 .

    (1.图像处理:)

    1.1将图像加载为RGB像素图 . 在C#中使用 system.drawing.bitmap 是微不足道的 . 在没有简单支持成像的语言中,只需将图像转换为portable pixmap format(PPM)(Unix文本表示,生成大文件)或一些您可以轻松阅读的简单二进制文件格式,例如BMPTGA . 在Unix中为ImageMagick,在Windows中为IrfanView .

    1.2如前所述,您可以通过将每个像素的(R G B)/ 3作为灰色调的指示符来简化数据,然后对该值进行阈值处理以生成黑白表 . 假设0 =黑色且255 =白色,接近200的东西将取出JPEG伪像 .

    (2.解决方案:)

    2.1深度优先搜索:使用起始位置初始化空堆栈,收集可用的后续移动,随机选择一个并推入堆栈,继续直到达到结束或去除 . 在通过弹出堆栈来进行后退回溯时,您需要跟踪 Map 上访问的位置,这样当您收集可用的移动时,您永远不会两次使用相同的路径 . 动画非常有趣 .

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

    2.3墙跟随器:从几何学上讲,迷宫是折叠/旋绕管 . 如果你把手放在墙上,你最终会找到出口;)这并不总是有效 . 有一定的假设:完美的迷宫等,例如,某些迷宫包含岛屿 . 看一看;这很有趣 .

    (3.评论:)

    这是棘手的 . 如果以一些简单的数组正式表示,很容易解决迷宫,每个元素都是具有北,东,南和西墙和访问标志字段的单元类型 . 但是考虑到你试图用手绘草图来做这件事,它会变得混乱 . 老实说,我认为试图使草图合理化会让你疯狂 . 这类似于相当复杂的计算机视觉问题 . 也许直接进入图像 Map 可能更容易但更浪费 .

  • 5

    这是使用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

    如果你没有填充一些边框像素会发生这种情况(哈!)......

    solution version where the solver goes around the maze

    完全披露:在我找到这个之前,我问过并自己回答了一个问题 . 然后通过SO的魔力,发现这一个作为顶级"Related Questions"之一 . 我以为我会使用这个迷宫作为一个额外的测试用例...我很高兴地发现我的答案也适用于这个应用程序,几乎没有修改 .

  • 22

    此解决方案是用Python编写的 . 感谢Mikhail关于图像准备的指示 .

    An animated Breadth-First Search:

    Animated version of BFS

    The Completed Maze:

    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 __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])
    

    Note: 标记白色访问像素灰色 . 这消除了对访问列表的需要,但这需要在绘制路径之前从磁盘再次加载图像文件(如果您不想要最终路径和所有路径的合成图像) .

    A blank version of the maze I used.

相关问题