资讯动态

DicomPrint 源码实战:DICOM 打印协议对接与批量出片避坑指南

发布时间:2026/10/9 16:36:37 来源:尧图企业网站定制
简介DicomPrint-master 是一套面向医疗影像开发者的 DICOM 打印工具源码聚焦医学影像的胶片打印、布局定制与尺寸调整适合具备一定 DICOM 协议与图像处理基础的技术人员参考。资源包共 57 个文件约 15.21MB以 C# 源码cs、csproj、sln为主体辅以 dcm 示例影像、txt 与 doc/docx 说明文档、uml 设计图、jpg 截图及 dll 依赖库覆盖 PrintSCU、PrintSCP 服务、Common 公共模块与 Sample 示例等目录结构清晰便于按模块研读。内容涉及图像解析、胶片格式设置、尺寸调整、质量控制、元数据处理、预览与批处理等打印流程并附有 DICOM 一致性声明与需求说明可帮助读者理解 SCU/SCP 交互机制与打印服务实现思路。目前已有 598 人学习下载适合用于医疗影像打印功能的二次开发或技术方案参考。1. 从一台老激光相机说起DicomPrint 到底解决什么问题如果你在影像科或医疗软件公司待过大概率见过这样的场景一台服役多年的激光相机还在出片但工作站系统已经换了两代新系统导出的 DICOM 文件就是送不进那台老机器。厂商给的方案是整机升级报价六位数起步。这时候有人翻出一个叫 DicomPrint 的小工具把 DICOM 文件重新封装成打印协议能识别的格式老相机又活了。DicomPrint-master 这个源码包做的就是这件事——把 DICOM 影像文件转换成符合 DICOM Print 服务规范的打印请求对接支持 DICOM 打印的相机或打印服务端。它不是一个完整的 PACS也不是阅片器而是一个专注在「影像输出到胶片/纸张」这一环的中间件。适合谁用医院信息科的工程师、医疗设备厂商的售后、做影像系统集成的开发者以及需要把 DICOM 打印流程跑通的学生和研究者。它解决的核心问题是让已经存在的 DICOM 影像通过标准协议送到打印设备而不是依赖某一家厂商的私有接口。这个包在网上的讨论不多属于那种「知道的人当宝贝不知道的人搜不到」的类型。我拿到之后花了两天把打印链路跑通中间踩了几个不大不小的坑下面按实际复现的顺序拆开讲。2. DICOM Print 协议栈与 DicomPrint 的模块拆解2.1 为什么不能直接调打印机驱动普通文档打印走的是操作系统打印子系统Word 排版好交给驱动驱动转成打印机认识的页面描述语言。DICOM 打印完全不是这条路。DICOM Print 是一套独立的应用层协议定义在 DICOM 标准的 PS3.4 服务类中属于 Print Management Service Class。它规定了影像怎么组织成 Film Session、Film Box、Image Box 三层结构以及打印参数胶片尺寸、方向、灰度、边框、注释怎么通过 DIMSE 消息传递。换句话说DICOM 打印不是「把文件发给打印机」而是「和打印服务端建立关联协商参数逐层创建打印对象最后触发打印」。DicomPrint 的价值就在于把这套协议栈封装成了可调用的代码你不用从零实现 DIMSE 的 PDU 编解码。2.2 源码目录里有什么解压 DicomPrint-master 之后目录结构大致是这样的不同版本可能略有差异以实际为准DicomPrint-master/ ├── src/ # 核心源码 │ ├── dicom_print.py # 打印服务主逻辑 │ ├── dimse_client.py # DIMSE 协议客户端 │ ├── film_session.py # Film Session 管理 │ └── utils.py # 标签解析、日志等辅助 ├── config/ │ └── print_config.ini # 打印参数配置 ├── samples/ # 示例 DICOM 文件 ├── requirements.txt └── README.md核心逻辑集中在dicom_print.py和dimse_client.py。前者负责把 DICOM 文件读进来、提取像素数据和元信息后者负责和打印服务端建立 Association、发送 N-CREATE / N-SET / N-ACTION 等操作。film_session.py管理的是打印任务的生命周期——一个 Film Session 下可以挂多个 Film Box每个 Film Box 对应一张胶片里面再放多个 Image Box。2.3 依赖与运行环境requirements.txt里通常包含 pydicom、pynetdicom 这两个库。pydicom 负责解析 DICOM 文件pynetdicom 负责 DIMSE 协议通信。这两个库是 Python 医疗影像处理的事实标准版本兼容性还算好但要注意 pynetdicom 在 2.x 之后 API 有变动老代码可能需要调整。安装依赖pip install pydicom pynetdicom如果你的环境里已经有这两个库检查一下版本python -c import pydicom; print(pydicom.__version__) python -c import pynetdicom; print(pynetdicom.__version__)pydicom 建议 2.3 以上pynetdicom 建议 2.0 以上。低于这个版本部分 DIMSE 消息的构造方式不一样跑起来会报AttributeError。2.4 打印参数配置怎么读config/print_config.ini是打印行为的控制中心。典型内容如下[printer] ae_title DICOM_PRINT_SCP host 192.168.1.100 port 104 [film] film_size 14INX17IN film_orientation PORTRAIT film_destination MAGAZINE medium_type CLEAR FILM number_of_copies 1 [image] magnification_type REPLICATE polarity NORMAL这里每个参数都对应 DICOM Print 服务类里的一个属性。ae_title是打印服务端的 AE Title必须和对方配置一致否则 Association 直接被拒。film_size支持的值取决于打印设备常见的有 8INX10IN、10INX12IN、14INX17IN。film_orientation控制胶片方向PORTRAIT 是纵向LANDSCAPE 是横向。medium_type指定介质类型CLEAR FILM 是透明胶片PAPER 是纸。我一般会先确认打印服务端支持哪些值再回来改配置。盲目填一个设备不支持的值N-CREATE 会返回失败状态码但错误信息不一定直观。3. 从 DICOM 文件到打印请求完整链路实操3.1 建立 Association 与验证连通性第一步不是急着打印而是确认能和打印服务端建立 Association。DicomPrint 里通常有一个测试脚本或者可以直接调用的函数。我习惯先用 pynetdicom 自带的echoscu工具做一次 C-ECHOpython -m pynetdicom.echoscu 192.168.1.100 104 -aet DICOM_PRINT_SCP如果返回C-ECHO response received说明网络和 AE Title 都没问题。如果超时或拒绝先查三件事IP 和端口对不对、AE Title 大小写是否匹配、防火墙有没有放行。DICOM 的 AE Title 是区分大小写的DICOM_PRINT_SCP和dicom_print_scp在协议层面是两个不同的实体。连通性确认后在代码里建立 Associationfrom pynetdicom import AE from pynetdicom.sop_class import BasicFilmSession ae AE(ae_titleDICOM_PRINT_SCU) ae.add_requested_context(BasicFilmSession) assoc ae.associate(192.168.1.100, 104, ae_titleDICOM_PRINT_SCP) if assoc.is_established: print(Association established) assoc.release() else: print(Association failed)这段代码里DICOM_PRINT_SCU是发起方你的程序的 AE TitleDICOM_PRINT_SCP是接收方打印服务端的 AE Title。add_requested_context声明你要用 Basic Film Session 这个 SOP Class。实际打印时还需要加上 Basic Film Box、Basic Grayscale Image Box 等上下文DicomPrint 的dimse_client.py里已经封装好了。3.2 读取 DICOM 文件并提取像素数据打印的前提是能把 DICOM 文件读进来。pydicom 的dcmread是入口import pydicom from pydicom.pixel_data_handlers.util import apply_voi_lut ds pydicom.dcmread(samples/ct_001.dcm) # 提取像素数组 pixel_array ds.pixel_array # 应用 VOI LUT让灰度映射符合显示习惯 if VOILUTSequence in ds or WindowCenter in ds: pixel_array apply_voi_lut(pixel_array, ds) print(fImage shape: {pixel_array.shape}) print(fBits allocated: {ds.BitsAllocated}) print(fPhotometric interpretation: {ds.PhotometricInterpretation})这里有几个关键点。pixel_array拿到的是原始像素值但 DICOM 文件里通常带有窗宽窗位信息直接打印原始值可能对比度不对。apply_voi_lut会按照文件里的 VOI LUT 或 Window Center/Width 做映射输出更接近阅片效果的灰度。BitsAllocated告诉你每个像素占多少位8 位和 16 位的处理方式不同。PhotometricInterpretation如果是 MONOCHROME1表示灰度值越高越黑打印时需要反转极性否则出来的胶片是负片效果。3.3 构造 Film Session 与 Film BoxDICOM Print 的对象模型是三层Film Session 是打印任务的容器Film Box 代表一张胶片Image Box 代表胶片上的一个影像位置。DicomPrint 把这套流程封装成了几个函数调用from src.dicom_print import DicomPrinter printer DicomPrinter(config_pathconfig/print_config.ini) # 创建 Film Session session printer.create_film_session() # 创建 Film Box指定胶片参数 film_box printer.create_film_box( session, film_size14INX17IN, orientationPORTRAIT, image_display_formatSTANDARD\2,2 # 2x2 布局 ) # 往 Film Box 里填充影像 printer.set_image_box(film_box, pixel_array, position1) # 触发打印 printer.print_film_box(film_box)image_display_format的STANDARD\2,2表示一张胶片上排 2 行 2 列共 4 个影像位置。这个值必须和打印设备支持的能力匹配有些设备只支持 1x1 或 2x2填 3x3 会报错。position参数指定影像放在哪个格子里从 1 开始编号。3.4 参数对照与常见取值下面这张表是我在实际对接中整理的参数对照不同设备可能有差异但大方向一致参数常见取值说明film_size8INX10IN / 10INX12IN / 14INX17IN胶片物理尺寸必须设备支持film_orientationPORTRAIT / LANDSCAPE纵向或横向image_display_formatSTANDARD\1,1 / STANDARD\2,2行列布局magnification_typeREPLICATE / BILINEAR / CUBIC缩放算法polarityNORMAL / REVERSE灰度极性medium_typeCLEAR FILM / BLUE FILM / PAPER介质类型film_destinationMAGAZINE / PROCESSOR输出目标magnification_type选 REPLICATE 是最近邻插值速度快但边缘可能有锯齿BILINEAR 和 CUBIC 更平滑但计算量大。如果打印的是 CT 或 MR 的灰度影像REPLICATE 通常够用。film_destination选 MAGAZINE 表示输出到胶片仓选 PROCESSOR 表示直接进冲洗机取决于设备配置。4. 避坑与排查打印链路里最容易翻车的五个点4.1 Association 被拒报 Abstract Syntax Not Supported现象C-ECHO 能通但一发起打印请求就断连日志里显示Abstract Syntax Not Supported。原因打印服务端不支持你请求的 SOP Class。DICOM Print 涉及多个 SOP ClassBasic Film Session、Basic Film Box、Basic Grayscale Image Box、Basic Color Image Box 是分开的。你只请求了 Film Session但后续要创建 Image Box如果没提前声明对应的上下文服务端就会拒绝。解决在建立 Association 时把所有需要的 SOP Class 都加进去from pynetdicom.sop_class import ( BasicFilmSession, BasicFilmBox, BasicGrayscaleImageBox, Printer ) ae.add_requested_context(BasicFilmSession) ae.add_requested_context(BasicFilmBox) ae.add_requested_context(BasicGrayscaleImageBox) ae.add_requested_context(Printer)如果打印的是彩色影像把 BasicGrayscaleImageBox 换成 BasicColorImageBox。4.2 打印出来的胶片是全黑或全白现象打印任务成功但胶片上要么一片黑要么一片白看不到影像内容。原因像素数据的位深和打印服务端期望的不一致。DICOM 文件里BitsAllocated可能是 16但打印服务端只接受 8 位。或者PhotometricInterpretation是 MONOCHROME1你没有做极性反转。解决在发送影像之前做归一化import numpy as np # 归一化到 0-255 pixel_array pixel_array.astype(np.float32) pixel_array (pixel_array - pixel_array.min()) / (pixel_array.max() - pixel_array.min()) pixel_array (pixel_array * 255).astype(np.uint8) # 如果是 MONOCHROME1反转 if ds.PhotometricInterpretation MONOCHROME1: pixel_array 255 - pixel_array归一化之前先确认pixel_array不是全零。有些 DICOM 文件的像素数据在PixelData之外需要先解压。4.3 N-CREATE 返回 0x0110 状态码现象创建 Film Box 时返回0x0110提示Processing Failure。原因image_display_format的值和film_size不匹配。比如 14INX17IN 的胶片填了STANDARD\3,3但设备只支持 2x2。或者film_orientation填了设备不支持的方向。解决先查设备的能力列表。如果拿不到文档用最保守的配置试STANDARD\1,1 PORTRAIT 14INX17IN。跑通之后再逐步调整。DicomPrint 的日志里会打印服务端返回的完整状态码0x0110是通用处理失败具体原因要看设备日志。4.4 中文注释乱码现象胶片上打印的患者姓名或检查描述显示为乱码。原因DICOM 的字符集由SpecificCharacterSet标签控制。如果文件里是 ISO_IR 100Latin-1但打印服务端按 GB18030 解析中文就会乱。解决在读取 DICOM 文件时显式指定字符集ds pydicom.dcmread(samples/ct_001.dcm, specific_tags[SpecificCharacterSet]) print(ds.SpecificCharacterSet)如果文件里没有这个标签或者值是 ISO_IR 100而你需要打印中文可以在发送前把相关标签转成 UTF-8 并设置SpecificCharacterSet为 ISO_IR 192。但要注意不是所有打印设备都支持 UTF-8老设备可能只认 GB18030。4.5 打印任务排队但不输出现象所有 DIMSE 操作都返回成功但胶片就是不出来。原因Film Session 创建后没有正确关闭或者film_destination设置不对。有些设备需要显式发送 N-ACTION 触发打印有些则在 Film Box 关闭时自动触发。解决检查 DicomPrint 的print_film_box方法是否发送了 N-ACTION。如果没有手动加from pynetdicom.sop_class import FilmSession # 触发打印 printer.assoc.send_n_action( datasetNone, action_type1, class_uidFilmSession, instance_uidsession.SOPInstanceUID )action_type1表示打印。如果设备要求先关闭 Film Session 再打印顺序不能反。5. 进阶批量打印与打印队列的稳定性技巧单张打印跑通之后实际场景往往是批量出片。一个检查几十张影像手动一张张调不现实。DicomPrint 本身没有批量队列管理但可以在它上面包一层。我一般会写一个简单的队列脚本把待打印的 DICOM 文件列表读进来按检查号分组每个检查创建一个 Film Session组内影像按序列号排序后依次填入 Film Box。关键点是控制并发——不要同时开多个 Association打印服务端的连接数通常有限。import os import time from src.dicom_print import DicomPrinter printer DicomPrinter(config_pathconfig/print_config.ini) dicom_files sorted([ os.path.join(samples, f) for f in os.listdir(samples) if f.endswith(.dcm) ]) # 按检查号分组 groups {} for f in dicom_files: ds pydicom.dcmread(f, stop_before_pixelsTrue) study_uid ds.StudyInstanceUID groups.setdefault(study_uid, []).append(f) for study_uid, files in groups.items(): session printer.create_film_session() for idx, filepath in enumerate(files, start1): ds pydicom.dcmread(filepath) pixel_array ds.pixel_array film_box printer.create_film_box( session, film_size14INX17IN, orientationPORTRAIT, image_display_formatSTANDARD\\2,2 ) printer.set_image_box(film_box, pixel_array, positionidx) printer.print_film_box(film_box) time.sleep(0.5) # 给设备留出处理时间 printer.release_session(session)time.sleep(0.5)这个延迟看起来不起眼但少了它连续快速发送 N-CREATE 时设备可能来不及响应导致后续请求超时。具体延迟多久取决于设备性能我遇到过需要 1 秒的也遇到过 0.2 秒就够的。建议从 0.5 秒开始试不稳定就往上加。另一个技巧是加日志和重试。打印失败不一定是代码问题可能是设备忙、胶片用完、网络抖动。对 N-CREATE 和 N-ACTION 做三次重试每次间隔 2 秒能解决大部分偶发失败。重试之前先释放当前 Association重新建立连接避免状态残留。验证打印结果是否正确的办法先打一张测试图用 DICOM 查看器打开原始文件对比胶片上的影像。重点看四个角的位置对不对、灰度有没有反转、注释文字是否清晰。如果胶片上影像偏移或缩放不对回去调magnification_type和image_display_format。从那以后我每次对接新的打印设备都强制走一遍「C-ECHO → 单张 1x1 → 单张 2x2 → 批量队列」这个顺序不跳步。跳步省下的十分钟往往要用两小时排查来还。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