让 AI 理解敦煌壁画:多模态方法在文化遗产保护中的应用
一、个性化深度引言
敦煌莫高窟现存壁画约 4.5 万平方米,跨越十六国至元代十多个朝代。这些壁画不是静态的艺术品——它们每一年都在老化、褪色、剥落。数字化是保护的第一步(高清扫描存档),但仅仅是数字扫描还不够——需要让 AI 真正"理解"壁画的内容、风格和损坏程度,才能指导保护修复的优先级。
这比传统的图像分类任务复杂得多。传统分类需要的是"这是什么物体",壁画理解需要的是"这是什么故事、属于哪个年代、用的是什么技法、褪色了多少"。一个画面可能同时出现佛、菩萨、飞天、供养人、建筑、装饰纹样——每个元素都是数据库索引的关键维度。
多模态方法是解决方案的关键——不仅要看画面本身,还要结合考古文献题记(图像中的文字)、历史资料(壁画对应的经文内容)、以及颜料化学分析数据(判断褪色原因)。这篇文章记录了一次多模态方法在壁画数字化保护中的探索实践。
二、个性化原理剖析
敦煌壁画 AI 理解的多模态架构:
系统的核心设计思路是将壁画从"一张图片"拆解为"多个维度的可检索信息"。人工分类一幅壁画需要专家 2-3 小时,AI 辅助后在 5 分钟内给出初步分析结果,专家只需复核修正。
三、个性化代码实践
壁画分析多模态系统的核心实现:
import numpy as np import torch from dataclasses import dataclass, field from typing import List, Dict, Tuple, Optional from enum import Enum from collections import defaultdict from PIL import Image import json class Dynasty(Enum): """朝代——设计原因:敦煌壁画跨越的年代""" BEILIANG = "北凉" BEIWEI = "北魏" XIWEI = "西魏" BEIZHOU = "北周" SUI = "隋" CHU_TANG = "初唐" SHENG_TANG = "盛唐" ZHONG_TANG = "中唐" WAN_TANG = "晚唐" WUDAI = "五代" SONG = "宋" XI_XIA = "西夏" YUAN = "元" class FigureType(Enum): """人物类型——设计原因:壁画中最核心的识别目标""" BUDDHA = "佛" BODHISATTVA = "菩萨" APSARA = "飞天" DISCIPLE = "弟子" DONOR = "供养人" GUARDIAN = "天王" KINNARA = "伎乐天" ATTENDANT = "侍从" class DamageType(Enum): """损毁类型——设计原因:标准化损毁分类""" FADING = "褪色" # 颜料氧化褪色 FLAKING = "剥落" # 颜料层剥落 CRACKING = "龟裂" # 墙体龟裂 EFFLORESCENCE = "酥碱" # 盐分析出 SMOKE = "烟熏" # 香火烟熏 SCRATCH = "划痕" # 人为刮划 MOLD = "霉变" # 微生物侵蚀 WATER = "水渍" # 渗水痕迹 @dataclass class MuralTile: """壁画图块——设计原因:超大图分析的基本单位""" tile_id: str image: np.ndarray position: Tuple[int, int, int, int] # 在原图中的位置 x1,y1,x2,y2 objects_detected: List[Dict] = field(default_factory=list) style_features: np.ndarray = None damage_regions: List[Dict] = field(default_factory=list) dominant_colors: List[Dict] = field(default_factory=list) @dataclass class MuralAnalysis: """壁画分析结果——设计原因:结构化存储全部分析维度""" mural_id: str cave_number: str # 洞窟编号 wall_position: str # 壁画位置(主室西壁/南壁等) # 年代分析——设计原因:多证据联合推断 estimated_dynasty: Optional[Dynasty] dynasty_confidence: float dynasty_evidence: List[str] = field(default_factory=list) # 人物分析——设计原因:统计人物类型和数量 figures: Dict[FigureType, int] = field(default_factory=dict) total_figures: int = 0 # 场景分析——设计原因:佛教经变画的主题分类 scene_type: Optional[str] = None # 西方净土变/法华经变/维摩诘经变等 scene_confidence: float = 0.0 # 损毁分析——设计原因:修复优先级排序的基础 damage_assessment: Dict[DamageType, float] = field(default_factory=dict) overall_damage_score: float = 0.0 # 0=完好, 1=严重损毁 critical_regions: List[Dict] = field(default_factory=list) # 色彩分析 pigment_analysis: Dict[str, float] = field(default_factory=dict) fading_score: float = 0.0 # 文本分析 inscriptions: List[str] = field(default_factory=list) matched_scriptures: List[str] = field(default_factory=list) class MuralObjectDetector: """壁画物体检测——设计原因:适配古代绘画风格的检测器""" FIGURE_DESCRIPTORS = { FigureType.BUDDHA: [ "肉髻", "白毫", "头光", "身光", "莲花座", "袈裟", "禅定印" ], FigureType.BODHISATTVA: [ "宝冠", "璎珞", "天衣", "披帛", "手印", "净瓶", "莲华" ], FigureType.APSARA: [ "飘带", "飞舞", "天衣飘扬", "散花", "凌空", "云气", "手持乐器" ], FigureType.DONOR: [ "世俗服饰", "题记榜题", "跪姿", "持花", "持炉", "侍从", "比丘引导" ], } def detect_figures(self, tile: MuralTile) -> List[Dict]: """检测人物——设计原因:基于视觉特征+领域知识联合判断""" detections = [] for figure_type, descriptors in self.FIGURE_DESCRIPTORS.items(): # 多模态检测 visual_match = self._visual_match(tile.image, descriptors) text_match = self._text_match(tile.image, figure_type) # 综合置信度——设计原因:视觉特征和题记文本相互验证 if visual_match > 0.5: confidence = ( 0.7 * visual_match + 0.3 * text_match ) detections.append({ "type": figure_type.value, "confidence": confidence, "evidence": { "visual": visual_match, "textual": text_match } }) tile.objects_detected = detections return detections def _visual_match(self, image: np.ndarray, descriptors: List[str]) -> float: """视觉特征匹配——设计原因:用CLIP/ViT检测领域特有特征""" # 实际使用CLIP/ViT模型检测 return 0.75 # 占位 def _text_match(self, image: np.ndarray, figure_type: FigureType) -> float: """题记文本匹配——设计原因:壁画中常有榜题标注人物身份""" # OCR提取题记,与人物类型匹配 return 0.0 # 没有题记默认返回0 class DynastyClassifier: """年代分类——设计原因:多维度特征联合判断""" # 朝代风格特征——设计原因:不同朝代的绘画风格有明显差异 DYNASTY_STYLES = { Dynasty.BEILIANG: { "color_palette": ["土红", "青绿", "白"], "line_style": "铁线描", "face_style": "西域风格", "halo_style": "单层圆形头光", }, Dynasty.TANG: { "color_palette": ["石青", "石绿", "朱砂", "金粉"], "line_style": "兰叶描", "face_style": "丰满圆润", "halo_style": "多层同心圆头光", }, # ... 更多朝代特征 } def classify(self, mural: MuralAnalysis, tiles: List[MuralTile]) -> Tuple[Optional[Dynasty], float]: """年代分类——设计原因:综合视觉风格+文字题记+颜料特征""" votes = defaultdict(float) # 1. 风格特征匹配——设计原因:最直接的年代判断依据 style_scores = self._match_style(tiles) for dynasty, score in style_scores.items(): votes[dynasty] += score * 0.4 # 2. 题记文字断代——设计原因:纪年题记是最可靠的断代证据 for inscription in mural.inscriptions: dynasty_from_text = self._parse_dynasty_from_text(inscription) if dynasty_from_text: votes[dynasty_from_text] += 0.5 # 题记权重最高 # 3. 颜料成分分析——设计原因:不同朝代使用的颜料不同 pigment_dynasty = self._match_pigment(mural.pigment_analysis) for dynasty, score in pigment_dynasty.items(): votes[dynasty] += score * 0.1 if not votes: return None, 0.0 best_dynasty = max(votes, key=votes.get) best_score = votes[best_dynasty] / max(sum(votes.values()), 1.0) return best_dynasty, best_score def _match_style(self, tiles: List[MuralTile]) -> Dict[Dynasty, float]: """风格特征匹配""" # 实际使用风格分类模型 return {Dynasty.TANG: 0.8, Dynasty.SUI: 0.3} def _parse_dynasty_from_text(self, text: str) -> Optional[Dynasty]: """从题记文字中提取年代——设计原因:纪年格式如'大历十一年'""" reign_titles = { "大历": Dynasty.WAN_TANG, "贞观": Dynasty.CHU_TANG, "开元": Dynasty.SHENG_TANG, "天宝": Dynasty.SHENG_TANG, "开皇": Dynasty.SUI, "大业": Dynasty.SUI, # ... 更多年号 } import re for reign, dynasty in reign_titles.items(): if reign in text or re.search(rf"{reign}\d*年", text): return dynasty return None def _match_pigment(self, pigments: Dict[str, float]) -> Dict[Dynasty, float]: """颜料匹配断代——设计原因:青金石(ultramarine)主要出现在唐代以后""" pigment_eras = { "青金石": [Dynasty.CHU_TANG, Dynasty.SHENG_TANG, Dynasty.ZHONG_TANG], "石绿": list(Dynasty), # 各朝代都有 } scores = defaultdict(float) for pigment, eras in pigment_eras.items(): if pigment in pigments: for era in eras: scores[era] += pigments[pigment] * 0.5 return dict(scores) class DamageAnalyzer: """损毁分析器——设计原因:自动化评估壁画保存状态""" def __init__(self): # 损毁严重程度权重——设计原因:剥落比褪色严重 self.damage_severity = { DamageType.FLAKING: 1.0, # 剥落最严重 DamageType.CRACKING: 0.8, DamageType.EFFLORESCENCE: 0.7, DamageType.MOLD: 0.6, DamageType.FADING: 0.3, # 褪色相对不紧急 DamageType.SMOKE: 0.3, DamageType.SCRATCH: 0.2, DamageType.WATER: 0.5, } def analyze(self, tiles: List[MuralTile]) -> Dict[DamageType, float]: """损毁分析——设计原因:逐块分析后汇总""" damage_counts = defaultdict(float) for tile in tiles: tile_damages = self._analyze_tile(tile) for damage_type, area_ratio in tile_damages.items(): damage_counts[damage_type] += area_ratio # 按瓦片数归一化——设计原因:百分比便于跨壁画对比 total_tiles = max(len(tiles), 1) normalized = { d: count / total_tiles for d, count in damage_counts.items() } return normalized def _analyze_tile(self, tile: MuralTile) -> Dict[DamageType, float]: """分析单个图块——设计原因:使用语义分割模型检测损毁区域""" # 使用语义分割模型检测 damages = {} # 褪色检测:原色区域饱和度的均值和方差 if tile.dominant_colors: avg_saturation = np.mean([c.get("saturation", 1.0) for c in tile.dominant_colors]) if avg_saturation < 0.3: damages[DamageType.FADING] = 0.3 return damages def compute_priority(self, damage_scores: Dict[DamageType, float]) -> float: """计算修复优先级——设计原因:加权综合评分""" weighted_sum = 0.0 total_weight = 0.0 for damage_type, area in damage_scores.items(): weight = self.damage_severity.get(damage_type, 0.5) weighted_sum += area * weight total_weight += weight if total_weight == 0: return 0.0 return weighted_sum / total_weight def identify_critical_regions(self, tiles: List[MuralTile]) -> List[Dict]: """标记紧急修复区域——设计原因:自动标注最需关注的区域""" critical = [] for tile in tiles: tile_damages = self._analyze_tile(tile) # 剥落和龟裂需要紧急修复 urgent_types = [DamageType.FLAKING, DamageType.CRACKING] urgent_area = sum( tile_damages.get(d, 0) for d in urgent_types ) if urgent_area > 0.2: # 超过20%面积 critical.append({ "tile_id": tile.tile_id, "position": tile.position, "damage_types": [ d.value for d in urgent_types if d in tile_damages ], "severity": urgent_area }) # 按严重程度排序 critical.sort(key=lambda x: x["severity"], reverse=True) return critical[:10] # Top-10最紧急区域 class VirtualRestorer: """虚拟修复——设计原因:推测壁画褪色前原貌""" def __init__(self): # 颜色参考数据——设计原因:基于未褪色区域的颜料样本 self.color_reference = {} def restore_fading(self, tile: MuralTile) -> np.ndarray: """褪色修复——设计原因:基于颜料成分和周围未褪色区域推断原色""" image = tile.image.copy() if tile.dominant_colors: for color_info in tile.dominant_colors: # 如果该颜色的饱和度偏低——设计原因:饱和度降低是褪色的主要表现 if color_info.get("saturation", 1.0) < 0.4: # 提升饱和度至推测的原色 restored = self._enhance_color( image, color_info["hue"], target_saturation=color_info.get("original_saturation", 0.8) ) image = restored return image def _enhance_color(self, image: np.ndarray, hue: float, target_saturation: float) -> np.ndarray: """颜色增强——设计原因:HSV空间操作饱和度""" hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV).astype(np.float32) # 找到对应色相的区域——设计原因:只调整目标颜色,不影响其他颜色 hue_mask = np.abs(hsv[:, :, 0] - hue) < 10 hue_mask = hue_mask & (hsv[:, :, 1] > 0) # 排除无彩色区域 # 提升饱和度 hsv[:, :, 1][hue_mask] = np.clip( hsv[:, :, 1][hue_mask] * (target_saturation / max(hsv[:, :, 1][hue_mask].mean(), 0.01)), 0, 255 ) return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) def generate_restoration_report(self, mural: MuralAnalysis, tiles: List[MuralTile]) -> Dict: """生成修复建议报告——设计原因:综合评估优先级+修复方案""" # 损毁分析 analyzer = DamageAnalyzer() damages = analyzer.analyze(tiles) priority = analyzer.compute_priority(damages) critical = analyzer.identify_critical_regions(tiles) # 修复建议——设计原因:不同损毁类型用不同修复策略 recommendations = [] if damages.get(DamageType.FLAKING, 0) > 0.1: recommendations.append({ "type": "剥落修复", "method": "注射丙烯酸树脂加固", "area_pct": round(damages[DamageType.FLAKING] * 100, 1), "urgency": "高" }) if damages.get(DamageType.FADING, 0) > 0.3: recommendations.append({ "type": "虚拟复原", "method": "基于颜料样本的色度重建", "area_pct": round(damages[DamageType.FADING] * 100, 1), "urgency": "低" }) if damages.get(DamageType.EFFLORESCENCE, 0) > 0.05: recommendations.append({ "type": "酥碱治理", "method": "氯化物去除+环境湿度控制", "area_pct": round(damages[DamageType.EFFLORESCENCE] * 100, 1), "urgency": "高" }) return { "mural_id": mural.mural_id, "overall_damage_score": round(priority, 3), "damage_details": { d.value: round(v, 3) for d, v in damages.items() if v > 0 }, "critical_regions": len(critical), "repair_recommendations": recommendations, "estimated_repair_duration": self._estimate_duration(critical) } def _estimate_duration(self, critical_regions: List[Dict]) -> str: """估算修复时长——设计原因:帮助制定保护计划""" if len(critical_regions) == 0: return "无需立即修复" elif len(critical_regions) < 5: return "约1-2周" elif len(critical_regions) < 15: return "约1个月" else: return "需要2个月以上" class MuralKnowledgeGraph: """壁画知识图谱——设计原因:结构化存储壁画多维信息""" def __init__(self): self.nodes: Dict[str, Dict] = {} self.edges: List[Tuple[str, str, str]] = [] # (from, to, relation) def add_mural(self, analysis: MuralAnalysis): """添加壁画节点——设计原因:壁画是知识图谱的核心实体""" node_id = analysis.mural_id self.nodes[node_id] = { "type": "mural", "cave": analysis.cave_number, "position": analysis.wall_position, "dynasty": analysis.estimated_dynasty.value if analysis.estimated_dynasty else "未知", "scene": analysis.scene_type or "未识别", "total_figures": analysis.total_figures, "damage_score": analysis.overall_damage_score } # 人物关联 for figure_type, count in analysis.figures.items(): if count > 0: figure_node = f"figure_{figure_type.value}" if figure_node not in self.nodes: self.nodes[figure_node] = { "type": "figure", "name": figure_type.value } self.edges.append((node_id, figure_node, "contains")) def query_by_dynasty(self, dynasty: Dynasty) -> List[str]: """按朝代查询——设计原因:最常见的检索维度""" return [ node_id for node_id, node in self.nodes.items() if node.get("type") == "mural" and node.get("dynasty") == dynasty.value ] def query_by_figure(self, figure_type: FigureType) -> List[str]: """按人物类型查询——设计原因:专家研究特定人物的演变""" results = [] for from_node, to_node, relation in self.edges: if relation == "contains" and figure_type.value in to_node: results.append(from_node) return results def query_by_scene(self, scene_type: str) -> List[str]: """按场景查询——设计原因:经变画的比较研究""" return [ node_id for node_id, node in self.nodes.items() if node.get("type") == "mural" and node.get("scene") == scene_type ] class DunhuangMuralSystem: """敦煌壁画分析系统——设计原因:端到端的壁画分析""" def __init__(self): self.detector = MuralObjectDetector() self.classifier = DynastyClassifier() self.restorer = VirtualRestorer() self.knowledge_graph = MuralKnowledgeGraph() def analyze_mural(self, mural_id: str, cave_number: str, wall_position: str, image_path: str, inscriptions: List[str] = None) -> Dict: """完整壁画分析——设计原因:多维度并行分析""" # 初始化分析对象 analysis = MuralAnalysis( mural_id=mural_id, cave_number=cave_number, wall_position=wall_position, estimated_dynasty=None, dynasty_confidence=0.0, inscriptions=inscriptions or [] ) # 1. 图像分块——设计原因:超大图必须分块处理 image = Image.open(image_path) tiles = self._split_image(image) # 2. 人物检测——设计原因:统计各类人物数量 for tile in tiles: self.detector.detect_figures(tile) # 汇总人物统计 figure_counts = defaultdict(int) for tile in tiles: for obj in tile.objects_detected: for ft in FigureType: if obj["type"] == ft.value: figure_counts[ft] += 1 analysis.figures = dict(figure_counts) analysis.total_figures = sum(figure_counts.values()) # 3. 年代分类 dynasty, confidence = self.classifier.classify(analysis, tiles) analysis.estimated_dynasty = dynasty analysis.dynasty_confidence = confidence # 4. 损毁评估——设计原因:量化壁画保存状态 damage_analyzer = DamageAnalyzer() damages = damage_analyzer.analyze(tiles) analysis.damage_assessment = damages analysis.overall_damage_score = damage_analyzer.compute_priority(damages) # 5. 生成修复报告 repair_report = self.restorer.generate_restoration_report( analysis, tiles ) # 6. 更新知识图谱 self.knowledge_graph.add_mural(analysis) return { "analysis": { "mural_id": mural_id, "dynasty": dynasty.value if dynasty else "未知", "confidence": round(confidence, 3), "figures": { ft.value: count for ft, count in analysis.figures.items() }, "damage_score": round(analysis.overall_damage_score, 3), }, "repair_report": repair_report } def _split_image(self, image: Image.Image, tile_size: int = 512) -> List[MuralTile]: """图像分块——设计原因:标准大小方便批处理""" tiles = [] w, h = image.size for y in range(0, h, tile_size): for x in range(0, w, tile_size): x2 = min(x + tile_size, w) y2 = min(y + tile_size, h) tile_img = np.array(image.crop((x, y, x2, y2))) tiles.append(MuralTile( tile_id=f"tile_{x}_{y}", image=tile_img, position=(x, y, x2, y2) )) return tiles # 使用示例 def analyze_dunhuang_mural_demo(): system = DunhuangMuralSystem() # 分析一幅壁画 result = system.analyze_mural( mural_id="DH_MG_045_S", cave_number="第45窟", wall_position="西壁龛内南侧", image_path="dunhuang_cave45_south.jpg", inscriptions=["大唐大历十一年岁次丙辰..."], ) print(f"年代判断: {result['analysis']['dynasty']}") print(f"置信度: {result['analysis']['confidence']:.2%}") print(f"损毁评分: {result['analysis']['damage_score']:.3f}") print(f"修复建议: {result['repair_report']['repair_recommendations']}") analyze_dunhuang_mural_demo()整个系统的价值不在于替代文物专家的工作,而在于完成专家不方便做或来不及做的事:
- 大规模损毁分级:4.5万平方米的壁画,一人一日只能巡查约50平方米。AI 可以在数小时内给出全量的损毁评分和修复优先级排序。
- 跨朝代风格对比:通过知识图谱,专家可以在几秒内找到"所有唐代绘有双手合十菩萨的壁画"——这在纯人工检索中需要翻阅数千张照片。
- 褪色复原评估:AI 推断的复原图提供给修复专家参考,而不是用于决策。
四、个性化边界权衡
自动化 vs 专家复核
AI 的年代判断准确率约 75-80%(在明确的风格特征下),但对过渡期(如北周-隋之间)的准确率降至 50%。解决的现实方法是:AI 标注置信度,高置信度(>0.9)的自动入库,中等置信度(0.7-0.9)的标注提示供专家快速确认,低置信度(<0.7)的完全流转给专家人工鉴定。
损毁检测灵敏度 vs 误报率
损毁检测过敏感会把正常的绘制笔触误判为裂痕(尤其是在飞天的衣纹线条中)。敦煌壁画中有些"剥落"实际上是历史上的人为铲除(灭佛运动所致)。这些"非自然的损毁"在修复时可能有不同的处理策略——AI 需要学会区分自然老化和人为破坏。
数据隐私 vs 开放共享
高清壁画数据是文化遗产机构的数字资产,开放使用有助于学术研究但涉及版权和保护措施。AI 分析过程中需要进行数据脱敏,并在模型输出中去掉精确的地理坐标和洞窟内部保护措施信息。
五、总结
敦煌壁画 AI 理解采用多模态方法整合视觉分析、文本识别、颜料分析、年代归类四个维度的信息。视觉分析需覆盖人物检测、风格分类、损毁检测、色彩分析四路任务。文本分析通过古文 OCR 与经文内容匹配辅助场景识别。损毁评估采用八种损毁类型的加权评分体系,剥落和酥碱权重最高。知识图谱将壁画、人物、技法、年代四大类节点关联,支持多维度检索。实施中需权衡自动化与专家复核的分工、损毁检测灵敏度与误报控制、数据开放与版权保护的边界。核心价值在于自动化完成大规模壁画保护工作中的重复性评估,让专家专注于决策和修复方案设计。