# 实体对象智能识别

# 概述

实体对象智能识别能够在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. 关键算法实现

# 块参照实体检索算法

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

# 块可视化高亮算法

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

# 实体选择算法

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

# 缩略图生成算法

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

# 智能识别算法

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

# 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

# 配置加载

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