# 实体对象智能识别
# 概述
实体对象智能识别能够在CAD图纸中智能识别和匹配相似的实体对象。该系统采用模式匹配算法,支持几何变换(缩放、旋转),并提供了丰富的配置选项和可视化界面。
系统提供两种主要的识别方式:
# 1. 块参照实体识别
针对CAD图纸中以块(Block)形式存在的对象,系统能够:
- 批量获取所有块参照:自动检索图纸中的所有块参照实体,支持从元数据或表达式查询获取
- 块信息管理:显示块名称、对象ID、图层名称、边界范围、位置坐标、旋转角度、缩放比例等详细信息
- 可视化展示:通过动画线框和高亮显示所有块的位置和边界
- 交互式操作:支持点击表格行定位到对应块,支持显示/隐藏所有块的可视化效果
- 悬浮提示:鼠标悬停时显示块的详细属性信息,包括属性数据
# 2. 智能对象识别
针对不是块形式的普通CAD实体对象,系统提供:
- 模式匹配识别:基于用户选择的参考实体,在图纸中智能匹配相似的对象
- 几何变换支持:支持缩放、旋转等几何变换的容差匹配
- 多条件筛选:支持颜色、图层、文本内容等多种匹配条件
- 配置化管理:支持保存和加载识别配置,提高识别效率
# 核心功能特性
# 1. 智能实体选择
- 多种选择模式:支持框选、点选、多边形选择三种实体选择方式
- 实时预览:选择后立即显示所选实体数量
- 边界计算:自动计算所选实体的边界范围
# 2. 几何变换支持
- 缩放变换:支持设置缩放比例范围(0.1-10倍)
- 旋转变换:支持设置旋转角度范围(0-360度)
- 变换容差:可配置变换匹配的容差值
# 3. 匹配条件配置
- 颜色匹配:可设置是否要求实体颜色相同
- 图层匹配:可设置是否要求实体在同一图层
- 文本内容匹配:可设置文本类实体是否要求内容相同
- 自定义匹配规则:支持针对不同实体类型配置匹配属性
# 4. 配置管理系统
- 配置保存:支持将识别配置保存到服务器
- 配置加载:支持从服务器加载已保存的配置
- 配置覆盖:支持覆盖同名配置
- 配置删除:支持删除不需要的配置
# 5. 缩略图生成
- 自动生成:选择实体后自动生成120x80像素的缩略图
- 配置关联:缩略图与配置一起保存,便于快速识别
- 实时更新:选择实体变化时自动更新缩略图
# 使用场景
# 1. 块参照实体管理
- 块实体统计:快速统计图纸中所有块参照的数量和类型分布
- 块位置定位:通过表格快速定位到指定块的位置,便于查看和编辑
- 块属性查看:查看块的详细属性信息,包括位置、旋转角度、缩放比例等
- 块重复性分析:识别相同块名的多个实例,分析设计重复性
# 2. CAD图纸标准化检查
- 检查图纸中相似元素的一致性
- 发现不规范的设计元素
# 3. 重复元素检测
- 识别图纸中的重复设计
- 优化图纸存储和传输
# 4. 设计模式分析
- 分析图纸中的设计模式
- 提取可复用的设计元素
# 5. 智能对象匹配
- 基于参考实体智能匹配相似对象
- 支持几何变换的模糊匹配
- 提高CAD图纸处理的自动化程度
# 技术实现架构
# 1. 核心数据结构
# 模式配置数据
let patternConfig: any = {
"name": "",
"sameColor": true,
"sameLayer": false,
"global": {
"match": {
"all": {
"equalMatchProperty": ["color"]
},
"AcDbCircle": {
"scaleMatchProperty": ["radius"]
},
"AcDbArc": {
"scaleMatchProperty": ["radius"]
},
"AcDbLine": {
"scaleMatchProperty": ["length"]
},
"AcDbText": {
"scaleMatchProperty": ["height"]
}
}
},
"rules": [{
"tolerance": 0.00001,
"translateTolerance": 0.015,
"transform": {
"scale": {
"min": 0.5,
"max": 2.0
},
"rotation": {
"min": 0,
"max": 360
}
},
"referenceObjectIds": []
}]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 2. 关键算法实现
# 块参照实体检索算法
const detectAllBlockRefsObjects = async () => {
try {
removeAntPathAnimateLine();
let svc = map.getService();
let metadata = await svc.metadata();
let blockReferences = []
if("blockReferences" in metadata) {
// 从元数据中获取块参照信息(新版本)
let blockRefs = metadata.blockReferences;
if (blockRefs) {
blockReferences = JSON.parse(blockRefs);
}
} else {
// 通过表达式查询获取块参照信息(旧版本兼容)
if (map.logInfo) map.logInfo("正在获取所有块数据,请稍候...", "success", 2000);
let query = await svc.exprQueryFeature({
expr: `gOutReturn := if((gInFeatureType == 'AcDbBlockReference'), 1, 0);`,
fields: "", // 查询所有字段
geom: false, // 内存模式
useCache: true,
limit: 1000000 // 查找所有块参照
});
if (query.error) {
ElMessage({
type: 'error',
message: query.error,
});
return;
}
blockReferences = query.result.map((r: any) => {
return {
objectId: r.objectid,
blockName: r.blockname,
layerName: r.layername,
bounds: r.bounds,
position: r.positon,
...r
};
});
}
// 保存数据并创建可视化效果
blockRefsData.value = blockReferences;
let data = getData();
createAntPathAnimateLine(data);
createHighlightBlocks(data);
ElMessage({
type: 'success',
message: `共找到 ${blockReferences.length} 个块参照实体。`,
});
} catch (error) {
showError("获取块参照实体失败:" + (error as any).message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 块可视化高亮算法
const createHighlightBlocks = (data: any) => {
let polygons = data
let polygon = new vjmap.Polygon({
data: polygons,
fillColor: ['case', ['to-boolean', ['feature-state', 'hover']], '#0ff', '#f00'],
// 默认透明,鼠标悬停时显示高亮
fillOpacity: ['case', ['to-boolean', ['feature-state', 'hover']], 0.1, 0.0],
fillOutlineColor: ['get', 'color'],
isHoverPointer: true,
isHoverFeatureState: true
});
polygon.addTo(map);
// 悬浮提示功能
polygon.hoverPopup((f: any, popup: any) => {
let bounds = vjmap.GeoBounds.fromDataExtent(f);
popup.setLngLat([bounds.center().x, bounds.max.y]);
return `<h3>块名称: ${f.properties.blockName}</h3>图层名称: ${f.properties.layerName}<br>对象ID: ${f.properties.objectId}${f.properties.attributeDef ? '<br>属性数据: ' + f.properties.attributeDef : ''}`
}, { anchor: 'bottom' });
polygonOverlay = polygon;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 实体选择算法
const select = async () => {
removeAntPathAnimateLine()
let result = await selectFeatures(map, null, btnCtx, false)
if (!result || !result.features) return;
currentFeatures.value = result.features
patternConfig.rules[patternConfig.rules.length - 1].referenceObjectIds = result.features
// 选择后生成缩略图
await generateThumbnail()
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 缩略图生成算法
const generateThumbnail = async () => {
if (currentFeatures.value.length === 0) {
thumbnailImage.value = ''
return
}
try {
isGeneratingThumbnail.value = true
// 获取所有选中对象的边界
let allBounds = null
for (const feature of currentFeatures.value) {
const bounds = vjmap.GeoBounds.fromString(feature.properties.bounds)
if (!allBounds) {
allBounds = bounds
} else {
allBounds.updateByBounds(bounds)
}
}
if (allBounds) {
// 生成 120x80 的缩略图
let objectIds = currentFeatures.value.map((item: any) => item.properties.objectid)
const base64Image = await getObjectsThumbnail(map, objectIds, allBounds, 120, 80)
thumbnailImage.value = base64Image
}
} catch (error) {
console.error('Generate thumbnail failed:', error)
thumbnailImage.value = ''
} finally {
isGeneratingThumbnail.value = false
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 智能识别算法
const detectAllObjects = async () => {
try {
if (!formData.value.keepLastResult) {
removeAntPathAnimateLine();
}
if (currentFeatures.value.length == 0) {
ElMessage({
type: 'error',
message: '请先选择要识别的实体',
})
return;
}
let svc = map.getService();
ElMessage({
type: 'success',
message: '正在执行智能识别,请稍候...',
})
// 调用后端识别服务
let res = await svc.execCommand("objectDetection", {
mapid: svc.currentMapParam()?.mapid,
version: svc.currentMapParam()?.version,
layer: svc.currentMapParam()?.layer,
pattern: JSON.stringify(patternConfig),
detailedLog: formData.value.detailedLog,
bounds: formData.value.bounds
}, "_null", "v1");
// 处理识别结果
if (res && res.result && res.result.length > 0) {
// 在地图上显示识别结果
let geoDatas = [];
for (let i = 0; i < res.result.length; i++) {
const bounds = vjmap.GeoBounds.fromString(res.result[i].bounds)
const pts = bounds.toPointArray();
pts.push(pts[0])
geoDatas.push({
points: map.toLngLat(pts.map(p => vjmap.geoPoint(p))),
properties: {
color: "#ff0000"
}
})
}
// 创建动画线图层
let antPathAnimateLine = vjmap.createAntPathAnimateLineLayer(map, geoDatas, {
fillColor1: "#f00",
fillColor2: formData.value.keepLastResult ? vjmap.randomColor() : "#0ff",
canvasWidth: 128,
canvasHeight: 32,
frameCount: 4,
lineWidth: 2,
lineOpacity: 0.8
});
map._dectionAntPathAnimateLines = map._dectionAntPathAnimateLines || [];
map._dectionAntPathAnimateLines.push(antPathAnimateLine);
ElMessage({
type: 'success',
message: `共找到 ${geoDatas.length} 个匹配的对象。`,
})
}
} catch (error) {
showError("识别失败:" + (error as any).message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 4. 配置管理实现
# 配置保存
const saveConfig = async () => {
try {
// 确保配置列表已加载,用于检查重复名称
if (configList.value.length === 0) {
await loadConfigList()
}
// 获取配置名称
const name = await getInput('保存配置', '请输入配置名称', patternConfig.name || '')
if (!name || typeof name !== 'string') {
ElMessage.warning('配置名称不能为空')
return
}
// 检查名称是否重复
const exists = checkConfigExists(name)
let shouldOverwrite = false
if (exists) {
try {
const action = await ElMessageBox.confirm(
`配置名称 "${name}" 已存在,请选择操作`,
'名称重复',
{
confirmButtonText: '覆盖',
cancelButtonText: '新增',
distinguishCancelAndClose: true,
type: 'warning'
}
)
shouldOverwrite = true
patternConfig.name = name
} catch (error: any) {
if (error === 'cancel') {
patternConfig.name = name
} else {
return
}
}
} else {
patternConfig.name = name
}
// 如果是覆盖操作,先删除旧配置
if (shouldOverwrite) {
const existingConfig = configList.value.find((c: any) => c.name === name)
if (existingConfig) {
const svc = map.getService()
const deleteUrl = svc.serviceUrl(`recognizer/delete?filename=${existingConfig.filePrefix}`)
await svc.httpDel(deleteUrl)
}
}
// 添加缩略图到配置中
if (thumbnailImage.value) {
patternConfig.thumbnail = thumbnailImage.value
}
const svc = map.getService()
const url = svc.serviceUrl("recognizer/add")
const response = await svc.httpPost(url, patternConfig)
const result = response.data
if (result.code === 0) {
ElMessage.success(shouldOverwrite ? '配置覆盖成功' : '配置保存成功')
await loadConfigList()
} else {
ElMessage.error('配置保存失败')
}
} catch (error) {
console.error('Save config failed:', error)
ElMessage.error('配置保存失败')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 配置加载
const selectConfig = async (config: any) => {
try {
selectedConfigId.value = config.filePrefix
// 获取配置详细数据
const svc = map.getService()
const url = svc.serviceUrl(`recognizer/detail?filenames=${config.filePrefix}`)
const response = await svc.httpGet(url)
const result = response.data
if (result.code === 0 && result.data && result.data.length > 0) {
const configData = result.data[0]
// 加载配置数据
patternConfig = configData
// 同步表单数据
if (configData.rules && configData.rules.length > 0) {
const rule = configData.rules[0]
if (rule.transform) {
if (rule.transform.scale) {
formData.value.allowScale = true
formData.value.scaleMin = rule.transform.scale.min || 0.5
formData.value.scaleMax = rule.transform.scale.max || 2
} else {
formData.value.allowScale = false
}
if (rule.transform.rotation) {
formData.value.allowRotation = true
formData.value.rotationMin = rule.transform.rotation.min || 0
formData.value.rotationMax = rule.transform.rotation.max || 360
} else {
formData.value.allowRotation = false
}
}
}
// 同步其他配置项
formData.value.sameColor = configData.sameColor || false
formData.value.sameLayer = configData.sameLayer || false
// 更新缩略图
if (configData.thumbnail) {
thumbnailImage.value = configData.thumbnail
} else {
thumbnailImage.value = ''
}
currentFeatures.value = patternConfig.rules[patternConfig.rules.length - 1].referenceObjectIds
ElMessage.success(`已加载配置: ${config.name}`)
}
} catch (error) {
console.error('Load config failed:', error)
ElMessage.error('选择配置失败')
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
← 地图比较 服务端渲染表达式语法 →