牛耕分解+形态学分割 全覆盖路径规划(二)Part1. 分割

书接上文:牛耕分解+形态学分割 全覆盖路径规划(一)

前置文章1:房屋区域分割算法 Morphological Segmentation

前置文章2:牛耕覆盖算法 Boustrophedon Coverage Path Planning

项目地址:ipa320 / ipa_coverage_planning


由于博主先找的是覆盖然后再找分区,所以针对的是ipa_coverage_planning项目进行研究,不排除有其他更好的方法或者已经优化过的方法,在后续的学习中会持续补充。由于本文章在wordpress中编辑,部分代码粘贴会出现bug,所以建议下载原项目看


在上文中,我们将形态学分割和牛耕规划程序进行合并,将分割后的各个房间进行顺序规划,每个房间的规划用A*连接在一起。在本篇章中,我将着重对代码进行分析和调试,以及将多个用到的库进行剪切合并。

 

Part 1. morphological segmentation 形态学分割

源码:


void MorphologicalSegmentation::segmentMap(const cv::Mat &map_to_be_labeled, cv::Mat &segmented_map, double map_resolution_from_subscription,double room_area_factor_lower_limit, double room_area_factor_upper_limit)
{/*This segmentation algorithm does:* 1. collect the map data* 2. erode the map to extract contours* 3. find the extracted contures and save them if they fullfill the room-area criterion* 4. draw and fill the saved contoures in a clone of the map from 1. with a random colour* 5. get the obstacle information from the original map and draw them in the clone from 4.* 6. spread the coloured regions to the white Pixels*/// make two map clones to work withcv::Mat temporary_map_to_find_rooms = map_to_be_labeled.clone(); // map to find the rooms and for eroding//**************erode temporary_map until last possible room found****************// erode map a specified amount of timesstd::vector<std::vector<cv::Point>> saved_contours; // saving variable for every contour that is between the upper and the lower limitROS_INFO("starting eroding");for (int counter = 0; counter < 73; counter++){// erode the map one timecv::Mat eroded_map;cv::Point anchor(-1, -1); // needed for opencv erodecv::erode(temporary_map_to_find_rooms, eroded_map, cv::Mat(), anchor, 1);// save the more eroded maptemporary_map_to_find_rooms = eroded_map;// Save the eroded map in a second map, which is used to find the contours. This is neccesarry, because// the function findContours changes the given map and would make it impossible to work any further with itcv::Mat contour_map = eroded_map.clone();// find Contours in the more eroded mapstd::vector<std::vector<cv::Point>> temporary_contours; // temporary saving-variable// hierarchy saves if the contours are hole-contours:// hierarchy[{0,1,2,3}]={next contour (same level), previous contour (same level), child contour, parent contour}// child-contour = 1 if it has one, = -1 if not, same for parent_contourstd::vector<cv::Vec4i> hierarchy;
#if CV_MAJOR_VERSION <= 3cv::findContours(contour_map, temporary_contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
#elsecv::findContours(contour_map, temporary_contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
#endifif (temporary_contours.size() != 0){// check every contour if it fullfills the criteria of a roomfor (int current_contour = 0; current_contour < temporary_contours.size(); current_contour++){ // only take first level contours --> second level contours belong to holes and doesn't need to be looked atif (hierarchy[current_contour][3] == -1){// check if contour is large/small enough for a roomdouble room_area = map_resolution_from_subscription * map_resolution_from_subscription * cv::contourArea(temporary_contours[current_contour]);// subtract the area from the hole contours inside the found contour, because the contour area grows extremly large if it is a closed loopfor (int hole = 0; hole < temporary_contours.size(); hole++){if (hierarchy[hole][3] == current_contour) // check if the parent of the hole is the current looked at contour{room_area -= map_resolution_from_subscription * map_resolution_from_subscription * cv::contourArea(temporary_contours[hole]);}}if (room_area_factor_lower_limit < room_area && room_area < room_area_factor_upper_limit){// save contour for later drawing in mapsaved_contours.push_back(temporary_contours[current_contour]);// make region black if room found --> region doesn't need to be looked at anymore
#if CV_MAJOR_VERSION <= 3cv::drawContours(temporary_map_to_find_rooms, temporary_contours, current_contour, cv::Scalar(0), CV_FILLED, 8, hierarchy, 2);
#elsecv::drawContours(temporary_map_to_find_rooms, temporary_contours, current_contour, cv::Scalar(0), cv::FILLED, 8, hierarchy, 2);
#endif}}}}}//*******************draw contures in new map***********************std::cout << "Segmentation Found " << saved_contours.size() << " rooms." << std::endl;// draw filled contoures in new_map_to_draw_contours_ with random colour if this colour hasn't been used yetcv::Mat new_map_to_draw_contours; // map for drawing the found contoursmap_to_be_labeled.convertTo(segmented_map, CV_32SC1, 256, 0);std::vector<cv::Scalar> already_used_coloures; // vector for saving the already used colouresfor (int idx = 0; idx < saved_contours.size(); idx++){bool drawn = false;	  // checking-variable if contour has been drawnint draw_counter = 0; // counter to exit loop if it gets into an endless-loop (e.g. when there are more rooms than possible)do{draw_counter++;cv::Scalar fill_colour(rand() % 52224 + 13056);if (!contains(already_used_coloures, fill_colour) || draw_counter > 250){// if colour is unique draw Contour in map
#if CV_MAJOR_VERSION <= 3cv::drawContours(segmented_map, saved_contours, idx, fill_colour, CV_FILLED);
#elsecv::drawContours(segmented_map, saved_contours, idx, fill_colour, cv::FILLED);
#endifalready_used_coloures.push_back(fill_colour); // add colour to used colouresdrawn = true;}} while (!drawn);}//*************************obstacles***********************// get obstacle informations and draw them into the new mapROS_INFO("starting getting obstacle information");for (int row = 0; row < map_to_be_labeled.rows; ++row){for (int col = 0; col < map_to_be_labeled.cols; ++col){// find obstacles = black pixelsif (map_to_be_labeled.at<unsigned char>(row, col) == 0){segmented_map.at<int>(row, col) = 0;}}}ROS_INFO("drawn obstacles in map");//**************spread the colored region by making white pixel around a contour their color****************// spread the coloured regions to the white PixelswavefrontRegionGrowing(segmented_map);ROS_INFO("filled white pixels in new map");
}

 

