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

news2024/11/22 9:02:47

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

前置文章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 with
	cv::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 times
	std::vector<std::vector<cv::Point>> saved_contours; // saving variable for every contour that is between the upper and the lower limit

	ROS_INFO("starting eroding");
	for (int counter = 0; counter < 73; counter++)
	{
		// erode the map one time
		cv::Mat eroded_map;
		cv::Point anchor(-1, -1); // needed for opencv erode
		cv::erode(temporary_map_to_find_rooms, eroded_map, cv::Mat(), anchor, 1);

		// save the more eroded map
		temporary_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 it
		cv::Mat contour_map = eroded_map.clone();
		// find Contours in the more eroded map
		std::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_contour
		std::vector<cv::Vec4i> hierarchy;
#if CV_MAJOR_VERSION <= 3
		cv::findContours(contour_map, temporary_contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
#else
		cv::findContours(contour_map, temporary_contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
#endif
		if (temporary_contours.size() != 0)
		{
			// check every contour if it fullfills the criteria of a room
			for (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 at
				if (hierarchy[current_contour][3] == -1)
				{
					// check if contour is large/small enough for a room
					double 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 loop
					for (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 map
						saved_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 <= 3
						cv::drawContours(temporary_map_to_find_rooms, temporary_contours, current_contour, cv::Scalar(0), CV_FILLED, 8, hierarchy, 2);
#else
						cv::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 yet
	cv::Mat new_map_to_draw_contours; // map for drawing the found contours
	map_to_be_labeled.convertTo(segmented_map, CV_32SC1, 256, 0);
	std::vector<cv::Scalar> already_used_coloures; // vector for saving the already used coloures
	for (int idx = 0; idx < saved_contours.size(); idx++)
	{
		bool drawn = false;	  // checking-variable if contour has been drawn
		int 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 <= 3
				cv::drawContours(segmented_map, saved_contours, idx, fill_colour, CV_FILLED);
#else
				cv::drawContours(segmented_map, saved_contours, idx, fill_colour, cv::FILLED);
#endif
				already_used_coloures.push_back(fill_colour); // add colour to used coloures
				drawn = true;
			}
		} while (!drawn);
	}
	//*************************obstacles***********************
	// get obstacle informations and draw them into the new map
	ROS_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 pixels
			if (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 Pixels
	wavefrontRegionGrowing(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 drawn
	int 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 coloures
			drawn = 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 pixels
	if (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 colour
					bool 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.coloradmin.cn/o/2134913.html

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

相关文章

基于Kubernetes部署Spark:spark on kubernetes

什么是spark&#xff1f; spark是一种基于内存的快速、通用、可扩展的的数据分析计算引擎。 Hadoop、Hive、Spark是什么关系&#xff1f; 大数据技术生态中&#xff0c;Hadoop、Hive、Spark是什么关系&#xff1f;| 通俗易懂科普向_哔哩哔哩_bilibili Hadoop 与 HDFS (Hado…

【腾讯云】AI驱动TDSQL-C Serveress 数据库技术实战营-如何是从0到1体验电商可视化分析小助手得统计功能,一句话就能输出目标统计图

欢迎来到《小5讲堂》 这是《腾讯云》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 背景效果图流程图创建数据库基本信息数据库配置设置密码控制台开启…

腾讯联合多所高校出品!AI游戏生成模型 GameGen-O 轻松生成高质量游戏元素

最近&#xff0c;香港和中国的几所大学与腾讯联合研发了一个名为 GameGen-O 的 AI 模型&#xff0c;专门用于创建开放世界视频游戏的模拟。 GameGen-O 的功能不仅仅限于生成高质量的游戏内容&#xff0c;它还模拟了多种游戏引擎的特性&#xff0c;能够创造出丰富多样的游戏元素…

字符分类函数和字符串函数

Hello~,欢迎大家来到我的博客进行学习&#xff01; 目录 1.字符分类函数&#x1f618;1.1解释以及使用&#x1f92f;1.2将字符分类函数组合使用&#x1f47b;2 .字符转换函数&#x1f9d0; 1.字符分类函数&#x1f618; 1.1解释以及使用&#x1f92f; 在键盘上敲的字符有许多…

揭秘高效日志管理:解锁数据宝藏,驱动业务精准决策

作者简介&#xff1a;我是团团儿&#xff0c;是一名专注于云计算领域的专业创作者&#xff0c;感谢大家的关注 座右铭&#xff1a; 云端筑梦&#xff0c;数据为翼&#xff0c;探索无限可能&#xff0c;引领云计算新纪元 个人主页&#xff1a;团儿.-CSDN博客 目录 前言&#…

duckdb 连接postgres 和 jdbc 的使用

why&#xff1f; 主要是特别快 嵌入式&#xff0c;不需要服务器&#xff0c;使用超级方便 扩展机制灵活&#xff0c;可以直接读取CSV、JSON、Parquet等文件 Parquet文件格式详解&#xff08;含行、列式存储区别&#xff09;_parquet格式-CSDN博客 采用列式存储&#xff08;用…

边缘计算网关:连接中心计算与边缘设备的重要桥梁-天拓四方

一、边缘计算网关&#xff1a;重新定义信息高速公路的“路标” 边缘计算网关&#xff0c;作为边缘计算生态系统中的核心组件&#xff0c;不仅承载着数据传输的功能&#xff0c;更是智能信息处理的关键节点。它通过分布式计算架构&#xff0c;将数据处理任务前置到网络边缘&…

JDK 收费了,怎么应对?难道 JDK 8 一直用下去吗?

最近遇到一个问题&#xff1a;Oracle JDK 8 在 ARM 机器上性能特别差&#xff01; 我的第一直觉就是是不是 JDK 8 在 ARM 不兼容&#xff1f; 为了研究这个问题&#xff0c;我探究了一圈&#xff0c;主要是找找有没有同行遇到这个问题&#xff0c;在找的过程中呢&#xff0c;发…

基于vue框架的宠物领养系统l3a76(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,宠物信息,宠物领养,宠物品种,团队信息,入团申请,团队活动 开题报告内容 基于Vue框架的宠物领养系统开题报告 一、引言 随着宠物文化的普及和人们对宠物情感的日益加深&#xff0c;宠物领养已成为社会关注的热点之一。然而&#…

# 大模型的第一个杀手级应用场景出来了

大家终于都意识到大模型首先改变的是软件行业自己&#xff0c;而软件的根基是代码生成。代码生成第一波就是AI辅助开发&#xff0c;这个会是大模型第一个杀手级应用。大家苦苦逼问自己的大模型杀手级应用&#xff0c;为什么会是辅助编程&#xff0c;这里说下什么&#xff1a; 必…

利用shuji还原webpack打包源码

0 前言 前段时间做一个银行的项目&#xff0c;是在别人已经打过好多次的基础上继续打&#xff0c;而且时间很短&#xff0c;也是没办法要有产出&#xff0c;这个银行很多站点都是webpack打包&#xff0c;就新学了一个点&#xff1a;利用shuji获取webpack打包站源码&#xff08…

2025届京东校招薪酬全面上调,加薪20%!

就在近日京东官方发布声明&#xff0c;今年 2025 届校招岗位的薪资将全面上调&#xff0c;再次加薪&#xff01;其中&#xff0c;采销、技术、产品等核心岗位薪酬上调不低于20%&#xff0c;产研类岗位更狠&#xff0c;算法岗平均起薪涨幅超75%&#xff0c;硬件和设计等岗位起薪…

2024年06月中国电子学会青少年软件编程(图形化)等级考试试卷(一级)答案 + 解析

青少年软件编程&#xff08;图形化&#xff09;等级考试试卷&#xff08;一级&#xff09; 分数&#xff1a;100 题数&#xff1a;37 一、单选题 音乐Video Game1的时长将近8秒&#xff0c;点击一次角色&#xff0c;下列哪个程序不能完整地播放音乐两次&#xff1f;&#xff0…

java实现根据延迟法定退休政策计算退休年龄

一、计算规则 从2025年1月1日起&#xff0c;男职工和原法定退休年龄为五十五周岁的女职工&#xff0c;法定退休年龄每四个月延迟一个月&#xff0c;分别逐步延迟至六十三周岁和五十八周岁&#xff1b;原法定退休年龄为五十周岁的女职工&#xff0c;法定退休年龄每二个月延迟一…

应急响应实战---是谁修改了我的密码?

前言&#xff1a;此次应急响应为真实案例&#xff0c;客户反馈无法通过密码登录服务器&#xff0c;疑似服务器被入侵 0x01 如何找回密码&#xff1f; 客户服务器为windows server2019&#xff0c;运维平台为PVE平台&#xff1b;实际上无论是windows系统或者是linux系统&#…

从简单分析到智能问数,Smartbi AIChat让数据回归业务

大数据产业创新服务媒体 ——聚焦数据 改变商业 在某科技公司&#xff0c;资深数据分析师李晨&#xff08;化名&#xff09;正忙于分析新产品的市场表现。面对传统自助式BI工具&#xff0c;李晨在功能界面中手动设置各种查询条件&#xff0c;进行了一番复杂的拖拉拽操作&#…

spring模块(六)spring监听器(3)广播与异步问题

发布事件和监听器之间默认是同步的&#xff1b;监听器则是广播形式。demo&#xff1a; event&#xff1a; package com.listener.demo.event;import com.listener.demo.dto.UserLogDTO; import org.springframework.context.ApplicationEvent;public class MyLogEvent extends…

界面控件DevExpress中文教程:如何PDF图形对象的可见性?

DevExpress拥有.NET开发需要的所有平台控件&#xff0c;包含600多个UI控件、报表平台、DevExpress Dashboard eXpressApp 框架、适用于 Visual Studio的CodeRush等一系列辅助工具。屡获大奖的软件开发平台DevExpress 近期重要版本v24.1已正式发布&#xff0c;该版本拥有众多新产…

数据资产盘点

数据资产盘点包含调研诊断、数据盘点、数据对标校正、分类分级、权责划分、数据资产目录建立六大环节。调研诊断&#xff1a;通常采用访谈或案头梳理的方式&#xff0c;对 IT 整体建设情况、业务系统数据情况进行调研&#xff0c;框定数据资产管理范围、聚焦目标。 数据盘点&a…

spring整合mabatis框架(druid连接池)

spring整合mabatis框架&#xff0c;duird连接池&#xff0c;Junit5测试框架 1&#xff09;创建Maven工程 2&#xff09;导入相关的依赖 <!--springContext依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-contex…