第2204篇:建筑图纸的智能解析——工程图纸AI理解的工程实践
2026/4/30大约 7 分钟
第2204篇:建筑图纸的智能解析——工程图纸AI理解的工程实践
适读人群:建筑/土木/施工信息化领域的Java工程师 | 阅读时长:约15分钟 | 核心价值:工程图纸AI解析的完整工程方案,从CAD图纸到结构化工程数据
做建筑行业信息化的同学都知道,图纸是整个行业的"核心数据资产",但也是最难被数字化处理的数据。
一栋楼的图纸可能有几百张,每张里面有密密麻麻的标注、尺寸线、图例、文字说明。传统做法是专业的预算员手工看图计算工程量,一个熟练的预算员一天能处理3-5张复杂图纸,已经很高效了。
我们做的项目是把这个过程AI化:扫描或导出图纸图片,AI自动识别房间、墙体、门窗,提取面积、周长等工程量数据。
建筑图纸处理和一般文档图像处理的区别在于:图纸的语义是高度专业化的,要理解标准图例、轴网体系、比例尺、剖面指示符等建筑制图规范。
一、建筑图纸的类型与处理策略
建筑图纸类型:
├── 平面图(最常见)
│ ├── 总平面图 → 提取建筑位置、场地关系
│ ├── 标准层平面图 → 提取房间、面积、门窗
│ └── 屋顶平面图
├── 立面图 → 提取外观特征、窗洞位置
├── 剖面图 → 提取楼层高度、结构关系
├── 详图/节点图 → 提取构造做法、材料规格
└── 结构图 → 提取梁柱位置、构件截面不同类型的图纸用不同的提取策略。这篇重点讲最有工程价值的平面图处理。
二、平面图分析的工程实现
@Service
public class FloorPlanAnalyzer {
private final VisionService visionService;
private final ObjectMapper objectMapper;
/**
* 平面图整体分析
*/
public FloorPlanData analyze(byte[] floorPlanImage, String projectInfo) {
// 第一步:整体分析(了解图纸基本信息)
FloorPlanMeta meta = extractMeta(floorPlanImage, projectInfo);
// 第二步:识别功能区域(房间分类)
List<RoomData> rooms = extractRooms(floorPlanImage, meta);
// 第三步:识别门窗
List<OpeningData> openings = extractOpenings(floorPlanImage);
// 第四步:计算工程量
EngineeringQuantities quantities = calculateQuantities(rooms, openings, meta);
return new FloorPlanData(meta, rooms, openings, quantities);
}
private FloorPlanMeta extractMeta(byte[] image, String projectInfo) {
String prompt = """
这是一张建筑平面图。请提取以下基本信息:
1. 图纸比例(如1:100, 1:50)
2. 图纸名称/图号
3. 楼层信息(第几层)
4. 轴网范围(最外层轴线编号)
""" + (projectInfo != null ? "项目信息:" + projectInfo + "\n" : "") + """
返回JSON:
{
"drawingTitle": "图纸名称",
"drawingNumber": "图号",
"scale": "比例如1:100",
"scaleRatio": 比例数字如100,
"floor": "楼层如3F或地下1层",
"gridXMax": "X方向最大轴线编号",
"gridYMax": "Y方向最大轴线编号"
}
只返回JSON。
""";
VisionRequest request = VisionRequest.builder()
.images(List.of(ImageInput.fromBytes(image, "image/jpeg")))
.prompt(prompt)
.metadata(Map.of("detail", "high"))
.build();
String response = visionService.analyzeImage(request).getContent();
return parseMeta(response);
}
private List<RoomData> extractRooms(byte[] image, FloorPlanMeta meta) {
String prompt = """
这是一张建筑平面图,比例为 %s。
请识别图中所有房间/功能区域,提取以下信息:
1. 房间名称(如卧室、客厅、厨房、卫生间、走廊等)
2. 房间的轴线编号范围(如A-B轴、1-3轴)
3. 净尺寸(长×宽,单位mm)
4. 净面积(平方米,精确到0.01)
注意:
- 面积要根据比例尺和图纸上的标注尺寸计算
- 卫生间面积通常2-6平方米
- 卧室面积通常10-20平方米
- 如果尺寸无法读取,在字段中填null
返回JSON数组,每个元素是一个房间:
[
{
"name": "房间名称",
"gridRange": "轴线范围",
"length": 净长mm,
"width": 净宽mm,
"area": 净面积m²,
"perimeter": 净周长m
}
]
只返回JSON数组。
""".formatted(meta.scale() != null ? meta.scale() : "未知");
VisionRequest request = VisionRequest.builder()
.images(List.of(ImageInput.fromBytes(image, "image/jpeg")))
.prompt(prompt)
.metadata(Map.of("detail", "high"))
.build();
String response = visionService.analyzeImage(request).getContent();
return parseRooms(response);
}
private List<OpeningData> extractOpenings(byte[] image) {
String prompt = """
这是一张建筑平面图。请识别所有门窗洞口。
建筑平面图中:
- 门用弧形或矩形表示,通常有宽度标注
- 窗用多条平行线表示,通常在墙体位置
- 洞口有编号(如M1、C1等)
请提取:
[
{
"id": "编号如M1/C1",
"type": "门/窗/洞",
"width": 宽度mm,
"height": 高度mm(如有),
"location": "位置描述"
}
]
只返回JSON数组。
""";
VisionRequest request = VisionRequest.builder()
.images(List.of(ImageInput.fromBytes(image, "image/jpeg")))
.prompt(prompt)
.build();
String response = visionService.analyzeImage(request).getContent();
return parseOpenings(response);
}
/**
* 根据提取的数据计算工程量
*/
private EngineeringQuantities calculateQuantities(List<RoomData> rooms,
List<OpeningData> openings,
FloorPlanMeta meta) {
// 总建筑面积
double totalArea = rooms.stream()
.filter(r -> !isCirculationSpace(r.name())) // 排除走廊等交通面积
.mapToDouble(RoomData::area)
.sum();
// 按功能区分类统计
Map<String, Double> areaByFunction = rooms.stream()
.collect(Collectors.groupingBy(
r -> classifyRoom(r.name()),
Collectors.summingDouble(RoomData::area)
));
// 门窗统计
long doorCount = openings.stream().filter(o -> "门".equals(o.type())).count();
long windowCount = openings.stream().filter(o -> "窗".equals(o.type())).count();
// 墙体周长估算
double wallPerimeter = rooms.stream().mapToDouble(RoomData::perimeter).sum() / 2;
return new EngineeringQuantities(totalArea, areaByFunction,
(int) doorCount, (int) windowCount, wallPerimeter);
}
private boolean isCirculationSpace(String roomName) {
return roomName != null && (roomName.contains("走廊") || roomName.contains("楼梯")
|| roomName.contains("电梯") || roomName.contains("前室"));
}
private String classifyRoom(String roomName) {
if (roomName == null) return "其他";
if (roomName.contains("卧室") || roomName.contains("主卧") || roomName.contains("次卧"))
return "卧室";
if (roomName.contains("客厅") || roomName.contains("起居")) return "客厅";
if (roomName.contains("厨房")) return "厨房";
if (roomName.contains("卫生间") || roomName.contains("浴室") || roomName.contains("厕所"))
return "卫生间";
if (roomName.contains("办公") || roomName.contains("会议")) return "办公区";
return "其他";
}
// 省略 parseMeta, parseRooms, parseOpenings 方法
private FloorPlanMeta parseMeta(String json) { return null; }
private List<RoomData> parseRooms(String json) { return List.of(); }
private List<OpeningData> parseOpenings(String json) { return List.of(); }
// 数据模型
public record FloorPlanMeta(String drawingTitle, String drawingNumber,
String scale, int scaleRatio, String floor,
String gridXMax, String gridYMax) {}
public record RoomData(String name, String gridRange, double length,
double width, double area, double perimeter) {}
public record OpeningData(String id, String type, int width,
Integer height, String location) {}
public record EngineeringQuantities(double totalArea, Map<String, Double> areaByFunction,
int doorCount, int windowCount,
double wallPerimeter) {}
public record FloorPlanData(FloorPlanMeta meta, List<RoomData> rooms,
List<OpeningData> openings,
EngineeringQuantities quantities) {}
}三、大比例图纸的分块处理
对于复杂的大比例图纸,整张图喂给VLM效果不好,需要分块:
@Service
public class LargeFloorPlanProcessor {
private final FloorPlanAnalyzer analyzer;
/**
* 将大图分块处理,然后合并结果
*/
public FloorPlanAnalyzer.FloorPlanData processLargeFloorPlan(byte[] largeImage,
int blockRows,
int blockCols) {
Mat image = bytesToMat(largeImage);
int blockWidth = image.cols() / blockCols;
int blockHeight = image.rows() / blockRows;
List<FloorPlanAnalyzer.FloorPlanData> blockResults = new ArrayList<>();
for (int row = 0; row < blockRows; row++) {
for (int col = 0; col < blockCols; col++) {
// 裁剪当前块
int x = col * blockWidth;
int y = row * blockHeight;
Rect blockRect = new Rect(x, y,
Math.min(blockWidth, image.cols() - x),
Math.min(blockHeight, image.rows() - y));
Mat block = new Mat(image, blockRect);
byte[] blockBytes = matToBytes(block);
// 分析当前块
try {
FloorPlanAnalyzer.FloorPlanData blockData =
analyzer.analyze(blockBytes, "分块分析 [" + row + "," + col + "]");
blockResults.add(blockData);
} catch (Exception e) {
// 某块分析失败,继续处理其他块
log.warn("块[{},{}]分析失败: {}", row, col, e.getMessage());
}
}
}
// 合并所有块的结果
return mergeBlockResults(blockResults);
}
private FloorPlanAnalyzer.FloorPlanData mergeBlockResults(
List<FloorPlanAnalyzer.FloorPlanData> blockResults) {
// 合并所有房间(去重,基于名称+面积)
List<FloorPlanAnalyzer.RoomData> allRooms = blockResults.stream()
.flatMap(b -> b.rooms().stream())
.collect(Collectors.toList());
// 简单去重(相同名称且面积相差<1平方米的认为是同一个房间)
List<FloorPlanAnalyzer.RoomData> deduped = deduplicateRooms(allRooms);
// 取第一个块的meta
FloorPlanAnalyzer.FloorPlanMeta meta = blockResults.isEmpty()
? null : blockResults.get(0).meta();
// 合并开口数据
List<FloorPlanAnalyzer.OpeningData> allOpenings = blockResults.stream()
.flatMap(b -> b.openings().stream())
.collect(Collectors.toList());
return new FloorPlanAnalyzer.FloorPlanData(meta, deduped, allOpenings, null);
}
private List<FloorPlanAnalyzer.RoomData> deduplicateRooms(
List<FloorPlanAnalyzer.RoomData> rooms) {
Map<String, FloorPlanAnalyzer.RoomData> uniqueRooms = new LinkedHashMap<>();
for (FloorPlanAnalyzer.RoomData room : rooms) {
String key = room.name() + "_" + (int) room.area();
uniqueRooms.putIfAbsent(key, room);
}
return new ArrayList<>(uniqueRooms.values());
}
private Mat bytesToMat(byte[] bytes) {
return Imgcodecs.imdecode(new MatOfByte(bytes), Imgcodecs.IMREAD_COLOR);
}
private byte[] matToBytes(Mat mat) {
MatOfByte mob = new MatOfByte();
Imgcodecs.imencode(".jpg", mat, mob);
return mob.toArray();
}
}四、工程量报表生成
提取完数据,生成工程量报表:
@Service
public class QuantityTakeoffReporter {
/**
* 生成工程量清单报表
*/
public String generateReport(FloorPlanAnalyzer.FloorPlanData floorPlanData,
String projectName) {
StringBuilder report = new StringBuilder();
report.append("# 工程量清单\n\n");
report.append("项目名称:").append(projectName).append("\n");
report.append("图纸:").append(floorPlanData.meta().drawingTitle()).append("\n");
report.append("楼层:").append(floorPlanData.meta().floor()).append("\n");
report.append("比例:").append(floorPlanData.meta().scale()).append("\n\n");
// 面积汇总
report.append("## 面积汇总\n\n");
report.append("| 功能区 | 面积(m²) |\n|--------|----------|\n");
FloorPlanAnalyzer.EngineeringQuantities q = floorPlanData.quantities();
if (q != null && q.areaByFunction() != null) {
q.areaByFunction().forEach((func, area) ->
report.append(String.format("| %s | %.2f |\n", func, area)));
report.append(String.format("| **合计** | **%.2f** |\n\n", q.totalArea()));
}
// 房间明细
report.append("## 房间明细\n\n");
report.append("| 房间名称 | 长(mm) | 宽(mm) | 面积(m²) |\n");
report.append("|---------|--------|--------|----------|\n");
for (FloorPlanAnalyzer.RoomData room : floorPlanData.rooms()) {
report.append(String.format("| %s | %.0f | %.0f | %.2f |\n",
room.name(), room.length(), room.width(), room.area()));
}
// 门窗统计
if (q != null) {
report.append(String.format("\n## 门窗统计\n\n门:%d 樘,窗:%d 樘\n",
q.doorCount(), q.windowCount()));
}
return report.toString();
}
}五、精度局限与工程经验
建筑图纸AI解析的局限性要清楚认识:
VLM对尺寸标注的识别有误差:复杂图纸里,尺寸线很密集,VLM有时会把标注值对应错,建议提取后和已知参考面积做交叉验证。
比例尺识别是关键:如果比例尺识别错了,所有面积计算都会偏差。建议在Prompt里明确要求VLM先确认比例尺,再做面积计算。
适合的场景:
- 快速粗估(误差10%以内可接受):可以直接用
- 招投标精算(误差<1%要求):需要大量人工核对,AI只做辅助
在我们的实际项目里,AI提取后的工程量数据,与人工算量相比,面积误差在5-8%,对于概算阶段完全够用,节省了预算员60%的时间。
