Beautifi4 函数详细说明文档
文件
map_beautificaition/mapbeautifi.cpp
概述
Beautifi4 是地图美化算法中的一个重要函数,主要用于对SLAM生成的地图进行后处理优化。该函数通过轮廓检测、膨胀操作、最大内接矩形算法等技术,对地图进行智能美化处理,生成房型图。下面以一个例图展示每步的效果
函数签名
void MapBeauti::Beautifi4(uint8_t* map_data, int map_height, int map_width, int search_distance, int level)
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
map_data | uint8_t* | 地图数据指针,存储像素值 |
map_height | int | 地图高度(像素行数) |
map_width | int | 地图宽度(像素列数) |
search_distance | int | 搜索距离参数(当前版本未使用) |
level | int | 处理级别,影响最小矩形面积阈值,level越大越接近矩形 |
像素值含义
| 像素值 | 含义 | 说明 |
|---|---|---|
| 0 | 黑色 | 障碍物/墙壁 |
| 127 | 灰色 | 未知区域/边界 |
| 255 | 白色 | 自由空间 |
| 100 | 家具值 | 家具标记(furniture_value) |
算法流程
1. 参数验证与初始化
// 安全检查
if (map_data == nullptr || map_height <= 0 || map_width <= 0) {
return;
}
if (level <= 0) {
level = 1; // 设置默认值
}
2. 膨胀操作
对灰色点(127)进行膨胀处理:
- 如果灰色点的8连通邻域内有黑色点(0),则将该灰色点置为黑色
- 重复执行2次以确保充分膨胀
for (int i = 0; i < 2; i++) {
memcpy(tmp, map_data, map_height * map_width);
for (int y = 0; y < map_height; ++y) {
for (int x = 0; x < map_width; ++x) {
if (tmp[y * map_width + x] == 127) {
// 检查8连通邻域
bool has_black = false;
for (int dy = -1; dy <= 1 && !has_black; ++dy) {
for (int dx = -1; dx <= 1 && !has_black; ++dx) {
// 检查邻域内是否有黑色点
}
}
if (has_black) {
map_data[y * map_width + x] = 0;
}
}
}
}
}
3. 轮廓检测
使用 _ContourFinder 进行轮廓检测:
// 创建二值化图像
memset(tmp, 0, map_height * map_width);
for (int i = 0; i < map_height * map_width; i++) {
if (map_data[i] == 127) { // 灰色点
tmp[i] = 0;
} else {
tmp[i] = 255;
}
}
// 轮廓检测
_ContourFinder cf_binary;
cf_binary.Find((const int8_t*)tmp, map_width, map_height, map_width);
const auto& new_wall_contour = cf_binary.GetContours();
4. 选择最大轮廓
遍历所有检测到的轮廓,选择点数最多的轮廓作为主要处理对象:
_ContourFinder::Contour* contour = new_wall_contour->at(0);
int max_pt_size = contour->points.size();
for (size_t i = 0; i < new_wall_contour->size(); ++i) {
if (new_wall_contour->at(i) != nullptr &&
int32_t(new_wall_contour->at(i)->points.size()) > max_pt_size) {
contour = new_wall_contour->at(i);
max_pt_size = new_wall_contour->at(i)->points.size();
}
}
5. 轮廓填充与边界处理
使用 DrawContourFill 函数填充轮廓内部,并处理轮廓边界:
// 填充轮廓
DrawContourFill(contour->points, tmp, 255, map_width, map_height);
// 轮廓外清除
for (int y = 0; y < map_height; ++y) {
for (int x = 0; x < map_width; ++x) {
if (tmp[y * map_width + x] != 255) {
map_data[y * map_width + x] = 127;
}
}
}
// 轮廓边界处理
for (const auto& pt : contour->points) {
// 将轮廓点周围7x7范围内的黑色点置为家具值
for (int dy = -3; dy <= 3; ++dy) {
for (int dx = -3; dx <= 3; ++dx) {
// 边界检查和像素值修改
}
}
}
6. 外接矩形计算
计算轮廓的外接矩形,并在地图上绘制矩形边界:
// 计算外接矩形
int min_x = map_width, max_x = -1;
int min_y = map_height, max_y = -1;
for (int y = 0; y < map_height; y++) {
for (int x = 0; x < map_width; x++) {
if (tmp[y * map_width + x] == 255) {
min_x = std::min(min_x, x);
max_x = std::max(max_x, x);
min_y = std::min(min_y, y);
max_y = std::max(max_y, y);
}
}
}
// 绘制矩形边界
for (int x = min_x; x <= max_x; ++x) {
map_data[min_y * map_width + x] = 0; // 上边
map_data[max_y * map_width + x] = 0; // 下边
}
for (int y = min_y; y <= max_y; ++y) {
map_data[y * map_width + min_x] = 0; // 左边
map_data[y * map_width + max_x] = 0; // 右边
}
7. 最大内接矩形算法
使用单调栈算法寻找最大内接矩形:
int* height = new int[map_width];
int rect_area = (max_x - min_x + 1) * (max_y - min_y + 1);
// 根据矩形面积设置最小阈值
int min_rect_area = 0;
if(rect_area < 2000){
min_rect_area = 50 * level;
} else if(rect_area < 10000){
min_rect_area = 100 * level;
} else{
min_rect_area = 300 * level;
}
// 单调栈算法寻找最大矩形
while (true) {
memset(height, 0, sizeof(int) * map_width);
for (int y = min_y; y <= max_y; ++y) {
// 更新高度数组
for (int x = min_x; x <= max_x; ++x) {
if (tmp[y * map_width + x] == 255) {
height[x] += 1;
} else {
height[x] = 0;
}
}
// 单调栈处理
std::vector<int> stack;
// ... 单调栈算法实现
}
// 如果最大矩形面积小于阈值,退出循环
if (max_area < min_rect_area) {
break;
}
// 将最大矩形区域涂黑
for (int y = max_top; y <= max_bottom; ++y) {
for (int x = max_left; x <= max_right; ++x) {
tmp[y * map_width + x] = 0;
map_data[y * map_width + x] = 127;
}
}
}
8. 后处理
最后调用 process_gray_points 和 process_black_points 进行最终的后处理:
process_gray_points(map_data, map_width, map_height);
process_black_points(map_data, map_width, map_height, true);
process_gray_points给所有的灰色点加上黑色边界
process_black_points 去掉多余的黑边
关键特性
1. 安全性检查
- 空指针检查
- 参数有效性验证
- 内存分配失败处理
- 整数溢出保护
2. 自适应阈值
根据外接矩形面积动态调整最小矩形面积阈值:
- 小面积区域(<2000):50 × level
- 中等面积区域(<10000):100 × level
- 大面积区域(≥10000):300 × level
3. 轮廓优化
- 自动选择最大轮廓进行处理
- 轮廓边界智能处理
- 轮廓填充算法
4. 矩形检测
- 使用单调栈算法高效寻找最大内接矩形
- 迭代处理直到无法找到足够大的矩形
- 动态更新处理区域
使用示例
// 创建地图数据
uint8_t* map_data = new uint8_t[height * width];
// ... 初始化地图数据
// 调用Beautifi4函数
MapBeauti::Beautifi4(map_data, height, width, 10, 2);
// 处理完成后的地图数据可用于后续应用
注意事项
- 内存管理:函数内部会分配临时内存,确保在函数结束时正确释放
- 参数范围:
level参数建议在1-5之间,过大的值可能导致过度处理 - 地图尺寸:建议地图尺寸不要过大,以避免内存问题
- 像素值:确保输入地图的像素值符合预期(0, 127, 255等)
性能考虑
- 时间复杂度:O(n²),其中n为地图像素总数
- 空间复杂度:O(n),主要用于临时数组存储
- 主要瓶颈:轮廓检测和单调栈算法
扩展性
该函数设计具有良好的扩展性:
- 可以通过修改
level参数调整处理强度 - 可以扩展支持不同的像素值含义
- 可以集成更多的后处理算法
调用
关于beautifi函数的调用,位于 protocol/src/maps/navi_map_utils.cpp文件中的GetAPPShowMap中,在美化之后,需要特别处理扩展出来的区域,与IOT协议后需要做如下处理
-
扩展的区域与最邻近的分区ID保持一致
- 找出所有分区的边界线
- 找出期望的扩展区域轮廓。同时记录轮廓相邻的分区id
- 找出id对应的分界线
- 每个轮廓上的点判断位于分界线的哪一端从而置位
void NaviMapUtils::EraseMosaic(MapData& map_data, const MapData& seg_map_data) { auto map_param = map_data.GetMapParam(); uint8_t *map_data_ptr = map_data.GetMapDataPtr(); int neighbor[8][2] = {{-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1}}; auto GetSlamIndex = [&](int y, int x) -> int { return y * map_param.width_ + x; }; auto IsInSlamBound = [&](int slam_index) -> bool { return (slam_index >= 0 && slam_index < (map_param.height_ * map_param.width_)); }; // 一次性检测所有分区间的边界线并缓存 std::map<std::pair<uint8_t, uint8_t>, std::vector<BoundaryLine>> cached_boundary_lines; // 一次遍历检测所有边界线 // 检测水平边界线 for (int y = 0; y < map_param.height_ - 1; y++) { int segment_start = -1; uint8_t current_region1 = 0, current_region2 = 0; for (int x = 0; x < map_param.width_; x++) { int upper_idx = GetSlamIndex(y, x); int lower_idx = GetSlamIndex(y + 1, x); if (IsInSlamBound(upper_idx) && IsInSlamBound(lower_idx)) { uint8_t upper_region = map_data_ptr[upper_idx]; uint8_t lower_region = map_data_ptr[lower_idx]; // 检查是否是边界(两个像素属于不同分区且都是有效分区) if (upper_region != lower_region && upper_region >= 1 && upper_region <= 31 && lower_region >= 1 && lower_region <= 31) { if (segment_start == -1) { // 开始新的边界线段 segment_start = x; current_region1 = upper_region; current_region2 = lower_region; } else if (upper_region != current_region1 || lower_region != current_region2) { // 结束当前线段(分区对发生变化) auto key1 = std::make_pair(current_region1, current_region2); auto key2 = std::make_pair(current_region2, current_region1); BoundaryLine line(BoundaryLine::HORIZONTAL, y, segment_start, x - 1, current_region1, current_region2); cached_boundary_lines[key1].push_back(line); cached_boundary_lines[key2].push_back(line); // 开始新的线段 segment_start = x; current_region1 = upper_region; current_region2 = lower_region; } } else { if (segment_start != -1) { // 结束当前线段 auto key1 = std::make_pair(current_region1, current_region2); auto key2 = std::make_pair(current_region2, current_region1); BoundaryLine line(BoundaryLine::HORIZONTAL, y, segment_start, x - 1, current_region1, current_region2); cached_boundary_lines[key1].push_back(line); cached_boundary_lines[key2].push_back(line); segment_start = -1; } } } } // 处理行末的线段 if (segment_start != -1) { auto key1 = std::make_pair(current_region1, current_region2); auto key2 = std::make_pair(current_region2, current_region1); BoundaryLine line(BoundaryLine::HORIZONTAL, y, segment_start, map_param.width_ - 1, current_region1, current_region2); cached_boundary_lines[key1].push_back(line); cached_boundary_lines[key2].push_back(line); } } // 检测垂直边界线 for (int x = 0; x < map_param.width_ - 1; x++) { int segment_start = -1; uint8_t current_region1 = 0, current_region2 = 0; for (int y = 0; y < map_param.height_; y++) { int left_idx = GetSlamIndex(y, x); int right_idx = GetSlamIndex(y, x + 1); if (IsInSlamBound(left_idx) && IsInSlamBound(right_idx)) { uint8_t left_region = map_data_ptr[left_idx]; uint8_t right_region = map_data_ptr[right_idx]; // 检查是否是边界(两个像素属于不同分区且都是有效分区) if (left_region != right_region && left_region >= 1 && left_region <= 31 && right_region >= 1 && right_region <= 31) { if (segment_start == -1) { // 开始新的边界线段 segment_start = y; current_region1 = left_region; current_region2 = right_region; } else if (left_region != current_region1 || right_region != current_region2) { // 结束当前线段(分区对发生变化) auto key1 = std::make_pair(current_region1, current_region2); auto key2 = std::make_pair(current_region2, current_region1); BoundaryLine line(BoundaryLine::VERTICAL, x, segment_start, y - 1, current_region1, current_region2); cached_boundary_lines[key1].push_back(line); cached_boundary_lines[key2].push_back(line); // 开始新的线段 segment_start = y; current_region1 = left_region; current_region2 = right_region; } } else { if (segment_start != -1) { // 结束当前线段 auto key1 = std::make_pair(current_region1, current_region2); auto key2 = std::make_pair(current_region2, current_region1); BoundaryLine line(BoundaryLine::VERTICAL, x, segment_start, y - 1, current_region1, current_region2); cached_boundary_lines[key1].push_back(line); cached_boundary_lines[key2].push_back(line); segment_start = -1; } } } } // 处理列末的线段 if (segment_start != -1) { auto key1 = std::make_pair(current_region1, current_region2); auto key2 = std::make_pair(current_region2, current_region1); BoundaryLine line(BoundaryLine::VERTICAL, x, segment_start, map_param.height_ - 1, current_region1, current_region2); cached_boundary_lines[key1].push_back(line); cached_boundary_lines[key2].push_back(line); } } std::queue<std::pair<int, int>> queue; for (int slam_y = 0; slam_y < map_param.height_; slam_y++) { for (int slam_x = 0; slam_x < map_param.width_; slam_x++) { int offset_index = GetSlamIndex(slam_y, slam_x); if (IsInSlamBound(offset_index) && (map_data_ptr[offset_index] == 255 || map_data_ptr[offset_index] == FURNITURE_VALUE)) { int detected_value = map_data_ptr[offset_index]; std::vector<std::pair<int, int>> contours; std::queue<std::pair<int, int>> q; std::pair<int, int> root(slam_x, slam_y); contours.emplace_back(root); q.push(root); // 收集相邻的分区ID std::set<uint8_t> adjacent_regions; while (!q.empty()) { std::pair<int, int> cur = q.front(); q.pop(); for (const auto &n : neighbor) { int newX = cur.first + n[1]; int newY = cur.second + n[0]; int slam_offset_index = GetSlamIndex(newY, newX); if (IsInSlamBound(slam_offset_index)) { if (map_data_ptr[slam_offset_index] == detected_value) { std::pair<int, int> newPoint(newX, newY); contours.emplace_back(newPoint); q.push(newPoint); map_data_ptr[slam_offset_index] = detected_value == 255 ? 254 : 253; // 临时标记 } else if (map_data_ptr[slam_offset_index] >= 1 && map_data_ptr[slam_offset_index] <= 31) { adjacent_regions.insert(map_data_ptr[slam_offset_index]); } } } } // 将contours_furniture中的点合并到contours中 int white_contour_size = contours.size(); if (adjacent_regions.size() >= 1) { if (adjacent_regions.size() == 1) { // 只有一个相邻分区,直接分配 uint8_t region_id = *adjacent_regions.begin(); // 先处理普通contours for (auto p : contours) { int slam_offset_index = GetSlamIndex(p.second, p.first); if (IsInSlamBound(slam_offset_index)) { map_data_ptr[slam_offset_index] = map_data_ptr[slam_offset_index] == 253 ? region_id + 32 : region_id; } } } else { // 多个相邻分区,使用几何位置签名匹配 std::vector<BoundaryLine> boundary_lines; // 从缓存中获取所有相关的边界线 std::vector<uint8_t> regions(adjacent_regions.begin(), adjacent_regions.end()); for (size_t i = 0; i < regions.size(); i++) { for (size_t j = i + 1; j < regions.size(); j++) { auto key = std::make_pair(regions[i], regions[j]); auto it = cached_boundary_lines.find(key); if (it != cached_boundary_lines.end()) { const auto& lines = it->second; boundary_lines.insert(boundary_lines.end(), lines.begin(), lines.end()); } } } // 为每个分区选择一个代表点 std::map<uint8_t, std::pair<int, int>> region_representatives; for (uint8_t region : adjacent_regions) { // 在连通域附近寻找该分区的代表点 bool found = false; const int search_radius = 2; // 固定搜索半径为2 for (auto& contour_point : contours) { for (int dy = -search_radius; dy <= search_radius && !found; dy++) { for (int dx = -search_radius; dx <= search_radius && !found; dx++) { if (dx == 0 && dy == 0) continue; int nx = contour_point.first + dx; int ny = contour_point.second + dy; int nidx = GetSlamIndex(ny, nx); if (IsInSlamBound(nidx) && map_data_ptr[nidx] == region) { region_representatives[region] = {nx, ny}; found = true; } } } } } // 计算每个分区代表点的几何签名(相对于所有边界线的位置) std::map<uint8_t, std::vector<int>> region_signatures; for (const auto& pair : region_representatives) { uint8_t region = pair.first; auto rep_point = pair.second; std::vector<int> signature; for (const auto& line : boundary_lines) { // 计算代表点相对于边界线的位置 if (line.type == BoundaryLine::HORIZONTAL) { if (rep_point.second <= line.coordinate) { signature.push_back(-1); // 上方 } else if (rep_point.second > line.coordinate) { signature.push_back(1); // 下方 } // else { // signature.push_back(0); // 在线上 // } } else { // VERTICAL if (rep_point.first <= line.coordinate) { signature.push_back(-1); // 左方 } else if (rep_point.first > line.coordinate) { signature.push_back(1); // 右方 } // else { // signature.push_back(0); // 在线上 // } } } region_signatures[region] = signature; } // 对连通域中的每个点进行分区分配 for (auto p : contours) { int slam_offset_index = GetSlamIndex(p.second, p.first); if (!IsInSlamBound(slam_offset_index)) { continue; } if (boundary_lines.empty() || region_signatures.empty()) { // 回退到邻域投票 std::vector<int> map_id_cnt(32, 0); for (const auto &n : neighbor) { int newX = p.first + n[1]; int newY = p.second + n[0]; int neighbor_index = GetSlamIndex(newY, newX); if (IsInSlamBound(neighbor_index)) { int tmp_id = map_data_ptr[neighbor_index]; if (tmp_id >= 1 && tmp_id <= 31) { map_id_cnt[tmp_id]++; } } } uint8_t best_region = 0; int max_count = 0; for (uint8_t region : adjacent_regions) { if (map_id_cnt[region] > max_count) { max_count = map_id_cnt[region]; best_region = region; } } if (best_region >= 1 && best_region <= 31) { if(map_data_ptr[slam_offset_index] == 253) { map_data_ptr[slam_offset_index] = best_region + 32; } else { map_data_ptr[slam_offset_index] = best_region; } } } else { // 计算当前点的几何签名 std::vector<int> point_signature; for (const auto& line : boundary_lines) { if (line.type == BoundaryLine::HORIZONTAL) { if (p.second <= line.coordinate) { point_signature.push_back(-1); // 上方 } else if (p.second > line.coordinate) { point_signature.push_back(1); // 下方 } // else { // point_signature.push_back(0); // 在线上 // } } else { // VERTICAL if (p.first <= line.coordinate) { point_signature.push_back(-1); // 左方 } else if (p.first > line.coordinate) { point_signature.push_back(1); // 右方 } // else { // point_signature.push_back(0); // 在线上 // } } } // 寻找几何签名匹配的分区 uint8_t best_region = 0; int max_matches = -1; for (const auto& pair : region_signatures) { uint8_t region = pair.first; const auto& signature = pair.second; if (signature.size() != point_signature.size()) continue; int matches = 0; for (size_t i = 0; i < signature.size(); i++) { if (signature[i] == point_signature[i]) { matches++; } } if (matches > max_matches) { max_matches = matches; best_region = region; } } // 如果没有找到好的匹配,回退到邻域投票 if (best_region == 0 || max_matches < static_cast<int>(boundary_lines.size() / 2)) { std::vector<int> map_id_cnt(32, 0); for (const auto &n : neighbor) { int newX = p.first + n[1]; int newY = p.second + n[0]; int neighbor_index = GetSlamIndex(newY, newX); if (IsInSlamBound(neighbor_index)) { int tmp_id = map_data_ptr[neighbor_index]; if (tmp_id >= 1 && tmp_id <= 31) { map_id_cnt[tmp_id]++; } } } int max_count = 0; for (uint8_t region : adjacent_regions) { if (map_id_cnt[region] > max_count) { max_count = map_id_cnt[region]; best_region = region; } } } if (best_region >= 1 && best_region <= 31) { if(map_data_ptr[slam_offset_index] == 253) { map_data_ptr[slam_offset_index] = best_region + 32; } else { map_data_ptr[slam_offset_index] = best_region; } } } } } } else if (contours.size() < 400 && detected_value == 255) { for (auto p : contours) { int slam_offset_index = GetSlamIndex(p.second, p.first); if (IsInSlamBound(slam_offset_index)) { map_data_ptr[slam_offset_index] = 127; } } } } } } // 恢复临时标记 for (int slam_y = 0; slam_y < map_param.height_; slam_y++) { for (int slam_x = 0; slam_x < map_param.width_; slam_x++) { int offset_index = GetSlamIndex(slam_y, slam_x); if (IsInSlamBound(offset_index)) { if(map_data_ptr[offset_index] == 254) { map_data_ptr[offset_index] = 255; } else if(map_data_ptr[offset_index] == 253) { map_data_ptr[offset_index] = FURNITURE_VALUE; } } if (IsInSlamBound(offset_index) && map_data_ptr[offset_index] == 127) { for (int i = 0; i < 8; i++) { int tmp_y = slam_y + neighbor[i][0]; int tmp_x = slam_x + neighbor[i][1]; int tmp_offset_index = GetSlamIndex(tmp_y, tmp_x); if (IsInSlamBound(tmp_offset_index) && map_data_ptr[tmp_offset_index] > 0 && map_data_ptr[tmp_offset_index] <= 31) { map_data_ptr[tmp_offset_index] = 0; break; } } } } } } -
需要在高位置1来表示扩展区域,因为分区id为1-31,所以检测到的扩展分区应该等于32 + id