在前面的文章中,我们系统学习了DICOM的基础(文件结构、网络传输)、Web接口(DICOMweb)、以及与AI的集成(从像素数据到智能诊断)。这些内容覆盖了医学影像的存储、共享、分析与智能化等核心场景。但在真实的医疗环境中,DICOM的功能远不止于此——它还提供了一系列高级服务,用于保障影像数据的可靠性(如存储提交验证)和拓展应用场景(如硬拷贝打印)。
其中,存储提交服务(Storage Commitment)和打印服务(Print Management)是两类典型的高级服务:
存储提交服务:解决“影像真的被安全存储了吗?”的问题,通过DICOM协议确认图像已被目标系统(如PACS)持久化保存,避免因网络故障或存储错误导致数据丢失;
打印服务:支持将医学影像(如X光片、CT关键切片)直接输出到医用胶片打印机,生成符合临床诊断要求的硬拷贝(物理胶片),满足部分场景下医生对物理影像的需求。
本文将深入解析这两类高级服务的原理、应用场景,并通过工具实操与代码示例(如pydicom和Orthanc),带你体验如何在实际系统中调用它们。
在常规的DICOM存储流程(C-STORE)中,发送方(如CT设备)将图像发送给接收方(如PACS服务器),接收方返回“存储成功”的响应。但这个响应仅表示“我收到了数据”,并不保证数据已被持久化到磁盘(例如接收方可能只是暂存到内存,后续因崩溃或断电丢失)。
存储提交服务(Storage Commitment Service Class) 解决的就是这个信任问题:
发送方在完成C-STORE后,可以主动发起一个“存储提交请求”,要求接收方书面承诺:“我已经把这张图像安全地写到硬盘里了,即使系统崩溃也不会丢”。如果接收方无法承诺(例如存储失败),则会明确通知发送方。
典型应用场景:
法规合规:某些国家/地区的医疗法规要求关键影像(如急诊CT)必须确保存储可靠性;
数据备份:基层医院上传影像到云端PACS后,需要确认云端已真正保存,避免本地删除后云端丢失;
系统可靠性验证:在重要检查(如手术规划MRI)中,确保影像不会因中间环节故障而消失。
存储提交服务通过DICOM消息(N-ACTION和N-EVENT-REPORT)实现,分为两个阶段:
阶段1:C-STORE(常规存储)
发送方(SCU)将DICOM图像发送给接收方(SCP),接收方返回“存储成功”的响应(但此时可能未持久化)。
阶段2:存储提交请求(N-ACTION)
发送方向接收方发起“存储提交请求”,包含之前存储的图像的唯一标识符(SOP Instance UID列表),询问:“这些图像真的存好了吗?”。
接收方检查这些图像是否已持久化(例如写入磁盘),若成功则返回“承诺”(确认存储);
若失败(例如磁盘满、写入错误),则返回“拒绝”并说明原因。
阶段3(可选):事件报告(N-EVENT-REPORT)
接收方后续可能主动通知发送方存储状态的变化(例如延迟持久化后补发确认)。
Orthanc是一个支持存储提交服务的高级PACS服务器(默认配置可能需手动开启)。以下是验证步骤:
登录Orthanc的Web管理界面(http://localhost:8042),进入 “Settings” → “DICOM” → “Storage Commitment”,确保以下选项启用:
Enable Storage Commitment:勾选(默认可能关闭);
Storage Commitment SCP Port:指定接收存储提交请求的端口(通常与主端口分开,如10404)。
重启Orthanc使配置生效。
我们可以用Python的 pynetdicom 库模拟一个发送方(SCU),完成以下操作:
先通过C-STORE将DICOM文件发送到Orthanc;
再通过存储提交服务(N-ACTION)请求Orthanc确认该图像已持久化。
from pynetdicom import AE, StoragePresentationContexts, evt
from pynetdicom.sop_class import CTImageStorage, StorageCommitmentPushModel
import pydicom
# 1. 配置AE(应用实体)——发送方(SCU)
ae = AE()
# 添加支持的上下文:CT图像存储(用于C-STORE)和存储提交(用于N-ACTION)
ae.add_requested_context(CTImageStorage) # C-STORE的SOP类
ae.add_requested_context(StorageCommitmentPushModel) # 存储提交的SOP类
# 2. 读取本地DICOM文件
dcm_file = "example.dcm" # 替换为你的DICOM文件路径
dataset = pydicom.dcmread(dcm_file)
# 3. 连接到Orthanc服务器(假设Orthanc主服务端口104,存储提交服务端口10404)
pacs_ip = '127.0.0.1'
pacs_port_main = 104 # 主存储服务端口(C-STORE)
pacs_port_commit = 10404 # 存储提交服务端口(N-ACTION)
# --- 阶段1:C-STORE(存储图像到Orthanc)---
assoc_store = ae.associate(pacs_ip, pacs_port_main)
if assoc_store.is_established:
print("[阶段1] 正在通过C-STORE发送DICOM文件到Orthanc...")
status_store = assoc_store.send_c_store(dataset)
if status_store and status_store.Status in [0x0000, 0xB000]: # 0x0000=成功,0xB000=警告(部分成功)
print("[阶段1] C-STORE成功!图像已临时存储(待提交确认)")
sop_instance_uid = dataset.SOPInstanceUID # 获取该图像的唯一标识符
else:
print("[阶段1] C-STORE失败!状态码:", status_store.Status if status_store else "无响应")
exit()
assoc_store.release()
else:
print("[阶段1] 无法连接到Orthanc主服务!请检查IP和端口(默认104)")
exit()
# --- 阶段2:存储提交请求(N-ACTION)---
# 构造存储提交的请求数据(包含要验证的SOP Instance UID列表)
commit_dataset = pydicom.Dataset()
commit_dataset.TransactionUID = pydicom.uid.generate_uid() # 本次提交的唯一事务ID
commit_dataset.ReferencedSOPSequence = [
pydicom.Dataset({
'ReferencedSOPClassUID': CTImageStorage, # 引用的SOP类(与存储的图像类型一致)
'ReferencedSOPInstanceUID': sop_instance_uid # 引用的具体图像UID
})
]
# 关联到Orthanc的存储提交服务端口
assoc_commit = ae.associate(pacs_ip, pacs_port_commit)
if assoc_commit.is_established:
print("[阶段2] 正在向Orthanc发起存储提交请求...")
# 发送N-ACTION请求(Action Type ID=1表示“提交请求”)
status_commit = assoc_commit.send_n_action(
1, # Action Type ID: 1=提交请求
StorageCommitmentPushModel, # 服务类
commit_dataset
)
if status_commit and status_commit.Status in [0x0000, 0xB000]:
print("[阶段2] 存储提交成功!Orthanc确认图像已持久化")
# 可进一步解析status_commit.dataset获取详细信息(如失败原因)
else:
print("[阶段2] 存储提交失败!状态码:", status_commit.Status if status_commit else "无响应")
if status_commit and hasattr(status_commit, 'dataset'):
print("失败详情:", status_commit.dataset)
assoc_commit.release()
else:
print("[阶段2] 无法连接到Orthanc存储提交服务!请检查端口(默认10404)")
代码说明:
阶段1:通过C-STORE将DICOM文件发送到Orthanc(端口104),获取该图像的SOPInstanceUID(唯一标识符);
阶段2:通过存储提交服务(端口10404)向Orthanc发送N-ACTION请求,询问该SOPInstanceUID对应的图像是否已持久化;
结果验证:若Orthanc返回状态码0x0000,表示“已承诺存储”;若返回0xA700(拒绝)或0xCxxx(错误),则表示存储可能失败。
📌 注意:
Orthanc默认可能未启用存储提交服务(端口10404),需手动在配置文件(
Orthanc.json)中开启:{ "StorageCommitment": { "Enable": true, "Port": 10404 } }若测试环境无真实存储风险(如本地测试),存储提交可能总是返回成功(因为Orthanc默认会快速持久化),但在生产环境中(如网络不稳定或存储故障),该服务能有效发现问题。
尽管现代医学影像主要通过显示器查看(如PACS工作站、Web查看器),但在某些场景下,医生仍需要物理胶片(如手术室中的X光片、基层医院的传统阅片习惯)。DICOM打印服务(Print Management Service Class)就是为医学影像输出到医用胶片打印机而设计的标准协议。
打印服务支持:
将DICOM图像(如X光、CT切片)发送到胶片打印机,并按临床要求排版(如多张图像拼接在一张胶片上);
控制打印参数(如胶片尺寸、分辨率、对比度、标注文字);
验证打印机状态(如墨盒余量、纸张是否就绪)。
典型应用场景:
急诊手术:快速打印关键CT切片作为术中参考;
基层医院:通过胶片向患者展示检查结果(部分患者更信任物理影像);
教学与科研:打印典型病例的影像用于教学演示。
DICOM打印服务涉及三个核心角色:
SCU(Service Class User):发起打印请求的设备(如医生工作站、PACS系统);
SCP(Service Class Provider):提供打印服务的设备(如医用胶片打印机,如Agfa、Konica Minolta的专业打印机);
Film Session(胶片会话)与 Film Box(胶片盒):打印任务的逻辑单元(一个Film Session可包含多张Film Box,每张Film Box对应一张胶片上的图像布局)。
典型打印流程:
SCU向SCP查询打印机状态(确认打印机在线、有纸、墨盒正常);
SCU创建打印任务:定义Film Session(整体任务参数,如打印份数)和Film Box(单张胶片的图像与排版参数,如图像位置、窗宽窗位);
SCU发送DICOM图像到指定的Film Box;
SCU触发打印指令,SCP将图像输出到胶片打印机。
由于医用胶片打印机通常需要专用硬件(且配置复杂),本文通过模拟场景演示打印服务的核心逻辑(使用pydicom构造打印请求,但实际执行需连接真实打印机)。
确保你有一个支持DICOM打印服务的医用胶片打印机(如Agfa DryStar 5503、Konica Minolta Bizhub Medical),并已配置好DICOM网络参数(IP地址、端口通常为104或自定义)。
以下代码展示了如何用pydicom构造一个打印请求(包含Film Session和Film Box),并发送到打印机的DICOM服务端口(假设打印机IP为192.168.1.100,端口为104):
from pynetdicom import AE, PrintManagementPresentationContexts
from pynetdicom.sop_class import BasicFilmSessionSOPClass, BasicFilmBoxSOPClass, BasicGrayscaleImageBoxSOPClass
import pydicom
# 1. 配置AE(应用实体)——打印请求发起方(SCU)
ae = AE()
# 添加支持的上下文:打印会话、打印盒、图像盒
ae.add_requested_context(BasicFilmSessionSOPClass) # 胶片会话
ae.add_requested_context(BasicFilmBoxSOPClass) # 胶片盒(单张胶片)
ae.add_requested_context(BasicGrayscaleImageBoxSOPClass) # 灰度图像盒(单张图像)
# 2. 读取要打印的DICOM图像(例如一张X光片)
dcm_file = "xray_example.dcm" # 替换为你的X光/DICOM文件路径
dataset = pydicom.dcmread(dcm_file)
# 3. 连接到打印机的DICOM服务端口(假设打印机IP和端口)
printer_ip = '192.168.1.100' # 替换为你的打印机IP
printer_port = 104 # 通常为104(需根据打印机配置调整)
# --- 步骤1:创建Film Session(胶片会话,定义整体任务参数)---
film_session_ds = pydicom.Dataset()
film_session_ds.SOPClassUID = BasicFilmSessionSOPClass
film_session_ds.SOPInstanceUID = pydicom.uid.generate_uid() # 本次会话的唯一ID
film_session_ds.ConfigurationStatus = 'CONFIRMED' # 会话状态(已确认)
film_session_ds.PrintPriority = 'MEDIUM' # 打印优先级(LOW/MEDIUM/HIGH)
film_session_ds.MediumType = 'BLUE-FILM' # 胶片类型(如蓝片、白片)
film_session_ds.FilmDestination = 'MAGAZINE' # 胶片输出位置(如暗盒、托盘)
film_session_ds.FilmSessionLabel = '急诊X光打印' # 会话标签(可选)
# 关联到打印机并发送Film Session创建请求
assoc_print = ae.associate(printer_ip, printer_port)
if assoc_print.is_established:
print("[步骤1] 正在向打印机创建胶片会话...")
status_session = assoc_print.send_n_create(
film_session_ds.SOPInstanceUID,
BasicFilmSessionSOPClass,
film_session_ds
)
if status_session and status_session.Status in [0x0000, 0xB000]:
print("[步骤1] 胶片会话创建成功!Session UID:", film_session_ds.SOPInstanceUID)
session_uid = film_session_ds.SOPInstanceUID
else:
print("[步骤1] 胶片会话创建失败!状态码:", status_session.Status if status_session else "无响应")
assoc_print.release()
exit()
assoc_print.release()
else:
print("[步骤1] 无法连接到打印机!请检查IP和端口")
exit()
# --- 步骤2:创建Film Box(胶片盒,定义单张胶片的图像与排版)---
film_box_ds = pydicom.Dataset()
film_box_ds.SOPClassUID = BasicFilmBoxSOPClass
film_box_ds.SOPInstanceUID = pydicom.uid.generate_uid() # 本次胶片盒的唯一ID
film_box_ds.ImageDisplayFormat = 'STANDARD\1,1' # 图像排版格式(1张图像占满胶片)
film_box_ds.AnnotationDisplayFormatID = 'ANNOTATION' # 是否显示标注
film_box_ds.FilmOrientation = 'PORTRAIT' # 胶片方向(竖向/横向)
film_box_ds.FilmSizeID = '8INX10IN' # 胶片尺寸(如8x10英寸)
film_box_ds.MagnificationType = 'NONE' # 放大类型(无放大)
film_box_ds.RequestedImageSize = '2048x2048' # 请求的图像尺寸(像素)
film_box_ds.ConfigurationStatus = 'CONFIRMED' # 胶片盒状态
# 关联到之前创建的Film Session(通过Session SOP Instance UID)
film_box_ds.ReferencedFilmSessionSOPInstanceUID = session_uid
# --- 步骤3:添加图像到Film Box(定义要打印的DICOM图像)---
image_box_ds = pydicom.Dataset()
image_box_ds.SOPClassUID = BasicGrayscaleImageBoxSOPClass
image_box_ds.SOPInstanceUID = pydicom.uid.generate_uid() # 本次图像盒的唯一ID
image_box_ds.ImagePosition = 1 # 图像在胶片上的位置(1表示第一张)
image_box_ds.PresentationLUTShape = 'IDENTITY' # 灰度映射(默认线性)
image_box_ds.BasicGrayscaleImageSequence = [dataset] # 包含要打印的DICOM图像数据集
# 将图像盒添加到胶片盒
film_box_ds.BasicImageBoxSequence = [image_box_ds]
# 发送Film Box创建请求到打印机
assoc_print = ae.associate(printer_ip, printer_port)
if assoc_print.is_established:
print("[步骤2] 正在向打印机创建胶片盒(包含图像)...")
status_box = assoc_print.send_n_create(
film_box_ds.SOPInstanceUID,
BasicFilmBoxSOPClass,
film_box_ds
)
if status_box and status_box.Status in [0x0000, 0xB000]:
print("[步骤2] 胶片盒创建成功!Box UID:", film_box_ds.SOPInstanceUID)
box_uid = film_box_ds.SOPInstanceUID
else:
print("[步骤2] 胶片盒创建失败!状态码:", status_box.Status if status_box else "无响应")
assoc_print.release()
exit()
assoc_print.release()
else:
print("[步骤2] 无法连接到打印机!")
exit()
# --- 步骤3:触发打印指令(实际发送打印命令)---
# (注:完整实现需调用N-ACTION或N-EVENT-REPORT触发打印,此处简化为提示)
print("[步骤3] 胶片盒已创建,理论上可触发打印!实际需调用打印机的'打印'动作(如N-ACTION Type ID=2)")
print("(模拟结束:真实环境需连接支持DICOM打印的医用胶片打印机,并配置正确的IP/端口)")
代码说明:
Film Session:定义打印任务的“全局参数”(如胶片类型、打印份数、输出位置);
Film Box:定义单张胶片的“局部参数”(如图像排版、尺寸、包含的DICOM图像);
BasicGrayscaleImageBoxSOPClass:指定要打印的DICOM图像(需包含像素数据)。
📌 关键限制:
本代码仅演示了打印请求的构造逻辑(创建Film Session和Film Box),实际触发打印需额外调用N-ACTION(Action Type ID=2表示“打印”);
真实环境中必须连接支持DICOM打印的医用胶片打印机(普通打印机无法解析DICOM打印协议),且需配置正确的IP、端口和认证信息;
大多数开发者更常用“软拷贝”(显示器查看)而非打印,因此打印服务在现代PACS中使用频率较低,但在特定场景(如手术室、基层医院)仍有不可替代性。
DICOM的高级服务(存储提交与打印)虽然不如存储、网络传输那样常用,但在关键场景下保障了医学影像的可靠性与拓展性:
服务类型
核心价值
典型应用场景
存储提交服务
确保DICOM图像被目标系统(如PACS)真正持久化存储,避免数据丢失
法规合规、重要检查(如急诊CT)、云端备份验证
打印服务
将医学影像输出到医用胶片,生成物理硬拷贝
手术室参考、基层医院传统阅片、教学演示
对于开发者而言,掌握这些高级服务意味着可以:
构建更可靠的医疗影像系统(如基层医院上传影像后,通过存储提交确认云端PACS已保存,再删除本地副本);
满足特殊场景需求(如为患者提供胶片打印服务,或集成医用打印机到PACS工作流);
深入理解DICOM标准的全面能力(不仅是基础存储与传输,还包括质量保障与扩展应用)。