迷宫问题图解 : 基于骨架提取、四邻域

news2024/12/26 13:29:00

目录

1. 迷宫的连通域

2. How to remove branch ?

3. 基于4邻域的 remove 分支

3.1 找到分支的端点

3.2 4邻域的 remove 分支

3.3 循环移除分支

3.4 code

4. 迷宫路线

4.1 预处理

4.2 提取骨架

4.3 分支的端点

4.4 去除分支的端点

4.5 循环去除分支

4.6 迭代过程展示

4.7 结果展示

4.8 代码

5. show

6. Acknowledge


1. 迷宫的连通域

之前对迷宫求解的问题感兴趣,看了很多求解的算法,大部分都是深度搜索啥的。

由于本人对算法不是很敏感,因此想看看能不能将自己所学的和迷宫问题联系起来。从俯视的角度来看,迷宫就是一直2d图片,既然是图像,就可以尝试使用数字图像处理的方法来解决。

 例如一副迷宫图像,黑色的是围墙,白色的是道路。

迷宫求解其实就是在白色的像素域中找到一条可以连接出入口的连通域

连通域很好找,这里不再介绍,opencv里面也有专门的函数找连通域。

但是问题就是,往往这种连通域里面,有很多的分支,会将迷宫路线走向死路。所以现在求解迷宫问题的思路就是,如何将分支去除?

2. How to remove branch ?

一开始的时候,这个地方卡了很久。

后来想到了下面这种邻域移除的方法,不能保证完全正确,但能处理自己预期的迷宫问题...

OK,here we go ....

当时想了很久,其实我当时一直陷入了一个误区。

例如,从A点走到B点,显然红线是唯一的路径,两条黑线就是死路。所以问题是在这一条连通域当中,怎么去除黑线,只保留红线。

 当时想了很多,例如距离变换中草原大火的概念从端点开始''点火'',或者计算连通域当中像素点的个数只保留最大的那条连通域等等。问题是,这条红色的线是人为标记出来的能通的路线,对计算机来看,黑色和红色的线没有任何差异,所以之前的想法压根不起作用。

在误区里面卡了很久,后来在一副图里面突然有了灵感,类似下面这种的闪电图。

从这幅图来看,其实天空就是迷宫的起点,大地就是迷宫的出口。闪电划过,有一道连接了天空和大地,就是迷宫的解,其余的分支就是迷宫中对应的死路。

这样看这副闪电图片,很容易联想到树枝,主干长成后,分支慢慢向四周长开。

类似这个闪电图,假设图中最粗的主干(迷宫解的路线)是最先形成的,其余的细小的分支是慢慢从主干上形成的。那么去除这些分支,不就是从分支的端点,慢慢向主干靠拢的过程吗(相当于分支形成的逆过程)

简单来说,就是找到所以分支的端点,然后顺着分支慢慢往回走,直到走到主干上,这条分支就被消除了。

3. 基于4邻域的 remove 分支

一般来说,迷宫都是上下左右的道路,这里不考虑那些斜着走的迷宫

这里用数组建立一个小型的迷宫,为下面展示用

3.1 找到分支的端点

这里先来介绍如何找到分支的端点

因为迷宫都是上下左右的路线,那么对于分支来说,端点只有四种情况。 

红色代表主干,黑色是分支端点的四种情况A、B、C、D

  • A就是分支是从右往左走的端点
  • B就是分支是从左往右走的端点
  • C就是分支是从下往上走的端点
  • D就是分支是从上往下走的端点

因为端点只有这四种情况,那么找到端点只需要找到匹配的模板就行了。因此, 找端点这里利用 形态学 - 击中-击不中变换 特性

击中-击不中 变换大概的意思就是,和 kernel 一样的图像区域会被显示,不一样的显示为背景。 

击中-击不中的kernel中:1代表前景,0代表不感兴趣的点、-1代表背景点

 这里代码很简单,不做解释。就是将四周分支端点的情况放在 3 * 3 的模板里面,和原图去匹配即可。

