836 lines
34 KiB
Python
836 lines
34 KiB
Python
|
#!/usr/bin/env python3
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
"""
|
|||
|
仙工SMAP到华睿地图包转换器
|
|||
|
输入仙工SMAP文件,输出华睿地图包和站点映射表
|
|||
|
"""
|
|||
|
|
|||
|
import json
|
|||
|
import os
|
|||
|
import sys
|
|||
|
import argparse
|
|||
|
import zipfile
|
|||
|
from datetime import datetime
|
|||
|
from PIL import Image
|
|||
|
import numpy as np
|
|||
|
|
|||
|
class SmapToHuaruiConverter:
|
|||
|
def __init__(self):
|
|||
|
self.smap_data = None
|
|||
|
self.map_name = ""
|
|||
|
self.map_width = 0
|
|||
|
self.map_height = 0
|
|||
|
self.x_attr_min = 0
|
|||
|
self.y_attr_min = 0
|
|||
|
self.pgm_data = None
|
|||
|
self.huarui_stations = []
|
|||
|
self.station_mapping = []
|
|||
|
|
|||
|
def load_smap_file(self, smap_path):
|
|||
|
"""加载SMAP文件"""
|
|||
|
try:
|
|||
|
with open(smap_path, 'r', encoding='utf-8') as f:
|
|||
|
self.smap_data = json.load(f)
|
|||
|
print(f"SMAP文件加载成功: {smap_path}")
|
|||
|
return True
|
|||
|
except FileNotFoundError:
|
|||
|
print(f"文件不存在: {smap_path}")
|
|||
|
return False
|
|||
|
except json.JSONDecodeError as e:
|
|||
|
print(f"SMAP文件格式错误: {e}")
|
|||
|
return False
|
|||
|
except Exception as e:
|
|||
|
print(f"加载SMAP文件失败: {e}")
|
|||
|
return False
|
|||
|
|
|||
|
def set_map_parameters(self, map_name, map_width, map_height, x_attr_min, y_attr_min):
|
|||
|
"""设置地图参数"""
|
|||
|
self.map_name = map_name
|
|||
|
self.map_width = map_width
|
|||
|
self.map_height = map_height
|
|||
|
self.x_attr_min = x_attr_min
|
|||
|
self.y_attr_min = y_attr_min
|
|||
|
print(f"地图参数设置完成: {map_name} ({map_width}x{map_height})")
|
|||
|
|
|||
|
def generate_pgm_from_pointcloud(self):
|
|||
|
"""从点云数据生成PGM地图,参照map_generator.html的方法"""
|
|||
|
if not self.smap_data or 'normalPosList' not in self.smap_data:
|
|||
|
print("SMAP数据中未找到normalPosList")
|
|||
|
return False
|
|||
|
|
|||
|
normal_pos_list = self.smap_data['normalPosList']
|
|||
|
if not normal_pos_list:
|
|||
|
print("normalPosList为空")
|
|||
|
return False
|
|||
|
|
|||
|
print(f"开始生成PGM地图,点云数量: {len(normal_pos_list)}")
|
|||
|
|
|||
|
# 过滤和验证点云数据
|
|||
|
valid_points = []
|
|||
|
invalid_count = 0
|
|||
|
|
|||
|
for i, point in enumerate(normal_pos_list):
|
|||
|
# 检查点是否有必要的坐标字段
|
|||
|
if isinstance(point, dict) and 'x' in point and 'y' in point:
|
|||
|
# 检查坐标是否为有效数字
|
|||
|
try:
|
|||
|
x = float(point['x'])
|
|||
|
y = float(point['y'])
|
|||
|
valid_points.append({'x': x, 'y': y})
|
|||
|
except (ValueError, TypeError):
|
|||
|
invalid_count += 1
|
|||
|
if invalid_count <= 5: # 只显示前5个错误
|
|||
|
print(f"点 {i} 坐标无效: {point}")
|
|||
|
else:
|
|||
|
invalid_count += 1
|
|||
|
if invalid_count <= 5: # 只显示前5个错误
|
|||
|
print(f"点 {i} 格式无效: {point}")
|
|||
|
|
|||
|
if invalid_count > 0:
|
|||
|
print(f"发现 {invalid_count} 个无效点,已过滤掉")
|
|||
|
|
|||
|
if not valid_points:
|
|||
|
print("没有有效的点云数据")
|
|||
|
return False
|
|||
|
|
|||
|
print(f"有效点云数量: {len(valid_points)}")
|
|||
|
|
|||
|
# 计算点云范围
|
|||
|
xs = [p['x'] for p in valid_points]
|
|||
|
ys = [p['y'] for p in valid_points]
|
|||
|
min_x, max_x = min(xs), max(xs)
|
|||
|
min_y, max_y = min(ys), max(ys)
|
|||
|
range_x = max_x - min_x
|
|||
|
range_y = max_y - min_y
|
|||
|
|
|||
|
print(f"点云范围: X[{min_x:.2f}, {max_x:.2f}], Y[{min_y:.2f}, {max_y:.2f}]")
|
|||
|
|
|||
|
# 设置分辨率和图像尺寸(参照map_generator.html的方法)
|
|||
|
resolution = 0.02 # 2cm/pixel
|
|||
|
width = max(512, int(np.ceil(range_x / resolution)))
|
|||
|
height = max(512, int(np.ceil(range_y / resolution)))
|
|||
|
|
|||
|
print(f"PGM尺寸: {width}x{height}, 分辨率: {resolution}m/pixel")
|
|||
|
|
|||
|
# 创建图像数据,初始化为255(自由空间)
|
|||
|
image_data = np.full(width * height, 255, dtype=np.uint8)
|
|||
|
|
|||
|
# 将点云数据映射到图像(参照map_generator.html的逻辑)
|
|||
|
for point in valid_points:
|
|||
|
pixel_x = int(np.floor((point['x'] - min_x) / resolution))
|
|||
|
pixel_y = int(np.floor((point['y'] - min_y) / resolution))
|
|||
|
|
|||
|
if 0 <= pixel_x < width and 0 <= pixel_y < height:
|
|||
|
# Y轴翻转
|
|||
|
index = (height - 1 - pixel_y) * width + pixel_x
|
|||
|
image_data[index] = 255 # 自由空间
|
|||
|
|
|||
|
# 在周围添加一些障碍物用于测试(参照map_generator.html)
|
|||
|
radius = 2 # 模拟障碍物半径
|
|||
|
for dx in range(-radius, radius + 1):
|
|||
|
for dy in range(-radius, radius + 1):
|
|||
|
nx = pixel_x + dx
|
|||
|
ny = pixel_y + dy
|
|||
|
if 0 <= nx < width and 0 <= ny < height:
|
|||
|
n_index = (height - 1 - ny) * width + nx
|
|||
|
# 只在边界处添加障碍物
|
|||
|
if dx == -radius or dx == radius or dy == -radius or dy == radius:
|
|||
|
if np.random.random() < 0.1: # 10%的概率添加障碍物
|
|||
|
image_data[n_index] = 0 # 障碍物
|
|||
|
|
|||
|
# 保存PGM数据
|
|||
|
self.pgm_data = {
|
|||
|
'width': width,
|
|||
|
'height': height,
|
|||
|
'data': image_data,
|
|||
|
'resolution': resolution,
|
|||
|
'origin_x': min_x,
|
|||
|
'origin_y': min_y
|
|||
|
}
|
|||
|
|
|||
|
# 统计像素值分布
|
|||
|
unique, counts = np.unique(image_data, return_counts=True)
|
|||
|
pixel_stats = dict(zip(unique, counts))
|
|||
|
stats_str = ', '.join([f'{"自由空间" if k == 255 else "障碍物" if k == 0 else str(k)}:{v}' for k, v in pixel_stats.items()])
|
|||
|
print(f"像素统计: {stats_str}")
|
|||
|
|
|||
|
print("PGM地图生成完成")
|
|||
|
return True
|
|||
|
|
|||
|
def generate_huarui_stations(self):
|
|||
|
"""生成华睿站点数据"""
|
|||
|
if not self.smap_data or 'advancedPointList' not in self.smap_data:
|
|||
|
print("SMAP数据中未找到advancedPointList")
|
|||
|
return False
|
|||
|
|
|||
|
advanced_point_list = self.smap_data['advancedPointList']
|
|||
|
if not advanced_point_list:
|
|||
|
print("advancedPointList为空")
|
|||
|
return False
|
|||
|
|
|||
|
print(f"开始生成华睿站点,仙工站点数量: {len(advanced_point_list)}")
|
|||
|
|
|||
|
# 类型映射
|
|||
|
type_mapping = {
|
|||
|
'ActionPoint': {'type': 1, 'prefix': '01', 'name': '动作站点'},
|
|||
|
'LocationMark': {'type': 0, 'prefix': '02', 'name': '路径站点'},
|
|||
|
'ParkPoint': {'type': 7, 'prefix': '03', 'name': '停靠站点'},
|
|||
|
'ChargePoint': {'type': 6, 'prefix': '04', 'name': '充电站点'}
|
|||
|
}
|
|||
|
|
|||
|
self.huarui_stations = []
|
|||
|
self.station_mapping = []
|
|||
|
station_counter = 1
|
|||
|
|
|||
|
for smap_station in advanced_point_list:
|
|||
|
station_info = type_mapping.get(smap_station['className'], {'type': 0, 'prefix': '02', 'name': '普通站点'})
|
|||
|
station_id = f"{station_info['prefix']}{str(station_counter).zfill(6)}"
|
|||
|
|
|||
|
# 坐标转换(米转毫米)
|
|||
|
x = round(smap_station['pos']['x'] * 1000)
|
|||
|
y = round(smap_station['pos']['y'] * 1000)
|
|||
|
|
|||
|
# 创建华睿站点
|
|||
|
huarui_station = {
|
|||
|
"content": station_id,
|
|||
|
"coordinate": {"x": x, "y": y},
|
|||
|
"name": station_id,
|
|||
|
"type": station_info['type'],
|
|||
|
"extraTypes": [],
|
|||
|
"isTurn": 0,
|
|||
|
"shelfModel": 1,
|
|||
|
"evadeNode": 1,
|
|||
|
"shelfIsTurn": 1,
|
|||
|
"isHandoverArea": 0,
|
|||
|
"isVirtualPoint": False,
|
|||
|
"isTurnRotatableRange": [0, 360000],
|
|||
|
"QRCodeAngle": 0,
|
|||
|
"isNavigationMarkPoint": 0,
|
|||
|
"contentEdges": [],
|
|||
|
"isTextureInitPoint": True,
|
|||
|
"navigationMarkPoint": -1,
|
|||
|
"navTypeV3": ["QRCode", "Texture", "Laser", "Mileage"],
|
|||
|
"isIgnorePayload": False,
|
|||
|
"isIgnoreAGV": False,
|
|||
|
"machineAngle": 0,
|
|||
|
"slopeDistance": [1000, 1000, 1000, 1000],
|
|||
|
"actionObstacleType": [],
|
|||
|
"obstacleType": [1, 1, 1, 1, 2, 8] if station_info['type'] == 1 else [0, 0, 0, 0, 2, 8],
|
|||
|
"slope": [0, 0, 0, 0],
|
|||
|
"shelfModelAndAngles": [{
|
|||
|
"model": 2 if station_info['type'] == 1 else 1,
|
|||
|
"angle": [999000],
|
|||
|
"forkToothRadio": "front",
|
|||
|
"forkToothFrontToStackDistance": 0,
|
|||
|
"forkToothBackToStackDistance": 999
|
|||
|
}],
|
|||
|
"movPrecExpand": True,
|
|||
|
"areaType": 1,
|
|||
|
"driftSet": {"x": 0, "y": 0},
|
|||
|
"identifyType": [0],
|
|||
|
"pickupRelease": 0,
|
|||
|
"extraRotExpansion": True,
|
|||
|
"palletAlignment": "center",
|
|||
|
"palletAlignmentType": 0,
|
|||
|
"custom": "",
|
|||
|
"navSignType": "QRCode",
|
|||
|
"highlightDetection": False,
|
|||
|
"AvoidObstaclesDeceleration": [1000, 1000, 1000, 1000],
|
|||
|
"AvoidObstaclesEmergencyStop": [50, 50, 50, 50],
|
|||
|
"isNavSignPoint": False,
|
|||
|
"shelfMode": 2 if station_info['type'] == 1 else 1,
|
|||
|
"id": station_id,
|
|||
|
"code": "1" if station_info['type'] == 1 else "0",
|
|||
|
"desp": 0,
|
|||
|
"shelfAngle": [999000],
|
|||
|
"SafetyDistance": [[3000, 1000], [3000, 1000], [3000, 1000], [3000, 1000]],
|
|||
|
"SafetyShape": [["Rectage"], ["Rectage"], ["Rectage"], ["Rectage"]],
|
|||
|
"barrierSwitch": 1,
|
|||
|
"edges": []
|
|||
|
}
|
|||
|
|
|||
|
self.huarui_stations.append(huarui_station)
|
|||
|
|
|||
|
# 创建映射关系
|
|||
|
mapping = {
|
|||
|
'smap_name': smap_station['instanceName'],
|
|||
|
'smap_type': smap_station['className'],
|
|||
|
'smap_type_desc': station_info['name'],
|
|||
|
'huarui_id': station_id,
|
|||
|
'huarui_type': station_info['type'],
|
|||
|
'coordinate': {'x': x, 'y': y}
|
|||
|
}
|
|||
|
self.station_mapping.append(mapping)
|
|||
|
|
|||
|
station_counter += 1
|
|||
|
|
|||
|
# 生成路径连接
|
|||
|
self._generate_path_connections()
|
|||
|
|
|||
|
print(f"华睿站点生成完成,共{len(self.huarui_stations)}个站点")
|
|||
|
return True
|
|||
|
|
|||
|
def _generate_path_connections(self):
|
|||
|
"""生成站点之间的路径连接"""
|
|||
|
if not self.smap_data or 'advancedCurveList' not in self.smap_data:
|
|||
|
return
|
|||
|
|
|||
|
# 创建instanceName到stationId的映射
|
|||
|
instance_to_station = {}
|
|||
|
for i, mapping in enumerate(self.station_mapping):
|
|||
|
instance_to_station[mapping['smap_name']] = mapping['huarui_id']
|
|||
|
|
|||
|
# 获取所有DegenerateBezier类型的路径
|
|||
|
paths = [curve for curve in self.smap_data['advancedCurveList']
|
|||
|
if curve.get('className') == 'DegenerateBezier']
|
|||
|
|
|||
|
print(f"处理路径连接,共{len(paths)}条路径")
|
|||
|
|
|||
|
# 为每个站点生成contentEdges
|
|||
|
for station in self.huarui_stations:
|
|||
|
station['contentEdges'] = []
|
|||
|
|
|||
|
for path in paths:
|
|||
|
if not path.get('startPos') or not path.get('endPos'):
|
|||
|
continue
|
|||
|
|
|||
|
start_name = path['startPos'].get('instanceName')
|
|||
|
end_name = path['endPos'].get('instanceName')
|
|||
|
|
|||
|
if not start_name or not end_name:
|
|||
|
continue
|
|||
|
|
|||
|
start_station_id = instance_to_station.get(start_name)
|
|||
|
end_station_id = instance_to_station.get(end_name)
|
|||
|
|
|||
|
# 如果当前站点是路径的起点,添加到终点的连接
|
|||
|
if start_station_id == station['name'] and end_station_id:
|
|||
|
target_station = next((s for s in self.huarui_stations if s['name'] == end_station_id), None)
|
|||
|
if target_station:
|
|||
|
edge = {
|
|||
|
"destination": target_station['content'],
|
|||
|
"leftWidth": -1,
|
|||
|
"rightWidth": -1,
|
|||
|
"startExpandDistance": -1,
|
|||
|
"endExpandDistance": -1,
|
|||
|
"needFollow": 1,
|
|||
|
"weight": 1,
|
|||
|
"avoidScene": 1,
|
|||
|
"aspect": "3",
|
|||
|
"turningMode": 0,
|
|||
|
"freePlan": 0,
|
|||
|
"edgeTurnAble": 1,
|
|||
|
"speed": 2000,
|
|||
|
"tangentAngle": 999000,
|
|||
|
"outTangentAngle": 999000,
|
|||
|
"navMode": 0,
|
|||
|
"trackId": 0,
|
|||
|
"bypass": False,
|
|||
|
"pointAccuracy": -1,
|
|||
|
"angleAccuracy": -1000,
|
|||
|
"orientObject": "",
|
|||
|
"deviceTravelDirection": "unlimited",
|
|||
|
"retrograde": False,
|
|||
|
"ignoreSensor": [],
|
|||
|
"shelfIsPass": 1,
|
|||
|
"trafficAMR": [],
|
|||
|
"trafficType": 0,
|
|||
|
"trafficLoad": [],
|
|||
|
"extraLockExpansion": True,
|
|||
|
"headstock": "withLine",
|
|||
|
"unloadReachMode": 0,
|
|||
|
"loadReachMode": 0,
|
|||
|
"useSingleLaserLocate": False,
|
|||
|
"custom": "",
|
|||
|
"loadPass": 0
|
|||
|
}
|
|||
|
|
|||
|
# 添加DegenerateBezier的controlPoints支持
|
|||
|
if path.get('controlPos1') and path.get('controlPos2'):
|
|||
|
# 转换控制点坐标(米转毫米)
|
|||
|
control_points = [
|
|||
|
[
|
|||
|
round(path['controlPos1']['x'] * 1000),
|
|||
|
round(path['controlPos1']['y'] * 1000)
|
|||
|
],
|
|||
|
[
|
|||
|
round(path['controlPos2']['x'] * 1000),
|
|||
|
round(path['controlPos2']['y'] * 1000)
|
|||
|
]
|
|||
|
]
|
|||
|
edge["controlPoints"] = control_points
|
|||
|
|
|||
|
station['contentEdges'].append(edge)
|
|||
|
|
|||
|
# 如果当前站点是路径的终点,添加到起点的连接(双向路径)
|
|||
|
if end_station_id == station['name'] and start_station_id:
|
|||
|
target_station = next((s for s in self.huarui_stations if s['name'] == start_station_id), None)
|
|||
|
if target_station:
|
|||
|
edge = {
|
|||
|
"destination": target_station['content'],
|
|||
|
"leftWidth": -1,
|
|||
|
"rightWidth": -1,
|
|||
|
"startExpandDistance": -1,
|
|||
|
"endExpandDistance": -1,
|
|||
|
"needFollow": 1,
|
|||
|
"weight": 1,
|
|||
|
"avoidScene": 1,
|
|||
|
"aspect": "3",
|
|||
|
"turningMode": 0,
|
|||
|
"freePlan": 0,
|
|||
|
"edgeTurnAble": 1,
|
|||
|
"speed": 2000,
|
|||
|
"tangentAngle": 999000,
|
|||
|
"outTangentAngle": 999000,
|
|||
|
"navMode": 0,
|
|||
|
"trackId": 0,
|
|||
|
"bypass": False,
|
|||
|
"pointAccuracy": -1,
|
|||
|
"angleAccuracy": -1000,
|
|||
|
"orientObject": "",
|
|||
|
"deviceTravelDirection": "unlimited",
|
|||
|
"retrograde": False,
|
|||
|
"ignoreSensor": [],
|
|||
|
"shelfIsPass": 1,
|
|||
|
"trafficAMR": [],
|
|||
|
"trafficType": 0,
|
|||
|
"trafficLoad": [],
|
|||
|
"extraLockExpansion": True,
|
|||
|
"headstock": "withLine",
|
|||
|
"unloadReachMode": 0,
|
|||
|
"loadReachMode": 0,
|
|||
|
"useSingleLaserLocate": False,
|
|||
|
"custom": "",
|
|||
|
"loadPass": 0
|
|||
|
}
|
|||
|
|
|||
|
# 添加DegenerateBezier的controlPoints支持(反向路径)
|
|||
|
if path.get('controlPos1') and path.get('controlPos2'):
|
|||
|
# 反向路径的控制点顺序相反
|
|||
|
control_points = [
|
|||
|
[
|
|||
|
round(path['controlPos2']['x'] * 1000),
|
|||
|
round(path['controlPos2']['y'] * 1000)
|
|||
|
],
|
|||
|
[
|
|||
|
round(path['controlPos1']['x'] * 1000),
|
|||
|
round(path['controlPos1']['y'] * 1000)
|
|||
|
]
|
|||
|
]
|
|||
|
edge["controlPoints"] = control_points
|
|||
|
|
|||
|
station['contentEdges'].append(edge)
|
|||
|
|
|||
|
# 去重contentEdges
|
|||
|
unique_edges = []
|
|||
|
seen_destinations = set()
|
|||
|
for edge in station['contentEdges']:
|
|||
|
if edge['destination'] not in seen_destinations:
|
|||
|
seen_destinations.add(edge['destination'])
|
|||
|
unique_edges.append(edge)
|
|||
|
station['contentEdges'] = unique_edges
|
|||
|
|
|||
|
# 生成edges字段
|
|||
|
station['edges'] = []
|
|||
|
for edge in station['contentEdges']:
|
|||
|
station['edges'].append({
|
|||
|
"destination": edge['destination'],
|
|||
|
"weight": edge['weight']
|
|||
|
})
|
|||
|
|
|||
|
def generate_topo_json(self):
|
|||
|
"""生成topo.json文件"""
|
|||
|
topo = {
|
|||
|
"map": {
|
|||
|
"name": self.map_name,
|
|||
|
"type": "topo",
|
|||
|
"width": self.map_width,
|
|||
|
"height": self.map_height,
|
|||
|
"xAttrMin": self.x_attr_min,
|
|||
|
"yAttrMin": self.y_attr_min,
|
|||
|
"initSpeed": 2,
|
|||
|
"obstacleType": [0, 0, 0, 0, 7, 8],
|
|||
|
"actionObstacleType": [],
|
|||
|
"confidence": [],
|
|||
|
"yawThreshold": [],
|
|||
|
"forbiddenArea": [],
|
|||
|
"operableArea": [],
|
|||
|
"freePlan": 0,
|
|||
|
"turningMode": -1,
|
|||
|
"margin": 0,
|
|||
|
"rcsImageName": "",
|
|||
|
"relocationImageName": "",
|
|||
|
"shelfModel": 0,
|
|||
|
"version": "3.0",
|
|||
|
"navType": "qrcode",
|
|||
|
"devicePartition": None,
|
|||
|
"saveDistance": 1000,
|
|||
|
"brakeDistance": 1000,
|
|||
|
"initSimple": "Rectage",
|
|||
|
"timeStamp": int(datetime.now().timestamp() * 1000),
|
|||
|
"times": 34,
|
|||
|
"icsImageName": "",
|
|||
|
"widthFor-1": 800,
|
|||
|
"expandFor-1": 100,
|
|||
|
"mp_widthFor-1": 800,
|
|||
|
"mp_expandFor-1": 0,
|
|||
|
"mutiple_pallet_generate": True
|
|||
|
},
|
|||
|
"nodes": self.huarui_stations,
|
|||
|
"attrArea": [],
|
|||
|
"relatingAreas": [],
|
|||
|
"virtualPoint": [],
|
|||
|
"guidelineConfigs": [
|
|||
|
{
|
|||
|
"lineId": 1,
|
|||
|
"sensor": "camera",
|
|||
|
"direction": 0,
|
|||
|
"width": 200,
|
|||
|
"line": [1000, 0],
|
|||
|
"offset": 150
|
|||
|
}
|
|||
|
]
|
|||
|
}
|
|||
|
return json.dumps(topo, indent=2, ensure_ascii=False)
|
|||
|
|
|||
|
def generate_yaml_config(self):
|
|||
|
"""生成YAML配置文件"""
|
|||
|
if not self.pgm_data:
|
|||
|
return ""
|
|||
|
|
|||
|
yaml_content = f"""image: {self.map_name}.pgm
|
|||
|
resolution: {self.pgm_data['resolution']:.6f}
|
|||
|
origin: [{self.pgm_data['origin_x']:.6f}, {self.pgm_data['origin_y']:.6f}, 0.000000]
|
|||
|
negate: 0
|
|||
|
occupied_thresh: 0.65
|
|||
|
free_thresh: 0.196
|
|||
|
width: {self.pgm_data['width']}
|
|||
|
height: {self.pgm_data['height']}
|
|||
|
intensity_threshold: 1000.000000
|
|||
|
intensities:
|
|||
|
"""
|
|||
|
return yaml_content
|
|||
|
|
|||
|
def generate_pgm_file(self):
|
|||
|
"""生成PGM文件内容"""
|
|||
|
if not self.pgm_data:
|
|||
|
return None
|
|||
|
|
|||
|
header = f"P5\n# Generated by SMAP to Huarui Converter\n{self.pgm_data['width']} {self.pgm_data['height']}\n255\n"
|
|||
|
header_bytes = header.encode('ascii')
|
|||
|
|
|||
|
# 确保数据格式正确
|
|||
|
image_array = np.array(self.pgm_data['data'], dtype=np.uint8)
|
|||
|
|
|||
|
pgm_content = header_bytes + image_array.tobytes()
|
|||
|
return pgm_content
|
|||
|
|
|||
|
def generate_background_jpg(self):
|
|||
|
"""生成background.jpg文件,参照map_generator.html的转换逻辑"""
|
|||
|
if not self.pgm_data:
|
|||
|
return None
|
|||
|
|
|||
|
# 获取PGM数据
|
|||
|
width = self.pgm_data['width']
|
|||
|
height = self.pgm_data['height']
|
|||
|
pgm_array = np.array(self.pgm_data['data'], dtype=np.uint8)
|
|||
|
|
|||
|
# 创建RGB图像数据
|
|||
|
rgb_data = np.zeros((height, width, 3), dtype=np.uint8)
|
|||
|
|
|||
|
# 按照map_generator.html的逻辑转换:
|
|||
|
# 255 (自由空间) -> 白色 (255, 255, 255)
|
|||
|
# 0 (障碍物) -> 黑色 (0, 0, 0)
|
|||
|
for i in range(len(pgm_array)):
|
|||
|
y = i // width
|
|||
|
x = i % width
|
|||
|
value = pgm_array[i]
|
|||
|
|
|||
|
if value == 255:
|
|||
|
# 自由空间 -> 白色
|
|||
|
rgb_data[y, x] = [255, 255, 255]
|
|||
|
else:
|
|||
|
# 障碍物 -> 黑色
|
|||
|
rgb_data[y, x] = [0, 0, 0]
|
|||
|
|
|||
|
# 创建PIL图像
|
|||
|
image = Image.fromarray(rgb_data, mode='RGB')
|
|||
|
|
|||
|
return image
|
|||
|
|
|||
|
def generate_station_mapping_sql(self):
|
|||
|
"""生成站点映射SQL文件"""
|
|||
|
# 生成SQL内容
|
|||
|
sql_content = []
|
|||
|
|
|||
|
# 添加注释说明
|
|||
|
sql_content.append("-- 仙工SMAP到华睿站点映射表")
|
|||
|
sql_content.append(f"-- 地图名称: {self.map_name}")
|
|||
|
sql_content.append(f"-- 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|||
|
sql_content.append(f"-- 总站点数: {len(self.station_mapping)}")
|
|||
|
sql_content.append("")
|
|||
|
|
|||
|
# 创建站点映射表
|
|||
|
sql_content.append("-- 创建站点映射表")
|
|||
|
sql_content.append("CREATE TABLE IF NOT EXISTS station_mapping (")
|
|||
|
sql_content.append(" Sequence INT PRIMARY KEY,")
|
|||
|
sql_content.append(" SeerNodeId VARCHAR(255) NOT NULL,")
|
|||
|
sql_content.append(" HuaruiNodeId VARCHAR(255) NOT NULL,")
|
|||
|
sql_content.append(" pot_x INT NOT NULL,")
|
|||
|
sql_content.append(" pot_y INT NOT NULL")
|
|||
|
sql_content.append(");")
|
|||
|
sql_content.append("")
|
|||
|
|
|||
|
# 创建路径映射表
|
|||
|
sql_content.append("-- 创建路径映射表(包含贝塞尔曲线控制点)")
|
|||
|
sql_content.append("CREATE TABLE IF NOT EXISTS path_mapping (")
|
|||
|
sql_content.append(" Sequence INT PRIMARY KEY,")
|
|||
|
sql_content.append(" SourceSeerNodeId VARCHAR(255) NOT NULL,")
|
|||
|
sql_content.append(" SourceHuaruiNodeId VARCHAR(255) NOT NULL,")
|
|||
|
sql_content.append(" SourceNodeX INT NOT NULL,")
|
|||
|
sql_content.append(" SourceNodeY INT NOT NULL,")
|
|||
|
sql_content.append(" TargetSeerNodeId VARCHAR(255) NOT NULL,")
|
|||
|
sql_content.append(" TargetHuaruiNodeId VARCHAR(255) NOT NULL,")
|
|||
|
sql_content.append(" TargetNodeX INT NOT NULL,")
|
|||
|
sql_content.append(" TargetNodeY INT NOT NULL,")
|
|||
|
sql_content.append(" ControlPoint1X INT DEFAULT NULL,")
|
|||
|
sql_content.append(" ControlPoint1Y INT DEFAULT NULL,")
|
|||
|
sql_content.append(" ControlPoint2X INT DEFAULT NULL,")
|
|||
|
sql_content.append(" ControlPoint2Y INT DEFAULT NULL")
|
|||
|
sql_content.append(");")
|
|||
|
sql_content.append("")
|
|||
|
|
|||
|
# 清空表数据
|
|||
|
sql_content.append("-- 清空现有数据")
|
|||
|
sql_content.append("DELETE FROM station_mapping;")
|
|||
|
sql_content.append("DELETE FROM path_mapping;")
|
|||
|
sql_content.append("")
|
|||
|
|
|||
|
# 插入站点映射数据
|
|||
|
sql_content.append("-- 插入站点映射数据")
|
|||
|
for i, mapping in enumerate(self.station_mapping, 1):
|
|||
|
insert_sql = (
|
|||
|
f"INSERT INTO station_mapping (Sequence, SeerNodeId, HuaruiNodeId, pot_x, pot_y) "
|
|||
|
f"VALUES ({i}, '{mapping['smap_name']}', '{mapping['huarui_id']}', "
|
|||
|
f"{mapping['coordinate']['x']}, {mapping['coordinate']['y']});"
|
|||
|
)
|
|||
|
sql_content.append(insert_sql)
|
|||
|
|
|||
|
sql_content.append("")
|
|||
|
|
|||
|
# 插入路径映射数据
|
|||
|
sql_content.append("-- 插入路径映射数据")
|
|||
|
path_sequence = 1
|
|||
|
|
|||
|
# 创建名称到ID的映射
|
|||
|
seer_name_to_id = {mapping['smap_name']: mapping['smap_name'] for mapping in self.station_mapping}
|
|||
|
huarui_id_to_name = {mapping['huarui_id']: mapping['huarui_id'] for mapping in self.station_mapping}
|
|||
|
|
|||
|
# 创建站点名称到坐标的映射
|
|||
|
station_coords = {}
|
|||
|
for mapping in self.station_mapping:
|
|||
|
station_coords[mapping['huarui_id']] = mapping['coordinate']
|
|||
|
|
|||
|
# 获取原始路径数据
|
|||
|
if self.smap_data and 'advancedCurveList' in self.smap_data:
|
|||
|
paths = [curve for curve in self.smap_data['advancedCurveList']
|
|||
|
if curve.get('className') == 'DegenerateBezier']
|
|||
|
|
|||
|
# 创建instanceName到华睿ID的映射
|
|||
|
instance_to_huarui = {}
|
|||
|
for mapping in self.station_mapping:
|
|||
|
instance_to_huarui[mapping['smap_name']] = mapping['huarui_id']
|
|||
|
|
|||
|
for path in paths:
|
|||
|
if not path.get('startPos') or not path.get('endPos'):
|
|||
|
continue
|
|||
|
|
|||
|
start_name = path['startPos'].get('instanceName')
|
|||
|
end_name = path['endPos'].get('instanceName')
|
|||
|
|
|||
|
if not start_name or not end_name:
|
|||
|
continue
|
|||
|
|
|||
|
start_huarui_id = instance_to_huarui.get(start_name)
|
|||
|
end_huarui_id = instance_to_huarui.get(end_name)
|
|||
|
|
|||
|
if not start_huarui_id or not end_huarui_id:
|
|||
|
continue
|
|||
|
|
|||
|
# 获取坐标
|
|||
|
start_coord = station_coords.get(start_huarui_id, {'x': 0, 'y': 0})
|
|||
|
end_coord = station_coords.get(end_huarui_id, {'x': 0, 'y': 0})
|
|||
|
|
|||
|
# 获取控制点
|
|||
|
control1_x = None
|
|||
|
control1_y = None
|
|||
|
control2_x = None
|
|||
|
control2_y = None
|
|||
|
|
|||
|
if path.get('controlPos1'):
|
|||
|
control1_x = round(path['controlPos1']['x'] * 1000)
|
|||
|
control1_y = round(path['controlPos1']['y'] * 1000)
|
|||
|
|
|||
|
if path.get('controlPos2'):
|
|||
|
control2_x = round(path['controlPos2']['x'] * 1000)
|
|||
|
control2_y = round(path['controlPos2']['y'] * 1000)
|
|||
|
|
|||
|
# 插入路径数据
|
|||
|
control1_x_str = str(control1_x) if control1_x is not None else "NULL"
|
|||
|
control1_y_str = str(control1_y) if control1_y is not None else "NULL"
|
|||
|
control2_x_str = str(control2_x) if control2_x is not None else "NULL"
|
|||
|
control2_y_str = str(control2_y) if control2_y is not None else "NULL"
|
|||
|
|
|||
|
insert_sql = (
|
|||
|
f"INSERT INTO path_mapping (Sequence, SourceSeerNodeId, SourceHuaruiNodeId, "
|
|||
|
f"SourceNodeX, SourceNodeY, TargetSeerNodeId, TargetHuaruiNodeId, "
|
|||
|
f"TargetNodeX, TargetNodeY, ControlPoint1X, ControlPoint1Y, "
|
|||
|
f"ControlPoint2X, ControlPoint2Y) "
|
|||
|
f"VALUES ({path_sequence}, '{start_name}', '{start_huarui_id}', "
|
|||
|
f"{start_coord['x']}, {start_coord['y']}, '{end_name}', '{end_huarui_id}', "
|
|||
|
f"{end_coord['x']}, {end_coord['y']}, {control1_x_str}, {control1_y_str}, "
|
|||
|
f"{control2_x_str}, {control2_y_str});"
|
|||
|
)
|
|||
|
sql_content.append(insert_sql)
|
|||
|
path_sequence += 1
|
|||
|
|
|||
|
sql_content.append("")
|
|||
|
sql_content.append("-- 查询所有映射数据")
|
|||
|
sql_content.append("SELECT * FROM station_mapping ORDER BY Sequence;")
|
|||
|
sql_content.append("")
|
|||
|
sql_content.append("-- 查询所有路径数据")
|
|||
|
sql_content.append("SELECT * FROM path_mapping ORDER BY Sequence;")
|
|||
|
|
|||
|
return '\n'.join(sql_content)
|
|||
|
|
|||
|
def create_map_package(self, output_dir):
|
|||
|
"""创建华睿地图包"""
|
|||
|
if not os.path.exists(output_dir):
|
|||
|
os.makedirs(output_dir)
|
|||
|
|
|||
|
print(f"开始创建华睿地图包到目录: {output_dir}")
|
|||
|
|
|||
|
# 创建ZIP文件
|
|||
|
zip_path = os.path.join(output_dir, f"{self.map_name}.zip")
|
|||
|
|
|||
|
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|||
|
# 1. topo.json
|
|||
|
topo_content = self.generate_topo_json()
|
|||
|
zipf.writestr("topo.json", topo_content)
|
|||
|
print("topo.json")
|
|||
|
|
|||
|
# 2. PGM文件
|
|||
|
pgm_content = self.generate_pgm_file()
|
|||
|
if pgm_content:
|
|||
|
zipf.writestr(f"{self.map_name}.pgm", pgm_content)
|
|||
|
print(f"{self.map_name}.pgm")
|
|||
|
|
|||
|
# 3. YAML配置文件
|
|||
|
yaml_content = self.generate_yaml_config()
|
|||
|
if yaml_content:
|
|||
|
zipf.writestr(f"{self.map_name}.yaml", yaml_content)
|
|||
|
print(f"{self.map_name}.yaml")
|
|||
|
|
|||
|
# 4. background.jpg
|
|||
|
bg_image = self.generate_background_jpg()
|
|||
|
if bg_image:
|
|||
|
import io
|
|||
|
img_buffer = io.BytesIO()
|
|||
|
bg_image.save(img_buffer, format='JPEG', quality=95)
|
|||
|
zipf.writestr("background.jpg", img_buffer.getvalue())
|
|||
|
print("background.jpg")
|
|||
|
|
|||
|
# 5. dbVersion.txt
|
|||
|
db_version = datetime.now().strftime('%Y%m%d%H%M')
|
|||
|
zipf.writestr("dbVersion.txt", db_version)
|
|||
|
print("dbVersion.txt")
|
|||
|
|
|||
|
print(f"华睿地图包创建完成: {zip_path}")
|
|||
|
return zip_path
|
|||
|
|
|||
|
def create_station_mapping_sql(self, output_dir):
|
|||
|
"""创建站点映射SQL文件"""
|
|||
|
if not os.path.exists(output_dir):
|
|||
|
os.makedirs(output_dir)
|
|||
|
|
|||
|
sql_content = self.generate_station_mapping_sql()
|
|||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|||
|
sql_path = os.path.join(output_dir, f"{self.map_name}_StationMapping_{timestamp}.sql")
|
|||
|
|
|||
|
with open(sql_path, 'w', encoding='utf-8') as f:
|
|||
|
f.write(sql_content)
|
|||
|
|
|||
|
print(f"站点映射表创建完成: {sql_path}")
|
|||
|
return sql_path
|
|||
|
|
|||
|
def convert(self, smap_path, map_name, map_width, map_height, x_attr_min, y_attr_min, output_dir="output"):
|
|||
|
"""执行完整的转换流程"""
|
|||
|
print("开始SMAP到华睿地图包转换")
|
|||
|
print("=" * 60)
|
|||
|
|
|||
|
# 1. 加载SMAP文件
|
|||
|
if not self.load_smap_file(smap_path):
|
|||
|
return False
|
|||
|
|
|||
|
# 2. 设置地图参数
|
|||
|
self.set_map_parameters(map_name, map_width, map_height, x_attr_min, y_attr_min)
|
|||
|
|
|||
|
# 3. 生成PGM地图
|
|||
|
if not self.generate_pgm_from_pointcloud():
|
|||
|
return False
|
|||
|
|
|||
|
# 4. 生成华睿站点
|
|||
|
if not self.generate_huarui_stations():
|
|||
|
return False
|
|||
|
|
|||
|
# 5. 创建华睿地图包
|
|||
|
map_package_path = self.create_map_package(output_dir)
|
|||
|
|
|||
|
# 6. 创建站点映射表(SQL格式)
|
|||
|
mapping_sql_path = self.create_station_mapping_sql(output_dir)
|
|||
|
|
|||
|
print("=" * 60)
|
|||
|
print("转换完成!")
|
|||
|
print(f"华睿地图包: {map_package_path}")
|
|||
|
print(f"站点映射表: {mapping_sql_path}")
|
|||
|
|
|||
|
return True
|
|||
|
|
|||
|
def main():
|
|||
|
parser = argparse.ArgumentParser(description='仙工SMAP到华睿地图包转换器')
|
|||
|
parser.add_argument('smap_path', help='仙工SMAP文件路径')
|
|||
|
parser.add_argument('map_name', help='地图名称')
|
|||
|
parser.add_argument('map_width', type=int, help='地图宽度(mm)')
|
|||
|
parser.add_argument('map_height', type=int, help='地图高度(mm)')
|
|||
|
parser.add_argument('x_attr_min', type=int, help='X最小值(mm)')
|
|||
|
parser.add_argument('y_attr_min', type=int, help='Y最小值(mm)')
|
|||
|
parser.add_argument('-o', '--output', default='output', help='输出目录(默认: output)')
|
|||
|
|
|||
|
args = parser.parse_args()
|
|||
|
|
|||
|
# 检查SMAP文件是否存在
|
|||
|
if not os.path.exists(args.smap_path):
|
|||
|
print(f"SMAP文件不存在: {args.smap_path}")
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
# 创建转换器并执行转换
|
|||
|
converter = SmapToHuaruiConverter()
|
|||
|
success = converter.convert(
|
|||
|
args.smap_path,
|
|||
|
args.map_name,
|
|||
|
args.map_width,
|
|||
|
args.map_height,
|
|||
|
args.x_attr_min,
|
|||
|
args.y_attr_min,
|
|||
|
args.output
|
|||
|
)
|
|||
|
|
|||
|
if success:
|
|||
|
print("\n 转换成功完成!")
|
|||
|
sys.exit(0)
|
|||
|
else:
|
|||
|
print("\n 转换失败!")
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
main()
|