整个算法分为6步操作(差不多吧,自己按感觉分的),下面将逐步进行测试和解析。

Step 1. 腐蚀地图

ROS_INFO("started Testing");
cv::Mat Mattemporary_map_to_find_rooms_test = temporary_map_to_find_rooms.clone();
size_t test_times = 7;
for (int counter = 0; counter < test_times; counter++)
{cv::Mat eroded_map_test;cv::Point anchor_test(-1, -1);cv::erode(Mattemporary_map_to_find_rooms_test, eroded_map_test, cv::Mat(), anchor_test, 1);Mattemporary_map_to_find_rooms_test = eroded_map_test;
}
cv::imshow("Mattemporary_map_to_find_rooms_test", Mattemporary_map_to_find_rooms_test);

创建一些测试的数据,腐蚀 test_times 次看看腐蚀的到的效果是什么样的(由于很多像我一样平时用不到opencv,所以慢一点来看),从下图中我们可以看出来,腐蚀后黑色区域向外/内蔓延了,图左为原图,图右为腐蚀一次的图(这也是常用的保证安全距离的方式)

图1 原始图像
图2 腐蚀一次后图像

Step2. 提取轮廓

// find Contours in map
cv::Mat contour_map_test = Mattemporary_map_to_find_rooms_test.clone();
std::vector<std::vector<cv::Point>> temporary_contours_test;
std::vector<cv::Vec4i> hierarchy_test;
cv::findContours(contour_map_test, temporary_contours_test, hierarchy_test, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
// display
cv::Mat display_map = cv::Mat::ones(temporary_map_to_find_rooms.size(), temporary_map_to_find_rooms.type()) * 255;
cv::drawContours(display_map, temporary_contours_test, -1, cv::Scalar(0, 0, 0), 1);
// cv::imshow("Contours", display_map);

可能很多人和我一样对于轮廓提取这一部分没有直观的认知。使用opencv的findContours进行轮廓提取,得到的是二维的Point点集,将其在空白图像上可视化,操作对应于上述代码,左图为腐蚀一次后的图,右图为提取的轮廓:

图1 腐蚀一次后图像
图2 提取轮廓

在这一部分比较难理解的是 hierarchy ,我在这里借助GPT解释一下:

hierarchy 是一个向量,用于存储轮廓的层次结构信息。在OpenCV中,轮廓可以有不同的层次关系,例如,一个轮廓可能是另一个轮廓的子轮廓(内部轮廓),或者两个轮廓可能是平行的(同一层级)。hierarchy 向量中的每个元素都是一个 cv::Vec4i 类型,包含四个整数,分别表示:

  1. Next Contour (同一层级的下一个轮廓):这个值指向同一层级的下一个轮廓。如果没有下一个轮廓,则为-1。
  2. Previous Contour (同一层级的上一个轮廓):这个值指向同一层级的上一个轮廓。如果没有上一个轮廓,则为-1。
  3. First Child (第一个子轮廓):这个值指向当前轮廓的第一个子轮廓。如果当前轮廓没有子轮廓,则为-1。
  4. Parent Contour (父轮廓):这个值指向当前轮廓的父轮廓。如果当前轮廓没有父轮廓,则为-1。
通过这种方式, hierarchy 向量提供了一种方法来理解和操作图像中轮廓之间的复杂关系。例如,你可以使用这些信息来区分和处理图像中的主要物体和它们的内部空洞,或者来识别和分离相邻但独立的物体。

我们尝试输出一下 temporary_contours :
for (size_t i = 0; i < hierarchy_test.size(); ++i)
{std::cout << "Hierarchy " << i << ": ";for (int j = 0; j < 4; ++j){std::cout << hierarchy_test[i][j] << " ";}std::cout << std::endl;
}

结果为:

Hierarchy 0: -1 -1 1 -1
Hierarchy 1: -1 -1 -1 0

根据上述结果,可以看到检测出两个轮廓,轮廓1是父轮廓,轮廓2是子轮廓其父轮廓是轮廓1(大概是图中的那个小轮廓吧)

Step3. 寻找房间

if (temporary_contours_test.size() != 0)
{for (int current_contour = 0; current_contour < temporary_contours_test.size(); current_contour++){if (hierarchy_test[current_contour][3] == -1){double room_area = map_resolution_from_subscription * map_resolution_from_subscription * cv::contourArea(temporary_contours_test[current_contour]);for (int hole = 0; hole < temporary_contours_test.size(); hole++)if (hierarchy_test[hole][3] == current_contour)room_area -= map_resolution_from_subscription * map_resolution_from_subscription * cv::contourArea(temporary_contours_test[hole]);if (room_area_factor_lower_limit < room_area && room_area < room_area_factor_upper_limit){saved_contours.push_back(temporary_contours_test[current_contour]);cv::drawContours(Mattemporary_map_to_find_rooms_test, temporary_contours_test, current_contour, cv::Scalar(0), cv::FILLED, 8, hierarchy_test, 2);}}}
}

根据轮廓找房间的逻辑大概对应于上述代码的几个步骤:

  • 1. 遍历每一个父轮廓,利用OpenCV的 contourArea 函数计算该轮廓的面积
  • 2. 删除父轮廓中的子轮廓的面积(障碍物)
  • 3. 如果除去子轮廓的面积符合开始设定的面积范围,将该轮廓保存在数组中,将地图中该轮廓范围填充(置为不可到达)
由于腐蚀 1 次无法看到效果,我们尝试多次腐蚀然后找一个房间,将腐蚀次数变为7,查看效果,左上为原图,右上为腐蚀5次,左下为提取的轮廓,右下为填充查找到房间后的地图:

在这里插入图片描述

可以看到经过五次腐蚀,左上的房间独立了出来,且轮廓的面积达到了房间面积的要求,将该轮廓记录,并清除该房间。随后用右下的地图继续查找。

Step4. 回到Step1重复腐蚀并查找

执行腐蚀的次数是认为设定的,源码中设定了73次,我认为实际可能不需要这么多次,不过多执行几次并没有坏处

Step5. 填充轮廓

map_to_be_labeled.convertTo(segmented_map, CV_32SC1, 256, 0);
std::vector<cv::Scalar> already_used_coloures_test;
for (int idx = 0; idx < saved_contours.size(); idx++)
{bool drawn = false;	  // checking-variable if contour has been drawnint draw_counter = 0; // counter to exit loop if it gets into an endless-loop (e.g. when there are more rooms than possible)do{draw_counter++;cv::Scalar fill_colour(rand() % 52224 + 13056);if (!contains(already_used_coloures_test, fill_colour) || draw_counter > 250){cv::drawContours(segmented_map, saved_contours, idx, fill_colour, cv::FILLED);already_used_coloures_test.push_back(fill_colour); // add colour to used colouresdrawn = true;}} while (!drawn);
}
for (int row = 0; row < map_to_be_labeled.rows; ++row)
{for (int col = 0; col < map_to_be_labeled.cols; ++col){if (map_to_be_labeled.at<unsigned char>(row, col) == 0){segmented_map.at<int>(row, col) = 0;}}
}
cv::Mat display_map_test;
segmented_map.convertTo(display_map_test,CV_8U);cv::imshow("filled", display_map_test);

对照上面的找到一个房间后填充,填充后补充原图中不可到达区域,效果如下图,虽然能看出这个房间,但是填充的并不满,所以有后续操作
在这里插入图片描述

step6. 扩散填充
wavefrontRegionGrowing(segmented_map);
这一步操作是将彩色区域填充至白色区域,调用了wavefront_region_growing.cpp的函数,详细看一下wavefrontRegionGrowing是怎么实现的

// spreading image is supposed to be of type CV_32SC1
void wavefrontRegionGrowing(cv::Mat&amp; image)
{//This function spreads the colored regions of the given map to the neighboring white pixelsif (image.type()!=CV_32SC1){std::cout &lt;&lt; "Error: wavefrontRegionGrowing: provided image is not of type CV_32SC1." &lt;&lt; std::endl;return;}cv::Mat spreading_map = image.clone();bool finished = false;while (finished == false){finished = true;for (int row = 1; row &lt; spreading_map.rows-1; ++row){for (int column = 1; column &lt; spreading_map.cols-1; ++column){if (spreading_map.at(row, column) &gt; 65279)		// unassigned pixels{//check 3x3 area around white pixel for fillcolour, if filled Pixel around fill white pixel with that colourbool set_value = false;for (int row_counter = -1; row_counter &lt;= 1 &amp;&amp; set_value==false; ++row_counter){for (int column_counter = -1; column_counter &lt;= 1 &amp;&amp; set_value==false; ++column_counter){int value = image.at<int>(row + row_counter, column + column_counter);if (value != 0 &amp;&amp; value &lt;= 65279){spreading_map.at<int>(row, column) = value;set_value = true;finished = false;	// keep on iterating the wavefront propagation until no more changes occur}}}}}}image = spreading_map.clone();}
}

该操作主要包含以下几个步骤:

  • 检查输入数据类型,地图类型必须为CV_32SC1
  • 遍历图像像素,如果像素未被分配(像素值大于 65279),则检查周围3*3范围
  • 如果3*3范围内存在色彩像素,则设置为该值,set_value用于标记是否修改该像素的值
  • 不停的进行着色,直到没有未被分配的色彩为止(finished = true)
最终完成区域的填充

完整的房间分割后的图:

图1 原始图像
图2 分区后图像

结尾总结:

至此,图像的分割完成了,但是这样的填充方法有缺陷,在得到的图中也可以很明显的看出来,由于填充顺序是从左上到右下遍历,所以房间可能会存在左边缺少右边突出的现象。思考一下优化方向:① 针对独立房间的方法,ipa项目中采用的是不断的腐蚀并提取轮廓,是否能够改进; ② 针对填充房间,ipa项目中采用的是对像素点周围3*3范围进行查找,是否能优化

 

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

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

相关文章

2.Jmeter安装配置,核心目录详情,组件和作用域

一、Jmeter安装配置以及核心目录详情 Jmeter基于java语言来开发&#xff0c;java需要jdk环境。 1.安装jdk并且配置jdk的环境变量。 2.jmeter只需要解压就可以使用了。 3.在D:\apache-jmeter-5.5\bin目录下双击jmeter.bat文件就可以启动使用了 backups&#xff1a;自动备份的目录…

开放混合 数据驱动 Cloudera的商业AI布局

8月7日在新加坡揭幕的Cloudera EVOLVE24大会&#xff0c;是全球系列活动的第一站&#xff0c;此后还将在全球多个国家陆续举行。大会从亚太地区起步&#xff0c;也彰显了Cloudera对亚太市场的重视。“亚太地区已经成为我们业务增长最快的区域。在混合云环境之下&#xff0c;越来…

rust + bevy 实现小游戏 打包成wasm放在浏览器环境运行

游戏界面 代码地址 github WASM运行 rustup target install wasm32-unknown-unknown cargo install wasm-server-runner cargo run --target wasm32-unknown-unknowncargo install wasm-bindgen-cli cargo build --release --target wasm32-unknown-unknown wasm-bindgen --…

海报生成用什么软件好?小白看这里

想要让你的信息在人群中脱颖而出吗&#xff1f;一张精心设计的海报无疑是最佳选择。 无论是宣传活动、展示作品还是装饰空间&#xff0c;海报都能以视觉的力量抓住人们的眼球。 但海报制作软件哪个好呢&#xff1f;别急&#xff0c;这里有五个超实用的海报制作软件&#xff0…

再次进阶 舞台王者 第八季完美童模全球赛代言人【赵御涵】赛场+秀场超燃合集!

7月20-23日&#xff0c;2024第八季完美童模全球总决赛在青岛圆满落幕。在盛大的颁奖典礼上&#xff0c;一位才能出众的少女——赵御涵迎来了她舞台生涯的璀璨时刻。 代言人——赵御涵&#xff0c;以璀璨童星之姿&#xff0c;优雅地踏上完美童模盛宴的绚丽舞台&#xff0c;作为开…

如何对离线数仓和准实时数仓进行精准把控?

数仓是指将企业中各个业务系统产生的数据进行汇总、清洗、转化和整合&#xff0c;以便为企业提供决策支持和数据分析的存储和管理系统。 离线数仓和准实时数仓&#xff0c;这两种数据仓库模式&#xff0c;各有其特点&#xff0c;根据其特点和适用的应用场景选择合适的仓库模式…

汇编实现从1加到1000(《X86汇编语言 从实模式到保护模式(第2版》) 第135页第2题解答)

题目: 编写一段主引导扇区程序,计算从1加到1000的和,并在屏幕上显示结果 输出结果: 代码: jmp near start text db 123...1000 start:mov ax,0x07c0mov ds,ax ;数据段从主引导区开始mov ax,0xb800mov es,ax ;显存地址从B8000物理地址开始mov si,text ;si指向text的第…

2024 年 .NET 高效开发精选实用类库

目录 前言 1、Entity Framework Core 2、Newtonsoft.Json 3、AutoMapper 4、HttpClient 5、Serilog 6、Hangfire 7、xUnit 8、OxyPlot 9、Task Parallel Library (TPL) 10、Elasticsearch.NET 和 NEST 总结 最后 前言 在平时开发中&#xff0c;好的类库能帮助我们…

华火10号店隆重开业,千城万店打造增长新引擎

风吹洛阳城&#xff0c;花开盛唐梦&#xff01;9月11日&#xff0c;相约在洛阳&#xff0c;在时光、空间与浪漫的交错中&#xff0c;华火10号店盛大开业。此次开业将为洛阳市民提供领先行业的绿色厨电产品&#xff0c;营造高端化、体验化、智慧化的门店氛围&#xff0c;打造极致…

说说synchronized的锁升级过程

在 JDK 1.6之前&#xff0c;synchronized 是一个重量级、效率比较低下的锁&#xff0c;但是在JDK 1.6后&#xff0c;JVM 为了提高锁的获取与释放效&#xff0c;,对 synchronized 进行了优化&#xff0c;引入了偏向锁和轻量级锁&#xff0c;至此&#xff0c;锁的状态有四种&…

echarts 3D地图

通过echats echats-gl 实现的3D地图页面。 先上效果图: 1.通过外边js引入方式,引入必要的js压缩文件 <script src="/static/vue-v2/vue.js"></script> <script src="/static/assets/echarts-v5/echarts.min.js"></script> &l…

从头开始学MyBatis—02基于xml和注解分别实现的增删改查

首先介绍此次使用的数据库结构&#xff0c;然后引出注意事项。 通过基于xml和基于注解的方式分别实现了增删改查&#xff0c;还有获取参数值、返回值的不同类型对比&#xff0c;帮助大家一次性掌握两种代码编写能力。 目录 数据库 数据库表 实体类 对应的实体类如下&#x…

Vue2 qrcode+html2canvas 实现二维码的生成和保存

1.安装 npm install qrcode npm install html2canvas 2.引用 import QRCode from qrcode import html2canvas from html2canvas 效果&#xff1a; 1. 二维码生成&#xff1a; 下载二维码图片&#xff1a; 二维码的内容&#xff1a; 实现代码&#xff1a; <template>…

重学SpringBoot3-SpringApplicationRunListener

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-SpringApplicationRunListener 1. 基本作用2. 如何实现2.1. 创建SpringApplicationRunListener2.2. 注册SpringApplicationRunListener2.3. 完整示例 3.…

初始爬虫5

响应码&#xff1a; 数据处理&#xff1a; re模块&#xff08;正则表达式&#xff09; re模块是Python中用于正则表达式操作的标准库。它提供了一些功能强大的方法来执行模式匹配和文本处理。以下是re模块的一些常见用法及其详细说明&#xff1a; 1. 基本用法 1.1 匹配模式 …

大势智慧与山东省国土测绘院签署战略合作协议

9月6日&#xff0c;山东省国土测绘院&#xff08;后简称山东院&#xff09;与武汉大势智慧科技有限公司&#xff08;后简称大势智慧&#xff09;签署战略合作协议。 山东院院长田中原、卫星应用中心主任相恒茂、基础测绘中心主任魏国忠、卫星应用中心高级工程师张奇伟&#xf…

记一次实战中对fastjson waf的绕过

最近遇到一个fastjson的站&#xff0c;很明显是有fastjson漏洞的&#xff0c;因为type这种字符&#xff0c;fastjson特征很明显的字符都被过滤了 于是开始了绕过之旅&#xff0c;顺便来学习一下如何waf 编码绕过 去网上搜索还是有绕过waf的文章&#xff0c;下面来分析一手&a…

性能测试-断言+自学说明(十二)

一、响应断言 需求;jmeter请求百度&#xff0c;断言响应结果中是否包含“百度一下&#xff0c;你就知道” 1、位置&#xff1a; http请求-断言-响应断言 2、类型 响应文本&#xff1a;断言响应体中包含的字符串 响应代码&#xff1a;断言响应状态码 3、断言步骤&#xf…

全文带你轻松备考OCM

OCM&#xff0c;作为Oracle公司授予的顶级专业认证&#xff0c;是数据库领域从业者梦寐以求的技术巅峰标志。它不仅是对个人技术深度与广度的全面肯定&#xff0c;更是职业道路上的一块重要里程碑。在踏上这段挑战之旅前&#xff0c;深入洞察OCM认证的精髓、考试细节及备考策略…

想要快速准备好性能数据?方法这不就来了!

性能测试的一般流程 收集性能需求——>编写性能脚本——>执行性能测试——>分析测试报告——>系统性能调优。 在收集性能需求后&#xff0c;我们会思考&#xff1a; 1.负载测试时并发时需要多少数据&#xff1f;例&#xff1a;登录&#xff1b; 2.DB数据是否和…