但是分支要注意一点,因为出入口其实也是一个端点,只不过是主干的端点。所以这里要去除这两个点,这里假设出入口就在图片的四周上

注意:出入口必须在整幅图像的四周。

利用击中击不中特性,找到的端点为:

3.2 4邻域的 remove 分支

现在来看看怎么移除这些分支

因为这里考虑的迷宫路线都是上下左右的线路

因此,只要在端点的4邻域中,找到可以走的道路,那么一定就是分支,也就是要移除的路线。

那么问题来了,怎么知道分支已经结束了呢,或者说,怎么判断分支走到了主干上?

很简单,因为分支一定是从主干的的中间分出来的。也就是说,分支连接在主干的端点,肯定有两条路可以走,一个通到出口,一个通到入口。对应于数字图像的表达,就是从远离主干的分支端点出发,如果4邻域中只有一个可以通的道路,那么这个点在分支上。如果这个点4邻域有两个或者以上的点,那么这个点在主干上。

代码如下:

 这里要传入两张图片,一个是find_corner返回的端点图,一个是原图。

因为这里迷宫的可以走的路线设置为1,因此如果4邻域上的只有一个可以走的路,那么4邻域加起来等于1

3.3 循环移除分支

这里设置的是一次移除所有分支的端点,想要移除分支,只需要重复这个过程即可

  1. 找到所有分支的端点
  2. 移除这些端点
  3. 找到新的端点
  4. 移除新的端点...
  5. 重复这个过程

循环处理的代码为:

最后处理的结果为:

3.4 code

import numpy as np
import cv2

# 建立迷宫,0为墙壁,1为道路
img = np.array([
    [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0],
    [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
    [0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0],
    [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]],dtype=np.uint8)

# 拷贝地图
source = img.copy()

# 找到分支的端点
def find_corner(x):
    kernel_right = np.array([[0, -1, 0], [1, 1, -1], [0, -1, 0]])      # right
    dst_right = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_right)

    kernel_left = np.array([[0, -1, 0], [-1, 1,1], [0, -1, 0]])       # left
    dst_left = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_left)

    kernel_up= np.array([[0, 1, 0], [-1, 1, -1], [0, -1, 0]])         # up
    dst_up = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_up)

    kernel_down= np.array([[0, -1, 0], [-1, 1, -1], [0, 1, 0]])       # down
    dst_down = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_down)

    dst = dst_up+dst_down+dst_right+dst_left

    # 去除出入口
    dst[0,:] = 0        # 去除上面
    dst[-1,:] = 0       # 去除下面
    dst[:,0] = 0        # 去除左面
    dst[:,-1] = 0        # 去除右面

    return dst.astype(np.uint8)

# 去除一次端点
def branch(corner_img,img):
    x,y = np.where(corner_img)
    for i,j in zip(x,y):          # 各个角点的坐标
        if (img[i-1,j] + img[i+1,j] + img[i,j-1]+img[i,j+1] == 1):
            img[i,j] = 0

    return img

#  去除分支
while True:
    img_copy = img.copy()             # 拷贝地图,用作退出循环

    ret = find_corner(img)            # 找到端点
    img = branch(ret,img)             # 去除端点

    if (img_copy == img).all():       # 如果两次结果相同,则退出循环
        break

print(source)
print(img)

4. 迷宫路线

将代码修改,即可绘制出图像的迷宫路线

4.1 预处理

首先,将图像进行预处理

  •  首先将图像进行缩放
  • 阈值处理,将图像变成二值图像
  • 腐蚀可以增大背景图像,缩小前景像素点。为下面的细化减少运算量,也可以突出迷宫的最外层墙壁
  • 最后将像素点变成0 1 二值图像。因为四邻域中,设置的是4邻域加起来等于 1

4.2 提取骨架

提取迷宫道路的骨架可以方便更好的找出路线

具体的参考:形态学 - 细化

细化算法需要的kernel

 循环做击中击不中变换,找到骨架

4.3 分支的端点

和之前的一样

4.4 去除分支的端点

4.5 循环去除分支

4.6 迭代过程展示

