DICOM格式深度解析:超声AI数据处理全流程实战
医疗AI的落地离不开对DICOM标准的深刻理解。作为医学影像领域的"通用语言",DICOM不仅定义了图像存储格式,更构建了完整的信息交换体系。本文将从字节级解析到AI工程实践,系统讲解超声DICOM数据的处理要点,帮助开发者避开临床部署中的常见陷阱。
一、DICOM标准架构:不只是图像格式
1.1 DICOM的核心组成
DICOM(Digital Imaging and Communications in Medicine)远不止是一个图像文件格式,它是一个完整的医疗信息交换标准,包含三个关键层面:
| 层级 | 功能 | 技术要点 |
| 文件格式层 | 定义像素数据与元数据的封装方式 | DICOM Part 10: 文件格式规范,支持多帧、压缩、私有标签 |
| 网络协议层 | 规范设备间的数据传输 | DICOM Part 7-8: DIMSE服务与DICOM Web(RESTful API) |
| 结构化报告层 | 标准化诊断报告的编码 | DICOM SR: 支持测量值、发现、评估的机器可读格式 |
1.2 超声DICOM的特殊性
超声影像相比CT/MRI具有独特特性,这些特性直接影响AI处理策略:
多帧动态特性:超声DICOM常包含Cine Loop(电影回放),即时间序列的多帧图像。标准DICOM通过 Number of Frames (0028,0008)标签指示帧数,像素数据按时间顺序存储。
测量与注释叠加:超声设备常在图像上叠加测量卡尺(Calipers)、文本标签、增益曲线等UI元素。这些非解剖结构信息若不做清洗,会成为AI模型的干扰源。
私有标签泛滥:不同厂商(GE、Philips、Siemens、Canon)大量使用私有标签存储设备参数、图像处理参数,解析时需要特定的数据字典。
复合数据类型:除B模式灰阶图像外,还可能包含Color Doppler(彩色多普勒)、M-mode、弹性成像等多种数据类型,通过 Photometric Interpretation 标签区分。
二、DICOM数据解析:从字节到像素
2.1 文件结构解析
DICOM文件采用标签-值(Tag-Value)结构,每个标签由4字节组成(组号+元素号),后跟VR(Value Representation)和长度字段。
import pydicom
from pydicom.dataset import FileDataset
import numpy as np
def parse_ultrasound_dicom(file_path):
"""
解析超声DICOM文件,提取关键元数据与像素数据
"""
ds = pydicom.dcmread(file_path, force=True)
# 核心元数据提取
metadata = {
'patient_id': ds.get('PatientID', 'Unknown'),
'study_date': ds.get('StudyDate', 'Unknown'),
'modality': ds.get('Modality', 'Unknown'), # US = Ultrasound
'manufacturer': ds.get('Manufacturer', 'Unknown'),
'model': ds.get('ManufacturerModelName', 'Unknown'),
# 超声特有参数
'transducer_frequency': ds.get('TransducerFrequency', None), # 探头频率
'sampling_frequency': ds.get('SamplingFrequency', None), # 采样频率
'depth': ds.get('DepthOfScanField', None), # 扫描深度
# 图像维度
'rows': ds.Rows,
'columns': ds.Columns,
'number_of_frames': int(ds.get('NumberOfFrames', 1)),
'photometric_interpretation': ds.PhotometricInterpretation, # MONOCHROME2, RGB等
# 像素间距(物理尺度校准,关键!)
'pixel_spacing': ds.get('PixelSpacing', None), # [row_spacing, col_spacing] in mm
'imager_pixel_spacing': ds.get('ImagerPixelSpacing', None),
}
# 像素数据提取(处理多帧)
pixel_array = ds.pixel_array # shape: (frames, rows, cols) 或 (rows, cols)
# 像素值转换:存储值 -> 真实像素值(考虑RescaleSlope/Intercept)
if 'RescaleSlope' in ds and 'RescaleIntercept' in ds:
pixel_array = pixel_array * ds.RescaleSlope + ds.RescaleIntercept
# 窗宽窗位调整(VOI LUT)
if 'WindowCenter' in ds and 'WindowWidth' in ds:
center = ds.WindowCenter
width = ds.WindowWidth
if isinstance(center, pydicom.multival.MultiValue):
center = center[0]
if isinstance(width, pydicom.multival.MultiValue):
width = width[0]
min_val = center - width / 2
max_val = center + width / 2
pixel_array = np.clip(pixel_array, min_val, max_val)
pixel_array = (pixel_array - min_val) / width * 255.0
return metadata, pixel_array.astype(np.uint8)
# 使用示例
meta, pixels = parse_ultrasound_dicom('heart_echo.dcm')
print(f"影像维度: {pixels.shape}, 探头频率: {meta['transducer_frequency']} MHz")
2.2 关键标签详解
超声测量相关标签(DICOM SR核心):
(0018,5010) : 灰阶软拷贝呈现状态查找表描述
(0028,3002) : LUT描述符(用于像素值映射)
(0018,9807) : 超声颜色数据呈现(Color Data Type)
(0018,6024) : 区域位置(Region Location Min X0/Y0, Max X1/Y1)
多帧同步标签:
(0028,0008) : Number of Frames
(0018,1063) : Frame Time (每帧时间间隔,微秒)
(0028,0009) : Frame Increment Pointer(指示帧间变化参数)
三、AI预处理流水线:从原始DICOM到模型输入
3.1 超声DICOM的特有预处理挑战
根据最新研究,超声AI预处理需解决以下核心问题:
1. 去标识化与隐私保护 DICOM包含丰富的患者信息(姓名、ID、出生日期等),必须在训练前彻底脱敏:
from pydicom import dcmread
from pydicom.uid import generate_uid
def anonymize_dicom(input_path, output_path):
"""
DICOM去标识化:保留影像数据,移除患者隐私信息
"""
ds = dcmread(input_path)
# 删除患者信息标签
private_tags = [
'PatientName', 'PatientID', 'PatientBirthDate',
'PatientSex', 'PatientAge', 'PatientWeight',
'InstitutionName', 'ReferringPhysicianName',
'PerformingPhysicianName', 'OperatorsName'
]
for tag in private_tags:
if tag in ds:
delattr(ds, tag)
# 生成新的Study/Series/Instance UID,切断关联
ds.StudyInstanceUID = generate_uid()
ds.SeriesInstanceUID = generate_uid()
ds.SOPInstanceUID = generate_uid()
# 清空私有标签(组号为奇数的标签)
for elem in list(ds.elements()):
if elem.tag.group % 2 == 1: # 私有标签组号为奇数
del ds[elem.tag]
ds.save_as(output_path)
2. 去除屏幕叠加层(De-annotation)
超声图像常包含设备UI叠加(测量卡尺、文本、图标),这些必须清除以避免数据泄露:
import cv2
import numpy as np
from skimage import morphology
def remove_ultrasound_overlays(image):
"""
去除超声图像中的UI叠加层(彩色标记、文本区域)
基于颜色空间分析与形态学操作
"""
# 转换为HSV颜色空间,便于检测彩色UI元素
if len(image.shape) == 3:
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
# 检测高饱和度区域(彩色标记)
saturation = hsv[:, :, 1]
color_mask = saturation > 50 # 饱和度阈值
# 形态学闭运算填充小孔
color_mask = morphology.binary_closing(color_mask, morphology.disk(3))
# 创建掩码:保留灰度区域,去除彩色区域
gray_mask = ~color_mask
# 对掩码区域进行修复(Inpainting)
inpainted = cv2.inpaint(image, color_mask.astype(np.uint8) * 255, 3, cv2.INPAINT_TELEA)
return inpainted
return image # 单通道图像无需处理
def crop_ultrasound_region(image):
"""
裁剪有效超声区域,去除黑色边框与文字区域
基于Otsu阈值与最大连通域分析[^19^]
"""
# 灰度化(如为彩色)
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
else:
gray = image
# Otsu自动阈值二值化
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 寻找最大连通域(假设为超声扇形/矩形区域)
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary, connectivity=8)
# 过滤小区域,保留主体
min_area = image.shape[0] * image.shape[1] * 0.1 # 至少占图像10%
largest_label = 1
largest_area = stats[1, cv2.CC_STAT_AREA]
for i in range(2, num_labels):
if stats[i, cv2.CV_STAT_AREA] > largest_area and stats[i, cv2.CC_STAT_AREA] > min_area:
largest_label = i
largest_area = stats[i, cv2.CC_STAT_AREA]
# 创建掩码并裁剪
mask = (labels == largest_label).astype(np.uint8) * 255
x, y, w, h = stats[largest_label, cv2.CC_STAT_LEFT], stats[largest_label, cv2.CC_STAT_TOP], \
stats[largest_label, cv2.CC_STAT_WIDTH], stats[largest_label, cv2.CC_STAT_HEIGHT]
cropped = image[y:y+h, x:x+w]
return cropped
3. 标准化处理流水线
综合多项研究,标准的超声DICOM预处理流:
import albumentations as A
from albumentations.pytorch import ToTensorV2
class UltrasoundPreprocessor:
"""
超声DICOM标准化预处理类
支持单帧与多帧(Cine Loop)处理
"""
def __init__(self, target_size=(224, 224), normalize=True):
self.target_size = target_size
self.normalize = normalize
# 训练时的数据增强
self.train_transform = A.Compose([
A.Resize(target_size[0], target_size[1]),
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2, rotate_limit=15, p=0.5),
A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),
A.GaussNoise(var_limit=(10.0, 50.0), p=0.2),
A.Blur(blur_limit=3, p=0.2),
A.Normalize(mean=0.5, std=0.5) if normalize else A.NoOp(),
ToTensorV2()
])
# 推理时的预处理(无增强)
self.infer_transform = A.Compose([
A.Resize(target_size[0], target_size[1]),
A.Normalize(mean=0.5, std=0.5) if normalize else A.NoOp(),
ToTensorV2()
])
def process_dicom(self, dicom_path, mode='infer', extract_key_frames=False):
"""
处理DICOM文件,返回模型输入张量
Args:
dicom_path: DICOM文件路径
mode: 'train' 或 'infer'
extract_key_frames: 是否从多帧中提取关键帧(如收缩期/舒张期)
"""
# 1. 解析DICOM
meta, pixel_array = parse_ultrasound_dicom(dicom_path)
# 2. 处理多帧:提取关键帧或平均池化
if meta['number_of_frames'] > 1:
if extract_key_frames:
# 基于光流或图像质量选择关键帧
frames = self._extract_key_frames(pixel_array, n_frames=3)
else:
# 简单平均或采样中间帧
frames = [pixel_array[len(pixel_array)//2]] # 取中间帧
else:
frames = [pixel_array] if pixel_array.ndim == 2 else pixel_array
# 3. 逐帧预处理
processed_frames = []
for frame in frames:
# 去UI叠加
frame = remove_ultrasound_overlays(frame)
# 裁剪有效区域
frame = crop_ultrasound_region(frame)
# 归一化到0-255
frame = self._normalize_intensity(frame)
# 转换为3通道(复制灰度)
if len(frame.shape) == 2:
frame = np.stack([frame] * 3, axis=-1)
# 应用变换
transform = self.train_transform if mode == 'train' else self.infer_transform
transformed = transform(image=frame)
processed_frames.append(transformed['image'])
# 4. 堆叠为多帧张量 (T, C, H, W) 或单帧 (C, H, W)
if len(processed_frames) > 1:
return torch.stack(processed_frames), meta
else:
return processed_frames[0], meta
def _normalize_intensity(self, image):
"""强度归一化:基于百分位数去噪,Min-Max归一化"""
# 去除极值(死像素、饱和区)
p2, p98 = np.percentile(image, (2, 98))
image = np.clip(image, p2, p98)
# Min-Max归一化到0-255
image = (image - image.min()) / (image.max() - image.min()) * 255.0
return image.astype(np.uint8)
def _extract_key_frames(self, video_frames, n_frames=3):
"""基于图像质量指标提取关键帧(如清晰度、对比度)"""
quality_scores = []
for frame in video_frames:
# 使用拉普拉斯算子评估清晰度
laplacian_var = cv2.Laplacian(frame, cv2.CV_64F).var()
quality_scores.append(laplacian_var)
# 选择质量最高的n_frames帧
top_indices = np.argsort(quality_scores)[-n_frames:]
top_indices = sorted(top_indices) # 保持时序
return [video_frames[i] for i in top_indices]
四、DICOM SR:AI结果的标准化输出
4.1 为什么需要DICOM SR?
AI模型输出的结果(分割掩码、测量值、分类概率)需要无缝集成到临床工作流。传统方式(PDF报告、DICOM Secondary Capture截图)存在明显缺陷:
信息孤岛:截图无法被其他系统解析利用
转录错误:医生需手动将AI测量值录入报告,效率低且易出错
无法追溯:缺乏标准化的置信度、算法版本等元数据
DICOM Structured Report (SR) 解决了上述问题,它将AI发现编码为机器可读、可索引、可互操作的标准化格式。
4.2 DICOM SR架构
DICOM SR采用树状结构组织内容,每个节点包含:
概念名称(Concept Name):SNOMED CT或DICOM标准编码,定义"这是什么"
概念值(Concept Value):实际数据(编码值、数值、文本、坐标等)
关系类型(Relationship Type):与父节点的关系(CONTAINS, HAS OBS CONTEXT, INFERRED FROM等)
典型SR内容结构:
Document Title (US Report)
├── Patient Information (CONTAINER)
│ ├── Patient Name
│ └── Patient ID
├── Findings (CONTAINER)
│ ├── Finding #1 (CONTAINER)
│ │ ├── Finding Site (CODE): Left Ventricle
│ │ ├── Finding Type (CODE): Wall Motion Abnormality
│ │ ├── Probability (NUM): 0.92
│ │ └── Bounding Box (SCOORD): (x1,y1,x2,y2)
│ └── Finding #2 (CONTAINER)
│ ├── Finding Site (CODE): Aortic Valve
│ └── ...
└── Measurements (CONTAINER)
├── Ejection Fraction (NUM): 58%
└── Left Ventricular Mass (NUM): 145g
4.3 创建AI DICOM SR的Python实现
from pydicom import Dataset
from pydicom.sr.codedict import codes # SNOMED CT编码
from pydicom.sr.coding import Code
from pydicom.uid import ExplicitVRLittleEndian, generate_uid
def create_ai_ultrasound_sr(original_dicom, ai_findings, output_path):
"""
创建包含AI发现的DICOM SR
Args:
original_dicom: 原始超声DICOM数据集(用于引用)
ai_findings: AI模型输出结果列表,每项包含:
- finding_type: 发现类型编码
- site: 解剖部位编码
- probability: 置信度
- bbox: 边界框坐标 (x1, y1, x2, y2)
- measurement: 测量值(可选)
"""
# 创建SR数据集
sr = Dataset()
# 文件元信息
sr.file_meta = Dataset()
sr.file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.88.33' # Comprehensive 3D SR
sr.file_meta.MediaStorageSOPInstanceUID = generate_uid()
sr.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
# 患者与研究信息(从原DICOM复制)
sr.PatientName = original_dicom.PatientName
sr.PatientID = original_dicom.PatientID
sr.StudyInstanceUID = original_dicom.StudyInstanceUID
sr.SeriesInstanceUID = generate_uid() # 新Series
sr.SOPClassUID = sr.file_meta.MediaStorageSOPClassUID
sr.SOPInstanceUID = sr.file_meta.MediaStorageSOPInstanceUID
# SR特定标签
sr.Modality = 'SR'
sr.CompletionFlag = 'COMPLETE'
sr.VerificationFlag = 'UNVERIFIED' # AI结果待医生确认
sr.ContentDate = datetime.now().strftime('%Y%m%d')
sr.ContentTime = datetime.now().strftime('%H%M%S')
# 创建内容序列(Content Sequence)
content_sequence = []
# 1. 文档标题
title_item = Dataset()
title_item.RelationshipType = 'HAS CONCEPT MOD'
title_item.ValueType = 'CODE'
title_item.ConceptNameCodeSequence = [Code('121144', 'DCM', 'Document Title')]
title_item.ConceptCodeSequence = [Code('126000', 'DCM', 'US Procedure Report')]
content_sequence.append(title_item)
# 2. 添加AI发现
for idx, finding in enumerate(ai_findings, 1):
# 发现容器
finding_container = Dataset()
finding_container.RelationshipType = 'CONTAINS'
finding_container.ValueType = 'CONTAINER'
finding_container.ConceptNameCodeSequence = [Code('121070', 'DCM', 'Finding')]
finding_container.ContinuityOfContent = 'SEPARATE'
# 子内容:发现类型
type_item = Dataset()
type_item.RelationshipType = 'CONTAINS'
type_item.ValueType = 'CODE'
type_item.ConceptNameCodeSequence = [Code('121071', 'DCM', 'Finding Type')]
type_item.ConceptCodeSequence = [Code(
finding['finding_type_code'],
finding['finding_type_scheme'],
finding['finding_type_meaning']
)]
# 子内容:解剖部位
site_item = Dataset()
site_item.RelationshipType = 'CONTAINS'
site_item.ValueType = 'CODE'
site_item.ConceptNameCodeSequence = [Code('G-C0E3', 'SRT', 'Finding Site')]
site_item.ConceptCodeSequence = [Code(
finding['site_code'],
finding['site_scheme'],
finding['site_meaning']
)]
# 子内容:AI置信度
prob_item = Dataset()
prob_item.RelationshipType = 'CONTAINS'
prob_item.ValueType = 'NUM'
prob_item.ConceptNameCodeSequence = [Code('121401', 'DCM', 'Probability')]
prob_item.MeasuredValueSequence = [Dataset()]
prob_item.MeasuredValueSequence[0].NumericValue = finding['probability']
prob_item.MeasuredValueSequence[0].MeasurementUnitsCodeSequence = [
Code('%', 'UCUM', 'percent')
]
# 子内容:图像引用(关联到原图)
image_ref_item = Dataset()
image_ref_item.RelationshipType = 'INFERRED FROM'
image_ref_item.ValueType = 'IMAGE'
image_ref_item.ReferencedSOPSequence = [Dataset()]
image_ref_item.ReferencedSOPSequence[0].ReferencedSOPClassUID = original_dicom.SOPClassUID
image_ref_item.ReferencedSOPSequence[0].ReferencedSOPInstanceUID = original_dicom.SOPInstanceUID
# 子内容:空间坐标(边界框)
if 'bbox' in finding:
bbox_item = Dataset()
bbox_item.RelationshipType = 'CONTAINS'
bbox_item.ValueType = 'SCOORD'
bbox_item.ConceptNameCodeSequence = [Code('121191', 'DCM', 'Bounding Box')]
bbox_item.GraphicType = 'POLYLINE'
# 多边形坐标: (x1,y1), (x2,y1), (x2,y2), (x1,y2), (x1,y1)
x1, y1, x2, y2 = finding['bbox']
bbox_item.GraphicData = [x1, y1, x2, y1, x2, y2, x1, y2, x1, y1]
bbox_item.ContentSequence = [image_ref_item] # 关联到图像
# 组装发现容器
finding_container.ContentSequence = [type_item, site_item, prob_item]
if 'bbox' in finding:
finding_container.ContentSequence.append(bbox_item)
content_sequence.append(finding_container)
# 3. 添加算法信息(可追溯性)
algo_item = Dataset()
algo_item.RelationshipType = 'CONTAINS'
algo_item.ValueType = 'CONTAINER'
algo_item.ConceptNameCodeSequence = [Code('121422', 'DCM', 'Algorithm Identification')]
algo_name = Dataset()
algo_name.RelationshipType = 'CONTAINS'
algo_name.ValueType = 'TEXT'
algo_name.ConceptNameCodeSequence = [Code('121423', 'DCM', 'Algorithm Name')]
algo_name.TextValue = 'EchoNet-Dynamic v2.1'
algo_version = Dataset()
algo_version.RelationshipType = 'CONTAINS'
algo_version.ValueType = 'TEXT'
algo_version.ConceptNameCodeSequence = [Code('121424', 'DCM', 'Algorithm Version')]
algo_version.TextValue = '2.1.0'
algo_item.ContentSequence = [algo_name, algo_version]
content_sequence.append(algo_item)
# 设置内容序列
sr.ContentSequence = content_sequence
# 保存
sr.save_as(output_path)
return sr
# 使用示例
ai_results = [
{
'finding_type_code': '414165007',
'finding_type_scheme': 'SCT',
'finding_type_meaning': 'Left ventricular systolic dysfunction',
'site_code': '87878005',
'site_scheme': 'SCT',
'site_meaning': 'Left ventricle',
'probability': 0.89,
'bbox': (120, 80, 400, 320) # 像素坐标
}
]
create_ai_ultrasound_sr(original_ds, ai_results, 'ai_report.dcm')
五、临床集成与工作流
5.1 AI到SR的自动化流水线
根据最新临床研究,完整的AI-SR集成工作流应包含:
┌─────────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ 超声设备采集 │ -> │ PACS存储 │ -> │ AI分析节点 │ -> │ DICOM SR生成 │
│ (DICOM C-Store)│ │ │ │ (GPU推理) │ │ (结构化报告) │
└─────────────────┘ └─────────────┘ └─────────────┘ └─────────────────┘
│
v
┌─────────────────┐ ┌─────────────┐ ┌─────────────────────────┐
│ 医生审核确认 │ <- │ 报告工作站 │ <- │ SR导入结构化报告平台 │
│ (修改/拒绝/确认)│ │ │ │ (如PowerScribe) │
└─────────────────┘ └─────────────┘ └─────────────────────────┘
关键集成点:
DICOM路由:使用DICOM Router(如Orthanc、DCMTK)自动转发检查到AI节点
SR网关:通过DICOM SR Gateway映射AI输出到报告模板
双向确认:医生可修改AI结果,修改后的数据应反馈用于模型迭代
5.2 性能与效果验证
研究表明,采用DICOM SR的AI集成相比传统方式具有显著优势:
| 指标 | 自由文本报告 | 传统结构化报告 | AI+SR流水线 |
| 平均报告时间 | 85.8秒 | 85.6秒 | 66.8秒 (p<0.001) |
| 报告完整性评分 | 3.18/5 | 4.41/5 | 4.51/5 (p<0.001) |
| 数据转录错误 | 高(手动输入) | 中 | 低(自动填充) |
| 二次利用能力 | 差(非结构化) | 中 | 优(标准化编码) |
六、工程实践建议
6.1 开源工具链推荐
| 工具 | 用途 | 推荐场景 |
| pydicom | DICOM解析/创建 | 所有Python DICOM操作 |
| SimpleITK | 医学图像处理 | 重采样、滤波、格式转换 |
| Orthanc | DICOM服务器/路由 | 测试环境PACS模拟 |
| DCMTK | DICOM网络传输 | C-Store/SCU/SCP实现 |
| highdicom | DICOM SR/SEG创建 | 结构化报告与分割对象 |
| MONAI | 医学AI框架 | 端到端深度学习流水线 |
6.2 常见陷阱与对策
1. 像素间距(Pixel Spacing)不一致
问题:不同设备、不同深度设置导致物理尺度差异
对策:始终读取 PixelSpacing 或 ImagerPixelSpacing ,将像素坐标转换为物理坐标(mm)后再进行测量
2. 多帧内存爆炸
问题:心脏超声Cine Loop可能包含100+帧,直接加载导致OOM
对策:采用惰性加载(lazy loading)或帧采样策略,非关键帧不进入内存
3. 私有标签解析失败
问题:厂商私有标签(如GE的 0019,xx10 )无标准字典
对策:维护厂商特定的数据字典,或使用 ds.private_block 接口动态解析
4. 压缩传输语法兼容性
问题:设备使用JPEG 2000、RLE等压缩,pydicom默认不支持
对策:安装 pylibjpeg 插件,或转储为未压缩格式处理
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)