4.7 结果展示

4.8 代码

完整代码:

import numpy as np
import cv2

# 预处理
def pre_process(x):
    img_pre = cv2.resize(x, (400, 400), interpolation=cv2.INTER_LINEAR)             # 将图像设定到固定大小
    _, img_pre = cv2.threshold(img_pre, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)  # 阈值处理
    kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(3,3))
    img_pre = cv2.erode(img_pre,kernel,iterations=2)                                # 腐蚀
    dst = img_pre / 255                                                             # 将像素值变成 0 1
    return dst.astype(np.uint8)

# 提取骨架
def thin(img):  # 细化算法

    # 8 个细化 kernel
    B1 = np.array([[-1, -1, -1], [0, 1, 0], [1, 1, 1]])
    B2 = np.array([[0, -1, -1], [1, 1, -1], [1, 1, 0]])
    B3 = np.array([[1, 0, -1], [1, 1, -1], [1, 0, -1]])
    B4 = np.array([[1, 1, 0], [1, 1, -1], [0, -1, -1]])
    B5 = np.array([[1, 1, 1], [0, 1, 0], [-1, -1, -1]])
    B6 = np.array([[0, 1, 1], [-1, 1, 1], [-1, -1, 0]])
    B7 = np.array([[-1, 0, 1], [-1, 1, 1], [-1, 0, 1]])
    B8 = np.array([[-1, -1, 0], [-1, 1, 1], [0, 1, 1]])

    while True:  # 循环迭代
        tmp = img  # 将上一步的操作暂存
        for i in range(8):  # 循环迭代八次
            ret1 = cv2.morphologyEx(img, cv2.MORPH_HITMISS, B1)  # B1 对图像做 击中-击不中变换
            ret1 = img - ret1  # 原图 减去 上一步击中-击不中的结果

            ret2 = cv2.morphologyEx(ret1, cv2.MORPH_HITMISS, B2)  # 将上步的结果作为新的输入
            ret2 = ret1 - ret2

            ret3 = cv2.morphologyEx(ret2, cv2.MORPH_HITMISS, B3)
            ret3 = ret2 - ret3

            ret4 = cv2.morphologyEx(ret3, cv2.MORPH_HITMISS, B4)
            ret4 = ret3 - ret4

            ret5 = cv2.morphologyEx(ret4, cv2.MORPH_HITMISS, B5)
            ret5 = ret4 - ret5

            ret6 = cv2.morphologyEx(ret5, cv2.MORPH_HITMISS, B6)
            ret6 = ret5 - ret6

            ret7 = cv2.morphologyEx(ret6, cv2.MORPH_HITMISS, B7)
            ret7 = ret6 - ret7

            ret8 = cv2.morphologyEx(ret7, cv2.MORPH_HITMISS, B8)
            ret8 = ret7 - ret8

            img = ret8  # 八次迭代完成 保存结果

        if (img == tmp).all():  # 如果所有结构元遍历的结果不再发生变化,则操作完成
            dst = img  # 保留细化结果
            break

    return dst.astype(np.uint8)

# 找到端点
def find_corner(x):
    kernel_right = np.array([[0, -1, 0], [1, 1, -1], [0, -1, 0]])      # right
    dst_right = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_right)

    kernel_left = np.array([[0, -1, 0], [-1, 1,1], [0, -1, 0]])      # left
    dst_left = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_left)

    kernel_up= np.array([[0, 1, 0], [-1, 1, -1], [0, -1, 0]])      # up
    dst_up = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_up)

    kernel_down= np.array([[0, -1, 0], [-1, 1, -1], [0, 1, 0]])      # down
    dst_down = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel_down)

    dst = dst_up+dst_down+dst_right+dst_left

    dst[0,:] = 0        # 去除上面
    dst[-1,:] = 0       # 去除下面
    dst[:,0] = 0        # 去除左面
    dst[:,-1] = 0        # 去除右面

    return dst.astype(np.uint8)

# 去除一次分支
def branch(corner_img,img):
    x,y = np.where(corner_img)
    for i,j in zip(x,y):          # 各个角点的坐标
        if (img[i-1,j] + img[i+1,j] + img[i,j-1]+img[i,j+1] == 1):
            img[i,j] = 0
    return img


img = cv2.imread('c.png',0)     # 读取图像
img = pre_process(img)          # 预处理之后的图像
source = img.copy()             # 拷贝图像
img = thin(img)

#  去除分支
while True:
    img_copy = img.copy()       # 拷贝原图,用于退出循环

    ret = find_corner(img)      # 找到角点
    img = branch(ret,img)             # 去除角点

    # 显示路线的过程
    #cv2.imshow('img', img * 255)
    #cv2.waitKey()

    if (img_copy == img).all():
        break

source *= 255
dst = source + img *120
cv2.imshow('img',np.hstack((source,dst)))
cv2.waitKey()
cv2.destroyAllWindows()

5. show

这里是网上找到迷宫,和利用代码找到的迷宫解法 

6. Acknowledge

存在的问题:

  • 输入的迷宫图片必须按照指定的格式。例如,图片的四周必须是墙壁且出入口在四周
  • 迷宫需要保证黑色为墙壁,白色为道路。迷宫的走法是上下左右,没有斜着走的

Tips:

  • 骨架抽取选择细化是因为,其余的方法可能会产生不连续的骨架,导致迷宫求解失败

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/356048.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java-合并两个链表

每日一题 Java-合并两个链表 给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。 请你将 list1 中下标从 a 到 b 的全部节点都删除,并将list2 接在被删除节点的位置。 下图中蓝色边和节点展示了操作后的结果: 请你返回结果…

linux下redis安装 及常用命令

安装及常用命令 redis的yum方式安装 先查看是否已经安装redis执行命令 rpm -qa | grep redis如果存在,将存在的卸载:(-y 代表自动选择) yum remove xxx -y在线安装redis yum install redis安装本地已经下载好的redis安装包 yum localinstall redis6.2…

基于Spring、Spring MVC、MyBatis的招聘管理系统

文章目录项目介绍主要功能截图:首页账户管理招聘建议部分代码展示设计总结项目获取方式🍅 作者主页:Java韩立 🍅 简介:Java领域优质创作者🏆、 简历模板、学习资料、面试题库【关注我,都给你】 …

流程引擎之Camunda简介

背景Camunda 是支持 BPMN(工作流和流程自动化)、CMMN(案例管理) 和 DMN(业务决策管理) java 框架。Camunda 基于Activiti5 保留了 PVM,其开发团队也是从 activiti 中分裂出来的。Camunda 来自拉…

KubeSphere实战

文章目录一、KubeSphere平台安装1、Kubernetes上安装KubeSphere1.1 安装docker1.2 安装Kubernetes1.3 前置环境之nfs存储1.4 前置环境之metrics-server1.5 安装KubeSphere2、Linux单节点部署KubeSphere3、Linux多节点部署KubeSphere(推荐)二、KubeSphere实战1、多租户实战2、中…

Spring中的数据校验--进阶

分组校验 场景描述 在实际开发中经常会遇到这种情况:添加用户时,id是由后端生成的,不需要校验id是否为空,但是修改用户时就需要校验id是否为空。如果在接收参数的User实体类的id属性上添加NotNull,显然无法实现。这时…

【飞桨AI-Python小白逆袭大神课程】作业3-《青春有你2》选手数据分析

目录 一、数据准备 1、文件数据以json文件格式保存: 二、数据分析 2、数据分析四剑客: (1)Numpy (2)pandas (3)Matplotlib (4)PIL (5&#x…

操作系统题目收录(十一)

1、操作系统采用分页存储管理方式,要求()。 A:每个进程拥有一张页表,且进程的页表驻留在内存中B:每个进程拥有一张页表,但只有执行进程的页表驻留在内存中C:所有进程共享一张页表&a…

django项目实战(django+bootstrap实现增删改查)

目录 一、创建django项目 二、修改默认配置 三、配置数据库连接 四、创建表结构 五、在app当中创建静态文件 六、页面实战-部门管理 1、实现一个部门列表页面 2、实现新增部门页面 3、实现删除部门 4、实现部门编辑功能 七、模版的继承 1、创建模板layout.html 1&…

Django框架之模型视图--Session

Session 1 启用Session Django项目默认启用Session。 可以在settings.py文件中查看,如图所示 如需禁用session,将上图中的session中间件注释掉即可。 2 存储方式 在settings.py文件中,可以设置session数据的存储方式,可以保存…

基于springboot的网上图书商城的设计与实现(程序+详细设计文档)

大家好✌!我是CZ淡陌。在这里为大家分享优质的实战项目,本人在Java毕业设计领域有多年的经验,陆续会更新更多优质的Java实战项目,希望你能有所收获,少走一些弯路! 🍅更多优质项目👇&…

Rust学习入门--【17】Rust Slice(切片)类型

系列文章目录 Rust 语言是一种高效、可靠的通用高级语言,效率可以媲美 C / C 。本系列文件记录博主自学Rust的过程。欢迎大家一同学习。 Rust学习入门–【1】引言 Rust学习入门–【2】Rust 开发环境配置 Rust学习入门–【3】Cargo介绍 Rust学习入门–【4】Rust 输…

RocketMQ云服务器和本地基础安装搭建及可视化控制台安装使用

一起学编程,让生活更随和! 如果你觉得是个同道中人,欢迎关注博主gzh:【随和的皮蛋桑】。 专注于Java基础、进阶、面试以及计算机基础知识分享🐳。偶尔认知思考、日常水文🐌。 目录一、RocketMQ 介绍1、Ro…

分布式事务--理论基础

1、事务基础 1.1、什么是事务 事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。 1.2、本地事务 在同一个进程内,控制同一数据源的事务,称为本地事务。例如数据库事务。 在计…

PyTorch 并行训练 DistributedDataParallel完整代码示例

使用大型数据集训练大型深度神经网络 (DNN) 的问题是深度学习领域的主要挑战。 随着 DNN 和数据集规模的增加,训练这些模型的计算和内存需求也会增加。 这使得在计算资源有限的单台机器上训练这些模型变得困难甚至不可能。 使用大型数据集训练大型 DNN 的一些主要挑…

SpringBoot监控

文章目录一、PrometheusGrafana监控Springboot1、简介2、SpringBoot应用镜像搭建2.1 springboot应用创建2.2 镜像创建3、Prometheus3.1 概述3.2 Prometheus创建4、Grafana可视化监控4.1 可视化4.2 告警设置二、轻量级日志系统Loki1、简介1.1 介绍1.2 与ELK差异2、grafana loki日…

linux宝塔安装和部署node全栈项目

使用服务器:阿里云ECS系列 服务器操作系统: Alibaba Cloud Linux 2.1903 LTS 64位 连接服务器方式: Workbench远程连接 使用公网IP登录 Workbench远程桌面,使用命令安装linux宝塔面板操作服务器: 1.登录linux宝塔面板,使用终端命令安装linux宝塔 yum i…

【操作系统】计算机系统概述

文章目录操作系统的概念、功能和目标熟悉的操作系统计算机系统的层次结构操作系统的概念操作系统的功能和目标作为系统资源的管理者作为用户和计算机之间的接口作为最接近硬件的层次操作系统的四个特征并发共享并发和共享的关系虚拟异步操作系统的发展和分类手工操作阶段单道批…

1207. 大臣的旅费/树的直径【AcWing】

1207. 大臣的旅费 很久以前,T王国空前繁荣。 为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。 为节省经费,T国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大…

使用Docker-Compose搭建Redis集群

1. 集群配置3主3从由于仅用于测试,故我这里只用1台服务器进行模拟redis列表2.编写redis.conf在server上创建一个目录用于存放redis集群部署文件。这里我放的路径为/root/redis-cluster 在/opt/docker/redis-cluster目录下创建redis-1,redis-2,redis-3,redis-4,redis…